From dd4ee9b6577aa180dfb173730db4429ef5988b49 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 7 Apr 2025 16:57:28 +0100 Subject: [PATCH 01/21] Add AnnualField, implement on ProcessParameter --- examples/simple/process_parameters.csv | 14 ++-- src/asset.rs | 11 ++- src/input/process.rs | 11 ++- src/input/process/parameter.rs | 51 +++++++++----- src/lib.rs | 1 + src/process.rs | 5 +- src/simulation/optimisation.rs | 2 +- src/year.rs | 94 ++++++++++++++++++++++++++ 8 files changed, 151 insertions(+), 38 deletions(-) create mode 100644 src/year.rs diff --git a/examples/simple/process_parameters.csv b/examples/simple/process_parameters.csv index 3f7564125..b641ff93e 100644 --- a/examples/simple/process_parameters.csv +++ b/examples/simple/process_parameters.csv @@ -1,7 +1,7 @@ -process_id,start_year,end_year,capital_cost,fixed_operating_cost,variable_operating_cost,lifetime,discount_rate,capacity_to_activity -GASDRV,2020,2030,10.0,0.3,2.0,25,0.1,1.0 -GASPRC,2020,2030,7.0,0.21,0.5,25,0.1,1.0 -WNDFRM,2020,2030,1000.0,30.0,0.4,25,0.1,31.54 -GASCGT,2020,2030,700.0,21.0,0.55,30,0.1,31.54 -RGASBR,2020,2030,55.56,1.6668,0.16,15,0.1,1.0 -RELCHP,2020,2030,138.9,4.167,0.17,15,0.1,1.0 +process_id,start_year,end_year,capital_cost,fixed_operating_cost,variable_operating_cost,lifetime,discount_rate,capacity_to_activity,year +GASDRV,2020,2030,10.0,0.3,2.0,25,0.1,1.0,all +GASPRC,2020,2030,7.0,0.21,0.5,25,0.1,1.0,all +WNDFRM,2020,2030,1000.0,30.0,0.4,25,0.1,31.54,all +GASCGT,2020,2030,700.0,21.0,0.55,30,0.1,31.54,all +RGASBR,2020,2030,55.56,1.6668,0.16,15,0.1,1.0,all +RELCHP,2020,2030,138.9,4.167,0.17,15,0.1,1.0,all diff --git a/src/asset.rs b/src/asset.rs index 93bc9cad8..7f4af26fb 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -56,10 +56,10 @@ impl Asset { /// The last year in which this asset should be decommissioned pub fn decommission_year(&self) -> u32 { - self.commission_year + self.process.parameter.lifetime + self.commission_year + self.process.parameter.get(self.commission_year).lifetime } - /// Get the activity limits for this asset in a particular time slice + /// Get the activity limits for this asset in a particular time slice and year. pub fn get_activity_limits(&self, time_slice: &TimeSliceID) -> RangeInclusive { let limits = self.process.activity_limits.get(time_slice).unwrap(); let max_act = self.maximum_activity(); @@ -70,7 +70,12 @@ impl Asset { /// Maximum activity for this asset in a year pub fn maximum_activity(&self) -> f64 { - self.capacity * self.process.parameter.capacity_to_activity + self.capacity + * self + .process + .parameter + .get(self.commission_year) + .capacity_to_activity } } diff --git a/src/input/process.rs b/src/input/process.rs index e57add416..c297cf103 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -4,6 +4,7 @@ use crate::commodity::{Commodity, CommodityMap, CommodityType}; use crate::process::{ActivityLimitsMap, Process, ProcessFlow, ProcessMap, ProcessParameter}; use crate::region::RegionSelection; use crate::time_slice::TimeSliceInfo; +use crate::year::AnnualField; use anyhow::{bail, ensure, Context, Result}; use serde::Deserialize; use std::collections::{HashMap, HashSet}; @@ -94,7 +95,7 @@ struct ValidationParams<'a> { region_ids: &'a HashSet>, milestone_years: &'a [u32], time_slice_info: &'a TimeSliceInfo, - parameters: &'a HashMap, ProcessParameter>, + parameters: &'a HashMap, AnnualField>, availabilities: &'a HashMap, ActivityLimitsMap>, } @@ -105,7 +106,7 @@ fn validate_commodities( region_ids: &HashSet>, milestone_years: &[u32], time_slice_info: &TimeSliceInfo, - parameters: &HashMap, ProcessParameter>, + parameters: &HashMap, AnnualField>, availabilities: &HashMap, ActivityLimitsMap>, ) -> anyhow::Result<()> { let params = ValidationParams { @@ -180,7 +181,6 @@ fn validate_svd_commodity( .parameters .get(&*flow.process_id) .unwrap() - .years .contains(&year) && params .availabilities @@ -215,7 +215,7 @@ fn create_process_map( descriptions: I, mut availabilities: HashMap, ActivityLimitsMap>, mut flows: HashMap, Vec>, - mut parameters: HashMap, ProcessParameter>, + mut parameters: HashMap, AnnualField>, mut regions: HashMap, RegionSelection>, ) -> Result where @@ -265,7 +265,7 @@ mod tests { descriptions: Vec, availabilities: HashMap, ActivityLimitsMap>, flows: HashMap, Vec>, - parameters: HashMap, ProcessParameter>, + parameters: HashMap, AnnualField>, regions: HashMap, RegionSelection>, region_ids: HashSet>, } @@ -307,7 +307,6 @@ mod tests { .into_iter() .map(|id| { let parameter = ProcessParameter { - process_id: id.to_string(), years: 2010..=2020, capital_cost: 0.0, fixed_operating_cost: 0.0, diff --git a/src/input/process/parameter.rs b/src/input/process/parameter.rs index 40d6d4d0d..1045f4936 100644 --- a/src/input/process/parameter.rs +++ b/src/input/process/parameter.rs @@ -2,6 +2,7 @@ use super::super::*; use super::define_process_id_getter; use crate::process::ProcessParameter; +use crate::year::{deserialize_year, AnnualField, Year}; use ::log::warn; use anyhow::{ensure, Context, Result}; use serde::Deserialize; @@ -23,6 +24,8 @@ struct ProcessParameterRaw { lifetime: u32, discount_rate: Option, capacity_to_activity: Option, + #[serde(deserialize_with = "deserialize_year")] + year: Year, } define_process_id_getter! {ProcessParameterRaw} @@ -41,7 +44,6 @@ impl ProcessParameterRaw { self.validate()?; Ok(ProcessParameter { - process_id: self.process_id, years: start_year..=end_year, capital_cost: self.capital_cost, fixed_operating_cost: self.fixed_operating_cost, @@ -110,7 +112,7 @@ pub fn read_process_parameters( model_dir: &Path, process_ids: &HashSet>, year_range: &RangeInclusive, -) -> Result, ProcessParameter>> { +) -> Result, AnnualField>> { let file_path = model_dir.join(PROCESS_PARAMETERS_FILE_NAME); let iter = read_csv::(&file_path)?; read_process_parameters_from_iter(iter, process_ids, year_range) @@ -121,18 +123,28 @@ fn read_process_parameters_from_iter( iter: I, process_ids: &HashSet>, year_range: &RangeInclusive, -) -> Result, ProcessParameter>> +) -> Result, AnnualField>> where I: Iterator, { - let mut params = HashMap::new(); - for param in iter { - let param = param.into_parameter(year_range)?; - let id = process_ids.get_id(¶m.process_id)?; - ensure!( - params.insert(Rc::clone(&id), param).is_none(), - "More than one parameter provided for process {id}" - ); + let mut params: HashMap, AnnualField> = HashMap::new(); + for param_raw in iter { + let id = process_ids.get_id(¶m_raw.process_id)?; + let year = param_raw.year; + let param = param_raw.into_parameter(year_range)?; + + // Create AnnualField + let annual_field = match year { + Year::All => AnnualField::Constant(param), + Year::Single(year) => AnnualField::Variable([(year, param)].into_iter().collect()), + }; + + // Insert into the map + if let Some(existing) = params.get_mut(&id) { + existing.merge(&annual_field)?; + } else { + params.insert(Rc::clone(&id), annual_field); + } } Ok(params) } @@ -158,6 +170,7 @@ mod tests { lifetime, discount_rate, capacity_to_activity, + year: Year::All, } } @@ -167,7 +180,6 @@ mod tests { capacity_to_activity: f64, ) -> ProcessParameter { ProcessParameter { - process_id: "id".to_string(), years, capital_cost: 0.0, fixed_operating_cost: 0.0, @@ -296,6 +308,7 @@ mod tests { lifetime: 10, discount_rate: Some(1.0), capacity_to_activity: Some(1.0), + year: Year::All, }, ProcessParameterRaw { process_id: "B".into(), @@ -307,14 +320,14 @@ mod tests { lifetime: 10, discount_rate: Some(1.0), capacity_to_activity: Some(1.0), + year: Year::All, }, ]; let expected: HashMap, _> = [ ( "A".into(), - ProcessParameter { - process_id: "A".into(), + AnnualField::Constant(ProcessParameter { years: 2010..=2020, capital_cost: 1.0, fixed_operating_cost: 1.0, @@ -322,12 +335,11 @@ mod tests { lifetime: 10, discount_rate: 1.0, capacity_to_activity: 1.0, - }, + }), ), ( "B".into(), - ProcessParameter { - process_id: "B".into(), + AnnualField::Constant(ProcessParameter { years: 2015..=2020, capital_cost: 1.0, fixed_operating_cost: 1.0, @@ -335,7 +347,7 @@ mod tests { lifetime: 10, discount_rate: 1.0, capacity_to_activity: 1.0, - }, + }), ), ] .into_iter() @@ -362,6 +374,7 @@ mod tests { lifetime: 10, discount_rate: Some(1.0), capacity_to_activity: Some(1.0), + year: Year::All, }, ProcessParameterRaw { process_id: "B".into(), @@ -373,6 +386,7 @@ mod tests { lifetime: 10, discount_rate: Some(1.0), capacity_to_activity: Some(1.0), + year: Year::All, }, ProcessParameterRaw { process_id: "A".into(), @@ -384,6 +398,7 @@ mod tests { lifetime: 10, discount_rate: Some(1.0), capacity_to_activity: Some(1.0), + year: Year::All, }, ]; diff --git a/src/lib.rs b/src/lib.rs index b0aa62be8..8c8dd790b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,3 +13,4 @@ pub mod region; pub mod settings; pub mod simulation; pub mod time_slice; +pub mod year; diff --git a/src/process.rs b/src/process.rs index be53ff9b2..fe493a91f 100644 --- a/src/process.rs +++ b/src/process.rs @@ -3,6 +3,7 @@ use crate::commodity::Commodity; use crate::region::RegionSelection; use crate::time_slice::TimeSliceID; +use crate::year::AnnualField; use indexmap::IndexMap; use serde::Deserialize; use serde_string_enum::DeserializeLabeledStringEnum; @@ -25,7 +26,7 @@ pub struct Process { /// Commodity flows for this process pub flows: Vec, /// Additional parameters for this process - pub parameter: ProcessParameter, + pub parameter: AnnualField, /// The regions in which this process can operate pub regions: RegionSelection, } @@ -93,8 +94,6 @@ pub enum FlowType { /// Additional parameters for a process #[derive(PartialEq, Clone, Debug, Deserialize)] pub struct ProcessParameter { - /// A unique identifier for the process - pub process_id: String, /// The years in which this process is available for investment pub years: RangeInclusive, /// Overnight capital cost per unit capacity diff --git a/src/simulation/optimisation.rs b/src/simulation/optimisation.rs index 29b86645b..1f55b72a8 100644 --- a/src/simulation/optimisation.rs +++ b/src/simulation/optimisation.rs @@ -250,7 +250,7 @@ fn calculate_cost_coefficient( // Only applies if commodity is PAC if flow.is_pac { - coeff += asset.process.parameter.variable_operating_cost + coeff += asset.process.parameter.get(year).variable_operating_cost } // If there is a user-provided commodity cost for this combination of parameters, include it diff --git a/src/year.rs b/src/year.rs new file mode 100644 index 000000000..2e05af6b6 --- /dev/null +++ b/src/year.rs @@ -0,0 +1,94 @@ +#![allow(missing_docs)] +use anyhow::{bail, Result}; +use serde::de::Deserializer; +use serde::Deserialize; +use std::collections::{HashMap, HashSet}; + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub enum AnnualField { + Constant(T), + Variable(HashMap), +} + +impl AnnualField { + pub fn get(&self, year: u32) -> &T { + match self { + AnnualField::Constant(value) => value, + AnnualField::Variable(values) => values.get(&year).unwrap(), + } + } + + pub fn insert(&mut self, year: u32, value: T) -> Result<()> { + match self { + AnnualField::Constant(_) => { + bail!("Cannot insert into a constant field."); + } + AnnualField::Variable(values) => { + if values.contains_key(&year) { + bail!("Year {} already exists in variable field.", year); + } + values.insert(year, value); + Ok(()) + } + } + } + + pub fn merge(&mut self, other: &Self) -> Result<()> { + match (self, other) { + (AnnualField::Variable(values), AnnualField::Variable(other_values)) => { + for (year, value) in other_values.iter() { + if values.contains_key(year) { + bail!("Year {} already exists in variable field.", year); + } + values.insert(*year, value.clone()); + } + Ok(()) + } + (AnnualField::Constant(_), AnnualField::Constant(_)) => { + bail!("Cannot merge two constant fields.") + } + _ => bail!("Cannot merge constant and variable fields."), + } + } + + pub fn contains(&self, year: &u32) -> bool { + match self { + AnnualField::Constant(_) => true, + AnnualField::Variable(values) => values.contains_key(year), + } + } + + pub fn check_reference(&self, reference_years: &HashSet) -> Result<()> { + match self { + AnnualField::Constant(_) => Ok(()), + AnnualField::Variable(_) => { + for year in reference_years.iter() { + if !self.contains(year) { + bail!("Missing data for year {}.", year); + } + } + Ok(()) + } + } + } +} + +#[derive(PartialEq, Debug, Copy, Clone)] +pub enum Year { + All, + Single(u32), +} + +pub fn deserialize_year<'de, D>(deserialiser: D) -> Result +where + D: Deserializer<'de>, +{ + let value = String::deserialize(deserialiser)?; + if value == "all" { + Ok(Year::All) + } else if let Ok(n) = value.parse::() { + Ok(Year::Single(n)) + } else { + Err(serde::de::Error::custom("Invalid year format")) + } +} From 6fb1affe6e6a4f1a81b79524bbb984e215c06a75 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 8 Apr 2025 11:12:34 +0100 Subject: [PATCH 02/21] Move start and end year to processes table --- examples/simple/process_parameters.csv | 14 +-- examples/simple/processes.csv | 14 +-- src/asset.rs | 2 +- src/input/process.rs | 136 ++++++++++++---------- src/input/process/parameter.rs | 151 ++++--------------------- src/process.rs | 4 +- src/year.rs | 10 ++ 7 files changed, 124 insertions(+), 207 deletions(-) diff --git a/examples/simple/process_parameters.csv b/examples/simple/process_parameters.csv index b641ff93e..283a0913b 100644 --- a/examples/simple/process_parameters.csv +++ b/examples/simple/process_parameters.csv @@ -1,7 +1,7 @@ -process_id,start_year,end_year,capital_cost,fixed_operating_cost,variable_operating_cost,lifetime,discount_rate,capacity_to_activity,year -GASDRV,2020,2030,10.0,0.3,2.0,25,0.1,1.0,all -GASPRC,2020,2030,7.0,0.21,0.5,25,0.1,1.0,all -WNDFRM,2020,2030,1000.0,30.0,0.4,25,0.1,31.54,all -GASCGT,2020,2030,700.0,21.0,0.55,30,0.1,31.54,all -RGASBR,2020,2030,55.56,1.6668,0.16,15,0.1,1.0,all -RELCHP,2020,2030,138.9,4.167,0.17,15,0.1,1.0,all +process_id,capital_cost,fixed_operating_cost,variable_operating_cost,lifetime,discount_rate,capacity_to_activity,year +GASDRV,10.0,0.3,2.0,25,0.1,1.0,all +GASPRC,7.0,0.21,0.5,25,0.1,1.0,all +WNDFRM,1000.0,30.0,0.4,25,0.1,31.54,all +GASCGT,700.0,21.0,0.55,30,0.1,31.54,all +RGASBR,55.56,1.6668,0.16,15,0.1,1.0,all +RELCHP,138.9,4.167,0.17,15,0.1,1.0,all diff --git a/examples/simple/processes.csv b/examples/simple/processes.csv index b55837398..3acf39f33 100644 --- a/examples/simple/processes.csv +++ b/examples/simple/processes.csv @@ -1,7 +1,7 @@ -id,description -GASDRV,Dry gas extraction -GASPRC,Gas processing -WNDFRM,Wind farm -GASCGT,Gas combined cycle turbine -RGASBR,Gas boiler -RELCHP,Heat pump +id,description,start_year,end_year +GASDRV,Dry gas extraction,2020,2030 +GASPRC,Gas processing,2020,2030 +WNDFRM,Wind farm,2020,2030 +GASCGT,Gas combined cycle turbine,2020,2030 +RGASBR,Gas boiler,2020,2030 +RELCHP,Heat pump,2020,2030 diff --git a/src/asset.rs b/src/asset.rs index 7f4af26fb..7cba0710f 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -59,7 +59,7 @@ impl Asset { self.commission_year + self.process.parameter.get(self.commission_year).lifetime } - /// Get the activity limits for this asset in a particular time slice and year. + /// Get the activity limits for this asset in a particular time slice. pub fn get_activity_limits(&self, time_slice: &TimeSliceID) -> RangeInclusive { let limits = self.process.activity_limits.get(time_slice).unwrap(); let max_act = self.maximum_activity(); diff --git a/src/input/process.rs b/src/input/process.rs index c297cf103..7a3849e9c 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -5,9 +5,10 @@ use crate::process::{ActivityLimitsMap, Process, ProcessFlow, ProcessMap, Proces use crate::region::RegionSelection; use crate::time_slice::TimeSliceInfo; use crate::year::AnnualField; -use anyhow::{bail, ensure, Context, Result}; +use anyhow::{bail, ensure, Context, Ok, Result}; use serde::Deserialize; use std::collections::{HashMap, HashSet}; +use std::ops::RangeInclusive; use std::path::Path; use std::rc::Rc; @@ -33,12 +34,13 @@ macro_rules! define_process_id_getter { } use define_process_id_getter; -#[derive(PartialEq, Debug, Deserialize)] -struct ProcessDescription { +#[derive(Debug, Deserialize, PartialEq, Clone)] +struct ProcessRaw { id: Rc, description: String, + start_year: Option, + end_year: Option, } -define_id_getter! {ProcessDescription} /// Read process information from the specified CSV files. /// @@ -60,15 +62,14 @@ pub fn read_processes( time_slice_info: &TimeSliceInfo, milestone_years: &[u32], ) -> Result { - let file_path = model_dir.join(PROCESSES_FILE_NAME); - let descriptions = read_csv_id_file::(&file_path)?; - let process_ids = HashSet::from_iter(descriptions.keys().cloned()); - - let availabilities = read_process_availabilities(model_dir, &process_ids, time_slice_info)?; - let flows = read_process_flows(model_dir, &process_ids, commodities)?; let year_range = milestone_years[0]..=milestone_years[milestone_years.len() - 1]; - let parameters = read_process_parameters(model_dir, &process_ids, &year_range)?; - let regions = read_process_regions(model_dir, &process_ids, region_ids)?; + let mut processes = read_processes_file(model_dir, &year_range)?; + let process_ids = processes.keys().cloned().collect(); + + let mut availabilities = read_process_availabilities(model_dir, &process_ids, time_slice_info)?; + let mut flows = read_process_flows(model_dir, &process_ids, commodities)?; + let mut parameters = read_process_parameters(model_dir, &process_ids)?; + let mut regions = read_process_regions(model_dir, &process_ids, region_ids)?; // Validate commodities after the flows have been read validate_commodities( @@ -81,13 +82,69 @@ pub fn read_processes( &availabilities, )?; - create_process_map( - descriptions.into_values(), - availabilities, - flows, - parameters, - regions, - ) + // Add data to Process objects + for (id, process) in processes.iter_mut() { + process.activity_limits = availabilities.remove(id).unwrap(); + process.flows = flows.remove(id).unwrap(); + process.parameter = parameters.remove(id).unwrap(); + process.regions = regions.remove(id).unwrap(); + } + + // Create ProcessMap + let mut process_map = ProcessMap::new(); + for (id, process) in processes { + process_map.insert(id.clone(), process.into()); + } + + Ok(process_map) +} + +fn read_processes_file( + model_dir: &Path, + year_range: &RangeInclusive, +) -> Result, Process>> { + let file_path = model_dir.join(PROCESSES_FILE_NAME); + let processes_csv = read_csv(&file_path)?; + read_processes_file_from_iter(processes_csv, year_range) + .with_context(|| input_err_msg(&file_path)) +} + +fn read_processes_file_from_iter( + iter: I, + year_range: &RangeInclusive, +) -> Result, Process>> +where + I: Iterator, +{ + let mut processes = HashMap::new(); + for process_raw in iter { + let start_year = process_raw.start_year.unwrap_or(*year_range.start()); + let end_year = process_raw.end_year.unwrap_or(*year_range.end()); + + // Check year range is valid + ensure!( + start_year <= end_year, + "Error in parameter for process {}: start_year > end_year", + process_raw.id + ); + + let process = Process { + id: process_raw.id.clone(), + description: process_raw.description, + years: start_year..=end_year, + activity_limits: ActivityLimitsMap::new(), + flows: Vec::new(), + parameter: AnnualField::Empty, + regions: RegionSelection::default(), + }; + + ensure!( + processes.insert(process_raw.id, process).is_none(), + "Duplicate process ID" + ); + } + + Ok(processes) } struct ValidationParams<'a> { @@ -211,46 +268,6 @@ fn validate_svd_commodity( Ok(()) } -fn create_process_map( - descriptions: I, - mut availabilities: HashMap, ActivityLimitsMap>, - mut flows: HashMap, Vec>, - mut parameters: HashMap, AnnualField>, - mut regions: HashMap, RegionSelection>, -) -> Result -where - I: Iterator, -{ - descriptions - .map(|description| { - let id = &description.id; - let availabilities = availabilities - .remove(id) - .with_context(|| format!("No availabilities defined for process {id}"))?; - let flows = flows - .remove(id) - .with_context(|| format!("No commodity flows defined for process {id}"))?; - let parameter = parameters - .remove(id) - .with_context(|| format!("No parameters defined for process {id}"))?; - - // We've already checked that regions are defined for each process - let regions = regions.remove(id).unwrap(); - - let process = Process { - id: Rc::clone(id), - description: description.description, - activity_limits: availabilities, - flows, - parameter, - regions, - }; - - Ok((description.id, process.into())) - }) - .try_collect() -} - #[cfg(test)] mod tests { use crate::commodity::{CommodityCostMap, DemandMap}; @@ -307,7 +324,6 @@ mod tests { .into_iter() .map(|id| { let parameter = ProcessParameter { - years: 2010..=2020, capital_cost: 0.0, fixed_operating_cost: 0.0, variable_operating_cost: 0.0, diff --git a/src/input/process/parameter.rs b/src/input/process/parameter.rs index 1045f4936..e7859c00b 100644 --- a/src/input/process/parameter.rs +++ b/src/input/process/parameter.rs @@ -7,7 +7,6 @@ use ::log::warn; use anyhow::{ensure, Context, Result}; use serde::Deserialize; use std::collections::{HashMap, HashSet}; -use std::ops::RangeInclusive; use std::path::Path; use std::rc::Rc; @@ -16,8 +15,6 @@ const PROCESS_PARAMETERS_FILE_NAME: &str = "process_parameters.csv"; #[derive(PartialEq, Debug, Deserialize)] struct ProcessParameterRaw { process_id: String, - start_year: Option, - end_year: Option, capital_cost: f64, fixed_operating_cost: f64, variable_operating_cost: f64, @@ -30,21 +27,10 @@ struct ProcessParameterRaw { define_process_id_getter! {ProcessParameterRaw} impl ProcessParameterRaw { - fn into_parameter(self, year_range: &RangeInclusive) -> Result { - let start_year = self.start_year.unwrap_or(*year_range.start()); - let end_year = self.end_year.unwrap_or(*year_range.end()); - - // Check year range is valid - ensure!( - start_year <= end_year, - "Error in parameter for process {}: start_year > end_year", - self.process_id - ); - + fn into_parameter(self) -> Result { self.validate()?; Ok(ProcessParameter { - years: start_year..=end_year, capital_cost: self.capital_cost, fixed_operating_cost: self.fixed_operating_cost, variable_operating_cost: self.variable_operating_cost, @@ -111,18 +97,15 @@ impl ProcessParameterRaw { pub fn read_process_parameters( model_dir: &Path, process_ids: &HashSet>, - year_range: &RangeInclusive, ) -> Result, AnnualField>> { let file_path = model_dir.join(PROCESS_PARAMETERS_FILE_NAME); let iter = read_csv::(&file_path)?; - read_process_parameters_from_iter(iter, process_ids, year_range) - .with_context(|| input_err_msg(&file_path)) + read_process_parameters_from_iter(iter, process_ids).with_context(|| input_err_msg(&file_path)) } fn read_process_parameters_from_iter( iter: I, process_ids: &HashSet>, - year_range: &RangeInclusive, ) -> Result, AnnualField>> where I: Iterator, @@ -131,7 +114,7 @@ where for param_raw in iter { let id = process_ids.get_id(¶m_raw.process_id)?; let year = param_raw.year; - let param = param_raw.into_parameter(year_range)?; + let param = param_raw.into_parameter()?; // Create AnnualField let annual_field = match year { @@ -154,16 +137,12 @@ mod tests { use super::*; fn create_param_raw( - start_year: Option, - end_year: Option, lifetime: u32, discount_rate: Option, capacity_to_activity: Option, ) -> ProcessParameterRaw { ProcessParameterRaw { process_id: "id".to_string(), - start_year, - end_year, capital_cost: 0.0, fixed_operating_cost: 0.0, variable_operating_cost: 0.0, @@ -174,13 +153,8 @@ mod tests { } } - fn create_param( - years: RangeInclusive, - discount_rate: f64, - capacity_to_activity: f64, - ) -> ProcessParameter { + fn create_param(discount_rate: f64, capacity_to_activity: f64) -> ProcessParameter { ProcessParameter { - years, capital_cost: 0.0, fixed_operating_cost: 0.0, variable_operating_cost: 0.0, @@ -192,116 +166,50 @@ mod tests { #[test] fn test_param_raw_into_param_ok() { - let year_range = 2000..=2100; - // No missing values - let raw = create_param_raw(Some(2010), Some(2020), 1, Some(1.0), Some(0.0)); - assert_eq!( - raw.into_parameter(&year_range).unwrap(), - create_param(2010..=2020, 1.0, 0.0) - ); - - // Missing years - let raw = create_param_raw(None, None, 1, Some(1.0), Some(0.0)); - assert_eq!( - raw.into_parameter(&year_range).unwrap(), - create_param(2000..=2100, 1.0, 0.0) - ); + let raw = create_param_raw(1, Some(1.0), Some(0.0)); + assert_eq!(raw.into_parameter().unwrap(), create_param(1.0, 0.0)); // Missing discount_rate - let raw = create_param_raw(Some(2010), Some(2020), 1, None, Some(0.0)); - assert_eq!( - raw.into_parameter(&year_range).unwrap(), - create_param(2010..=2020, 0.0, 0.0) - ); + let raw = create_param_raw(1, None, Some(0.0)); + assert_eq!(raw.into_parameter().unwrap(), create_param(0.0, 0.0)); // Missing capacity_to_activity - let raw = create_param_raw(Some(2010), Some(2020), 1, Some(1.0), None); - assert_eq!( - raw.into_parameter(&year_range).unwrap(), - create_param(2010..=2020, 1.0, 1.0) - ); - } - - #[test] - fn test_param_raw_into_param_good_years() { - let year_range = 2000..=2100; - - // Normal case - assert!( - create_param_raw(Some(2000), Some(2100), 1, Some(1.0), Some(0.0)) - .into_parameter(&year_range) - .is_ok() - ); - - // start_year out of range - this is permitted - assert!( - create_param_raw(Some(1999), Some(2100), 1, Some(1.0), Some(0.0)) - .into_parameter(&year_range) - .is_ok() - ); - - // end_year out of range - this is permitted - assert!( - create_param_raw(Some(2000), Some(2101), 1, Some(1.0), Some(0.0)) - .into_parameter(&year_range) - .is_ok() - ); - } - - #[test] - #[should_panic] - fn test_param_raw_into_param_bad_years() { - let year_range = 2000..=2100; - - // start_year after end_year - assert!( - create_param_raw(Some(2001), Some(2000), 1, Some(1.0), Some(0.0)) - .into_parameter(&year_range) - .is_ok() - ); + let raw = create_param_raw(1, Some(1.0), None); + assert_eq!(raw.into_parameter().unwrap(), create_param(1.0, 1.0)); } #[test] fn test_param_raw_validate_bad_lifetime() { // lifetime = 0 - assert!( - create_param_raw(Some(2000), Some(2100), 0, Some(1.0), Some(0.0)) - .validate() - .is_err() - ); + assert!(create_param_raw(0, Some(1.0), Some(0.0)) + .validate() + .is_err()); } #[test] fn test_param_raw_validate_bad_discount_rate() { // discount rate = -1 - assert!( - create_param_raw(Some(2000), Some(2100), 0, Some(-1.0), Some(0.0)) - .validate() - .is_err() - ); + assert!(create_param_raw(0, Some(-1.0), Some(0.0)) + .validate() + .is_err()); } #[test] fn test_param_raw_validate_bad_capt2act() { // capt2act = -1 - assert!( - create_param_raw(Some(2000), Some(2100), 0, Some(1.0), Some(-1.0)) - .validate() - .is_err() - ); + assert!(create_param_raw(0, Some(1.0), Some(-1.0)) + .validate() + .is_err()); } #[test] fn test_read_process_parameters_from_iter_good() { - let year_range = 2000..=2100; let process_ids = ["A".into(), "B".into()].into_iter().collect(); let params_raw = [ ProcessParameterRaw { process_id: "A".into(), - start_year: Some(2010), - end_year: Some(2020), capital_cost: 1.0, fixed_operating_cost: 1.0, variable_operating_cost: 1.0, @@ -312,8 +220,6 @@ mod tests { }, ProcessParameterRaw { process_id: "B".into(), - start_year: Some(2015), - end_year: Some(2020), capital_cost: 1.0, fixed_operating_cost: 1.0, variable_operating_cost: 1.0, @@ -328,7 +234,6 @@ mod tests { ( "A".into(), AnnualField::Constant(ProcessParameter { - years: 2010..=2020, capital_cost: 1.0, fixed_operating_cost: 1.0, variable_operating_cost: 1.0, @@ -340,7 +245,6 @@ mod tests { ( "B".into(), AnnualField::Constant(ProcessParameter { - years: 2015..=2020, capital_cost: 1.0, fixed_operating_cost: 1.0, variable_operating_cost: 1.0, @@ -353,21 +257,17 @@ mod tests { .into_iter() .collect(); let actual = - read_process_parameters_from_iter(params_raw.into_iter(), &process_ids, &year_range) - .unwrap(); + read_process_parameters_from_iter(params_raw.into_iter(), &process_ids).unwrap(); assert_eq!(expected, actual); } #[test] fn test_read_process_parameters_from_iter_bad_multiple_params() { - let year_range = 2000..=2100; let process_ids = ["A".into(), "B".into()].into_iter().collect(); let params_raw = [ ProcessParameterRaw { process_id: "A".into(), - start_year: Some(2010), - end_year: Some(2020), capital_cost: 1.0, fixed_operating_cost: 1.0, variable_operating_cost: 1.0, @@ -378,8 +278,6 @@ mod tests { }, ProcessParameterRaw { process_id: "B".into(), - start_year: Some(2015), - end_year: Some(2020), capital_cost: 1.0, fixed_operating_cost: 1.0, variable_operating_cost: 1.0, @@ -390,8 +288,6 @@ mod tests { }, ProcessParameterRaw { process_id: "A".into(), - start_year: Some(2015), - end_year: Some(2020), capital_cost: 1.0, fixed_operating_cost: 1.0, variable_operating_cost: 1.0, @@ -402,11 +298,6 @@ mod tests { }, ]; - assert!(read_process_parameters_from_iter( - params_raw.into_iter(), - &process_ids, - &year_range - ) - .is_err()); + assert!(read_process_parameters_from_iter(params_raw.into_iter(), &process_ids,).is_err()); } } diff --git a/src/process.rs b/src/process.rs index fe493a91f..d1dae7442 100644 --- a/src/process.rs +++ b/src/process.rs @@ -21,6 +21,8 @@ pub struct Process { pub id: Rc, /// A human-readable description for the process (e.g. dry gas extraction) pub description: String, + /// The years in which this process is available for investment + pub years: RangeInclusive, /// The activity limits for each time slice (as a fraction of maximum) pub activity_limits: ActivityLimitsMap, /// Commodity flows for this process @@ -94,8 +96,6 @@ pub enum FlowType { /// Additional parameters for a process #[derive(PartialEq, Clone, Debug, Deserialize)] pub struct ProcessParameter { - /// The years in which this process is available for investment - pub years: RangeInclusive, /// Overnight capital cost per unit capacity pub capital_cost: f64, /// Annual operating cost per unit capacity diff --git a/src/year.rs b/src/year.rs index 2e05af6b6..43c500c3c 100644 --- a/src/year.rs +++ b/src/year.rs @@ -6,6 +6,7 @@ use std::collections::{HashMap, HashSet}; #[derive(Debug, Clone, PartialEq, Deserialize)] pub enum AnnualField { + Empty, Constant(T), Variable(HashMap), } @@ -13,6 +14,7 @@ pub enum AnnualField { impl AnnualField { pub fn get(&self, year: u32) -> &T { match self { + AnnualField::Empty => panic!("AnnualField is empty."), AnnualField::Constant(value) => value, AnnualField::Variable(values) => values.get(&year).unwrap(), } @@ -30,6 +32,10 @@ impl AnnualField { values.insert(year, value); Ok(()) } + AnnualField::Empty => { + *self = AnnualField::Variable(HashMap::new()); + self.insert(year, value) + } } } @@ -53,6 +59,7 @@ impl AnnualField { pub fn contains(&self, year: &u32) -> bool { match self { + AnnualField::Empty => false, AnnualField::Constant(_) => true, AnnualField::Variable(values) => values.contains_key(year), } @@ -60,6 +67,9 @@ impl AnnualField { pub fn check_reference(&self, reference_years: &HashSet) -> Result<()> { match self { + AnnualField::Empty => { + bail!("AnnualField is empty. Cannot check reference years."); + } AnnualField::Constant(_) => Ok(()), AnnualField::Variable(_) => { for year in reference_years.iter() { From 91ac0a8d783bc1a0646a394f45bf91fbf0470330 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 8 Apr 2025 11:28:49 +0100 Subject: [PATCH 03/21] Update some tests --- src/asset.rs | 13 ++++++------- src/input/asset.rs | 9 +++++---- src/output.rs | 6 +++--- src/simulation/optimisation.rs | 12 ++++++++---- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index 7cba0710f..a239b9f80 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -59,7 +59,7 @@ impl Asset { self.commission_year + self.process.parameter.get(self.commission_year).lifetime } - /// Get the activity limits for this asset in a particular time slice. + /// Get the activity limits for this asset in a particular time slice pub fn get_activity_limits(&self, time_slice: &TimeSliceID) -> RangeInclusive { let limits = self.process.activity_limits.get(time_slice).unwrap(); let max_act = self.maximum_activity(); @@ -192,6 +192,7 @@ mod tests { use crate::process::{ActivityLimitsMap, FlowType, Process, ProcessFlow, ProcessParameter}; use crate::region::RegionSelection; use crate::time_slice::TimeSliceLevel; + use crate::year::AnnualField; use itertools::{assert_equal, Itertools}; use std::iter; @@ -202,8 +203,6 @@ mod tests { time_of_day: "day".into(), }; let process_param = ProcessParameter { - process_id: "process1".into(), - years: 2010..=2020, capital_cost: 5.0, fixed_operating_cost: 2.0, variable_operating_cost: 1.0, @@ -232,9 +231,10 @@ mod tests { let process = Rc::new(Process { id: "process1".into(), description: "Description".into(), + years: 2010..=2020, activity_limits, flows: vec![flow.clone()], - parameter: process_param.clone(), + parameter: AnnualField::Constant(process_param), regions: RegionSelection::All, }); let asset = Asset { @@ -251,8 +251,6 @@ mod tests { fn create_asset_pool() -> AssetPool { let process_param = ProcessParameter { - process_id: "process1".into(), - years: 2010..=2020, capital_cost: 5.0, fixed_operating_cost: 2.0, variable_operating_cost: 1.0, @@ -263,9 +261,10 @@ mod tests { let process = Rc::new(Process { id: "process1".into(), description: "Description".into(), + years: 2010..=2020, activity_limits: ActivityLimitsMap::new(), flows: vec![], - parameter: process_param.clone(), + parameter: AnnualField::Constant(process_param), regions: RegionSelection::All, }); let future = [2020, 2010] diff --git a/src/input/asset.rs b/src/input/asset.rs index 1a5497532..d5dc75935 100644 --- a/src/input/asset.rs +++ b/src/input/asset.rs @@ -94,14 +94,13 @@ mod tests { use super::*; use crate::process::{ActivityLimitsMap, Process, ProcessParameter}; use crate::region::RegionSelection; + use crate::year::AnnualField; use itertools::assert_equal; use std::iter; #[test] fn test_read_assets_from_iter() { let process_param = ProcessParameter { - process_id: "process1".into(), - years: 2010..=2020, capital_cost: 5.0, fixed_operating_cost: 2.0, variable_operating_cost: 1.0, @@ -112,9 +111,10 @@ mod tests { let process = Rc::new(Process { id: "process1".into(), description: "Description".into(), + years: 2010..=2020, activity_limits: ActivityLimitsMap::new(), flows: vec![], - parameter: process_param.clone(), + parameter: AnnualField::Constant(process_param.clone()), regions: RegionSelection::All, }); let processes = [(Rc::clone(&process.id), Rc::clone(&process))] @@ -187,9 +187,10 @@ mod tests { let process = Rc::new(Process { id: "process1".into(), description: "Description".into(), + years: 2010..=2020, activity_limits: ActivityLimitsMap::new(), flows: vec![], - parameter: process_param, + parameter: AnnualField::Constant(process_param), regions: RegionSelection::Some(["GBR".into()].into_iter().collect()), }); let asset_in = AssetRaw { diff --git a/src/output.rs b/src/output.rs index 92a24fcc2..a0a4f51d4 100644 --- a/src/output.rs +++ b/src/output.rs @@ -179,6 +179,7 @@ mod tests { use crate::process::{Process, ProcessParameter}; use crate::region::RegionSelection; use crate::time_slice::TimeSliceID; + use crate::year::AnnualField; use itertools::{assert_equal, Itertools}; use std::{collections::HashMap, iter}; use tempfile::tempdir; @@ -189,8 +190,6 @@ mod tests { let agent_id = "agent1".into(); let commission_year = 2015; let process_param = ProcessParameter { - process_id: "process1".to_string(), - years: 2010..=2020, capital_cost: 5.0, fixed_operating_cost: 2.0, variable_operating_cost: 1.0, @@ -201,9 +200,10 @@ mod tests { let process = Rc::new(Process { id: Rc::clone(&process_id), description: "Description".into(), + years: 2010..=2020, activity_limits: HashMap::new(), flows: vec![], - parameter: process_param.clone(), + parameter: AnnualField::Constant(process_param), regions: RegionSelection::All, }); diff --git a/src/simulation/optimisation.rs b/src/simulation/optimisation.rs index 1f55b72a8..47340f03f 100644 --- a/src/simulation/optimisation.rs +++ b/src/simulation/optimisation.rs @@ -250,7 +250,11 @@ fn calculate_cost_coefficient( // Only applies if commodity is PAC if flow.is_pac { - coeff += asset.process.parameter.get(year).variable_operating_cost + coeff += asset + .process + .parameter + .get(asset.commission_year) + .variable_operating_cost } // If there is a user-provided commodity cost for this combination of parameters, include it @@ -282,6 +286,7 @@ mod tests { use crate::process::{ActivityLimitsMap, FlowType, Process, ProcessParameter}; use crate::region::RegionSelection; use crate::time_slice::TimeSliceLevel; + use crate::year::AnnualField; use float_cmp::assert_approx_eq; use std::rc::Rc; @@ -291,8 +296,6 @@ mod tests { costs: CommodityCostMap, ) -> (Asset, ProcessFlow) { let process_param = ProcessParameter { - process_id: "process1".into(), - years: 2010..=2020, capital_cost: 5.0, fixed_operating_cost: 2.0, variable_operating_cost: 1.0, @@ -319,9 +322,10 @@ mod tests { let process = Rc::new(Process { id: "process1".into(), description: "Description".into(), + years: 2010..=2020, activity_limits: ActivityLimitsMap::new(), flows: vec![flow.clone()], - parameter: process_param.clone(), + parameter: AnnualField::Constant(process_param), regions: RegionSelection::All, }); let asset = Asset::new( From ab6595be4752dd3e303ce3c67a50d395c0f5716c Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 8 Apr 2025 13:11:24 +0100 Subject: [PATCH 04/21] Add validation check --- src/input/process.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/input/process.rs b/src/input/process.rs index 7a3849e9c..af30eaf1c 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -82,6 +82,17 @@ pub fn read_processes( &availabilities, )?; + // Check parameters cover all years of the process + for (id, parameter) in parameters.iter() { + let year_range = processes.get(id).unwrap().years.clone(); + let reference_years: HashSet = milestone_years + .iter() + .copied() + .filter(|year| year_range.contains(year)) + .collect(); + parameter.check_reference(&reference_years)? + } + // Add data to Process objects for (id, process) in processes.iter_mut() { process.activity_limits = availabilities.remove(id).unwrap(); From 41fd584b41ac8201fee3aec14c887817ade6e3c4 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 8 Apr 2025 16:09:14 +0100 Subject: [PATCH 05/21] Add ability to specify list of years separated by semicolons --- src/input/process/parameter.rs | 9 +++++++-- src/year.rs | 13 +++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/input/process/parameter.rs b/src/input/process/parameter.rs index e7859c00b..af08762e8 100644 --- a/src/input/process/parameter.rs +++ b/src/input/process/parameter.rs @@ -113,13 +113,18 @@ where let mut params: HashMap, AnnualField> = HashMap::new(); for param_raw in iter { let id = process_ids.get_id(¶m_raw.process_id)?; - let year = param_raw.year; + let year = param_raw.year.clone(); let param = param_raw.into_parameter()?; // Create AnnualField let annual_field = match year { Year::All => AnnualField::Constant(param), - Year::Single(year) => AnnualField::Variable([(year, param)].into_iter().collect()), + Year::Single(year) => { + AnnualField::Variable([(year, param.clone())].into_iter().collect()) + } + Year::Some(years) => { + AnnualField::Variable(years.iter().map(|y| (*y, param.clone())).collect()) + } }; // Insert into the map diff --git a/src/year.rs b/src/year.rs index 43c500c3c..d1c640023 100644 --- a/src/year.rs +++ b/src/year.rs @@ -83,10 +83,11 @@ impl AnnualField { } } -#[derive(PartialEq, Debug, Copy, Clone)] +#[derive(PartialEq, Debug, Clone)] pub enum Year { All, Single(u32), + Some(HashSet), } pub fn deserialize_year<'de, D>(deserialiser: D) -> Result @@ -95,10 +96,18 @@ where { let value = String::deserialize(deserialiser)?; if value == "all" { + // "all" years specified Ok(Year::All) } else if let Ok(n) = value.parse::() { + // Single year specified Ok(Year::Single(n)) } else { - Err(serde::de::Error::custom("Invalid year format")) + // Semicolon-separated list of years + let years: Result, _> = + value.split(';').map(|s| s.trim().parse::()).collect(); + match years { + Ok(years_set) if !years_set.is_empty() => Ok(Year::Some(years_set)), + _ => Err(serde::de::Error::custom("Invalid year format")), + } } } From 5c130928caf22228f4aeabab04d21915c847fc33 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Tue, 8 Apr 2025 16:29:45 +0100 Subject: [PATCH 06/21] Fix/delete failing tests --- src/input/process.rs | 81 ++------------------------------------------ 1 file changed, 2 insertions(+), 79 deletions(-) diff --git a/src/input/process.rs b/src/input/process.rs index af30eaf1c..4f574999f 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -290,27 +290,13 @@ mod tests { use super::*; struct ProcessData { - descriptions: Vec, availabilities: HashMap, ActivityLimitsMap>, - flows: HashMap, Vec>, parameters: HashMap, AnnualField>, - regions: HashMap, RegionSelection>, region_ids: HashSet>, } /// Returns example data (without errors) for processes fn get_process_data() -> ProcessData { - let descriptions = vec![ - ProcessDescription { - id: Rc::from("process1"), - description: "Process 1".to_string(), - }, - ProcessDescription { - id: Rc::from("process2"), - description: "Process 2".to_string(), - }, - ]; - let availabilities = ["process1", "process2"] .into_iter() .map(|id| { @@ -326,93 +312,30 @@ mod tests { }) .collect(); - let flows = ["process1", "process2"] - .into_iter() - .map(|id| (id.into(), vec![])) - .collect(); - let parameters = ["process1", "process2"] .into_iter() .map(|id| { - let parameter = ProcessParameter { + let parameter = AnnualField::Constant(ProcessParameter { capital_cost: 0.0, fixed_operating_cost: 0.0, variable_operating_cost: 0.0, lifetime: 1, discount_rate: 1.0, capacity_to_activity: 0.0, - }; - + }); (id.into(), parameter) }) .collect(); - let regions = ["process1", "process2"] - .into_iter() - .map(|id| (id.into(), RegionSelection::All)) - .collect(); - let region_ids = HashSet::from_iter(iter::once("GBR".into())); ProcessData { - descriptions, availabilities, - flows, parameters, - regions, region_ids, } } - #[test] - fn test_create_process_map_success() { - let data = get_process_data(); - let result = create_process_map( - data.descriptions.into_iter(), - data.availabilities, - data.flows, - data.parameters, - data.regions, - ) - .unwrap(); - - assert_eq!(result.len(), 2); - assert!(result.contains_key("process1")); - assert!(result.contains_key("process2")); - } - - /// Generate code for a test with data missing for a given field - macro_rules! test_missing { - ($field:ident) => { - let mut data = get_process_data(); - data.$field.remove("process1"); - - let result = create_process_map( - data.descriptions.into_iter(), - data.availabilities, - data.flows, - data.parameters, - data.regions, - ); - assert!(result.is_err()); - }; - } - - #[test] - fn test_create_process_map_missing_availabilities() { - test_missing!(availabilities); - } - - #[test] - fn test_create_process_map_missing_flows() { - test_missing!(flows); - } - - #[test] - fn test_create_process_map_missing_parameters() { - test_missing!(parameters); - } - #[test] fn test_validate_commodities() { let data = get_process_data(); From f33c86d65a8b72f7c3dc55bc772817d0a9d2974e Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 10 Apr 2025 10:41:54 +0100 Subject: [PATCH 07/21] Alternative with ProcessParameterMap --- src/asset.rs | 28 ++++++------ src/input/asset.rs | 15 ++---- src/input/process.rs | 22 +++++---- src/input/process/parameter.rs | 47 +++++++++++-------- src/output.rs | 13 +----- src/process.rs | 6 ++- src/simulation/optimisation.rs | 16 ++----- src/test_utils.rs | 64 ++++++++++++++++++++++++++ src/year.rs | 83 +--------------------------------- 9 files changed, 136 insertions(+), 158 deletions(-) create mode 100644 src/test_utils.rs diff --git a/src/asset.rs b/src/asset.rs index a239b9f80..3a34a56ee 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -56,7 +56,13 @@ impl Asset { /// The last year in which this asset should be decommissioned pub fn decommission_year(&self) -> u32 { - self.commission_year + self.process.parameter.get(self.commission_year).lifetime + self.commission_year + + self + .process + .parameter + .get(&self.commission_year) + .unwrap() + .lifetime } /// Get the activity limits for this asset in a particular time slice @@ -74,7 +80,8 @@ impl Asset { * self .process .parameter - .get(self.commission_year) + .get(&self.commission_year) + .unwrap() .capacity_to_activity } } @@ -189,10 +196,11 @@ impl AssetPool { mod tests { use super::*; use crate::commodity::{CommodityCostMap, CommodityType, DemandMap}; - use crate::process::{ActivityLimitsMap, FlowType, Process, ProcessFlow, ProcessParameter}; + use crate::process::{ + ActivityLimitsMap, FlowType, Process, ProcessFlow, ProcessParameter, ProcessParameterMap, + }; use crate::region::RegionSelection; use crate::time_slice::TimeSliceLevel; - use crate::year::AnnualField; use itertools::{assert_equal, Itertools}; use std::iter; @@ -202,14 +210,6 @@ mod tests { season: "winter".into(), time_of_day: "day".into(), }; - let process_param = ProcessParameter { - capital_cost: 5.0, - fixed_operating_cost: 2.0, - variable_operating_cost: 1.0, - lifetime: 5, - discount_rate: 0.9, - capacity_to_activity: 3.0, - }; let commodity = Rc::new(Commodity { id: "commodity1".into(), description: "Some description".into(), @@ -234,7 +234,7 @@ mod tests { years: 2010..=2020, activity_limits, flows: vec![flow.clone()], - parameter: AnnualField::Constant(process_param), + parameter: ProcessParameterMap::new(), regions: RegionSelection::All, }); let asset = Asset { @@ -264,7 +264,7 @@ mod tests { years: 2010..=2020, activity_limits: ActivityLimitsMap::new(), flows: vec![], - parameter: AnnualField::Constant(process_param), + parameter: ProcessParameterMap::new(), regions: RegionSelection::All, }); let future = [2020, 2010] diff --git a/src/input/asset.rs b/src/input/asset.rs index d5dc75935..631daee85 100644 --- a/src/input/asset.rs +++ b/src/input/asset.rs @@ -92,29 +92,20 @@ where #[cfg(test)] mod tests { use super::*; - use crate::process::{ActivityLimitsMap, Process, ProcessParameter}; + use crate::process::{ActivityLimitsMap, Process, ProcessParameterMap}; use crate::region::RegionSelection; - use crate::year::AnnualField; use itertools::assert_equal; use std::iter; #[test] fn test_read_assets_from_iter() { - let process_param = ProcessParameter { - capital_cost: 5.0, - fixed_operating_cost: 2.0, - variable_operating_cost: 1.0, - lifetime: 5, - discount_rate: 0.9, - capacity_to_activity: 1.0, - }; let process = Rc::new(Process { id: "process1".into(), description: "Description".into(), years: 2010..=2020, activity_limits: ActivityLimitsMap::new(), flows: vec![], - parameter: AnnualField::Constant(process_param.clone()), + parameter: ProcessParameterMap::new(), regions: RegionSelection::All, }); let processes = [(Rc::clone(&process.id), Rc::clone(&process))] @@ -190,7 +181,7 @@ mod tests { years: 2010..=2020, activity_limits: ActivityLimitsMap::new(), flows: vec![], - parameter: AnnualField::Constant(process_param), + parameter: ProcessParameterMap::new(), regions: RegionSelection::Some(["GBR".into()].into_iter().collect()), }); let asset_in = AssetRaw { diff --git a/src/input/process.rs b/src/input/process.rs index 4f574999f..18b32b596 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -1,10 +1,9 @@ //! Code for reading process-related information from CSV files. use super::*; use crate::commodity::{Commodity, CommodityMap, CommodityType}; -use crate::process::{ActivityLimitsMap, Process, ProcessFlow, ProcessMap, ProcessParameter}; +use crate::process::{ActivityLimitsMap, Process, ProcessFlow, ProcessMap, ProcessParameterMap}; use crate::region::RegionSelection; use crate::time_slice::TimeSliceInfo; -use crate::year::AnnualField; use anyhow::{bail, ensure, Context, Ok, Result}; use serde::Deserialize; use std::collections::{HashMap, HashSet}; @@ -68,7 +67,8 @@ pub fn read_processes( let mut availabilities = read_process_availabilities(model_dir, &process_ids, time_slice_info)?; let mut flows = read_process_flows(model_dir, &process_ids, commodities)?; - let mut parameters = read_process_parameters(model_dir, &process_ids)?; + let mut parameters = + read_process_parameters(model_dir, &process_ids, &processes, milestone_years)?; let mut regions = read_process_regions(model_dir, &process_ids, region_ids)?; // Validate commodities after the flows have been read @@ -90,7 +90,12 @@ pub fn read_processes( .copied() .filter(|year| year_range.contains(year)) .collect(); - parameter.check_reference(&reference_years)? + let parameter_years: HashSet = parameter.keys().copied().collect(); + ensure!( + parameter_years == reference_years, + "Error in parameter for process {}: years do not match the process years", + id + ); } // Add data to Process objects @@ -145,7 +150,7 @@ where years: start_year..=end_year, activity_limits: ActivityLimitsMap::new(), flows: Vec::new(), - parameter: AnnualField::Empty, + parameter: ProcessParameterMap::new(), regions: RegionSelection::default(), }; @@ -163,7 +168,7 @@ struct ValidationParams<'a> { region_ids: &'a HashSet>, milestone_years: &'a [u32], time_slice_info: &'a TimeSliceInfo, - parameters: &'a HashMap, AnnualField>, + parameters: &'a HashMap, ProcessParameterMap>, availabilities: &'a HashMap, ActivityLimitsMap>, } @@ -174,7 +179,7 @@ fn validate_commodities( region_ids: &HashSet>, milestone_years: &[u32], time_slice_info: &TimeSliceInfo, - parameters: &HashMap, AnnualField>, + parameters: &HashMap, ProcessParameterMap>, availabilities: &HashMap, ActivityLimitsMap>, ) -> anyhow::Result<()> { let params = ValidationParams { @@ -249,6 +254,7 @@ fn validate_svd_commodity( .parameters .get(&*flow.process_id) .unwrap() + .keys() .contains(&year) && params .availabilities @@ -291,7 +297,7 @@ mod tests { struct ProcessData { availabilities: HashMap, ActivityLimitsMap>, - parameters: HashMap, AnnualField>, + parameters: HashMap, ProcessParameterMap>, region_ids: HashSet>, } diff --git a/src/input/process/parameter.rs b/src/input/process/parameter.rs index af08762e8..d4b686d7e 100644 --- a/src/input/process/parameter.rs +++ b/src/input/process/parameter.rs @@ -1,8 +1,8 @@ //! Code for reading process parameters CSV file use super::super::*; use super::define_process_id_getter; -use crate::process::ProcessParameter; -use crate::year::{deserialize_year, AnnualField, Year}; +use crate::process::{Process, ProcessParameter, ProcessParameterMap}; +use crate::year::{deserialize_year, Year}; use ::log::warn; use anyhow::{ensure, Context, Result}; use serde::Deserialize; @@ -97,41 +97,52 @@ impl ProcessParameterRaw { pub fn read_process_parameters( model_dir: &Path, process_ids: &HashSet>, -) -> Result, AnnualField>> { + processes: &HashMap, Process>, + milestone_years: &[u32], +) -> Result, ProcessParameterMap>> { let file_path = model_dir.join(PROCESS_PARAMETERS_FILE_NAME); let iter = read_csv::(&file_path)?; - read_process_parameters_from_iter(iter, process_ids).with_context(|| input_err_msg(&file_path)) + read_process_parameters_from_iter(iter, process_ids, processes, milestone_years) + .with_context(|| input_err_msg(&file_path)) } fn read_process_parameters_from_iter( iter: I, process_ids: &HashSet>, -) -> Result, AnnualField>> + processes: &HashMap, Process>, + milestone_years: &[u32], +) -> Result, ProcessParameterMap>> where I: Iterator, { - let mut params: HashMap, AnnualField> = HashMap::new(); + let mut params: HashMap, ProcessParameterMap> = HashMap::new(); for param_raw in iter { let id = process_ids.get_id(¶m_raw.process_id)?; let year = param_raw.year.clone(); let param = param_raw.into_parameter()?; - // Create AnnualField - let annual_field = match year { - Year::All => AnnualField::Constant(param), + let entry = params.entry(id.clone()).or_default(); + let process = processes + .get(&id) + .ok_or_else(|| anyhow::anyhow!("Process {} not found", id))?; + let year_range = process.years.clone(); + + match year { Year::Single(year) => { - AnnualField::Variable([(year, param.clone())].into_iter().collect()) + entry.insert(year, param.clone()); } Year::Some(years) => { - AnnualField::Variable(years.iter().map(|y| (*y, param.clone())).collect()) + for year in years { + entry.insert(year, param.clone()); + } + } + Year::All => { + for year in milestone_years.iter() { + if year_range.contains(year) { + entry.insert(*year, param.clone()); + } + } } - }; - - // Insert into the map - if let Some(existing) = params.get_mut(&id) { - existing.merge(&annual_field)?; - } else { - params.insert(Rc::clone(&id), annual_field); } } Ok(params) diff --git a/src/output.rs b/src/output.rs index a0a4f51d4..e76b8fda0 100644 --- a/src/output.rs +++ b/src/output.rs @@ -176,10 +176,9 @@ impl DataWriter { #[cfg(test)] mod tests { use super::*; - use crate::process::{Process, ProcessParameter}; + use crate::process::{Process, ProcessParameterMap}; use crate::region::RegionSelection; use crate::time_slice::TimeSliceID; - use crate::year::AnnualField; use itertools::{assert_equal, Itertools}; use std::{collections::HashMap, iter}; use tempfile::tempdir; @@ -189,21 +188,13 @@ mod tests { let region_id = "GBR".into(); let agent_id = "agent1".into(); let commission_year = 2015; - let process_param = ProcessParameter { - capital_cost: 5.0, - fixed_operating_cost: 2.0, - variable_operating_cost: 1.0, - lifetime: 5, - discount_rate: 0.9, - capacity_to_activity: 3.0, - }; let process = Rc::new(Process { id: Rc::clone(&process_id), description: "Description".into(), years: 2010..=2020, activity_limits: HashMap::new(), flows: vec![], - parameter: AnnualField::Constant(process_param), + parameter: ProcessParameterMap::new(), regions: RegionSelection::All, }); diff --git a/src/process.rs b/src/process.rs index d1dae7442..c80f0b70c 100644 --- a/src/process.rs +++ b/src/process.rs @@ -3,7 +3,6 @@ use crate::commodity::Commodity; use crate::region::RegionSelection; use crate::time_slice::TimeSliceID; -use crate::year::AnnualField; use indexmap::IndexMap; use serde::Deserialize; use serde_string_enum::DeserializeLabeledStringEnum; @@ -28,7 +27,7 @@ pub struct Process { /// Commodity flows for this process pub flows: Vec, /// Additional parameters for this process - pub parameter: AnnualField, + pub parameter: ProcessParameterMap, /// The regions in which this process can operate pub regions: RegionSelection, } @@ -57,6 +56,9 @@ impl Process { /// availability. pub type ActivityLimitsMap = HashMap>; +/// A map of [`ProcessParameter`]s, keyed by year +pub type ProcessParameterMap = HashMap; + /// Represents a commodity flow for a given process #[derive(PartialEq, Debug, Deserialize, Clone)] pub struct ProcessFlow { diff --git a/src/simulation/optimisation.rs b/src/simulation/optimisation.rs index 47340f03f..6131652a9 100644 --- a/src/simulation/optimisation.rs +++ b/src/simulation/optimisation.rs @@ -253,7 +253,8 @@ fn calculate_cost_coefficient( coeff += asset .process .parameter - .get(asset.commission_year) + .get(&asset.commission_year) + .unwrap() .variable_operating_cost } @@ -283,10 +284,9 @@ fn calculate_cost_coefficient( mod tests { use super::*; use crate::commodity::{Commodity, CommodityCost, CommodityCostMap, CommodityType, DemandMap}; - use crate::process::{ActivityLimitsMap, FlowType, Process, ProcessParameter}; + use crate::process::{ActivityLimitsMap, FlowType, Process, ProcessParameterMap}; use crate::region::RegionSelection; use crate::time_slice::TimeSliceLevel; - use crate::year::AnnualField; use float_cmp::assert_approx_eq; use std::rc::Rc; @@ -295,14 +295,6 @@ mod tests { is_pac: bool, costs: CommodityCostMap, ) -> (Asset, ProcessFlow) { - let process_param = ProcessParameter { - capital_cost: 5.0, - fixed_operating_cost: 2.0, - variable_operating_cost: 1.0, - lifetime: 5, - discount_rate: 0.9, - capacity_to_activity: 1.0, - }; let commodity = Rc::new(Commodity { id: "commodity1".into(), description: "Some description".into(), @@ -325,7 +317,7 @@ mod tests { years: 2010..=2020, activity_limits: ActivityLimitsMap::new(), flows: vec![flow.clone()], - parameter: AnnualField::Constant(process_param), + parameter: ProcessParameterMap::new(), regions: RegionSelection::All, }); let asset = Asset::new( diff --git a/src/test_utils.rs b/src/test_utils.rs new file mode 100644 index 000000000..59fcb1351 --- /dev/null +++ b/src/test_utils.rs @@ -0,0 +1,64 @@ +use crate::agent::{Agent, AgentCommodity, AgentObjective, AgentSearchSpace, DecisionRule}; +use crate::commodity::Commodity; +use crate::process::{FlowType, Process, ProcessFlow, ProcessParameter}; +use crate::region::RegionSelection; +use std::collections::HashMap; +use std::ops::RangeInclusive; +use std::rc::Rc; + +impl Default for Agent { + fn default() -> Self { + Self { + id: Rc::from("agent1"), + description: "An agent".into(), + commodities: Vec::new(), + search_space: Vec::new(), + decision_rule: DecisionRule::Single, + capex_limit: None, + annual_cost_limit: None, + regions: RegionSelection::default(), + objectives: Vec::new(), + } + } +} + +impl Default for Commodity { + fn default() -> Self { + Self { + id: "commodity1".into(), + description: "A commodity".into(), + kind: crate::commodity::CommodityType::SupplyEqualsDemand, + time_slice_level: crate::time_slice::TimeSliceLevel::Annual, + costs: CommodityCostMap::new(), + demand: DemandMap::new(), + } + } +} + +impl Default for Process { + fn default() -> Self { + Self { + id: "process1".into(), + description: "Description".into(), + activity_limits: ActivityLimitsMap::new(), + flows: vec![], + parameter: ProcessParameter::default(), + regions: RegionSelection::default(), + } + } +} + +impl Default for ProcessParameter { + fn default() -> Self { + Self { + process_id: "process1".into(), + years: 2010..=2020, + capital_cost: 5.0, + fixed_operating_cost: 2.0, + variable_operating_cost: 1.0, + lifetime: 5, + discount_rate: 0.9, + capacity_to_activity: 1.0, + } + } +} diff --git a/src/year.rs b/src/year.rs index d1c640023..4d0a17b91 100644 --- a/src/year.rs +++ b/src/year.rs @@ -1,87 +1,8 @@ #![allow(missing_docs)] -use anyhow::{bail, Result}; +use anyhow::Result; use serde::de::Deserializer; use serde::Deserialize; -use std::collections::{HashMap, HashSet}; - -#[derive(Debug, Clone, PartialEq, Deserialize)] -pub enum AnnualField { - Empty, - Constant(T), - Variable(HashMap), -} - -impl AnnualField { - pub fn get(&self, year: u32) -> &T { - match self { - AnnualField::Empty => panic!("AnnualField is empty."), - AnnualField::Constant(value) => value, - AnnualField::Variable(values) => values.get(&year).unwrap(), - } - } - - pub fn insert(&mut self, year: u32, value: T) -> Result<()> { - match self { - AnnualField::Constant(_) => { - bail!("Cannot insert into a constant field."); - } - AnnualField::Variable(values) => { - if values.contains_key(&year) { - bail!("Year {} already exists in variable field.", year); - } - values.insert(year, value); - Ok(()) - } - AnnualField::Empty => { - *self = AnnualField::Variable(HashMap::new()); - self.insert(year, value) - } - } - } - - pub fn merge(&mut self, other: &Self) -> Result<()> { - match (self, other) { - (AnnualField::Variable(values), AnnualField::Variable(other_values)) => { - for (year, value) in other_values.iter() { - if values.contains_key(year) { - bail!("Year {} already exists in variable field.", year); - } - values.insert(*year, value.clone()); - } - Ok(()) - } - (AnnualField::Constant(_), AnnualField::Constant(_)) => { - bail!("Cannot merge two constant fields.") - } - _ => bail!("Cannot merge constant and variable fields."), - } - } - - pub fn contains(&self, year: &u32) -> bool { - match self { - AnnualField::Empty => false, - AnnualField::Constant(_) => true, - AnnualField::Variable(values) => values.contains_key(year), - } - } - - pub fn check_reference(&self, reference_years: &HashSet) -> Result<()> { - match self { - AnnualField::Empty => { - bail!("AnnualField is empty. Cannot check reference years."); - } - AnnualField::Constant(_) => Ok(()), - AnnualField::Variable(_) => { - for year in reference_years.iter() { - if !self.contains(year) { - bail!("Missing data for year {}.", year); - } - } - Ok(()) - } - } - } -} +use std::collections::HashSet; #[derive(PartialEq, Debug, Clone)] pub enum Year { From f992b15ec584fe7ede7a6aeae895d41b1deeeb79 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 10 Apr 2025 10:46:09 +0100 Subject: [PATCH 08/21] Delete file accidentally included --- src/test_utils.rs | 64 ----------------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 src/test_utils.rs diff --git a/src/test_utils.rs b/src/test_utils.rs deleted file mode 100644 index 59fcb1351..000000000 --- a/src/test_utils.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::agent::{Agent, AgentCommodity, AgentObjective, AgentSearchSpace, DecisionRule}; -use crate::commodity::Commodity; -use crate::process::{FlowType, Process, ProcessFlow, ProcessParameter}; -use crate::region::RegionSelection; -use std::collections::HashMap; -use std::ops::RangeInclusive; -use std::rc::Rc; - -impl Default for Agent { - fn default() -> Self { - Self { - id: Rc::from("agent1"), - description: "An agent".into(), - commodities: Vec::new(), - search_space: Vec::new(), - decision_rule: DecisionRule::Single, - capex_limit: None, - annual_cost_limit: None, - regions: RegionSelection::default(), - objectives: Vec::new(), - } - } -} - -impl Default for Commodity { - fn default() -> Self { - Self { - id: "commodity1".into(), - description: "A commodity".into(), - kind: crate::commodity::CommodityType::SupplyEqualsDemand, - time_slice_level: crate::time_slice::TimeSliceLevel::Annual, - costs: CommodityCostMap::new(), - demand: DemandMap::new(), - } - } -} - -impl Default for Process { - fn default() -> Self { - Self { - id: "process1".into(), - description: "Description".into(), - activity_limits: ActivityLimitsMap::new(), - flows: vec![], - parameter: ProcessParameter::default(), - regions: RegionSelection::default(), - } - } -} - -impl Default for ProcessParameter { - fn default() -> Self { - Self { - process_id: "process1".into(), - years: 2010..=2020, - capital_cost: 5.0, - fixed_operating_cost: 2.0, - variable_operating_cost: 1.0, - lifetime: 5, - discount_rate: 0.9, - capacity_to_activity: 1.0, - } - } -} From a7b3634e1c6954f9fa2ef7520dd088d5361aa295 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 11 Apr 2025 14:58:51 +0100 Subject: [PATCH 09/21] Remove leftover AnnualField references --- src/input/process.rs | 6 +++--- src/input/process/parameter.rs | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/input/process.rs b/src/input/process.rs index 18b32b596..b875a84e5 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -288,7 +288,7 @@ fn validate_svd_commodity( #[cfg(test)] mod tests { use crate::commodity::{CommodityCostMap, DemandMap}; - use crate::process::FlowType; + use crate::process::{FlowType, ProcessParameter}; use crate::time_slice::TimeSliceID; use crate::time_slice::TimeSliceLevel; use std::iter; @@ -321,14 +321,14 @@ mod tests { let parameters = ["process1", "process2"] .into_iter() .map(|id| { - let parameter = AnnualField::Constant(ProcessParameter { + let parameter = ProcessParameter { capital_cost: 0.0, fixed_operating_cost: 0.0, variable_operating_cost: 0.0, lifetime: 1, discount_rate: 1.0, capacity_to_activity: 0.0, - }); + }; (id.into(), parameter) }) .collect(); diff --git a/src/input/process/parameter.rs b/src/input/process/parameter.rs index d4b686d7e..c565b4e7d 100644 --- a/src/input/process/parameter.rs +++ b/src/input/process/parameter.rs @@ -249,25 +249,25 @@ mod tests { let expected: HashMap, _> = [ ( "A".into(), - AnnualField::Constant(ProcessParameter { + ProcessParameter { capital_cost: 1.0, fixed_operating_cost: 1.0, variable_operating_cost: 1.0, lifetime: 10, discount_rate: 1.0, capacity_to_activity: 1.0, - }), + }, ), ( "B".into(), - AnnualField::Constant(ProcessParameter { + ProcessParameter { capital_cost: 1.0, fixed_operating_cost: 1.0, variable_operating_cost: 1.0, lifetime: 10, discount_rate: 1.0, capacity_to_activity: 1.0, - }), + }, ), ] .into_iter() @@ -314,6 +314,6 @@ mod tests { }, ]; - assert!(read_process_parameters_from_iter(params_raw.into_iter(), &process_ids,).is_err()); + assert!(read_process_parameters_from_iter(params_raw.into_iter(), &process_ids).is_err()); } } From 3e7dc203d890accecf45aea98f7dfe14c807cb39 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 11 Apr 2025 15:08:48 +0100 Subject: [PATCH 10/21] Remove unused import --- src/asset.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index 3a34a56ee..b41834bdb 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -196,9 +196,7 @@ impl AssetPool { mod tests { use super::*; use crate::commodity::{CommodityCostMap, CommodityType, DemandMap}; - use crate::process::{ - ActivityLimitsMap, FlowType, Process, ProcessFlow, ProcessParameter, ProcessParameterMap, - }; + use crate::process::{ActivityLimitsMap, FlowType, Process, ProcessFlow, ProcessParameterMap}; use crate::region::RegionSelection; use crate::time_slice::TimeSliceLevel; use itertools::{assert_equal, Itertools}; @@ -250,14 +248,6 @@ mod tests { } fn create_asset_pool() -> AssetPool { - let process_param = ProcessParameter { - capital_cost: 5.0, - fixed_operating_cost: 2.0, - variable_operating_cost: 1.0, - lifetime: 5, - discount_rate: 0.9, - capacity_to_activity: 1.0, - }; let process = Rc::new(Process { id: "process1".into(), description: "Description".into(), From f889df9fa62249a22c76b1524177cf3a7d0652b0 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 14 Apr 2025 10:07:51 +0100 Subject: [PATCH 11/21] Move validation check --- src/input/process.rs | 16 ---------------- src/input/process/parameter.rs | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/input/process.rs b/src/input/process.rs index b875a84e5..751184e0c 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -82,22 +82,6 @@ pub fn read_processes( &availabilities, )?; - // Check parameters cover all years of the process - for (id, parameter) in parameters.iter() { - let year_range = processes.get(id).unwrap().years.clone(); - let reference_years: HashSet = milestone_years - .iter() - .copied() - .filter(|year| year_range.contains(year)) - .collect(); - let parameter_years: HashSet = parameter.keys().copied().collect(); - ensure!( - parameter_years == reference_years, - "Error in parameter for process {}: years do not match the process years", - id - ); - } - // Add data to Process objects for (id, process) in processes.iter_mut() { process.activity_limits = availabilities.remove(id).unwrap(); diff --git a/src/input/process/parameter.rs b/src/input/process/parameter.rs index c565b4e7d..f2b873ef5 100644 --- a/src/input/process/parameter.rs +++ b/src/input/process/parameter.rs @@ -145,6 +145,23 @@ where } } } + + // Check parameters cover all years of the process + for (id, parameter) in params.iter() { + let year_range = processes.get(id).unwrap().years.clone(); + let reference_years: HashSet = milestone_years + .iter() + .copied() + .filter(|year| year_range.contains(year)) + .collect(); + let parameter_years: HashSet = parameter.keys().copied().collect(); + ensure!( + parameter_years == reference_years, + "Error in parameter for process {}: years do not match the process years", + id + ); + } + Ok(params) } From 2ee72485dd2fe1127020f7e304d6b7d03711ee73 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 14 Apr 2025 10:27:25 +0100 Subject: [PATCH 12/21] Rename Year -> YearSelection --- src/input/process/parameter.rs | 22 +++++++++++----------- src/year.rs | 10 +++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/input/process/parameter.rs b/src/input/process/parameter.rs index f2b873ef5..f3600b50d 100644 --- a/src/input/process/parameter.rs +++ b/src/input/process/parameter.rs @@ -2,7 +2,7 @@ use super::super::*; use super::define_process_id_getter; use crate::process::{Process, ProcessParameter, ProcessParameterMap}; -use crate::year::{deserialize_year, Year}; +use crate::year::{deserialize_year, YearSelection}; use ::log::warn; use anyhow::{ensure, Context, Result}; use serde::Deserialize; @@ -22,7 +22,7 @@ struct ProcessParameterRaw { discount_rate: Option, capacity_to_activity: Option, #[serde(deserialize_with = "deserialize_year")] - year: Year, + year: YearSelection, } define_process_id_getter! {ProcessParameterRaw} @@ -128,15 +128,15 @@ where let year_range = process.years.clone(); match year { - Year::Single(year) => { + YearSelection::Single(year) => { entry.insert(year, param.clone()); } - Year::Some(years) => { + YearSelection::Some(years) => { for year in years { entry.insert(year, param.clone()); } } - Year::All => { + YearSelection::All => { for year in milestone_years.iter() { if year_range.contains(year) { entry.insert(*year, param.clone()); @@ -182,7 +182,7 @@ mod tests { lifetime, discount_rate, capacity_to_activity, - year: Year::All, + year: YearSelection::All, } } @@ -249,7 +249,7 @@ mod tests { lifetime: 10, discount_rate: Some(1.0), capacity_to_activity: Some(1.0), - year: Year::All, + year: YearSelection::All, }, ProcessParameterRaw { process_id: "B".into(), @@ -259,7 +259,7 @@ mod tests { lifetime: 10, discount_rate: Some(1.0), capacity_to_activity: Some(1.0), - year: Year::All, + year: YearSelection::All, }, ]; @@ -307,7 +307,7 @@ mod tests { lifetime: 10, discount_rate: Some(1.0), capacity_to_activity: Some(1.0), - year: Year::All, + year: YearSelection::All, }, ProcessParameterRaw { process_id: "B".into(), @@ -317,7 +317,7 @@ mod tests { lifetime: 10, discount_rate: Some(1.0), capacity_to_activity: Some(1.0), - year: Year::All, + year: YearSelection::All, }, ProcessParameterRaw { process_id: "A".into(), @@ -327,7 +327,7 @@ mod tests { lifetime: 10, discount_rate: Some(1.0), capacity_to_activity: Some(1.0), - year: Year::All, + year: YearSelection::All, }, ]; diff --git a/src/year.rs b/src/year.rs index 4d0a17b91..311070451 100644 --- a/src/year.rs +++ b/src/year.rs @@ -5,29 +5,29 @@ use serde::Deserialize; use std::collections::HashSet; #[derive(PartialEq, Debug, Clone)] -pub enum Year { +pub enum YearSelection { All, Single(u32), Some(HashSet), } -pub fn deserialize_year<'de, D>(deserialiser: D) -> Result +pub fn deserialize_year<'de, D>(deserialiser: D) -> Result where D: Deserializer<'de>, { let value = String::deserialize(deserialiser)?; if value == "all" { // "all" years specified - Ok(Year::All) + Ok(YearSelection::All) } else if let Ok(n) = value.parse::() { // Single year specified - Ok(Year::Single(n)) + Ok(YearSelection::Single(n)) } else { // Semicolon-separated list of years let years: Result, _> = value.split(';').map(|s| s.trim().parse::()).collect(); match years { - Ok(years_set) if !years_set.is_empty() => Ok(Year::Some(years_set)), + Ok(years_set) if !years_set.is_empty() => Ok(YearSelection::Some(years_set)), _ => Err(serde::de::Error::custom("Invalid year format")), } } From be12c26594c932f3892451cae91043a37b39f3fb Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 16 Apr 2025 15:04:46 +0100 Subject: [PATCH 13/21] Delete some tests, fix another --- src/input/process.rs | 4 +- src/input/process/parameter.rs | 98 ---------------------------------- 2 files changed, 3 insertions(+), 99 deletions(-) diff --git a/src/input/process.rs b/src/input/process.rs index 290b037e8..637f6eb14 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -298,6 +298,7 @@ mod tests { let parameters = ["process1", "process2"] .into_iter() .map(|id| { + let mut parameter_map: ProcessParameterMap = HashMap::new(); let parameter = ProcessParameter { capital_cost: 0.0, fixed_operating_cost: 0.0, @@ -306,7 +307,8 @@ mod tests { discount_rate: 1.0, capacity_to_activity: 0.0, }; - (id.into(), parameter) + parameter_map.insert(2020, parameter); + (id.into(), parameter_map) }) .collect(); diff --git a/src/input/process/parameter.rs b/src/input/process/parameter.rs index b96e7f0eb..e82525cb6 100644 --- a/src/input/process/parameter.rs +++ b/src/input/process/parameter.rs @@ -233,102 +233,4 @@ mod tests { .validate() .is_err()); } - - #[test] - fn test_read_process_parameters_from_iter_good() { - let process_ids = ["A".into(), "B".into()].into_iter().collect(); - - let params_raw = [ - ProcessParameterRaw { - process_id: "A".into(), - capital_cost: 1.0, - fixed_operating_cost: 1.0, - variable_operating_cost: 1.0, - lifetime: 10, - discount_rate: Some(1.0), - capacity_to_activity: Some(1.0), - year: YearSelection::All, - }, - ProcessParameterRaw { - process_id: "B".into(), - capital_cost: 1.0, - fixed_operating_cost: 1.0, - variable_operating_cost: 1.0, - lifetime: 10, - discount_rate: Some(1.0), - capacity_to_activity: Some(1.0), - year: YearSelection::All, - }, - ]; - - let expected: HashMap = [ - ( - "A".into(), - ProcessParameter { - capital_cost: 1.0, - fixed_operating_cost: 1.0, - variable_operating_cost: 1.0, - lifetime: 10, - discount_rate: 1.0, - capacity_to_activity: 1.0, - }, - ), - ( - "B".into(), - ProcessParameter { - capital_cost: 1.0, - fixed_operating_cost: 1.0, - variable_operating_cost: 1.0, - lifetime: 10, - discount_rate: 1.0, - capacity_to_activity: 1.0, - }, - ), - ] - .into_iter() - .collect(); - let actual = - read_process_parameters_from_iter(params_raw.into_iter(), &process_ids).unwrap(); - assert_eq!(expected, actual); - } - - #[test] - fn test_read_process_parameters_from_iter_bad_multiple_params() { - let process_ids = ["A".into(), "B".into()].into_iter().collect(); - - let params_raw = [ - ProcessParameterRaw { - process_id: "A".into(), - capital_cost: 1.0, - fixed_operating_cost: 1.0, - variable_operating_cost: 1.0, - lifetime: 10, - discount_rate: Some(1.0), - capacity_to_activity: Some(1.0), - year: YearSelection::All, - }, - ProcessParameterRaw { - process_id: "B".into(), - capital_cost: 1.0, - fixed_operating_cost: 1.0, - variable_operating_cost: 1.0, - lifetime: 10, - discount_rate: Some(1.0), - capacity_to_activity: Some(1.0), - year: YearSelection::All, - }, - ProcessParameterRaw { - process_id: "A".into(), - capital_cost: 1.0, - fixed_operating_cost: 1.0, - variable_operating_cost: 1.0, - lifetime: 10, - discount_rate: Some(1.0), - capacity_to_activity: Some(1.0), - year: YearSelection::All, - }, - ]; - - assert!(read_process_parameters_from_iter(params_raw.into_iter(), &process_ids).is_err()); - } } From efee072595fc5a3739220dfff6c6d4f47f202225 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 23 Apr 2025 14:46:35 +0100 Subject: [PATCH 14/21] Continue merge --- src/asset.rs | 2 +- src/input/asset.rs | 2 +- src/simulation/optimisation.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index 2e75d91e7..63f161d0b 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -200,7 +200,7 @@ impl AssetPool { mod tests { use super::*; use crate::commodity::{CommodityCostMap, CommodityType, DemandMap}; - use crate::process::{EnergyLimitsMap, FlowType, Process, ProcessFlow, ProcessParameter}; + use crate::process::{EnergyLimitsMap, FlowType, Process, ProcessFlow, ProcessParameterMap}; use crate::region::RegionSelection; use crate::time_slice::TimeSliceLevel; use itertools::{assert_equal, Itertools}; diff --git a/src/input/asset.rs b/src/input/asset.rs index 818be340b..a1be51e6d 100644 --- a/src/input/asset.rs +++ b/src/input/asset.rs @@ -95,7 +95,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::process::{EnergyLimitsMap, Process, ProcessParameter}; + use crate::process::{EnergyLimitsMap, Process, ProcessParameterMap}; use crate::region::RegionSelection; use itertools::assert_equal; use std::iter; diff --git a/src/simulation/optimisation.rs b/src/simulation/optimisation.rs index e14952fc5..f0a4141d4 100644 --- a/src/simulation/optimisation.rs +++ b/src/simulation/optimisation.rs @@ -267,7 +267,7 @@ fn calculate_cost_coefficient( mod tests { use super::*; use crate::commodity::{Commodity, CommodityCost, CommodityCostMap, CommodityType, DemandMap}; - use crate::process::{EnergyLimitsMap, FlowType, Process, ProcessParameter}; + use crate::process::{EnergyLimitsMap, FlowType, Process, ProcessParameterMap}; use crate::region::RegionSelection; use crate::time_slice::TimeSliceLevel; use float_cmp::assert_approx_eq; From 158492fdb671e0d45085277d4e7df4ee54db93fd Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 23 Apr 2025 15:31:32 +0100 Subject: [PATCH 15/21] Fix tests --- src/asset.rs | 35 +++++++++++++++++++++++++++++++--- src/input/process.rs | 4 +++- src/simulation/optimisation.rs | 17 +++++++++++++++-- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index 63f161d0b..a2ecf6a88 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -200,11 +200,14 @@ impl AssetPool { mod tests { use super::*; use crate::commodity::{CommodityCostMap, CommodityType, DemandMap}; - use crate::process::{EnergyLimitsMap, FlowType, Process, ProcessFlow, ProcessParameterMap}; + use crate::process::{ + EnergyLimitsMap, FlowType, Process, ProcessFlow, ProcessParameter, ProcessParameterMap, + }; use crate::region::RegionSelection; use crate::time_slice::TimeSliceLevel; use itertools::{assert_equal, Itertools}; use std::iter; + use std::ops::RangeInclusive; #[test] fn test_asset_get_energy_limits() { @@ -212,6 +215,19 @@ mod tests { season: "winter".into(), time_of_day: "day".into(), }; + let process_param = ProcessParameter { + capital_cost: 5.0, + fixed_operating_cost: 2.0, + variable_operating_cost: 1.0, + lifetime: 5, + discount_rate: 0.9, + capacity_to_activity: 3.0, + }; + let years = RangeInclusive::new(2010, 2020).collect_vec(); + let process_parameter_map: ProcessParameterMap = years + .iter() + .map(|&year| (year, process_param.clone())) + .collect(); let commodity = Rc::new(Commodity { id: "commodity1".into(), description: "Some description".into(), @@ -236,7 +252,7 @@ mod tests { years: 2010..=2020, energy_limits, flows: vec![flow.clone()], - parameter: ProcessParameterMap::new(), + parameter: process_parameter_map, regions: RegionSelection::All, }); let asset = Asset { @@ -252,13 +268,26 @@ mod tests { } fn create_asset_pool() -> AssetPool { + let process_param = ProcessParameter { + capital_cost: 5.0, + fixed_operating_cost: 2.0, + variable_operating_cost: 1.0, + lifetime: 5, + discount_rate: 0.9, + capacity_to_activity: 1.0, + }; + let years = RangeInclusive::new(2010, 2020).collect_vec(); + let process_parameter_map: ProcessParameterMap = years + .iter() + .map(|&year| (year, process_param.clone())) + .collect(); let process = Rc::new(Process { id: "process1".into(), description: "Description".into(), years: 2010..=2020, energy_limits: EnergyLimitsMap::new(), flows: vec![], - parameter: ProcessParameterMap::new(), + parameter: process_parameter_map, regions: RegionSelection::All, }); let future = [2020, 2010] diff --git a/src/input/process.rs b/src/input/process.rs index 8906a0690..d259d0963 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -310,7 +310,9 @@ mod tests { discount_rate: 1.0, capacity_to_activity: 0.0, }; - parameter_map.insert(2020, parameter); + for year in [2010, 2020] { + parameter_map.insert(year, parameter.clone()); + } (id.into(), parameter_map) }) .collect(); diff --git a/src/simulation/optimisation.rs b/src/simulation/optimisation.rs index f0a4141d4..634d7fd39 100644 --- a/src/simulation/optimisation.rs +++ b/src/simulation/optimisation.rs @@ -267,7 +267,9 @@ fn calculate_cost_coefficient( mod tests { use super::*; use crate::commodity::{Commodity, CommodityCost, CommodityCostMap, CommodityType, DemandMap}; - use crate::process::{EnergyLimitsMap, FlowType, Process, ProcessParameterMap}; + use crate::process::{ + EnergyLimitsMap, FlowType, Process, ProcessParameter, ProcessParameterMap, + }; use crate::region::RegionSelection; use crate::time_slice::TimeSliceLevel; use float_cmp::assert_approx_eq; @@ -278,6 +280,17 @@ mod tests { is_pac: bool, costs: CommodityCostMap, ) -> (Asset, ProcessFlow) { + let process_param = ProcessParameter { + capital_cost: 5.0, + fixed_operating_cost: 2.0, + variable_operating_cost: 1.0, + lifetime: 5, + discount_rate: 0.9, + capacity_to_activity: 1.0, + }; + let mut process_parameter_map = ProcessParameterMap::new(); + process_parameter_map.insert(2010, process_param.clone()); + process_parameter_map.insert(2020, process_param.clone()); let commodity = Rc::new(Commodity { id: "commodity1".into(), description: "Some description".into(), @@ -300,7 +313,7 @@ mod tests { years: 2010..=2020, energy_limits: EnergyLimitsMap::new(), flows: vec![flow.clone()], - parameter: ProcessParameterMap::new(), + parameter: process_parameter_map, regions: RegionSelection::All, }); let asset = Asset::new( From aa7e5f120efc32eaf763a41e60e7f81ef7e4e8eb Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 23 Apr 2025 16:24:20 +0100 Subject: [PATCH 16/21] Prevent duplicate insertion --- src/input/process/parameter.rs | 9 +++++---- src/lib.rs | 1 + src/utils.rs | 22 ++++++++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 src/utils.rs diff --git a/src/input/process/parameter.rs b/src/input/process/parameter.rs index e82525cb6..3f309353f 100644 --- a/src/input/process/parameter.rs +++ b/src/input/process/parameter.rs @@ -2,6 +2,7 @@ use super::super::*; use crate::id::IDCollection; use crate::process::{Process, ProcessID, ProcessParameter, ProcessParameterMap}; +use crate::utils::try_insert; use crate::year::{deserialize_year, YearSelection}; use ::log::warn; use anyhow::{ensure, Context, Result}; @@ -127,17 +128,17 @@ where match year { YearSelection::Single(year) => { - entry.insert(year, param.clone()); + try_insert(entry, year, param.clone())?; } YearSelection::Some(years) => { for year in years { - entry.insert(year, param.clone()); + try_insert(entry, year, param.clone())?; } } YearSelection::All => { for year in milestone_years.iter() { if year_range.contains(year) { - entry.insert(*year, param.clone()); + try_insert(entry, *year, param.clone())?; } } } @@ -155,7 +156,7 @@ where let parameter_years: HashSet = parameter.keys().copied().collect(); ensure!( parameter_years == reference_years, - "Error in parameter for process {}: years do not match the process years", + "Error in parameters for process {}: years do not match the process years", id ); } diff --git a/src/lib.rs b/src/lib.rs index 76a9757e1..d38c55f1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,4 +14,5 @@ pub mod region; pub mod settings; pub mod simulation; pub mod time_slice; +pub mod utils; pub mod year; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 000000000..62ccb7bdf --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,22 @@ +//! Utility functions. +use anyhow::{anyhow, Result}; +use std::collections::HashMap; +use std::hash::Hash; + +/// Inserts a key-value pair into a HashMap if the key does not already exist. +/// +/// If the key already exists, it returns an error with a message indicating the key's existence. +pub fn try_insert(map: &mut HashMap, key: K, value: V) -> Result<()> +where + K: Eq + Hash + std::fmt::Display, +{ + match map.entry(key) { + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(value); + Ok(()) + } + std::collections::hash_map::Entry::Occupied(entry) => { + Err(anyhow!("Key {} already exists in the map", entry.key())) + } + } +} From 928b8db5170a6f6041361b5c61e3ff88e16c42fe Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 23 Apr 2025 16:38:10 +0100 Subject: [PATCH 17/21] Finishing touches --- src/input/process/parameter.rs | 3 --- src/utils.rs | 7 +++---- src/year.rs | 12 +++++++----- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/input/process/parameter.rs b/src/input/process/parameter.rs index 3f309353f..dfde564b3 100644 --- a/src/input/process/parameter.rs +++ b/src/input/process/parameter.rs @@ -127,9 +127,6 @@ where let year_range = process.years.clone(); match year { - YearSelection::Single(year) => { - try_insert(entry, year, param.clone())?; - } YearSelection::Some(years) => { for year in years { try_insert(entry, year, param.clone())?; diff --git a/src/utils.rs b/src/utils.rs index 62ccb7bdf..cbfd97165 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,6 @@ //! Utility functions. use anyhow::{anyhow, Result}; +use std::collections::hash_map::Entry::{Occupied, Vacant}; use std::collections::HashMap; use std::hash::Hash; @@ -11,12 +12,10 @@ where K: Eq + Hash + std::fmt::Display, { match map.entry(key) { - std::collections::hash_map::Entry::Vacant(entry) => { + Vacant(entry) => { entry.insert(value); Ok(()) } - std::collections::hash_map::Entry::Occupied(entry) => { - Err(anyhow!("Key {} already exists in the map", entry.key())) - } + Occupied(entry) => Err(anyhow!("Key {} already exists in the map", entry.key())), } } diff --git a/src/year.rs b/src/year.rs index 311070451..111f63bdc 100644 --- a/src/year.rs +++ b/src/year.rs @@ -1,16 +1,21 @@ -#![allow(missing_docs)] +//! Code for working with years. use anyhow::Result; use serde::de::Deserializer; use serde::Deserialize; use std::collections::HashSet; +/// Represents a set of years. #[derive(PartialEq, Debug, Clone)] pub enum YearSelection { + /// Covers all years. It's up to the user to interpret this (e.g. could be all milestone years, + /// or all active years for a process etc.) All, - Single(u32), + /// Covers some years. Some(HashSet), } +/// Deserialises a year selection from a string. The string can be either "all", a single year, or a +/// semicolon-separated list of years (e.g. "2020;2021;2022" or "2020; 2021; 2022"). pub fn deserialize_year<'de, D>(deserialiser: D) -> Result where D: Deserializer<'de>, @@ -19,9 +24,6 @@ where if value == "all" { // "all" years specified Ok(YearSelection::All) - } else if let Ok(n) = value.parse::() { - // Single year specified - Ok(YearSelection::Single(n)) } else { // Semicolon-separated list of years let years: Result, _> = From 1ac522fe1e75efec7226a24a9f04a8416cf53ca3 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 23 Apr 2025 17:22:13 +0100 Subject: [PATCH 18/21] Case insensitive "all" --- src/year.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/year.rs b/src/year.rs index 111f63bdc..28e4a2d42 100644 --- a/src/year.rs +++ b/src/year.rs @@ -21,7 +21,7 @@ where D: Deserializer<'de>, { let value = String::deserialize(deserialiser)?; - if value == "all" { + if value.trim().eq_ignore_ascii_case("all") { // "all" years specified Ok(YearSelection::All) } else { @@ -30,7 +30,10 @@ where value.split(';').map(|s| s.trim().parse::()).collect(); match years { Ok(years_set) if !years_set.is_empty() => Ok(YearSelection::Some(years_set)), - _ => Err(serde::de::Error::custom("Invalid year format")), + _ => Err(serde::de::Error::custom(format!( + "Invalid year format: {}", + value + ))), } } } From d61d7e1242473d38491846d651b8df4a766f4045 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 28 Apr 2025 10:55:37 +0100 Subject: [PATCH 19/21] Apply suggestions from code review Co-authored-by: alexdewar --- examples/simple/process_parameters.csv | 14 +++++++------- src/asset.rs | 8 ++++---- src/input/asset.rs | 4 ++-- src/input/process.rs | 24 ++++++++++++++++-------- src/input/process/parameter.rs | 6 +++--- src/output.rs | 2 +- src/process.rs | 2 +- src/simulation/optimisation.rs | 4 ++-- src/utils.rs | 16 +++++++--------- 9 files changed, 43 insertions(+), 37 deletions(-) diff --git a/examples/simple/process_parameters.csv b/examples/simple/process_parameters.csv index 283a0913b..b9cb5d37a 100644 --- a/examples/simple/process_parameters.csv +++ b/examples/simple/process_parameters.csv @@ -1,7 +1,7 @@ -process_id,capital_cost,fixed_operating_cost,variable_operating_cost,lifetime,discount_rate,capacity_to_activity,year -GASDRV,10.0,0.3,2.0,25,0.1,1.0,all -GASPRC,7.0,0.21,0.5,25,0.1,1.0,all -WNDFRM,1000.0,30.0,0.4,25,0.1,31.54,all -GASCGT,700.0,21.0,0.55,30,0.1,31.54,all -RGASBR,55.56,1.6668,0.16,15,0.1,1.0,all -RELCHP,138.9,4.167,0.17,15,0.1,1.0,all +process_id,year,capital_cost,fixed_operating_cost,variable_operating_cost,lifetime,discount_rate,capacity_to_activity +GASDRV,all,10.0,0.3,2.0,25,0.1,1.0 +GASPRC,all,7.0,0.21,0.5,25,0.1,1.0 +WNDFRM,all,1000.0,30.0,0.4,25,0.1,31.54 +GASCGT,all,700.0,21.0,0.55,30,0.1,31.54 +RGASBR,all,55.56,1.6668,0.16,15,0.1,1.0 +RELCHP,all,138.9,4.167,0.17,15,0.1,1.0 diff --git a/src/asset.rs b/src/asset.rs index a2ecf6a88..a4c5d8c1e 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -61,7 +61,7 @@ impl Asset { self.commission_year + self .process - .parameter + .parameters .get(&self.commission_year) .unwrap() .lifetime @@ -83,7 +83,7 @@ impl Asset { self.capacity * self .process - .parameter + .parameters .get(&self.commission_year) .unwrap() .capacity_to_activity @@ -252,7 +252,7 @@ mod tests { years: 2010..=2020, energy_limits, flows: vec![flow.clone()], - parameter: process_parameter_map, + parameters: process_parameter_map, regions: RegionSelection::All, }); let asset = Asset { @@ -287,7 +287,7 @@ mod tests { years: 2010..=2020, energy_limits: EnergyLimitsMap::new(), flows: vec![], - parameter: process_parameter_map, + parameters: process_parameter_map, regions: RegionSelection::All, }); let future = [2020, 2010] diff --git a/src/input/asset.rs b/src/input/asset.rs index a1be51e6d..a99772a5b 100644 --- a/src/input/asset.rs +++ b/src/input/asset.rs @@ -108,7 +108,7 @@ mod tests { years: 2010..=2020, energy_limits: EnergyLimitsMap::new(), flows: vec![], - parameter: ProcessParameterMap::new(), + parameters: ProcessParameterMap::new(), regions: RegionSelection::All, }); let processes = [(process.id.clone(), Rc::clone(&process))] @@ -184,7 +184,7 @@ mod tests { years: 2010..=2020, energy_limits: EnergyLimitsMap::new(), flows: vec![], - parameter: ProcessParameterMap::new(), + parameters: ProcessParameterMap::new(), regions: RegionSelection::Some(["GBR".into()].into_iter().collect()), }); let asset_in = AssetRaw { diff --git a/src/input/process.rs b/src/input/process.rs index d259d0963..ede487686 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -58,7 +58,7 @@ pub fn read_processes( let mut processes = read_processes_file(model_dir, &year_range)?; let process_ids = processes.keys().cloned().collect(); - let mut availabilities = read_process_availabilities(model_dir, &process_ids, time_slice_info)?; + let mut energy_limits = read_process_availabilities(model_dir, &process_ids, time_slice_info)?; let mut flows = read_process_flows(model_dir, &process_ids, commodities)?; let mut parameters = read_process_parameters(model_dir, &process_ids, &processes, milestone_years)?; @@ -72,21 +72,29 @@ pub fn read_processes( milestone_years, time_slice_info, ¶meters, - &availabilities, + &energy_limits, )?; // Add data to Process objects for (id, process) in processes.iter_mut() { - process.energy_limits = availabilities.remove(id).unwrap(); - process.flows = flows.remove(id).unwrap(); - process.parameter = parameters.remove(id).unwrap(); - process.regions = regions.remove(id).unwrap(); + process.energy_limits = energy_limits + .remove(id) + .with_context(|| format!("Missing availabilities for process {id}"))?; + process.flows = flows + .remove(id) + .with_context(|| format!("Missing flows for process {id}"))?; + process.parameters = parameters + .remove(id) + .with_context(|| format!("Missing parameters for process {id}"))?; + process.regions = regions + .remove(id) + .with_context(|| format!("Missing regions for process {id}"))?; } // Create ProcessMap let mut process_map = ProcessMap::new(); for (id, process) in processes { - process_map.insert(id.clone(), process.into()); + process_map.insert(id, process.into()); } Ok(process_map) @@ -127,7 +135,7 @@ where years: start_year..=end_year, energy_limits: EnergyLimitsMap::new(), flows: Vec::new(), - parameter: ProcessParameterMap::new(), + parameters: ProcessParameterMap::new(), regions: RegionSelection::default(), }; diff --git a/src/input/process/parameter.rs b/src/input/process/parameter.rs index dfde564b3..87a1ce054 100644 --- a/src/input/process/parameter.rs +++ b/src/input/process/parameter.rs @@ -123,8 +123,8 @@ where let entry = params.entry(id.clone()).or_default(); let process = processes .get(&id) - .ok_or_else(|| anyhow::anyhow!("Process {} not found", id))?; - let year_range = process.years.clone(); + .with_context(|| format!("Process {id} not found"))?; + let year_range = &process.years; match year { YearSelection::Some(years) => { @@ -144,7 +144,7 @@ where // Check parameters cover all years of the process for (id, parameter) in params.iter() { - let year_range = processes.get(id).unwrap().years.clone(); + let year_range = &processes.get(id).unwrap().years; let reference_years: HashSet = milestone_years .iter() .copied() diff --git a/src/output.rs b/src/output.rs index ab18b6f97..0548dc3a0 100644 --- a/src/output.rs +++ b/src/output.rs @@ -198,7 +198,7 @@ mod tests { years: 2010..=2020, energy_limits: HashMap::new(), flows: vec![], - parameter: ProcessParameterMap::new(), + parameters: ProcessParameterMap::new(), regions: RegionSelection::All, }); diff --git a/src/process.rs b/src/process.rs index 0d04ad2cc..92d78d629 100644 --- a/src/process.rs +++ b/src/process.rs @@ -30,7 +30,7 @@ pub struct Process { /// Maximum annual commodity flows for this process pub flows: Vec, /// Additional parameters for this process - pub parameter: ProcessParameterMap, + pub parameters: ProcessParameterMap, /// The regions in which this process can operate pub regions: RegionSelection, } diff --git a/src/simulation/optimisation.rs b/src/simulation/optimisation.rs index 634d7fd39..bf945ce91 100644 --- a/src/simulation/optimisation.rs +++ b/src/simulation/optimisation.rs @@ -230,7 +230,7 @@ fn calculate_cost_coefficient( if flow.is_pac { coeff += asset .process - .parameter + .parameters .get(&asset.commission_year) .unwrap() .variable_operating_cost @@ -313,7 +313,7 @@ mod tests { years: 2010..=2020, energy_limits: EnergyLimitsMap::new(), flows: vec![flow.clone()], - parameter: process_parameter_map, + parameters: process_parameter_map, regions: RegionSelection::All, }); let asset = Asset::new( diff --git a/src/utils.rs b/src/utils.rs index cbfd97165..74ce92991 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,6 @@ //! Utility functions. -use anyhow::{anyhow, Result}; -use std::collections::hash_map::Entry::{Occupied, Vacant}; +use anyhow::{bail, Result}; +// use std::collections::hash_map::Entry::{Occupied, Vacant}; use std::collections::HashMap; use std::hash::Hash; @@ -9,13 +9,11 @@ use std::hash::Hash; /// If the key already exists, it returns an error with a message indicating the key's existence. pub fn try_insert(map: &mut HashMap, key: K, value: V) -> Result<()> where - K: Eq + Hash + std::fmt::Display, + K: Eq + Hash + std::fmt::Display + std::marker::Copy, { - match map.entry(key) { - Vacant(entry) => { - entry.insert(value); - Ok(()) - } - Occupied(entry) => Err(anyhow!("Key {} already exists in the map", entry.key())), + let existing = map.insert(key, value); + match existing { + Some(_) => bail!("Key {} already exists in the map", key), + None => Ok(()), } } From 8df052aff9c193859e7ef6888f0478bee001fca4 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 28 Apr 2025 11:53:01 +0100 Subject: [PATCH 20/21] Create parse_year_str, replace YearSelection --- src/input/process/parameter.rs | 36 ++++++++++------------- src/year.rs | 53 ++++++++++++---------------------- 2 files changed, 34 insertions(+), 55 deletions(-) diff --git a/src/input/process/parameter.rs b/src/input/process/parameter.rs index 87a1ce054..412cc24f3 100644 --- a/src/input/process/parameter.rs +++ b/src/input/process/parameter.rs @@ -3,7 +3,7 @@ use super::super::*; use crate::id::IDCollection; use crate::process::{Process, ProcessID, ProcessParameter, ProcessParameterMap}; use crate::utils::try_insert; -use crate::year::{deserialize_year, YearSelection}; +use crate::year::parse_year_str; use ::log::warn; use anyhow::{ensure, Context, Result}; use serde::Deserialize; @@ -15,14 +15,13 @@ const PROCESS_PARAMETERS_FILE_NAME: &str = "process_parameters.csv"; #[derive(PartialEq, Debug, Deserialize)] struct ProcessParameterRaw { process_id: String, + year: String, capital_cost: f64, fixed_operating_cost: f64, variable_operating_cost: f64, lifetime: u32, discount_rate: Option, capacity_to_activity: Option, - #[serde(deserialize_with = "deserialize_year")] - year: YearSelection, } impl ProcessParameterRaw { @@ -117,28 +116,25 @@ where let mut params: HashMap = HashMap::new(); for param_raw in iter { let id = process_ids.get_id_by_str(¶m_raw.process_id)?; - let year = param_raw.year.clone(); - let param = param_raw.into_parameter()?; let entry = params.entry(id.clone()).or_default(); let process = processes .get(&id) .with_context(|| format!("Process {id} not found"))?; - let year_range = &process.years; + let process_year_range = &process.years; + let process_years: Vec = milestone_years + .iter() + .copied() + .filter(|year| process_year_range.contains(year)) + .collect(); - match year { - YearSelection::Some(years) => { - for year in years { - try_insert(entry, year, param.clone())?; - } - } - YearSelection::All => { - for year in milestone_years.iter() { - if year_range.contains(year) { - try_insert(entry, *year, param.clone())?; - } - } - } + let parameter_years = + parse_year_str(¶m_raw.year, &process_years).with_context(|| { + format!("Invalid year for process {id}. Valid years are {process_years:?}") + })?; + let param = param_raw.into_parameter()?; + for year in parameter_years { + try_insert(entry, year, param.clone())?; } } @@ -178,7 +174,7 @@ mod tests { lifetime, discount_rate, capacity_to_activity, - year: YearSelection::All, + year: "all".to_string(), } } diff --git a/src/year.rs b/src/year.rs index 28e4a2d42..52ddbf524 100644 --- a/src/year.rs +++ b/src/year.rs @@ -1,39 +1,22 @@ //! Code for working with years. -use anyhow::Result; -use serde::de::Deserializer; -use serde::Deserialize; -use std::collections::HashSet; +use anyhow::{ensure, Result}; -/// Represents a set of years. -#[derive(PartialEq, Debug, Clone)] -pub enum YearSelection { - /// Covers all years. It's up to the user to interpret this (e.g. could be all milestone years, - /// or all active years for a process etc.) - All, - /// Covers some years. - Some(HashSet), -} - -/// Deserialises a year selection from a string. The string can be either "all", a single year, or a -/// semicolon-separated list of years (e.g. "2020;2021;2022" or "2020; 2021; 2022"). -pub fn deserialize_year<'de, D>(deserialiser: D) -> Result -where - D: Deserializer<'de>, -{ - let value = String::deserialize(deserialiser)?; - if value.trim().eq_ignore_ascii_case("all") { - // "all" years specified - Ok(YearSelection::All) - } else { - // Semicolon-separated list of years - let years: Result, _> = - value.split(';').map(|s| s.trim().parse::()).collect(); - match years { - Ok(years_set) if !years_set.is_empty() => Ok(YearSelection::Some(years_set)), - _ => Err(serde::de::Error::custom(format!( - "Invalid year format: {}", - value - ))), - } +/// Parse a string of years separated by semicolons into a vector of u32 years. +/// The string can be either "all" (case-insensitive), a single year, or a semicolon-separated list +/// of years (e.g. "2020;2021;2022" or "2020; 2021; 2022") +pub fn parse_year_str(s: &str, milestone_years: &[u32]) -> Result> { + if s.trim().eq_ignore_ascii_case("all") { + return Ok(Vec::from_iter(milestone_years.iter().copied())); } + + s.split(";") + .map(|y| { + let year = y.trim().parse::()?; + ensure!( + milestone_years.binary_search(&year).is_ok(), + "Invalid year {year}" + ); + Ok(year) + }) + .collect() } From 35b7d497381d34a39ea74c078e5e7e2063312240 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Mon, 28 Apr 2025 16:00:44 +0100 Subject: [PATCH 21/21] Apply suggestions --- src/input.rs | 17 ++++++++++++++++- src/input/process/parameter.rs | 1 - src/lib.rs | 1 - src/utils.rs | 19 ------------------- src/year.rs | 6 +++++- 5 files changed, 21 insertions(+), 23 deletions(-) delete mode 100644 src/utils.rs diff --git a/src/input.rs b/src/input.rs index 2780f1a50..758373484 100644 --- a/src/input.rs +++ b/src/input.rs @@ -7,8 +7,9 @@ use float_cmp::approx_eq; use indexmap::IndexMap; use itertools::Itertools; use serde::de::{Deserialize, DeserializeOwned, Deserializer}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::fs; +use std::hash::Hash; use std::path::Path; mod agent; @@ -137,6 +138,20 @@ where Ok(()) } +/// Inserts a key-value pair into a HashMap if the key does not already exist. +/// +/// If the key already exists, it returns an error with a message indicating the key's existence. +pub fn try_insert(map: &mut HashMap, key: K, value: V) -> Result<()> +where + K: Eq + Hash + Clone + std::fmt::Display, +{ + let existing = map.insert(key.clone(), value); + match existing { + Some(_) => bail!("Key {} already exists in the map", key), + None => Ok(()), + } +} + /// Read a model from the specified directory. /// /// # Arguments diff --git a/src/input/process/parameter.rs b/src/input/process/parameter.rs index 412cc24f3..2b760903c 100644 --- a/src/input/process/parameter.rs +++ b/src/input/process/parameter.rs @@ -2,7 +2,6 @@ use super::super::*; use crate::id::IDCollection; use crate::process::{Process, ProcessID, ProcessParameter, ProcessParameterMap}; -use crate::utils::try_insert; use crate::year::parse_year_str; use ::log::warn; use anyhow::{ensure, Context, Result}; diff --git a/src/lib.rs b/src/lib.rs index d38c55f1e..76a9757e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,5 +14,4 @@ pub mod region; pub mod settings; pub mod simulation; pub mod time_slice; -pub mod utils; pub mod year; diff --git a/src/utils.rs b/src/utils.rs deleted file mode 100644 index 74ce92991..000000000 --- a/src/utils.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Utility functions. -use anyhow::{bail, Result}; -// use std::collections::hash_map::Entry::{Occupied, Vacant}; -use std::collections::HashMap; -use std::hash::Hash; - -/// Inserts a key-value pair into a HashMap if the key does not already exist. -/// -/// If the key already exists, it returns an error with a message indicating the key's existence. -pub fn try_insert(map: &mut HashMap, key: K, value: V) -> Result<()> -where - K: Eq + Hash + std::fmt::Display + std::marker::Copy, -{ - let existing = map.insert(key, value); - match existing { - Some(_) => bail!("Key {} already exists in the map", key), - None => Ok(()), - } -} diff --git a/src/year.rs b/src/year.rs index 52ddbf524..99f3b0dc1 100644 --- a/src/year.rs +++ b/src/year.rs @@ -2,10 +2,14 @@ use anyhow::{ensure, Result}; /// Parse a string of years separated by semicolons into a vector of u32 years. +/// /// The string can be either "all" (case-insensitive), a single year, or a semicolon-separated list /// of years (e.g. "2020;2021;2022" or "2020; 2021; 2022") pub fn parse_year_str(s: &str, milestone_years: &[u32]) -> Result> { - if s.trim().eq_ignore_ascii_case("all") { + let s = s.trim(); + ensure!(!s.is_empty(), "No years provided"); + + if s.eq_ignore_ascii_case("all") { return Ok(Vec::from_iter(milestone_years.iter().copied())); }