diff --git a/docs/glossary.md b/docs/glossary.md index 15ebc80d9..e0c31b3e4 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -2,9 +2,7 @@ **Activity:** The flow of input/s or output/s of a *Process* that are limited by its capacity. For example, a 500MW power station can output 500MWh per hour of electrical power, or a 50MW -electrolyser consumes up to 50MWh per hour of electrical power to produce hydrogen. The -*Primary Activity Commodity* specifies which output/s or input/s are linked to the *Process* -capacity. +electrolyser consumes up to 50MWh per hour of electrical power to produce hydrogen. **Agent:** A decision-making entity in the system. An *Agent* is responsible for serving a user-specified portion of a *Commodity* demand or *Service Demand*. *Agents* invest in and operate @@ -27,14 +25,9 @@ data, including **Process** stock and commodity consumption/production. **Calibration:** The act of ensuring that the model represents the system being modelled in a historical base year. -**Capacity:** The maximum output (or input) of an *Asset*, as measured by units of the *Primary -Activity Commodity*. +**Capacity:** The maximum output (or input) of an *Asset*. -**Capital Cost:** The overnight capital cost of a process, measured in units of the *Primary -Activity Commodity* divided by CAP2ACT. CAP2ACT is a factor that converts 1 unit of capacity to -maximum activity of the primary activity commodity/ies per year. For example, if capacity is -measured in GW and activity is measured in PJ, CAP2ACT for the process is 31.536 because 1 GW of -capacity can produce 31.536 PJ energy output in a year. +**Capital Cost:** The overnight capital cost of a process. **Commodity:** A substance (e.g. CO2) or form of energy (e.g. electricity) that can be @@ -79,10 +72,6 @@ the next most expensive, etc, until demand is served. Also called “unit commit **Output Commodity/ies:** The commodities that flow out of a *Process*. -**Primary Activity Commodity (PAC):** The PACs specify which output/s are linked to the *Process* -capacity. The combined output of all PACs cannot exceed the *Asset's* capacity. A user can define -which output/s are PACs. Most, but not all *Process*es will have only one PAC. - **Process:** A blueprint of an available *Process* that converts input commodities to output commodities. *Process*es have economic attributes of capital cost, fixed operating cost per unit capacity, non-fuel variable operating cost per unit activity, and risk discount rate. They have @@ -114,9 +103,8 @@ Levelised Cost of X, etc. a model does not represent seasons or within-day (diurnal) variation). A typical model will have several diurnal time slices, and several seasonal time slices. -**Utilisation:** The percentage of an *Asset*s capacity that is actually used to produce *Primary -Activity Commodities*. Must be between 0 and 1, and can be measured at time slice, season, or year -level. +**Utilisation:** The percentage of an *Asset*'s capacity that is actually used to produce its +commodities. Must be between 0 and 1, and can be measured at time slice, season, or year level. -**Variable Operating Cost:** The variable operating cost charged per unit of input or output of the -*Primary Activity Commodity* of the *Process*. +**Variable Operating Cost:** The variable operating cost charged per unit of activity of the + *Process*. diff --git a/examples/simple/process_flows.csv b/examples/simple/process_flows.csv index f1ae84f24..88c03c51a 100644 --- a/examples/simple/process_flows.csv +++ b/examples/simple/process_flows.csv @@ -1,15 +1,15 @@ -process_id,commodity_id,regions,years,coeff,type,cost,is_pac -GASDRV,GASPRD,all,all,1.0,fixed,,true -GASPRC,GASPRD,all,all,-1.05,fixed,,false -GASPRC,GASNAT,all,all,1.0,fixed,,true -WNDFRM,ELCTRI,all,all,1.0,fixed,,true -GASCGT,GASNAT,all,all,-1.5,fixed,,false -GASCGT,ELCTRI,all,all,1.0,fixed,,true -RGASBR,GASNAT,all,all,-1.15,fixed,,false -RGASBR,RSHEAT,all,all,1.0,fixed,,true -RELCHP,ELCTRI,all,all,-0.33,fixed,,true -RELCHP,RSHEAT,all,all,1.0,fixed,,false -GASDRV,CO2EMT,all,all,5.113,fixed,,false -GASPRC,CO2EMT,all,all,2.5565,fixed,,false -GASCGT,CO2EMT,all,all,76.695,fixed,,false -RGASBR,CO2EMT,all,all,58.7995,fixed,,false +process_id,commodity_id,regions,years,coeff,type,cost +GASDRV,GASPRD,all,all,1.0,fixed, +GASPRC,GASPRD,all,all,-1.05,fixed, +GASPRC,GASNAT,all,all,1.0,fixed, +WNDFRM,ELCTRI,all,all,1.0,fixed, +GASCGT,GASNAT,all,all,-1.5,fixed, +GASCGT,ELCTRI,all,all,1.0,fixed, +RGASBR,GASNAT,all,all,-1.15,fixed, +RGASBR,RSHEAT,all,all,1.0,fixed, +RELCHP,ELCTRI,all,all,-0.33,fixed, +RELCHP,RSHEAT,all,all,1.0,fixed, +GASDRV,CO2EMT,all,all,5.113,fixed, +GASPRC,CO2EMT,all,all,2.5565,fixed, +GASCGT,CO2EMT,all,all,76.695,fixed, +RGASBR,CO2EMT,all,all,58.7995,fixed, diff --git a/schemas/input/process_flows.yaml b/schemas/input/process_flows.yaml index d75347af8..2a2ad5f18 100644 --- a/schemas/input/process_flows.yaml +++ b/schemas/input/process_flows.yaml @@ -5,8 +5,6 @@ notes: - Commodity flows can vary by region and year. - For each process, there must be entries covering all the years and regions in which the process operates. - - One (and only one) commodity flow for each region/year must be designated as the primary - activity commodity (PAC). fields: - name: process_id @@ -38,9 +36,3 @@ fields: type: number description: The cost per unit flow notes: Optional. If present, must be >0. - - name: is_pac - type: boolean - description: Whether this commodity flow is a primary activity commodity (PAC) - notes: | - One and only one commodity flow can be a PAC for a given combination of region and milestone - year diff --git a/schemas/input/process_parameters.yaml b/schemas/input/process_parameters.yaml index ccf92578e..313a96dd1 100644 --- a/schemas/input/process_parameters.yaml +++ b/schemas/input/process_parameters.yaml @@ -27,7 +27,7 @@ fields: description: Annual operating cost per unit capacity - name: variable_operating_cost type: number - description: Annual variable operating cost per unit activity, for PACs **only** + description: Annual variable operating cost per unit activity - name: lifetime type: integer description: Lifetime in years of an asset created from this process @@ -38,5 +38,5 @@ fields: notes: Must be positive. A warning will be issued if this number is >1. - name: capacity_to_activity type: number - description: Factor for calculating the maximum PAC consumption/production over a year. + description: Factor for calculating the maximum consumption/production over a year. notes: Must be >=0 diff --git a/src/asset.rs b/src/asset.rs index 1befaf649..6a0187c67 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -85,13 +85,11 @@ impl Asset { self.commission_year + self.process_parameter.lifetime } - /// 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 { + /// 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 - .energy_limits + .activity_limits .get(&( self.region_id.clone(), self.commission_year, @@ -100,12 +98,11 @@ impl Asset { .unwrap(); let max_act = self.maximum_activity(); - // Multiply the fractional energy limits by this asset's maximum activity to get energy // limits in real units (which are user defined) (max_act * limits.start())..=(max_act * limits.end()) } - /// Maximum activity for this asset (PAC energy produced/consumed per year) + /// Maximum activity for this asset pub fn maximum_activity(&self) -> f64 { self.capacity * self.process_parameter.capacity_to_activity } @@ -127,12 +124,6 @@ impl Asset { pub fn iter_flows(&self) -> impl Iterator { self.get_flows_map().values() } - - /// Iterate over the asset's Primary Activity Commodity flows - pub fn iter_pacs(&self) -> impl Iterator { - self.process - .iter_pacs(&self.region_id, self.commission_year) - } } /// A wrapper around [`Asset`] for storing references in maps. @@ -318,7 +309,7 @@ mod tests { use super::*; use crate::fixture::{assert_error, process}; use crate::process::{ - Process, ProcessEnergyLimitsMap, ProcessFlowsMap, ProcessParameter, ProcessParameterMap, + Process, ProcessActivityLimitsMap, ProcessFlowsMap, ProcessParameter, ProcessParameterMap, }; use itertools::{assert_equal, Itertools}; use rstest::{fixture, rstest}; @@ -393,7 +384,7 @@ mod tests { id: "process1".into(), description: "Description".into(), years: vec![2010, 2020], - energy_limits: ProcessEnergyLimitsMap::new(), + activity_limits: ProcessActivityLimitsMap::new(), flows: ProcessFlowsMap::new(), parameters: process_parameter_map, regions: HashSet::from(["GBR".into()]), @@ -416,7 +407,7 @@ mod tests { } #[test] - fn test_asset_get_energy_limits() { + fn test_asset_get_activity_limits() { let time_slice = TimeSliceID { season: "winter".into(), time_of_day: "day".into(), @@ -435,9 +426,9 @@ mod tests { .map(|&year| (("GBR".into(), year), process_param.clone())) .collect(); let fraction_limits = 1.0..=f64::INFINITY; - let mut energy_limits = ProcessEnergyLimitsMap::new(); + let mut activity_limits = ProcessActivityLimitsMap::new(); for year in [2010, 2020] { - energy_limits.insert( + activity_limits.insert( ("GBR".into(), year, time_slice.clone()), fraction_limits.clone(), ); @@ -446,7 +437,7 @@ mod tests { id: "process1".into(), description: "Description".into(), years: vec![2010, 2020], - energy_limits, + activity_limits, flows: ProcessFlowsMap::new(), parameters: process_parameter_map, regions: HashSet::from(["GBR".into()]), @@ -460,7 +451,7 @@ mod tests { ) .unwrap(); - assert_eq!(asset.get_energy_limits(&time_slice), 6.0..=f64::INFINITY); + assert_eq!(asset.get_activity_limits(&time_slice), 6.0..=f64::INFINITY); } #[rstest] diff --git a/src/fixture.rs b/src/fixture.rs index 1657b6ee8..81f44af72 100644 --- a/src/fixture.rs +++ b/src/fixture.rs @@ -7,7 +7,7 @@ use crate::agent::{ use crate::asset::{Asset, AssetPool}; use crate::commodity::{Commodity, CommodityID, CommodityLevyMap, CommodityType, DemandMap}; use crate::process::{ - Process, ProcessEnergyLimitsMap, ProcessFlowsMap, ProcessMap, ProcessParameter, + Process, ProcessActivityLimitsMap, ProcessFlowsMap, ProcessMap, ProcessParameter, ProcessParameterMap, }; use crate::region::RegionID; @@ -114,7 +114,7 @@ pub fn process( id: "process1".into(), description: "Description".into(), years: vec![2010, 2020], - energy_limits: ProcessEnergyLimitsMap::new(), + activity_limits: ProcessActivityLimitsMap::new(), flows: ProcessFlowsMap::new(), parameters: process_parameter_map, regions: region_ids, diff --git a/src/input/agent/search_space.rs b/src/input/agent/search_space.rs index 62cab44d8..6a10d0b34 100644 --- a/src/input/agent/search_space.rs +++ b/src/input/agent/search_space.rs @@ -157,7 +157,9 @@ where mod tests { use super::*; use crate::fixture::{agents, assert_error, region_ids}; - use crate::process::{ProcessEnergyLimitsMap, ProcessFlowsMap, ProcessID, ProcessParameterMap}; + use crate::process::{ + ProcessActivityLimitsMap, ProcessFlowsMap, ProcessID, ProcessParameterMap, + }; use crate::region::RegionID; use rstest::{fixture, rstest}; use std::iter; @@ -171,7 +173,7 @@ mod tests { id: id.clone(), description: "Description".into(), years: vec![2010, 2020], - energy_limits: ProcessEnergyLimitsMap::new(), + activity_limits: ProcessActivityLimitsMap::new(), flows: ProcessFlowsMap::new(), parameters: ProcessParameterMap::new(), regions: region_ids.clone(), diff --git a/src/input/process.rs b/src/input/process.rs index 227a05b72..e9419e2b3 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::{ - Process, ProcessEnergyLimitsMap, ProcessFlowsMap, ProcessID, ProcessMap, ProcessParameterMap, + Process, ProcessActivityLimitsMap, ProcessFlowsMap, ProcessID, ProcessMap, ProcessParameterMap, }; use crate::region::{parse_region_str, RegionID}; use crate::time_slice::{TimeSliceInfo, TimeSliceSelection}; @@ -55,7 +55,7 @@ pub fn read_processes( let mut processes = read_processes_file(model_dir, milestone_years, region_ids)?; let process_ids = processes.keys().cloned().collect(); - let mut energy_limits = + let mut activity_limits = read_process_availabilities(model_dir, &process_ids, &processes, time_slice_info)?; let mut flows = read_process_flows(model_dir, &process_ids, &processes, commodities)?; let mut parameters = read_process_parameters(model_dir, &process_ids, &processes)?; @@ -64,7 +64,7 @@ pub fn read_processes( validate_commodities( commodities, &flows, - &energy_limits, + &activity_limits, region_ids, milestone_years, time_slice_info, @@ -72,7 +72,7 @@ pub fn read_processes( // Add data to Process objects for (id, process) in processes.iter_mut() { - process.energy_limits = energy_limits + process.activity_limits = activity_limits .remove(id) .with_context(|| format!("Missing availabilities for process {id}"))?; process.flows = flows @@ -139,7 +139,7 @@ where id: process_raw.id.clone(), description: process_raw.description, years, - energy_limits: ProcessEnergyLimitsMap::new(), + activity_limits: ProcessActivityLimitsMap::new(), flows: ProcessFlowsMap::new(), parameters: ProcessParameterMap::new(), regions, @@ -158,7 +158,7 @@ where fn validate_commodities( commodities: &CommodityMap, flows: &HashMap, - availabilities: &HashMap, + availabilities: &HashMap, region_ids: &HashSet, milestone_years: &[u32], time_slice_info: &TimeSliceInfo, @@ -227,7 +227,7 @@ fn validate_svd_commodity( time_slice_info: &TimeSliceInfo, commodity: &Commodity, flows: &HashMap, - availabilities: &HashMap, + availabilities: &HashMap, region_id: &RegionID, year: &u32, ts_selection: &TimeSliceSelection, @@ -308,7 +308,6 @@ mod tests { coeff: -10.0, kind: FlowType::Fixed, cost: 1.0, - is_pac: false, }}, )]) } @@ -322,7 +321,6 @@ mod tests { coeff: 10.0, kind: FlowType::Fixed, cost: 1.0, - is_pac: false, }}, )]) } @@ -383,7 +381,6 @@ mod tests { coeff: 10.0, kind: FlowType::Fixed, cost: 1.0, - is_pac: false, }}, )]), )]) @@ -398,7 +395,7 @@ mod tests { ) { let availabilities = HashMap::from_iter(vec![( "process1".into(), - ProcessEnergyLimitsMap::from_iter(vec![( + ProcessActivityLimitsMap::from_iter(vec![( ("GBR".into(), 2010, time_slice.clone()), 0.1..=0.9, )]), @@ -427,7 +424,7 @@ mod tests { // Invalid scenario: no availability let availabilities = HashMap::from_iter(vec![( "process1".into(), - ProcessEnergyLimitsMap::from_iter(vec![( + ProcessActivityLimitsMap::from_iter(vec![( ("GBR".into(), 2010, time_slice.clone()), 0.0..=0.0, )]), diff --git a/src/input/process/availability.rs b/src/input/process/availability.rs index dd17079c1..b9a16b81c 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::{Process, ProcessEnergyLimitsMap, ProcessID}; +use crate::process::{Process, ProcessActivityLimitsMap, ProcessID}; use crate::region::parse_region_str; use crate::time_slice::TimeSliceInfo; use crate::year::parse_year_str; @@ -39,8 +39,8 @@ impl ProcessAvailabilityRaw { /// Calculate fraction of annual energy as availability multiplied by time slice length. /// - /// The resulting limits are max/min PAC energy produced/consumed in each timeslice per - /// cap2act units of capacity + /// The resulting limits are max/min energy produced/consumed in each timeslice per + /// `capacity_to_activity` units of capacity. fn to_bounds(&self, ts_length: f64) -> RangeInclusive { let value = self.value * ts_length; match self.limit_type { @@ -75,14 +75,14 @@ enum LimitType { /// /// # Returns /// -/// A [`HashMap`] with process IDs as the keys and [`ProcessEnergyLimitsMap`]s as the values or an +/// A [`HashMap`] with process IDs as the keys and [`ProcessActivityLimitsMap`]s as the values or an /// error. pub fn read_process_availabilities( model_dir: &Path, process_ids: &IndexSet, processes: &HashMap, 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( @@ -94,13 +94,13 @@ pub fn read_process_availabilities( .with_context(|| input_err_msg(&file_path)) } -/// Process raw process availabilities input data into [`ProcessEnergyLimitsMap`]s +/// Process raw process availabilities input data into [`ProcessActivityLimitsMap`]s fn read_process_availabilities_from_iter( iter: I, process_ids: &IndexSet, processes: &HashMap, time_slice_info: &TimeSliceInfo, -) -> Result> +) -> Result> where I: Iterator, { @@ -130,10 +130,10 @@ where // Get timeslices let ts_selection = time_slice_info.get_selection(&record.time_slice)?; - // Insert the energy limit into the map + // Insert the activity limit into the map let entry = map .entry(id.clone()) - .or_insert_with(ProcessEnergyLimitsMap::new); + .or_insert_with(ProcessActivityLimitsMap::new); for (time_slice, ts_length) in ts_selection.iter(time_slice_info) { let bounds = record.to_bounds(ts_length); @@ -149,14 +149,14 @@ where } } - validate_energy_limits_maps(&map, processes, time_slice_info)?; + validate_activity_limits_maps(&map, processes, time_slice_info)?; Ok(map) } -/// Check that every energy limits covers every time slice, and all regions/years of the process -fn validate_energy_limits_maps( - map: &HashMap, +/// Check that the activity limits cover every time slice and all regions/years of the process +fn validate_activity_limits_maps( + map: &HashMap, processes: &HashMap, time_slice_info: &TimeSliceInfo, ) -> Result<()> { diff --git a/src/input/process/flow.rs b/src/input/process/flow.rs index 6e32539a9..21f934c31 100644 --- a/src/input/process/flow.rs +++ b/src/input/process/flow.rs @@ -1,6 +1,6 @@ //! Code for reading process flows file use super::super::*; -use crate::commodity::{CommodityID, CommodityMap}; +use crate::commodity::CommodityMap; use crate::id::IDCollection; use crate::process::{FlowType, Process, ProcessFlow, ProcessFlowsMap, ProcessID}; use crate::region::parse_region_str; @@ -25,7 +25,6 @@ struct ProcessFlowRaw { #[serde(rename = "type")] kind: FlowType, cost: Option, - is_pac: bool, } impl ProcessFlowRaw { @@ -112,7 +111,6 @@ where coeff: record.coeff, kind: record.kind, cost: record.cost.unwrap_or(0.0), - is_pac: record.is_pac, }; // Insert flow into the map @@ -134,63 +132,26 @@ where } } - // Validate flows and sort flows so PACs are at the start for (process_id, map) in map.iter_mut() { let process = processes.get(process_id).unwrap(); validate_process_flows_map(process, map)?; - sort_flows(map); } Ok(map) } -/// Sort flows so PACs come first -fn sort_flows(map: &mut ProcessFlowsMap) { - for map in map.values_mut() { - map.sort_by(|_, a, _, b| b.is_pac.cmp(&a.is_pac)); - } -} - /// Validate flows for a process fn validate_process_flows_map(process: &Process, map: &ProcessFlowsMap) -> Result<()> { let process_id = process.id.clone(); - let reference_years = &process.years; - let reference_regions = &process.regions; - for year in reference_years.iter() { - for region in reference_regions { + for year in process.years.iter() { + for region in process.regions.iter() { // Check that the process has flows for this region/year - let flow_map = map.get(&(region.clone(), *year)).with_context(|| { - format!("Missing entry for process {process_id} in {region}/{year}") - })?; - - // Validate flows for this process/region/year - validate_flow_map(flow_map).with_context(|| { - format!("Invalid flows for process {process_id} in {region}/{year}") - })?; - } - } - Ok(()) -} - -/// Validate a vector of flows for a process in a given region/year -fn validate_flow_map(flow_map: &IndexMap) -> Result<()> { - // PACs must be either all inputs or all outputs - let mut flow_sign: Option = None; // False for inputs, true for outputs - for flow in flow_map.values().filter(|flow| flow.is_pac) { - // Check that flow sign is consistent - let current_flow_sign = flow.coeff > 0.0; - if let Some(flow_sign) = flow_sign { ensure!( - current_flow_sign == flow_sign, - "PACs are a mix of inputs and outputs", + map.contains_key(&(region.clone(), *year)), + "Missing entry for process {process_id} in {region}/{year}" ); } - flow_sign = Some(current_flow_sign); } - - // Check that at least one PAC is defined - ensure!(flow_sign.is_some(), "No PACs defined"); - Ok(()) } @@ -199,15 +160,10 @@ mod tests { use super::*; use crate::commodity::{Commodity, CommodityLevyMap, CommodityType, DemandMap}; use crate::time_slice::TimeSliceLevel; - use indexmap::indexmap; - use rstest::{fixture, rstest}; - fn create_process_flow_raw( - coeff: f64, - kind: FlowType, - cost: Option, - is_pac: bool, - ) -> ProcessFlowRaw { + use rstest::fixture; + + fn create_process_flow_raw(coeff: f64, kind: FlowType, cost: Option) -> ProcessFlowRaw { ProcessFlowRaw { process_id: "process".into(), commodity_id: "commodity".into(), @@ -216,45 +172,34 @@ mod tests { coeff, kind, cost, - is_pac, } } #[test] fn test_validate_flow_raw() { // Valid - let valid = create_process_flow_raw(1.0, FlowType::Fixed, Some(0.0), true); + let valid = create_process_flow_raw(1.0, FlowType::Fixed, Some(0.0)); assert!(valid.validate().is_ok()); // Invalid: Bad flow value - let invalid = create_process_flow_raw(0.0, FlowType::Fixed, Some(0.0), true); + let invalid = create_process_flow_raw(0.0, FlowType::Fixed, Some(0.0)); assert!(invalid.validate().is_err()); - let invalid = create_process_flow_raw(f64::NAN, FlowType::Fixed, Some(0.0), true); + let invalid = create_process_flow_raw(f64::NAN, FlowType::Fixed, Some(0.0)); assert!(invalid.validate().is_err()); - let invalid = create_process_flow_raw(f64::INFINITY, FlowType::Fixed, Some(0.0), true); + let invalid = create_process_flow_raw(f64::INFINITY, FlowType::Fixed, Some(0.0)); assert!(invalid.validate().is_err()); - let invalid = create_process_flow_raw(f64::NEG_INFINITY, FlowType::Fixed, Some(0.0), true); + let invalid = create_process_flow_raw(f64::NEG_INFINITY, FlowType::Fixed, Some(0.0)); assert!(invalid.validate().is_err()); // Invalid: Bad flow cost value - let invalid = create_process_flow_raw(1.0, FlowType::Fixed, Some(f64::NAN), true); + let invalid = create_process_flow_raw(1.0, FlowType::Fixed, Some(f64::NAN)); assert!(invalid.validate().is_err()); - let invalid = create_process_flow_raw(1.0, FlowType::Fixed, Some(f64::NEG_INFINITY), true); + let invalid = create_process_flow_raw(1.0, FlowType::Fixed, Some(f64::NEG_INFINITY)); assert!(invalid.validate().is_err()); - let invalid = create_process_flow_raw(1.0, FlowType::Fixed, Some(f64::INFINITY), true); + let invalid = create_process_flow_raw(1.0, FlowType::Fixed, Some(f64::INFINITY)); assert!(invalid.validate().is_err()); } - fn create_process_flow(commodity: Rc, coeff: f64, is_pac: bool) -> ProcessFlow { - ProcessFlow { - commodity, - coeff, - kind: FlowType::Fixed, - cost: 0.0, - is_pac, - } - } - #[fixture] fn commodity1() -> Commodity { Commodity { @@ -278,44 +223,4 @@ mod tests { levies: CommodityLevyMap::default(), } } - - #[rstest] - fn test_validate_flow_map_valid_single(commodity1: Commodity, commodity2: Commodity) { - // Valid: Single PAC - let flows = indexmap! { - commodity1.id.clone() => create_process_flow(commodity1.into(), 1.0, true), - commodity2.id.clone() => create_process_flow(commodity2.into(), 1.0, false), - }; - assert!(validate_flow_map(&flows).is_ok()); - } - - #[rstest] - fn test_validate_flow_map_valid_multiple(commodity1: Commodity, commodity2: Commodity) { - // Valid: Multiple PACs - let flows = indexmap! { - commodity1.id.clone() => create_process_flow(commodity1.into(), 1.0, true), - commodity2.id.clone() => create_process_flow(commodity2.into(), 1.0, true), - }; - assert!(validate_flow_map(&flows).is_ok()); - } - - #[rstest] - fn test_validate_flow_map_invalid_no_pacs(commodity1: Commodity, commodity2: Commodity) { - // Invalid: No PACs - let flows = indexmap! { - commodity1.id.clone() => create_process_flow(commodity1.into(), 1.0, false), - commodity2.id.clone() => create_process_flow(commodity2.into(), 1.0, false), - }; - assert!(validate_flow_map(&flows).is_err()); - } - - #[rstest] - fn test_validate_flow_map(commodity1: Commodity, commodity2: Commodity) { - // Invalid: Mixed PAC flow types - let flows = indexmap! { - commodity1.id.clone() => create_process_flow(commodity1.into(), 1.0, true), - commodity2.id.clone() => create_process_flow(commodity2.into(), -1.0, true), - }; - assert!(validate_flow_map(&flows).is_err()); - } } diff --git a/src/process.rs b/src/process.rs index cf1e99dc7..7da590a10 100644 --- a/src/process.rs +++ b/src/process.rs @@ -15,16 +15,11 @@ define_id_type! {ProcessID} /// A map of [`Process`]es, keyed by process ID pub type ProcessMap = IndexMap>; -/// A map indicating relative PAC energy limits for a [`Process`] throughout the year. +/// A map indicating activity 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 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 ProcessEnergyLimitsMap = HashMap<(RegionID, u32, TimeSliceID), RangeInclusive>; +/// The value is calculated as availability multiplied by time slice length. The limits are given as +/// ranges, depending on the user-specified limit type and value for availability. +pub type ProcessActivityLimitsMap = HashMap<(RegionID, u32, TimeSliceID), RangeInclusive>; /// A map of [`ProcessParameter`]s, keyed by region and year pub type ProcessParameterMap = HashMap<(RegionID, u32), Rc>; @@ -43,8 +38,8 @@ pub struct Process { pub description: String, /// The years in which this process is available for investment pub years: Vec, - /// Limits on PAC energy consumption/production for each time slice (as a fraction of maximum) - pub energy_limits: ProcessEnergyLimitsMap, + /// Limits on activity for each time slice (as a fraction of maximum) + pub activity_limits: ProcessActivityLimitsMap, /// Maximum annual commodity flows for this process pub flows: ProcessFlowsMap, /// Additional parameters for this process @@ -66,15 +61,6 @@ impl Process { .unwrap() // all regions and years are covered .contains_key(commodity_id) } - - /// Iterate over this process's Primary Activity Commodity flows - pub fn iter_pacs(&self, region_id: &RegionID, year: u32) -> impl Iterator { - self.flows - .get(&(region_id.clone(), year)) - .unwrap() - .values() - .take_while(|flow| flow.is_pac) - } } /// Represents a maximum annual commodity coeff for a given process @@ -91,11 +77,8 @@ pub struct ProcessFlow { /// Cost per unit flow. /// /// For example, cost per unit of natural gas produced. The user can apply it to any specified - /// flow, in contrast to [`ProcessParameter::variable_operating_cost`], which applies only to - /// PAC flows. + /// flow. pub cost: f64, - /// Whether this flow represents a Primary Activity Commodity - pub is_pac: bool, } impl ProcessFlow { @@ -153,18 +136,17 @@ pub struct ProcessParameter { pub capital_cost: f64, /// Annual operating cost per unit capacity pub fixed_operating_cost: f64, - /// Annual variable operating cost per unit activity, for PACs **only** + /// Annual variable operating cost per unit activity 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 consumption/production over a year. + /// Factor for calculating the maximum consumption/production over a year. /// - /// 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. + /// Used for converting one unit of capacity to maximum energy of asset 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, } @@ -327,7 +309,6 @@ mod tests { coeff: 1.0, kind: FlowType::Fixed, cost: 5.0, - is_pac: true, } } @@ -354,7 +335,6 @@ mod tests { coeff: 1.0, kind: FlowType::Fixed, cost: 5.0, - is_pac: true, } } @@ -381,7 +361,6 @@ mod tests { coeff: 1.0, kind: FlowType::Fixed, cost: 5.0, - is_pac: true, } } @@ -396,7 +375,6 @@ mod tests { coeff: 1.0, kind: FlowType::Fixed, cost: 0.0, - is_pac: true, }; assert_eq!(flow.get_levy(®ion_id, 2020, &time_slice), 0.0); @@ -413,7 +391,6 @@ mod tests { coeff: 1.0, kind: FlowType::Fixed, cost: 0.0, - is_pac: true, }; assert_eq!(flow.get_levy(®ion_id, 2020, &time_slice), 10.0); @@ -430,7 +407,6 @@ mod tests { coeff: 1.0, kind: FlowType::Fixed, cost: 0.0, - is_pac: true, }; assert_eq!(flow.get_levy(®ion_id, 2020, &time_slice), -5.0); @@ -443,7 +419,6 @@ mod tests { coeff: 1.0, kind: FlowType::Fixed, cost: 0.0, - is_pac: true, }; assert_eq!(flow.get_levy(&"USA".into(), 2020, &time_slice), 5.0); @@ -460,7 +435,6 @@ mod tests { coeff: 1.0, kind: FlowType::Fixed, cost: 0.0, - is_pac: true, }; assert_eq!(flow.get_levy(®ion_id, 2030, &time_slice), 7.0); @@ -473,7 +447,6 @@ mod tests { coeff: 1.0, kind: FlowType::Fixed, cost: 0.0, - is_pac: true, }; let different_time_slice = TimeSliceID { @@ -495,7 +468,6 @@ mod tests { coeff: 1.0, // Positive coefficient means production kind: FlowType::Fixed, cost: 0.0, - is_pac: true, }; assert_eq!(flow.get_levy(®ion_id, 2020, &time_slice), 0.0); @@ -512,7 +484,6 @@ mod tests { coeff: -1.0, // Negative coefficient means consumption kind: FlowType::Fixed, cost: 0.0, - is_pac: true, }; assert_eq!(flow.get_levy(®ion_id, 2020, &time_slice), 10.0); @@ -529,7 +500,6 @@ mod tests { coeff: 1.0, // Positive coefficient means production kind: FlowType::Fixed, cost: 0.0, - is_pac: true, }; assert_eq!(flow.get_levy(®ion_id, 2020, &time_slice), 10.0); @@ -546,7 +516,6 @@ mod tests { coeff: -1.0, // Negative coefficient means consumption kind: FlowType::Fixed, cost: 0.0, - is_pac: true, }; assert_eq!(flow.get_levy(®ion_id, 2020, &time_slice), 0.0); diff --git a/src/simulation/optimisation/constraints.rs b/src/simulation/optimisation/constraints.rs index 3db818ba4..da69fe885 100644 --- a/src/simulation/optimisation/constraints.rs +++ b/src/simulation/optimisation/constraints.rs @@ -103,8 +103,8 @@ fn add_commodity_balance_constraints( /// Add asset-level capacity and availability constraints. /// -/// For every asset at every time slice, the sum of the commodity flows for PACs must not exceed the -/// capacity limits, which are a product of the annual capacity, time slice length and process +/// For every asset at every time slice, the sum of the commodity flows for assets must not exceed +/// the capacity limits, which are a product of the annual capacity, time slice length and process /// availability. /// /// See description in [the dispatch optimisation documentation][1].