From 81a438318e204f364dbcdf1e8521a709f7e614f1 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 23 Apr 2025 11:30:34 +0100 Subject: [PATCH 1/3] Clarify language around energy and activity --- src/asset.rs | 17 +++++++------- src/input/asset.rs | 6 ++--- src/input/process.rs | 14 +++++------ src/input/process/availability.rs | 23 +++++++++--------- src/output.rs | 2 +- src/process.rs | 27 +++++++++++----------- src/simulation/optimisation.rs | 4 ++-- src/simulation/optimisation/constraints.rs | 2 +- 8 files changed, 49 insertions(+), 46 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index bca91ad7d..4c8ab9901 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -61,16 +61,17 @@ impl Asset { self.commission_year + self.process.parameter.lifetime } - /// 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(); + /// Get the energy limits for this asset in a particular time slice + /// This is an absolute max and min on the PAC energy produced/consumed in that time slice. + pub fn get_energy_limits(&self, time_slice: &TimeSliceID) -> RangeInclusive { + let limits = self.process.energy_limits.get(time_slice).unwrap(); let max_act = self.maximum_activity(); // Multiply the fractional capacity in self.process by this asset's actual capacity (max_act * limits.start())..=(max_act * limits.end()) } - /// Maximum activity for this asset in a year + /// Maximum activity for this asset (PAC energy produced/consumed per year) pub fn maximum_activity(&self) -> f64 { self.capacity * self.process.parameter.capacity_to_activity } @@ -186,7 +187,7 @@ impl AssetPool { mod tests { use super::*; use crate::commodity::{CommodityCostMap, CommodityType, DemandMap}; - use crate::process::{ActivityLimitsMap, FlowType, Process, ProcessFlow, ProcessParameter}; + use crate::process::{EnergyLimitsMap, FlowType, Process, ProcessFlow, ProcessParameter}; use crate::region::RegionSelection; use crate::time_slice::TimeSliceLevel; use itertools::{assert_equal, Itertools}; @@ -228,7 +229,7 @@ mod tests { let process = Rc::new(Process { id: "process1".into(), description: "Description".into(), - activity_limits, + energy_limits: activity_limits, flows: vec![flow.clone()], parameter: process_param.clone(), regions: RegionSelection::All, @@ -242,7 +243,7 @@ mod tests { commission_year: 2010, }; - assert_eq!(asset.get_activity_limits(&time_slice), 6.0..=f64::INFINITY); + assert_eq!(asset.get_energy_limits(&time_slice), 6.0..=f64::INFINITY); } fn create_asset_pool() -> AssetPool { @@ -258,7 +259,7 @@ mod tests { let process = Rc::new(Process { id: "process1".into(), description: "Description".into(), - activity_limits: ActivityLimitsMap::new(), + energy_limits: EnergyLimitsMap::new(), flows: vec![], parameter: process_param.clone(), regions: RegionSelection::All, diff --git a/src/input/asset.rs b/src/input/asset.rs index d0e1eec6f..f789fd575 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::{ActivityLimitsMap, Process, ProcessParameter}; + use crate::process::{EnergyLimitsMap, Process, ProcessParameter}; use crate::region::RegionSelection; use itertools::assert_equal; use std::iter; @@ -114,7 +114,7 @@ mod tests { let process = Rc::new(Process { id: "process1".into(), description: "Description".into(), - activity_limits: ActivityLimitsMap::new(), + energy_limits: EnergyLimitsMap::new(), flows: vec![], parameter: process_param.clone(), regions: RegionSelection::All, @@ -189,7 +189,7 @@ mod tests { let process = Rc::new(Process { id: "process1".into(), description: "Description".into(), - activity_limits: ActivityLimitsMap::new(), + energy_limits: EnergyLimitsMap::new(), flows: vec![], parameter: process_param, regions: RegionSelection::Some(["GBR".into()].into_iter().collect()), diff --git a/src/input/process.rs b/src/input/process.rs index 96d8b097f..47ebca85f 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -2,7 +2,7 @@ use super::*; use crate::commodity::{Commodity, CommodityID, CommodityMap, CommodityType}; use crate::process::{ - ActivityLimitsMap, Process, ProcessFlow, ProcessID, ProcessMap, ProcessParameter, + EnergyLimitsMap, Process, ProcessFlow, ProcessID, ProcessMap, ProcessParameter, }; use crate::region::{RegionID, RegionSelection}; use crate::time_slice::TimeSliceInfo; @@ -87,7 +87,7 @@ struct ValidationParams<'a> { milestone_years: &'a [u32], time_slice_info: &'a TimeSliceInfo, parameters: &'a HashMap, - availabilities: &'a HashMap, + availabilities: &'a HashMap, } /// Perform consistency checks for commodity flows. @@ -98,7 +98,7 @@ fn validate_commodities( milestone_years: &[u32], time_slice_info: &TimeSliceInfo, parameters: &HashMap, - availabilities: &HashMap, + availabilities: &HashMap, ) -> anyhow::Result<()> { let params = ValidationParams { flows, @@ -205,7 +205,7 @@ fn validate_svd_commodity( fn create_process_map( descriptions: I, - mut availabilities: HashMap, + mut availabilities: HashMap, mut flows: HashMap>, mut parameters: HashMap, mut regions: HashMap, @@ -232,7 +232,7 @@ where let process = Process { id: id.clone(), description: description.description, - activity_limits: availabilities, + energy_limits: availabilities, flows, parameter, regions, @@ -255,7 +255,7 @@ mod tests { struct ProcessData { descriptions: Vec, - availabilities: HashMap, + availabilities: HashMap, flows: HashMap>, parameters: HashMap, regions: HashMap, @@ -278,7 +278,7 @@ mod tests { let availabilities = ["process1", "process2"] .into_iter() .map(|id| { - let mut map = ActivityLimitsMap::new(); + let mut map = EnergyLimitsMap::new(); map.insert( TimeSliceID { season: "winter".into(), diff --git a/src/input/process/availability.rs b/src/input/process/availability.rs index 598367cfb..de211a903 100644 --- a/src/input/process/availability.rs +++ b/src/input/process/availability.rs @@ -1,7 +1,7 @@ //! Code for reading process availabilities CSV file use super::super::*; use crate::id::IDCollection; -use crate::process::{ActivityLimitsMap, ProcessID}; +use crate::process::{EnergyLimitsMap, ProcessID}; use crate::time_slice::TimeSliceInfo; use anyhow::{Context, Result}; use serde::Deserialize; @@ -44,25 +44,25 @@ enum LimitType { /// /// # Returns /// -/// A [`HashMap`] with process IDs as the keys and [`ActivityLimitsMap`]s as the values or an +/// A [`HashMap`] with process IDs as the keys and [`EnergyLimitsMap`]s as the values or an /// error. pub fn read_process_availabilities( model_dir: &Path, process_ids: &HashSet, time_slice_info: &TimeSliceInfo, -) -> Result> { +) -> Result> { let file_path = model_dir.join(PROCESS_AVAILABILITIES_FILE_NAME); let process_availabilities_csv = read_csv(&file_path)?; read_process_availabilities_from_iter(process_availabilities_csv, process_ids, time_slice_info) .with_context(|| input_err_msg(&file_path)) } -/// Process raw process availabilities input data into [`ActivityLimitsMap`]s +/// Process raw process availabilities input data into [`EnergyLimitsMap`]s fn read_process_availabilities_from_iter( iter: I, process_ids: &HashSet, time_slice_info: &TimeSliceInfo, -) -> Result> +) -> Result> where I: Iterator, { @@ -78,10 +78,11 @@ where let ts_selection = time_slice_info.get_selection(&record.time_slice)?; - let map = map.entry(process_id).or_insert_with(ActivityLimitsMap::new); + let map = map.entry(process_id).or_insert_with(EnergyLimitsMap::new); for (time_slice, ts_length) in time_slice_info.iter_selection(&ts_selection) { - // Calculate fraction of annual capacity as availability multiplied by time slice length + // Calculate fraction of annual energy as availability multiplied by time slice length + // The resulting limits are max/min energy per unit of capacity in each timeslice let value = record.value * ts_length; let bounds = match record.limit_type { LimitType::LowerBound => value..=f64::INFINITY, @@ -100,14 +101,14 @@ where } } - validate_capacity_maps(&map, time_slice_info)?; + validate_energy_limits_maps(&map, time_slice_info)?; Ok(map) } -/// Check that every capacity map has an entry for every time slice -fn validate_capacity_maps( - map: &HashMap, +/// Check that every energy limits map has an entry for every time slice +fn validate_energy_limits_maps( + map: &HashMap, time_slice_info: &TimeSliceInfo, ) -> Result<()> { for (process_id, map) in map.iter() { diff --git a/src/output.rs b/src/output.rs index fa3cc1359..0ac70a297 100644 --- a/src/output.rs +++ b/src/output.rs @@ -204,7 +204,7 @@ mod tests { let process = Rc::new(Process { id: process_id, description: "Description".into(), - activity_limits: HashMap::new(), + energy_limits: HashMap::new(), flows: vec![], parameter: process_param.clone(), regions: RegionSelection::All, diff --git a/src/process.rs b/src/process.rs index 9d45e7894..9bddf2803 100644 --- a/src/process.rs +++ b/src/process.rs @@ -23,9 +23,9 @@ pub struct Process { pub id: ProcessID, /// A human-readable description for the process (e.g. dry gas extraction) pub description: String, - /// The activity limits for each time slice (as a fraction of maximum) - pub activity_limits: ActivityLimitsMap, - /// Commodity flows for this process + /// Limits on PAC energy consumption/production for each time slice (as a fraction of maximum) + pub energy_limits: EnergyLimitsMap, + /// Maximum annual commodity flows for this process pub flows: Vec, /// Additional parameters for this process pub parameter: ProcessParameter, @@ -47,24 +47,25 @@ impl Process { } } -/// A map indicating activity limits for a [`Process`] throughout the year. +/// A map indicating relative PAC energy limits for a [`Process`] throughout the year. /// /// The value is calculated as availability multiplied by time slice length. Note that it is a -/// **fraction** of activity for the year; to calculate **actual** activity for a given time slice -/// you need to know the maximum activity for the specific instance of a [`Process`] in use. +/// **fraction** of energy for the year; to calculate **actual** energy limits for a given time +/// slice you need to know the maximum activity (energy per year) for the specific instance of a +/// [`Process`] in use. /// /// The limits are given as ranges, depending on the user-specified limit type and value for /// availability. -pub type ActivityLimitsMap = HashMap>; +pub type EnergyLimitsMap = HashMap>; -/// Represents a commodity flow for a given process +/// Represents a maximum annual commodity flow for a given process #[derive(PartialEq, Debug, Deserialize, Clone)] pub struct ProcessFlow { /// A unique identifier for the process pub process_id: String, /// The commodity produced or consumed by this flow pub commodity: Rc, - /// Commodity flow quantity relative to other commodity flows. + /// Maximum annual commodity flow quantity relative to other commodity flows. /// /// Positive value indicates flow out and negative value indicates flow in. pub flow: f64, @@ -102,16 +103,16 @@ pub struct ProcessParameter { pub capital_cost: f64, /// Annual operating cost per unit capacity pub fixed_operating_cost: f64, - /// Variable operating cost per unit activity, for PACs **only** + /// Annual variable operating cost per unit activity, for PACs **only** pub variable_operating_cost: f64, /// Lifetime in years of an asset created from this process pub lifetime: u32, /// Process-specific discount rate pub discount_rate: f64, - /// Factor for calculating the maximum PAC output over a year. + /// Factor for calculating the maximum PAC consumption/production over a year. /// - /// Used for converting one unit of capacity to maximum activity of the PAC per year. For - /// example, if capacity is measured in GW and activity is measured in PJ, the + /// Used for converting one unit of capacity to maximum energy of the PAC(s) per year. For + /// example, if capacity is measured in GW and energy is measured in PJ, the /// capacity_to_activity for the process is 31.536 because 1 GW of capacity can produce 31.536 /// PJ energy output in a year. pub capacity_to_activity: f64, diff --git a/src/simulation/optimisation.rs b/src/simulation/optimisation.rs index 0167c25a6..c360fe1fd 100644 --- a/src/simulation/optimisation.rs +++ b/src/simulation/optimisation.rs @@ -257,7 +257,7 @@ 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::{EnergyLimitsMap, FlowType, Process, ProcessParameter}; use crate::region::RegionSelection; use crate::time_slice::TimeSliceLevel; use float_cmp::assert_approx_eq; @@ -296,7 +296,7 @@ mod tests { let process = Rc::new(Process { id: "process1".into(), description: "Description".into(), - activity_limits: ActivityLimitsMap::new(), + energy_limits: EnergyLimitsMap::new(), flows: vec![flow.clone()], parameter: process_param.clone(), regions: RegionSelection::All, diff --git a/src/simulation/optimisation/constraints.rs b/src/simulation/optimisation/constraints.rs index 0abe29e7e..da2433557 100644 --- a/src/simulation/optimisation/constraints.rs +++ b/src/simulation/optimisation/constraints.rs @@ -213,7 +213,7 @@ fn add_asset_capacity_constraints( terms.push((var, 1.0)); } - let mut limits = asset.get_activity_limits(time_slice); + let mut limits = asset.get_energy_limits(time_slice); // If it's an input flow, the q's will be negative, so we need to invert the limits if is_input { From 93cc22c8af704c4823cb1b1d25a479cb0d9b21bb Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 23 Apr 2025 12:03:24 +0100 Subject: [PATCH 2/3] Minor tweak --- src/input/process/availability.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/input/process/availability.rs b/src/input/process/availability.rs index de211a903..18d71ed7c 100644 --- a/src/input/process/availability.rs +++ b/src/input/process/availability.rs @@ -82,7 +82,8 @@ where for (time_slice, ts_length) in time_slice_info.iter_selection(&ts_selection) { // Calculate fraction of annual energy as availability multiplied by time slice length - // The resulting limits are max/min energy per unit of capacity in each timeslice + // The resulting limits are max/min PAC energy produced/consumed in each timeslice per + // cap2act units of capacity let value = record.value * ts_length; let bounds = match record.limit_type { LimitType::LowerBound => value..=f64::INFINITY, From 51a57ac71da3c7a50fee18dd378a6c35418048cf Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Wed, 23 Apr 2025 13:00:04 +0100 Subject: [PATCH 3/3] Apply suggestions --- src/asset.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index 4c8ab9901..d59ee00b0 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -62,6 +62,7 @@ impl Asset { } /// Get the energy limits for this asset in a particular time slice + /// /// This is an absolute max and min on the PAC energy produced/consumed in that time slice. pub fn get_energy_limits(&self, time_slice: &TimeSliceID) -> RangeInclusive { let limits = self.process.energy_limits.get(time_slice).unwrap(); @@ -194,7 +195,7 @@ mod tests { use std::iter; #[test] - fn test_asset_get_activity_limits() { + fn test_asset_get_energy_limits() { let time_slice = TimeSliceID { season: "winter".into(), time_of_day: "day".into(), @@ -225,11 +226,11 @@ mod tests { is_pac: true, }; let fraction_limits = 1.0..=f64::INFINITY; - let activity_limits = iter::once((time_slice.clone(), fraction_limits)).collect(); + let energy_limits = iter::once((time_slice.clone(), fraction_limits)).collect(); let process = Rc::new(Process { id: "process1".into(), description: "Description".into(), - energy_limits: activity_limits, + energy_limits, flows: vec![flow.clone()], parameter: process_param.clone(), regions: RegionSelection::All,