diff --git a/src/commodity.rs b/src/commodity.rs index 0fc15d1fa..48badd88d 100644 --- a/src/commodity.rs +++ b/src/commodity.rs @@ -54,17 +54,9 @@ pub struct CommodityCost { pub value: f64, } -/// Used for looking up [`CommodityCost`]s in a [`CommodityCostMap`] -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -struct CommodityCostKey { - region_id: RegionID, - year: u32, - time_slice: TimeSliceID, -} - /// A data structure for easy lookup of [`CommodityCost`]s #[derive(PartialEq, Debug, Default, Clone)] -pub struct CommodityCostMap(HashMap); +pub struct CommodityCostMap(HashMap<(RegionID, u32, TimeSliceID), CommodityCost>); impl CommodityCostMap { /// Create a new, empty [`CommodityCostMap`] @@ -80,12 +72,7 @@ impl CommodityCostMap { time_slice: TimeSliceID, value: CommodityCost, ) -> Option { - let key = CommodityCostKey { - region_id, - year, - time_slice, - }; - self.0.insert(key, value) + self.0.insert((region_id, year, time_slice), value) } /// Retrieve a [`CommodityCost`] from the map @@ -95,12 +82,7 @@ impl CommodityCostMap { year: u32, time_slice: &TimeSliceID, ) -> Option<&CommodityCost> { - let key = CommodityCostKey { - region_id: region_id.clone(), - year, - time_slice: time_slice.clone(), - }; - self.0.get(&key) + self.0.get(&(region_id.clone(), year, time_slice.clone())) } } @@ -122,15 +104,7 @@ pub enum CommodityType { /// This data type is exported as this is the way in we want to look up demand outside of this /// module. #[derive(PartialEq, Debug, Clone, Default)] -pub struct DemandMap(HashMap); - -/// The key for a [`DemandMap`] -#[derive(PartialEq, Eq, Hash, Debug, Clone)] -struct DemandMapKey { - region_id: RegionID, - year: u32, - time_slice: TimeSliceID, -} +pub struct DemandMap(HashMap<(RegionID, u32, TimeSliceID), f64>); impl DemandMap { /// Create a new, empty [`DemandMap`] @@ -141,25 +115,14 @@ impl DemandMap { /// Retrieve the demand for the specified region, year and time slice pub fn get(&self, region_id: &RegionID, year: u32, time_slice: &TimeSliceID) -> f64 { self.0 - .get(&DemandMapKey { - region_id: region_id.clone(), - year, - time_slice: time_slice.clone(), - }) + .get(&(region_id.clone(), year, time_slice.clone())) .copied() .unwrap_or_else(|| panic!("Missing demand entry: {region_id}, {year}, {time_slice}")) } /// Insert a new demand entry for the specified region, year and time slice pub fn insert(&mut self, region_id: RegionID, year: u32, time_slice: TimeSliceID, demand: f64) { - self.0.insert( - DemandMapKey { - region_id, - year, - time_slice, - }, - demand, - ); + self.0.insert((region_id, year, time_slice), demand); } } diff --git a/src/input.rs b/src/input.rs index 1b08d337f..2780f1a50 100644 --- a/src/input.rs +++ b/src/input.rs @@ -9,7 +9,6 @@ use itertools::Itertools; use serde::de::{Deserialize, DeserializeOwned, Deserializer}; use std::collections::HashSet; use std::fs; -use std::hash::Hash; use std::path::Path; mod agent; diff --git a/src/input/commodity/demand.rs b/src/input/commodity/demand.rs index 2d801ec1c..8e5499fdc 100644 --- a/src/input/commodity/demand.rs +++ b/src/input/commodity/demand.rs @@ -1,7 +1,7 @@ //! Code for working with demand for a given commodity. Demand can vary by region, year and time //! slice. use super::super::*; -use super::demand_slicing::{read_demand_slices, DemandSliceMap, DemandSliceMapKey}; +use super::demand_slicing::{read_demand_slices, DemandSliceMap}; use crate::commodity::{CommodityID, DemandMap}; use crate::id::IDCollection; use crate::region::RegionID; @@ -27,18 +27,7 @@ struct Demand { } /// A map relating commodity, region and year to annual demand -pub type AnnualDemandMap = HashMap; - -/// A key for an [`AnnualDemandMap`] -#[derive(PartialEq, Eq, Hash, Debug)] -pub struct AnnualDemandMapKey { - /// The commodity to which this demand applies - commodity_id: CommodityID, - /// The region to which this demand applies - region_id: RegionID, - /// The simulation year to which this demand applies - year: u32, -} +pub type AnnualDemandMap = HashMap<(CommodityID, RegionID, u32), f64>; /// A set of commodity + region pairs pub type CommodityRegionPairs = HashSet<(CommodityID, RegionID)>; @@ -143,13 +132,12 @@ where "Demand must be a valid number greater than zero" ); - let key = AnnualDemandMapKey { - commodity_id: commodity_id.clone(), - region_id: region_id.clone(), - year: demand.year, - }; ensure!( - map.insert(key, demand.demand).is_none(), + map.insert( + (commodity_id.clone(), region_id.clone(), demand.year), + demand.demand + ) + .is_none(), "Duplicate demand entries (commodity: {}, region: {}, year: {})", commodity_id, region_id, @@ -163,13 +151,8 @@ where // milestone year for (commodity_id, region_id) in commodity_regions.iter() { for year in milestone_years.iter().copied() { - let key = AnnualDemandMapKey { - commodity_id: commodity_id.clone(), - region_id: region_id.clone(), - year, - }; ensure!( - map.contains_key(&key), + map.contains_key(&(commodity_id.clone(), region_id.clone(), year)), "Missing milestone year {year} for commodity {commodity_id} in region {region_id}" ); } @@ -196,15 +179,9 @@ fn compute_demand_maps( time_slice_info: &TimeSliceInfo, ) -> HashMap { let mut map = HashMap::new(); - for (demand_key, annual_demand) in demand.iter() { - let commodity_id = &demand_key.commodity_id; - let region_id = &demand_key.region_id; + for ((commodity_id, region_id, year), annual_demand) in demand.iter() { for time_slice in time_slice_info.iter_ids() { - let slice_key = DemandSliceMapKey { - commodity_id: commodity_id.clone(), - region_id: region_id.clone(), - time_slice: time_slice.clone(), - }; + let slice_key = (commodity_id.clone(), region_id.clone(), time_slice.clone()); // NB: This has already been checked, so shouldn't fail let demand_fraction = slices.get(&slice_key).unwrap(); @@ -217,7 +194,7 @@ fn compute_demand_maps( // Add a new demand entry map.insert( region_id.clone(), - demand_key.year, + *year, time_slice.clone(), annual_demand * demand_fraction, ); @@ -428,38 +405,10 @@ COM1,West,2020,13" HashSet::from_iter(["North".into(), "South".into(), "East".into(), "West".into()]); let milestone_years = [2020]; let expected = AnnualDemandMap::from_iter([ - ( - AnnualDemandMapKey { - commodity_id: "COM1".into(), - region_id: "North".into(), - year: 2020, - }, - 10.0, - ), - ( - AnnualDemandMapKey { - commodity_id: "COM1".into(), - region_id: "South".into(), - year: 2020, - }, - 11.0, - ), - ( - AnnualDemandMapKey { - commodity_id: "COM1".into(), - region_id: "East".into(), - year: 2020, - }, - 12.0, - ), - ( - AnnualDemandMapKey { - commodity_id: "COM1".into(), - region_id: "West".into(), - year: 2020, - }, - 13.0, - ), + (("COM1".into(), "North".into(), 2020), 10.0), + (("COM1".into(), "South".into(), 2020), 11.0), + (("COM1".into(), "East".into(), 2020), 12.0), + (("COM1".into(), "West".into(), 2020), 13.0), ]); let (demand, commodity_regions) = read_demand_file(dir.path(), &commodity_ids, ®ion_ids, &milestone_years).unwrap(); diff --git a/src/input/commodity/demand_slicing.rs b/src/input/commodity/demand_slicing.rs index fd0b2e6e4..aba906482 100644 --- a/src/input/commodity/demand_slicing.rs +++ b/src/input/commodity/demand_slicing.rs @@ -23,18 +23,7 @@ struct DemandSlice { } /// A map relating commodity, region and time slice to the fraction of annual demand -pub type DemandSliceMap = HashMap; - -/// A key for a [`DemandSliceMap`] -#[derive(PartialEq, Eq, Hash, Debug)] -pub struct DemandSliceMapKey { - /// The commodity to which this demand applies - pub commodity_id: CommodityID, - /// The region to which this demand applies - pub region_id: RegionID, - /// The time slice to which this demand applies - pub time_slice: TimeSliceID, -} +pub type DemandSliceMap = HashMap<(CommodityID, RegionID, TimeSliceID), f64>; /// Read demand slices from specified model directory. /// @@ -92,14 +81,8 @@ where let ts_selection = time_slice_info.get_selection(&slice.time_slice)?; for (ts, demand_fraction) in time_slice_info.calculate_share(&ts_selection, slice.fraction) { - let key = DemandSliceMapKey { - commodity_id: commodity_id.clone(), - region_id: region_id.clone(), - time_slice: ts.clone(), - }; - // Share demand between the time slices in proportion to duration - ensure!(demand_slices.insert(key, demand_fraction).is_none(), + ensure!(demand_slices.insert((commodity_id.clone(), region_id.clone(), ts.clone()), demand_fraction).is_none(), "Duplicate demand slicing entry (or same time slice covered by more than one entry) \ (commodity: {commodity_id}, region: {region_id}, time slice: {ts})" ); @@ -128,18 +111,14 @@ fn validate_demand_slices( time_slice_info .iter_ids() .map(|time_slice| { - let key = DemandSliceMapKey { - commodity_id: commodity_id.clone(), - region_id: region_id.clone(), - time_slice: time_slice.clone(), - }; - - demand_slices.get(&key).with_context(|| { - format!( - "Demand slice missing for time slice {} (commodity: {}, region {})", - time_slice, commodity_id, region_id - ) - }) + demand_slices + .get(&(commodity_id.clone(), region_id.clone(), time_slice.clone())) + .with_context(|| { + format!( + "Demand slice missing for time slice {} (commodity: {}, region {})", + time_slice, commodity_id, region_id + ) + }) }) .process_results(|iter| { check_fractions_sum_to_one(iter.copied()).context("Invalid demand fractions") @@ -186,11 +165,7 @@ mod tests { let time_slice = time_slice_info .get_time_slice_id_from_str("winter.day") .unwrap(); - let key = DemandSliceMapKey { - commodity_id: "COM1".into(), - region_id: "GBR".into(), - time_slice, - }; + let key = ("COM1".into(), "GBR".into(), time_slice); let expected = DemandSliceMap::from_iter(iter::once((key, 1.0))); assert_eq!( read_demand_slices_from_iter( @@ -258,47 +233,47 @@ mod tests { ]; let expected = DemandSliceMap::from_iter([ ( - DemandSliceMapKey { - commodity_id: "COM1".into(), - region_id: "GBR".into(), - time_slice: TimeSliceID { + ( + "COM1".into(), + "GBR".into(), + TimeSliceID { season: "summer".into(), time_of_day: "day".into(), }, - }, + ), 3.0 / 16.0, ), ( - DemandSliceMapKey { - commodity_id: "COM1".into(), - region_id: "GBR".into(), - time_slice: TimeSliceID { + ( + "COM1".into(), + "GBR".into(), + TimeSliceID { season: "summer".into(), time_of_day: "night".into(), }, - }, + ), 5.0 / 16.0, ), ( - DemandSliceMapKey { - commodity_id: "COM1".into(), - region_id: "GBR".into(), - time_slice: TimeSliceID { + ( + "COM1".into(), + "GBR".into(), + TimeSliceID { season: "winter".into(), time_of_day: "day".into(), }, - }, + ), 3.0 / 16.0, ), ( - DemandSliceMapKey { - commodity_id: "COM1".into(), - region_id: "GBR".into(), - time_slice: TimeSliceID { + ( + "COM1".into(), + "GBR".into(), + TimeSliceID { season: "winter".into(), time_of_day: "night".into(), }, - }, + ), 5.0 / 16.0, ), ]); diff --git a/src/simulation/optimisation.rs b/src/simulation/optimisation.rs index 50e1fc1da..0167c25a6 100644 --- a/src/simulation/optimisation.rs +++ b/src/simulation/optimisation.rs @@ -29,7 +29,7 @@ type Variable = highs::Col; /// 2. To keep track of the combination of parameters that each variable corresponds to, for when we /// are reading the results of the optimisation. #[derive(Default)] -pub struct VariableMap(IndexMap); +pub struct VariableMap(IndexMap<(AssetID, CommodityID, TimeSliceID), Variable>); impl VariableMap { /// Get the [`Variable`] corresponding to the given parameters. @@ -39,11 +39,7 @@ impl VariableMap { commodity_id: &CommodityID, time_slice: &TimeSliceID, ) -> Variable { - let key = VariableMapKey { - asset_id, - commodity_id: commodity_id.clone(), - time_slice: time_slice.clone(), - }; + let key = (asset_id, commodity_id.clone(), time_slice.clone()); *self .0 @@ -52,25 +48,6 @@ impl VariableMap { } } -/// A key for a [`VariableMap`] -#[derive(Eq, PartialEq, Hash)] -struct VariableMapKey { - asset_id: AssetID, - commodity_id: CommodityID, - time_slice: TimeSliceID, -} - -impl VariableMapKey { - /// Create a new [`VariableMapKey`] - fn new(asset_id: AssetID, commodity_id: CommodityID, time_slice: TimeSliceID) -> Self { - Self { - asset_id, - commodity_id, - time_slice, - } - } -} - /// The solution to the dispatch optimisation problem pub struct Solution<'a> { solution: highs::Solution, @@ -96,7 +73,9 @@ impl Solution<'_> { .0 .keys() .zip(self.solution.columns().iter().copied()) - .map(|(key, flow)| (key.asset_id, &key.commodity_id, &key.time_slice, flow)) + .map(|((asset_id, commodity_id, time_slice), flow)| { + (*asset_id, commodity_id, time_slice, flow) + }) } /// Keys and dual values for commodity balance constraints. @@ -227,9 +206,7 @@ fn add_variables( problem.add_column(coeff, 0.0..) }; - let key = - VariableMapKey::new(asset.id, flow.commodity.id.clone(), time_slice.clone()); - + let key = (asset.id, flow.commodity.id.clone(), time_slice.clone()); let existing = variables.0.insert(key, var).is_some(); assert!(!existing, "Duplicate entry for var"); } diff --git a/src/simulation/prices.rs b/src/simulation/prices.rs index 31e0f49b2..90d1ab348 100644 --- a/src/simulation/prices.rs +++ b/src/simulation/prices.rs @@ -8,12 +8,9 @@ use indexmap::IndexMap; use log::warn; use std::collections::{HashMap, HashSet}; -/// A combination of commodity ID and time slice -type CommodityPriceKey = (CommodityID, TimeSliceID); - /// A map relating commodity ID + time slice to current price (endogenous) #[derive(Default)] -pub struct CommodityPrices(IndexMap); +pub struct CommodityPrices(IndexMap<(CommodityID, TimeSliceID), f64>); impl CommodityPrices { /// Calculate commodity prices based on the result of the dispatch optimisation. @@ -65,10 +62,9 @@ impl CommodityPrices { // If the commodity flow is positive (produced PAC) if pac.flow > 0.0 { - let key: CommodityPriceKey = (commodity.id.clone(), time_slice.clone()); // Update the highest dual for this commodity/timeslice highest_duals - .entry(key) + .entry((commodity.id.clone(), time_slice.clone())) .and_modify(|current_dual| { if dual > *current_dual { *current_dual = dual;