diff --git a/src/commodity.rs b/src/commodity.rs index 48badd88..c5b27b5f 100644 --- a/src/commodity.rs +++ b/src/commodity.rs @@ -13,6 +13,12 @@ define_id_type! {CommodityID} /// A map of [`Commodity`]s, keyed by commodity ID pub type CommodityMap = IndexMap>; +/// A map of [`CommodityCost`]s, keyed by region ID, year and time slice ID +pub type CommodityCostMap = HashMap<(RegionID, u32, TimeSliceID), CommodityCost>; + +/// A map of demand values, keyed by region ID, year and time slice ID +pub type DemandMap = HashMap<(RegionID, u32, TimeSliceID), f64>; + /// A commodity within the simulation. Represents a substance (e.g. CO2) or form of energy (e.g. /// electricity) that can be produced and/or consumed by technologies in the model. #[derive(PartialEq, Debug, Deserialize)] @@ -54,38 +60,6 @@ pub struct CommodityCost { pub value: f64, } -/// A data structure for easy lookup of [`CommodityCost`]s -#[derive(PartialEq, Debug, Default, Clone)] -pub struct CommodityCostMap(HashMap<(RegionID, u32, TimeSliceID), CommodityCost>); - -impl CommodityCostMap { - /// Create a new, empty [`CommodityCostMap`] - pub fn new() -> Self { - Self(HashMap::new()) - } - - /// Insert a [`CommodityCost`] into the map - pub fn insert( - &mut self, - region_id: RegionID, - year: u32, - time_slice: TimeSliceID, - value: CommodityCost, - ) -> Option { - self.0.insert((region_id, year, time_slice), value) - } - - /// Retrieve a [`CommodityCost`] from the map - pub fn get( - &self, - region_id: &RegionID, - year: u32, - time_slice: &TimeSliceID, - ) -> Option<&CommodityCost> { - self.0.get(&(region_id.clone(), year, time_slice.clone())) - } -} - /// Commodity balance type #[derive(PartialEq, Debug, DeserializeLabeledStringEnum)] pub enum CommodityType { @@ -99,33 +73,6 @@ pub enum CommodityType { OutputCommodity, } -/// A map relating region, year and time slice to demand (in real units, not a fraction). -/// -/// 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<(RegionID, u32, TimeSliceID), f64>); - -impl DemandMap { - /// Create a new, empty [`DemandMap`] - pub fn new() -> DemandMap { - DemandMap::default() - } - - /// 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(&(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((region_id, year, time_slice), demand); - } -} - #[cfg(test)] mod tests { use super::*; @@ -138,9 +85,12 @@ mod tests { }; let value = 0.25; let mut map = DemandMap::new(); - map.insert("North".into(), 2020, time_slice.clone(), value); + map.insert(("North".into(), 2020, time_slice.clone()), value); - assert_eq!(map.get(&"North".into(), 2020, &time_slice), value) + assert_eq!( + map.get(&("North".into(), 2020, time_slice)).unwrap(), + &value + ) } #[test] @@ -155,8 +105,8 @@ mod tests { }; let mut map = CommodityCostMap::new(); assert!(map - .insert("GBR".into(), 2010, ts.clone(), value.clone()) + .insert(("GBR".into(), 2010, ts.clone()), value.clone()) .is_none()); - assert_eq!(map.get(&"GBR".into(), 2010, &ts).unwrap(), &value); + assert_eq!(map.get(&("GBR".into(), 2010, ts)).unwrap(), &value); } } diff --git a/src/input/commodity/cost.rs b/src/input/commodity/cost.rs index 8da53e98..00f86451 100644 --- a/src/input/commodity/cost.rs +++ b/src/input/commodity/cost.rs @@ -101,7 +101,7 @@ where }; ensure!( - map.insert(region_id.clone(), cost.year, time_slice.clone(), value) + map.insert((region_id.clone(), cost.year, time_slice.clone()), value) .is_none(), "Commodity cost entry covered by more than one time slice \ (region: {}, year: {}, time slice: {})", @@ -189,8 +189,8 @@ mod tests { value: cost2.value, }; let mut map = CommodityCostMap::new(); - map.insert("GBR".into(), cost1.year, time_slice.clone(), value1); - map.insert("FRA".into(), cost2.year, time_slice.clone(), value2); + map.insert(("GBR".into(), cost1.year, time_slice.clone()), value1); + map.insert(("FRA".into(), cost2.year, time_slice.clone()), value2); let expected = HashMap::from_iter([("commodity".into(), map)]); assert_eq!( read_commodity_costs_iter( diff --git a/src/input/commodity/demand.rs b/src/input/commodity/demand.rs index 8e5499fd..b9b2dec6 100644 --- a/src/input/commodity/demand.rs +++ b/src/input/commodity/demand.rs @@ -193,9 +193,7 @@ fn compute_demand_maps( // Add a new demand entry map.insert( - region_id.clone(), - *year, - time_slice.clone(), + (region_id.clone(), *year, time_slice.clone()), annual_demand * demand_fraction, ); } diff --git a/src/input/process.rs b/src/input/process.rs index 47ebca85..329cbb20 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -158,8 +158,11 @@ fn validate_svd_commodity( for region_id in params.region_ids.iter() { for year in params.milestone_years.iter().copied() { for time_slice in params.time_slice_info.iter_ids() { - let demand = commodity.demand.get(region_id, year, time_slice); - if demand > 0.0 { + let demand = commodity + .demand + .get(&(region_id.clone(), year, time_slice.clone())) + .unwrap(); + if demand > &0.0 { let mut has_producer = false; // We must check for producers in every time slice, region, and year. @@ -412,7 +415,7 @@ mod tests { for region in data.region_ids.iter() { for year in milestone_years { for time_slice in time_slice_info.iter_ids() { - demand_map.insert(region.clone(), year, time_slice.clone(), 0.5); + demand_map.insert((region.clone(), year, time_slice.clone()), 0.5); } } } diff --git a/src/simulation/optimisation.rs b/src/simulation/optimisation.rs index c360fe1f..96e8e628 100644 --- a/src/simulation/optimisation.rs +++ b/src/simulation/optimisation.rs @@ -231,8 +231,13 @@ fn calculate_cost_coefficient( coeff += asset.process.parameter.variable_operating_cost } - // If there is a user-provided commodity cost for this combination of parameters, include it - if let Some(cost) = flow.commodity.costs.get(&asset.region_id, year, time_slice) { + // If there is a user-provided cost for this commodity, include it + if !flow.commodity.costs.is_empty() { + let cost = flow + .commodity + .costs + .get(&(asset.region_id.clone(), year, time_slice.clone())) + .unwrap(); let apply_cost = match cost.balance_type { BalanceType::Net => true, BalanceType::Consumption => flow.flow < 0.0, @@ -344,7 +349,7 @@ mod tests { value: 2.0, }; let mut costs = CommodityCostMap::new(); - costs.insert("GBR".into(), 2010, time_slice.clone(), cost); + costs.insert(("GBR".into(), 2010, time_slice.clone()), cost); check_coeff!(1.0, false, costs.clone(), 3.0); check_coeff!(-1.0, false, costs, -1.0); @@ -354,7 +359,7 @@ mod tests { value: 2.0, }; let mut costs = CommodityCostMap::new(); - costs.insert("GBR".into(), 2010, time_slice.clone(), cost); + costs.insert(("GBR".into(), 2010, time_slice.clone()), cost); check_coeff!(1.0, false, costs.clone(), 3.0); check_coeff!(-1.0, false, costs, -3.0); @@ -364,7 +369,7 @@ mod tests { value: 2.0, }; let mut costs = CommodityCostMap::new(); - costs.insert("GBR".into(), 2010, time_slice.clone(), cost); + costs.insert(("GBR".into(), 2010, time_slice.clone()), cost); check_coeff!(1.0, false, costs.clone(), 1.0); check_coeff!(-1.0, false, costs, -3.0); @@ -374,7 +379,7 @@ mod tests { value: 2.0, }; let mut costs = CommodityCostMap::new(); - costs.insert("GBR".into(), 2010, time_slice.clone(), cost); + costs.insert(("GBR".into(), 2010, time_slice.clone()), cost); check_coeff!(1.0, true, costs.clone(), 4.0); check_coeff!(-1.0, true, costs, -2.0); } diff --git a/src/simulation/optimisation/constraints.rs b/src/simulation/optimisation/constraints.rs index da243355..3f880247 100644 --- a/src/simulation/optimisation/constraints.rs +++ b/src/simulation/optimisation/constraints.rs @@ -118,9 +118,10 @@ fn add_commodity_balance_constraints( CommodityType::SupplyEqualsDemand => 0.0, CommodityType::ServiceDemand => { match ts_selection { - TimeSliceSelection::Single(ref ts) => { - commodity.demand.get(region_id, year, ts) - } + TimeSliceSelection::Single(ref ts) => *commodity + .demand + .get(&(region_id.clone(), year, ts.clone())) + .unwrap(), // We currently only support specifying demand at the time slice level: // https://github.com/EnergySystemsModellingLab/MUSE_2.0/issues/391 _ => panic!(