diff --git a/examples/simple/process_flows.csv b/examples/simple/process_flows.csv index 459e75a42..f5e57322f 100644 --- a/examples/simple/process_flows.csv +++ b/examples/simple/process_flows.csv @@ -1,15 +1,15 @@ -process_id,commodity_id,flow,flow_type,flow_cost -GASDRV,GASPRD,1.0,fixed, -GASPRC,GASPRD,-1.05,fixed, -GASPRC,GASNAT,1.0,fixed, -WNDFRM,ELCTRI,1.0,fixed, -GASCGT,GASNAT,-1.5,fixed, -GASCGT,ELCTRI,1.0,fixed, -RGASBR,GASNAT,-1.15,fixed, -RGASBR,RSHEAT,1.0,fixed, -RELCHP,ELCTRI,-0.33,fixed, -RELCHP,RSHEAT,1.0,fixed, -GASDRV,CO2EMT,5.113,fixed, -GASPRC,CO2EMT,2.5565,fixed, -GASCGT,CO2EMT,76.695,fixed, -RGASBR,CO2EMT,58.7995,fixed, +process_id,commodity_id,flow,flow_type,flow_cost,is_pac +GASDRV,GASPRD,1.0,fixed,,true +GASPRC,GASPRD,-1.05,fixed,,false +GASPRC,GASNAT,1.0,fixed,,true +WNDFRM,ELCTRI,1.0,fixed,,true +GASCGT,GASNAT,-1.5,fixed,,false +GASCGT,ELCTRI,1.0,fixed,,true +RGASBR,GASNAT,-1.15,fixed,,false +RGASBR,RSHEAT,1.0,fixed,,true +RELCHP,ELCTRI,-0.33,fixed,,true +RELCHP,RSHEAT,1.0,fixed,,false +GASDRV,CO2EMT,5.113,fixed,,false +GASPRC,CO2EMT,2.5565,fixed,,false +GASCGT,CO2EMT,76.695,fixed,,false +RGASBR,CO2EMT,58.7995,fixed,,false diff --git a/examples/simple/process_pacs.csv b/examples/simple/process_pacs.csv deleted file mode 100644 index 73b3a7117..000000000 --- a/examples/simple/process_pacs.csv +++ /dev/null @@ -1,7 +0,0 @@ -process_id,commodity_id -GASDRV,GASPRD -GASPRC,GASNAT -WNDFRM,ELCTRI -GASCGT,ELCTRI -RGASBR,RSHEAT -RELCHP,RSHEAT diff --git a/src/input/asset.rs b/src/input/asset.rs index c11912100..42cf9e39d 100644 --- a/src/input/asset.rs +++ b/src/input/asset.rs @@ -114,7 +114,6 @@ mod tests { description: "Description".into(), availabilities: vec![], flows: vec![], - pacs: vec![], parameter: process_param.clone(), regions: RegionSelection::All, }); @@ -190,7 +189,6 @@ mod tests { description: "Description".into(), availabilities: vec![], flows: vec![], - pacs: 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 199c08d44..0b1b91c95 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -14,8 +14,6 @@ pub mod availability; use availability::read_process_availabilities; pub mod flow; use flow::read_process_flows; -pub mod pac; -use pac::read_process_pacs; pub mod parameter; use parameter::read_process_parameters; pub mod region; @@ -70,7 +68,6 @@ pub fn read_processes( let availabilities = read_process_availabilities(model_dir, &process_ids, time_slice_info)?; let flows = read_process_flows(model_dir, &process_ids, commodities)?; - let pacs = read_process_pacs(model_dir, &process_ids, commodities, &flows)?; let parameters = read_process_parameters(model_dir, &process_ids, year_range)?; let regions = read_process_regions(model_dir, &process_ids, region_ids)?; @@ -78,7 +75,6 @@ pub fn read_processes( descriptions.into_values(), availabilities, flows, - pacs, parameters, regions, ) @@ -88,7 +84,6 @@ fn create_process_map( descriptions: I, availabilities: GroupedMap, flows: GroupedMap, - pacs: GroupedMap>, parameters: HashMap, ProcessParameter>, regions: HashMap, RegionSelection>, ) -> Result, Rc>> @@ -98,7 +93,6 @@ where // Need to be mutable as we remove elements as we go along let mut availabilities = availabilities; let mut flows = flows; - let mut pacs = pacs; let mut parameters = parameters; let mut regions = regions; @@ -111,9 +105,6 @@ where let flows = flows .remove(id) .with_context(|| format!("No commodity flows defined for process {id}"))?; - let pacs = pacs - .remove(id) - .with_context(|| format!("No PACs defined for process {id}"))?; let parameter = parameters .remove(id) .with_context(|| format!("No parameters defined for process {id}"))?; @@ -126,7 +117,6 @@ where description: description.description, availabilities, flows, - pacs, parameter, regions, }; @@ -144,7 +134,6 @@ mod tests { descriptions: Vec, availabilities: GroupedMap, flows: GroupedMap, - pacs: GroupedMap>, parameters: HashMap, ProcessParameter>, regions: HashMap, RegionSelection>, } @@ -172,11 +161,6 @@ mod tests { .map(|id| (id.into(), vec![])) .collect(); - let pacs = ["process1", "process2"] - .into_iter() - .map(|id| (id.into(), vec![])) - .collect(); - let parameters = ["process1", "process2"] .into_iter() .map(|id| { @@ -204,7 +188,6 @@ mod tests { descriptions, availabilities, flows, - pacs, parameters, regions, } @@ -217,7 +200,6 @@ mod tests { data.descriptions.into_iter(), data.availabilities, data.flows, - data.pacs, data.parameters, data.regions, ) @@ -238,7 +220,6 @@ mod tests { data.descriptions.into_iter(), data.availabilities, data.flows, - data.pacs, data.parameters, data.regions, ); @@ -251,11 +232,6 @@ mod tests { test_missing!(availabilities); } - #[test] - fn test_create_process_map_missing_pacs() { - test_missing!(pacs); - } - #[test] fn test_create_process_map_missing_flows() { test_missing!(flows); diff --git a/src/input/process/flow.rs b/src/input/process/flow.rs index d2f835ea9..f5d164f3f 100644 --- a/src/input/process/flow.rs +++ b/src/input/process/flow.rs @@ -22,6 +22,7 @@ struct ProcessFlowRaw { #[serde(default)] flow_type: FlowType, flow_cost: Option, + is_pac: bool, } define_process_id_getter! {ProcessFlowRaw} @@ -46,42 +47,81 @@ fn read_process_flows_from_iter( where I: Iterator, { - iter.map(|flow| -> Result { - let commodity = commodities - .get(flow.commodity_id.as_str()) - .with_context(|| format!("{} is not a valid commodity ID", &flow.commodity_id))?; + let flows = iter + .map(|flow| -> Result { + let commodity = commodities + .get(flow.commodity_id.as_str()) + .with_context(|| format!("{} is not a valid commodity ID", &flow.commodity_id))?; - ensure!(flow.flow != 0.0, "Flow cannot be zero"); + ensure!(flow.flow != 0.0, "Flow cannot be zero"); - // Check that flow is not infinity, nan, etc. - ensure!( - flow.flow.is_normal(), - "Invalid value for flow ({})", - flow.flow - ); - - // **TODO**: https://github.com/EnergySystemsModellingLab/MUSE_2.0/issues/300 - ensure!( - flow.flow_type == FlowType::Fixed, - "Commodity flexible assets are not currently supported" - ); + // Check that flow is not infinity, nan, etc. + ensure!( + flow.flow.is_normal(), + "Invalid value for flow ({})", + flow.flow + ); - if let Some(flow_cost) = flow.flow_cost { + // **TODO**: https://github.com/EnergySystemsModellingLab/MUSE_2.0/issues/300 ensure!( - (0.0..f64::INFINITY).contains(&flow_cost), - "Invalid value for flow cost ({flow_cost}). Must be >=0." - ) - } + flow.flow_type == FlowType::Fixed, + "Commodity flexible assets are not currently supported" + ); - Ok(ProcessFlow { - process_id: flow.process_id, - commodity: Rc::clone(commodity), - flow: flow.flow, - flow_type: flow.flow_type, - flow_cost: flow.flow_cost.unwrap_or(0.0), + if let Some(flow_cost) = flow.flow_cost { + ensure!( + (0.0..f64::INFINITY).contains(&flow_cost), + "Invalid value for flow cost ({flow_cost}). Must be >=0." + ) + } + + Ok(ProcessFlow { + process_id: flow.process_id, + commodity: Rc::clone(commodity), + flow: flow.flow, + flow_type: flow.flow_type, + flow_cost: flow.flow_cost.unwrap_or(0.0), + is_pac: flow.is_pac, + }) }) - }) - .process_results(|iter| iter.into_id_map(process_ids))? + .process_results(|iter| iter.into_id_map(process_ids))??; + + validate_pac_flows(&flows)?; + + Ok(flows) +} + +/// Validate that the PACs for each process are either all inputs or all outputs. +/// +/// # Arguments +/// +/// * `flows` - A map of process IDs to process flows +/// +/// # Returns +/// An `Ok(())` if the check is successful, or an error. +fn validate_pac_flows(flows: &HashMap, Vec>) -> Result<()> { + for (process_id, flows) in flows.iter() { + let mut flow_sign: Option = None; // False for inputs, true for outputs + + for flow in flows.iter().filter(|flow| flow.is_pac) { + // Check that flow sign is consistent + let current_flow_sign = flow.flow > 0.0; + if let Some(flow_sign) = flow_sign { + ensure!( + current_flow_sign == flow_sign, + "PACs for process {process_id} are a mix of inputs and outputs", + ); + } + flow_sign = Some(current_flow_sign); + } + + ensure!( + flow_sign.is_some(), + "No PACs defined for process {process_id}" + ); + } + + Ok(()) } #[cfg(test)] @@ -117,6 +157,7 @@ mod tests { flow: 1.0, flow_type: FlowType::Fixed, flow_cost: Some(1.0), + is_pac: true, }, ProcessFlowRaw { process_id: "id1".into(), @@ -124,6 +165,7 @@ mod tests { flow: 1.0, flow_type: FlowType::Fixed, flow_cost: Some(1.0), + is_pac: false, }, ProcessFlowRaw { process_id: "id2".into(), @@ -131,6 +173,7 @@ mod tests { flow: 1.0, flow_type: FlowType::Fixed, flow_cost: Some(1.0), + is_pac: true, }, ]; @@ -144,6 +187,7 @@ mod tests { flow: 1.0, flow_type: FlowType::Fixed, flow_cost: 1.0, + is_pac: true, }, ProcessFlow { process_id: "id1".into(), @@ -151,6 +195,7 @@ mod tests { flow: 1.0, flow_type: FlowType::Fixed, flow_cost: 1.0, + is_pac: false, }, ], ), @@ -162,6 +207,7 @@ mod tests { flow: 1.0, flow_type: FlowType::Fixed, flow_cost: 1.0, + is_pac: true, }], ), ]); @@ -198,6 +244,7 @@ mod tests { flow: 1.0, flow_type: FlowType::Fixed, flow_cost: Some(1.0), + is_pac: true, }, ProcessFlowRaw { process_id: "id1".into(), @@ -205,6 +252,7 @@ mod tests { flow: 1.0, flow_type: FlowType::Fixed, flow_cost: Some(1.0), + is_pac: false, }, ]; @@ -236,6 +284,7 @@ mod tests { flow: $flow, flow_type: FlowType::Fixed, flow_cost: Some(1.0), + is_pac: true, }; assert!( read_process_flows_from_iter(iter::once(flow), &process_ids, &commodities) @@ -250,6 +299,94 @@ mod tests { check_bad_flow!(f64::NAN); } + #[test] + fn test_read_process_flows_from_iter_bad_pacs() { + let process_ids = ["id1".into(), "id2".into()].into_iter().collect(); + let commodities = ["commodity1", "commodity2"] + .into_iter() + .map(|id| { + let commodity = Commodity { + id: id.into(), + description: "Some description".into(), + kind: CommodityType::InputCommodity, + time_slice_level: TimeSliceLevel::Annual, + costs: CommodityCostMap::new(), + demand: DemandMap::new(), + }; + + (Rc::clone(&commodity.id), commodity.into()) + }) + .collect(); + + let flows_raw = [ + ProcessFlowRaw { + process_id: "id1".into(), + commodity_id: "commodity1".into(), + flow: 1.0, + flow_type: FlowType::Fixed, + flow_cost: Some(1.0), + is_pac: true, + }, + ProcessFlowRaw { + process_id: "id1".into(), + commodity_id: "commodity2".into(), + flow: -1.0, + flow_type: FlowType::Fixed, + flow_cost: Some(1.0), + is_pac: true, + }, + ]; + + assert!( + read_process_flows_from_iter(flows_raw.into_iter(), &process_ids, &commodities) + .is_err() + ); + } + + #[test] + fn test_read_process_flows_from_iter_no_pacs() { + let process_ids = ["id1".into(), "id2".into()].into_iter().collect(); + let commodities = ["commodity1", "commodity2"] + .into_iter() + .map(|id| { + let commodity = Commodity { + id: id.into(), + description: "Some description".into(), + kind: CommodityType::InputCommodity, + time_slice_level: TimeSliceLevel::Annual, + costs: CommodityCostMap::new(), + demand: DemandMap::new(), + }; + + (Rc::clone(&commodity.id), commodity.into()) + }) + .collect(); + + let flows_raw = [ + ProcessFlowRaw { + process_id: "id1".into(), + commodity_id: "commodity1".into(), + flow: 1.0, + flow_type: FlowType::Fixed, + flow_cost: Some(1.0), + is_pac: false, + }, + ProcessFlowRaw { + process_id: "id1".into(), + commodity_id: "commodity2".into(), + flow: 1.0, + flow_type: FlowType::Fixed, + flow_cost: Some(1.0), + is_pac: false, + }, + ]; + + assert!( + read_process_flows_from_iter(flows_raw.into_iter(), &process_ids, &commodities) + .is_err() + ); + } + #[test] fn test_read_process_flows_from_iter_flow_cost() { let process_ids = iter::once("id1".into()).collect(); @@ -272,6 +409,7 @@ mod tests { flow: 1.0, flow_type: FlowType::Fixed, flow_cost: Some($flow_cost), + is_pac: true, }; read_process_flows_from_iter(iter::once(flow), &process_ids, &commodities).is_ok() diff --git a/src/input/process/pac.rs b/src/input/process/pac.rs deleted file mode 100644 index 44eea9c1f..000000000 --- a/src/input/process/pac.rs +++ /dev/null @@ -1,261 +0,0 @@ -//! Code for reading Primary Activity Commodities (PACs) file -use super::define_process_id_getter; -use crate::commodity::Commodity; -use crate::input::*; -use crate::process::ProcessFlow; -use anyhow::{ensure, Context, Result}; -use itertools::Itertools; -use serde::Deserialize; -use std::collections::{HashMap, HashSet}; -use std::path::Path; -use std::rc::Rc; - -const PROCESS_PACS_FILE_NAME: &str = "process_pacs.csv"; - -/// Primary Activity Commodity -#[derive(PartialEq, Clone, Eq, Hash, Debug, Deserialize)] -struct ProcessPAC { - process_id: String, - commodity_id: String, -} -define_process_id_getter! {ProcessPAC} - -/// Read process Primary Activity Commodities (PACs) from the specified model directory. -/// -/// # Arguments -/// -/// * `model_dir` - Folder containing model configuration files -/// * `process_ids` - All possible process IDs -/// * `commodities` - Commodities for the model -pub fn read_process_pacs( - model_dir: &Path, - process_ids: &HashSet>, - commodities: &HashMap, Rc>, - flows: &HashMap, Vec>, -) -> Result, Vec>>> { - let file_path = model_dir.join(PROCESS_PACS_FILE_NAME); - let process_pacs_csv = read_csv(&file_path)?; - read_process_pacs_from_iter(process_pacs_csv, process_ids, commodities, flows) - .with_context(|| input_err_msg(&file_path)) -} - -/// Read process Primary Activity Commodities (PACs) from an iterator. -/// -/// # Arguments -/// -/// * `iter` - An iterator of `ProcessPAC`s -/// * `process_ids` - All possible process IDs -/// * `commodities` - Commodities for the model -/// -/// # Returns -/// -/// A `HashMap` with process IDs as keys and `Vec`s of commodities as values or an error. -fn read_process_pacs_from_iter( - iter: I, - process_ids: &HashSet>, - commodities: &HashMap, Rc>, - flows: &HashMap, Vec>, -) -> Result, Vec>>> -where - I: Iterator, -{ - // Keep track of previous PACs so we can check for duplicates - let mut existing_pacs = HashSet::new(); - - // Build hashmap of process ID to PAC commodities - let pacs = iter - .map(|pac| { - let process_id = process_ids.get_id(&pac.process_id)?; - let commodity = commodities - .get(pac.commodity_id.as_str()) - .with_context(|| format!("{} is not a valid commodity ID", &pac.commodity_id))?; - - // Check that commodity is valid and PAC is not a duplicate - ensure!(existing_pacs.insert(pac), "Duplicate PACs found"); - Ok((process_id, Rc::clone(commodity))) - }) - .process_results(|iter| iter.into_group_map())?; - - // Check that PACs for each process are either all inputs or all outputs - validate_pac_flows(&pacs, flows)?; - - // Return result - Ok(pacs) -} - -/// Validate that the PACs for each process are either all inputs or all outputs. -/// -/// # Arguments -/// -/// * `pacs` - A map of process IDs to PAC commodities -/// * `flows` - A map of process IDs to process flows -/// -/// # Returns -/// An `Ok(())` if the check is successful, or an error. -fn validate_pac_flows( - pacs: &HashMap, Vec>>, - flows: &HashMap, Vec>, -) -> Result<()> { - for (process_id, pacs) in pacs.iter() { - // Get the flows for the process (unwrap is safe as every process has associated flows) - let flows = flows.get(process_id).unwrap(); - - let mut flow_sign: Option = None; // False for inputs, true for outputs - for pac in pacs.iter() { - // Find the flow associated with the PAC - let flow = flows - .iter() - .find(|item| *item.commodity.id == *pac.id) - .with_context(|| { - format!( - "PAC {} for process {} must have an associated flow", - pac.id, process_id - ) - })?; - - // Check that flow sign is consistent - let current_flow_sign = flow.flow > 0.0; - if let Some(flow_sign) = flow_sign { - ensure!( - current_flow_sign == flow_sign, - "PACs for process {} are a mix of inputs and outputs", - process_id - ); - } - flow_sign = Some(current_flow_sign); - } - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::commodity::{CommodityCostMap, CommodityType, DemandMap}; - use crate::process::FlowType; - use crate::time_slice::TimeSliceLevel; - - #[test] - fn test_read_process_pacs_from_iter() { - // Prepare test data - let process_ids = ["id1".into(), "id2".into()].into_iter().collect(); - let commodities: HashMap, Rc> = ["commodity1", "commodity2"] - .into_iter() - .map(|id| { - let commodity = Commodity { - id: id.into(), - description: "Some description".into(), - kind: CommodityType::InputCommodity, - time_slice_level: TimeSliceLevel::Annual, - costs: CommodityCostMap::new(), - demand: DemandMap::new(), - }; - (Rc::clone(&commodity.id), commodity.into()) - }) - .collect(); - let flows: HashMap, Vec> = ["id1", "id2"] - .into_iter() - .map(|process_id| { - ( - process_id.into(), - ["commodity1", "commodity2"] - .into_iter() - .map(|commodity_id| ProcessFlow { - process_id: process_id.into(), - commodity: commodities.get(commodity_id).unwrap().clone(), - flow: 1.0, - flow_type: FlowType::Fixed, - flow_cost: 1.0, - }) - .collect(), - ) - }) - .collect(); - - // duplicate PAC - let pac = ProcessPAC { - process_id: "id1".into(), - commodity_id: "commodity1".into(), - }; - let pacs = [pac.clone(), pac]; - assert!( - read_process_pacs_from_iter(pacs.into_iter(), &process_ids, &commodities, &flows) - .is_err() - ); - - // invalid commodity ID - let bad_pac = ProcessPAC { - process_id: "id1".into(), - commodity_id: "other_commodity".into(), - }; - assert!(read_process_pacs_from_iter( - [bad_pac].into_iter(), - &process_ids, - &commodities, - &flows - ) - .is_err()); - - // Valid - let pacs = [ - ProcessPAC { - process_id: "id1".into(), - commodity_id: "commodity1".into(), - }, - ProcessPAC { - process_id: "id1".into(), - commodity_id: "commodity2".into(), - }, - ProcessPAC { - process_id: "id2".into(), - commodity_id: "commodity1".into(), - }, - ]; - let expected = [ - ( - "id1".into(), - [ - commodities.get("commodity1").unwrap(), - commodities.get("commodity2").unwrap(), - ] - .into_iter() - .cloned() - .collect(), - ), - ( - "id2".into(), - [commodities.get("commodity1").unwrap()] - .into_iter() - .cloned() - .collect(), - ), - ] - .into_iter() - .collect(); - assert!( - read_process_pacs_from_iter( - pacs.clone().into_iter(), - &process_ids, - &commodities, - &flows - ) - .unwrap() - == expected - ); - - // Invalid flows - // Making commodity1 an input so the PACs for process id1 are a mix of inputs and outputs - let mut flows = flows.clone(); - flows - .get_mut(&Rc::from("id1")) - .unwrap() - .iter_mut() - .find(|flow| flow.commodity.id == "commodity1".into()) - .unwrap() - .flow = -1.0; - assert!( - read_process_pacs_from_iter(pacs.into_iter(), &process_ids, &commodities, &flows) - .is_err() - ); - } -} diff --git a/src/process.rs b/src/process.rs index 596cb22ed..21a1b37e7 100644 --- a/src/process.rs +++ b/src/process.rs @@ -13,21 +13,27 @@ pub struct Process { pub description: String, pub availabilities: Vec, pub flows: Vec, - pub pacs: Vec>, pub parameter: ProcessParameter, pub regions: RegionSelection, } +impl Process { + /// Iterate over this process's Primary Activity Commodity flows + pub fn iter_pacs(&self) -> impl Iterator { + self.flows.iter().filter(|flow| flow.is_pac) + } +} + /// The availabilities for a process over time slices #[derive(PartialEq, Debug)] pub struct ProcessAvailability { - /// Unique identifier for the process (typically uses a structured naming convention). + /// Unique identifier for the process pub process_id: String, - /// The limit type – lower bound, upper bound or equality. + /// The limit type - lower bound, upper bound or equality pub limit_type: LimitType, - /// The time slice to which the availability applies. + /// The time slice to which the availability applies pub time_slice: TimeSliceSelection, - /// The availability value, between 0 and 1 inclusive. + /// The availability value, between 0 and 1 inclusive pub value: f64, } @@ -43,26 +49,35 @@ pub enum LimitType { #[derive(PartialEq, Debug, Deserialize, Clone)] pub struct ProcessFlow { - /// A unique identifier for the process (typically uses a structured naming convention). + /// A unique identifier for the process pub process_id: String, /// Identifies the commodity for the specified flow pub commodity: Rc, - /// Commodity flow quantity relative to other commodity flows. +ve value indicates flow out, -ve value indicates flow in. + /// Commodity flow quantity relative to other commodity flows. + /// + /// Positive value indicates flow out and negative value indicates flow in. pub flow: f64, /// Identifies if a flow is fixed or flexible. pub flow_type: FlowType, - /// Cost per unit flow. For example, cost per unit of natural gas produced. Differs from var_opex because the user can apply it to any specified flow, whereas var_opex applies to pac flow. + /// 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. pub flow_cost: f64, + /// Whether this flow represents a Primary Activity Commodity + pub is_pac: bool, } #[derive(PartialEq, Default, Debug, Clone, DeserializeLabeledStringEnum)] pub enum FlowType { #[default] #[string = "fixed"] - /// The input to output flow ratio is fixed. + /// The input to output flow ratio is fixed Fixed, #[string = "flexible"] - /// The flow ratio can vary, subject to overall flow of a specified group of commodities whose input/output ratio must be as per user input data. + /// The flow ratio can vary, subject to overall flow of a specified group of commodities whose + /// input/output ratio must be as per user input data Flexible, }