From 213336f0aabfd45a7ea8071077a15b397eb7fec7 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Mon, 23 Jun 2025 11:32:51 +0100 Subject: [PATCH] Add `is_primary_output` column to `process_flows.csv` Closes #630. --- examples/simple/process_flows.csv | 30 +-- examples/simple_mc/process_flows.csv | 40 ++-- src/input/process.rs | 12 +- src/input/process/flow.rs | 337 +++++++++++++++++++++------ src/process.rs | 33 ++- 5 files changed, 335 insertions(+), 117 deletions(-) diff --git a/examples/simple/process_flows.csv b/examples/simple/process_flows.csv index 88c03c51a..7b82aa0cc 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 -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, +process_id,commodity_id,regions,years,coeff,type,cost,is_primary_output +GASDRV,GASPRD,all,all,1.0,fixed,,true +GASPRC,GASPRD,all,all,-1.05,fixed,, +GASPRC,GASNAT,all,all,1.0,fixed,,true +WNDFRM,ELCTRI,all,all,1.0,fixed,, +GASCGT,GASNAT,all,all,-1.5,fixed,, +GASCGT,ELCTRI,all,all,1.0,fixed,,true +RGASBR,GASNAT,all,all,-1.15,fixed,, +RGASBR,RSHEAT,all,all,1.0,fixed,,true +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/examples/simple_mc/process_flows.csv b/examples/simple_mc/process_flows.csv index 60494622f..9f0bada6e 100644 --- a/examples/simple_mc/process_flows.csv +++ b/examples/simple_mc/process_flows.csv @@ -1,20 +1,20 @@ -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, -BIOPRO,BIOPRD,all,all,1.0,fixed, -BIOPLL,BIOPRD,all,all,-1.05,fixed, -BIOPLL,BIOPEL,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, -RBIOBL,BIOPEL,all,all,-1.2,fixed, -RBIOBL,RSHEAT,all,all,1,fixed, +process_id,commodity_id,regions,years,coeff,type,cost,is_primary_output +GASDRV,GASPRD,all,all,1.0,fixed,,true +GASPRC,GASPRD,all,all,-1.05,fixed,, +GASPRC,GASNAT,all,all,1.0,fixed,,true +BIOPRO,BIOPRD,all,all,1.0,fixed,, +BIOPLL,BIOPRD,all,all,-1.05,fixed,, +BIOPLL,BIOPEL,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,,true +RGASBR,GASNAT,all,all,-1.15,fixed,, +RGASBR,RSHEAT,all,all,1.0,fixed,,true +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,, +RBIOBL,BIOPEL,all,all,-1.2,fixed,, +RBIOBL,RSHEAT,all,all,1,fixed,, diff --git a/src/input/process.rs b/src/input/process.rs index d3d83dc71..65f346968 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -341,6 +341,7 @@ mod tests { coeff: EnergyPerActivity(-10.0), kind: FlowType::Fixed, cost: MoneyPerEnergy(1.0), + is_primary_output: false, }}, )]) } @@ -354,6 +355,7 @@ mod tests { coeff: EnergyPerActivity(10.0), kind: FlowType::Fixed, cost: MoneyPerEnergy(1.0), + is_primary_output: false, }}, )]) } @@ -421,6 +423,7 @@ mod tests { coeff: EnergyPerActivity(10.0), kind: FlowType::Fixed, cost: MoneyPerEnergy(1.0), + is_primary_output: false, }}, )]), )]) @@ -504,7 +507,8 @@ mod tests { commodity: commodity_other.into(), coeff: EnergyPerActivity(10.0), kind: FlowType::Fixed, - cost: MoneyPerEnergy(1.0) + cost: MoneyPerEnergy(1.0), + is_primary_output: false, }}, )]) } @@ -517,7 +521,8 @@ mod tests { commodity: commodity_other.into(), coeff: EnergyPerActivity(-10.0), kind: FlowType::Fixed, - cost: MoneyPerEnergy(1.0) + cost: MoneyPerEnergy(1.0), + is_primary_output: false, }}, )]) } @@ -593,7 +598,8 @@ mod tests { commodity: Rc::clone(&commodity_svd), coeff: EnergyPerActivity(-10.0), kind: FlowType::Fixed, - cost: MoneyPerEnergy(1.0) + cost: MoneyPerEnergy(1.0), + is_primary_output: false, }}, )]), ))); diff --git a/src/input/process/flow.rs b/src/input/process/flow.rs index 0f56c28bd..deddaa040 100644 --- a/src/input/process/flow.rs +++ b/src/input/process/flow.rs @@ -1,11 +1,12 @@ //! Code for reading process flows file use super::super::*; -use crate::commodity::CommodityMap; -use crate::process::{FlowType, Process, ProcessFlow, ProcessFlowsMap, ProcessID, ProcessMap}; -use crate::region::parse_region_str; +use crate::commodity::{CommodityID, CommodityMap}; +use crate::process::{FlowType, ProcessFlow, ProcessFlowsMap, ProcessID, ProcessMap}; +use crate::region::{parse_region_str, RegionID}; use crate::units::{EnergyPerActivity, MoneyPerEnergy}; use crate::year::parse_year_str; use anyhow::{ensure, Context, Result}; +use itertools::iproduct; use serde::Deserialize; use std::collections::HashMap; use std::path::Path; @@ -13,6 +14,10 @@ use std::rc::Rc; const PROCESS_FLOWS_FILE_NAME: &str = "process_flows.csv"; +type PrimaryOutputsKeys = (ProcessID, RegionID, u32); +type PrimaryOutputsValues = Vec<(CommodityID, Option)>; +type PrimaryOutputsMap = HashMap; + #[derive(PartialEq, Debug, Deserialize)] struct ProcessFlowRaw { process_id: String, @@ -24,6 +29,7 @@ struct ProcessFlowRaw { #[serde(rename = "type")] kind: FlowType, cost: Option, + is_primary_output: Option, } impl ProcessFlowRaw { @@ -74,7 +80,8 @@ fn read_process_flows_from_iter( where I: Iterator, { - let mut map: HashMap = HashMap::new(); + let mut flows_map: HashMap = HashMap::new(); + let mut primary_outputs = PrimaryOutputsMap::new(); for record in iter { record.validate()?; @@ -105,64 +112,147 @@ where let process_flow = ProcessFlow { commodity: Rc::clone(commodity), coeff: record.coeff, - kind: record.kind, + kind: FlowType::Fixed, cost: record.cost.unwrap_or(MoneyPerEnergy(0.0)), + is_primary_output: false, // set to false for now and we'll fix up later }; // Insert flow into the map - let region_year_map = map.entry(id.clone()).or_default(); - for year in record_years { - for region in record_regions.iter() { - let flows_map = region_year_map.entry((region.clone(), year)).or_default(); - let existing = flows_map - .insert(commodity.id.clone(), process_flow.clone()) - .is_some(); - ensure!( - !existing, - "Duplicate process flow entry for region {}, year {} and commodity {}", - region, - year, - commodity.id - ); - } + let region_year_map = flows_map.entry(id.clone()).or_default(); + for (year, region_id) in iproduct!(record_years, record_regions.iter()) { + let flows_map = region_year_map + .entry((region_id.clone(), year)) + .or_default(); + let existing = flows_map + .insert(commodity.id.clone(), process_flow.clone()) + .is_some(); + ensure!( + !existing, + "Duplicate process flow entry for region {}, year {} and commodity {}", + region_id, + year, + commodity.id + ); + + primary_outputs + .entry((id.clone(), region_id.clone(), year)) + .or_insert_with(|| Vec::with_capacity(1)) + .push((commodity.id.clone(), record.is_primary_output)) } } - for (process_id, map) in map.iter_mut() { - let process = processes.get(process_id).unwrap(); - validate_process_flows_map(process, map)?; - } + validate_flows_and_update_primary_output(processes, &mut flows_map, &primary_outputs)?; - Ok(map) + Ok(flows_map) } -/// Validate flows for a process -fn validate_process_flows_map(process: &Process, map: &ProcessFlowsMap) -> Result<()> { - let process_id = process.id.clone(); - for year in process.years.iter() { - for region in process.regions.iter() { +fn validate_flows_and_update_primary_output( + processes: &ProcessMap, + flows_map: &mut HashMap, + primary_outputs: &PrimaryOutputsMap, +) -> Result<()> { + for (process_id, map) in flows_map.iter_mut() { + let process = processes.get(process_id).unwrap(); + for (&year, region_id) in iproduct!(process.years.iter(), process.regions.iter()) { // Check that the process has flows for this region/year - ensure!( - map.contains_key(&(region.clone(), *year)), - "Missing entry for process {process_id} in {region}/{year}" - ); + let Some(flows) = map.get_mut(&(region_id.clone(), year)) else { + bail!("Missing entry for process {process_id} in {region_id}/{year}"); + }; + + let primary_outputs = primary_outputs + .get(&(process_id.clone(), region_id.clone(), year)) + .unwrap(); + + let inferred_primary_output = validate_or_infer_primary_output(flows, primary_outputs) + .with_context(|| { + format!( + "Invalid primary output configuration for process {} (region: {}, year: {})", + process_id, region_id, year + ) + })?; + + // The primary output was inferred (i.e. there was one output flow which wasn't assigned + // a value for is_primary_output). Update map. + if let Some(primary_output) = inferred_primary_output { + flows.get_mut(&primary_output).unwrap().is_primary_output = true; + } } } + Ok(()) } +fn validate_or_infer_primary_output( + flows_map: &IndexMap, + primary_outputs: &PrimaryOutputsValues, +) -> Result> { + let mut has_primary = false; + let mut output_flow = None; + let mut outputs_count = 0; + for (commodity_id, is_primary_output) in primary_outputs.iter() { + let is_output = flows_map.get(commodity_id).unwrap().coeff > EnergyPerActivity(0.0); + if is_output { + outputs_count += 1; + } + match *is_primary_output { + Some(true) => { + ensure!( + is_output, + "Commodity {commodity_id} cannot be the primary output as it is an input flow" + ); + ensure!( + !has_primary, + "Multiple commodities designated as primary outputs" + ); + has_primary = true; + } + None if is_output => { + output_flow = Some(commodity_id.clone()); + } + _ => {} + } + } + + // If all flows are inputs or user has designated a primary output explicitly, we're done + if has_primary || outputs_count == 0 { + return Ok(None); + } + + ensure!( + output_flow.is_some(), + "There are one or more output flows, but is_primary_output is explicitly set to false for these"); + + ensure!( + outputs_count == 1, + "There is more than one output flow, so one must be explicitly designated as the primary output"); + + // We can infer that the one output flow is the primary output + Ok(output_flow) +} + #[cfg(test)] mod tests { use super::*; - use crate::commodity::{Commodity, CommodityLevyMap, CommodityType, DemandMap}; - use crate::time_slice::TimeSliceLevel; + use crate::commodity::Commodity; + use crate::fixture::svd_commodity; - use rstest::fixture; + use rstest::rstest; + use std::rc::Rc; + + fn flow(commodity: Rc, coeff: f64) -> ProcessFlow { + ProcessFlow { + commodity, + coeff: EnergyPerActivity(coeff), + kind: FlowType::Fixed, + cost: MoneyPerEnergy(0.0), + is_primary_output: false, + } + } fn create_process_flow_raw( coeff: EnergyPerActivity, - kind: FlowType, cost: Option, + is_primary_output: Option, ) -> ProcessFlowRaw { ProcessFlowRaw { process_id: "process".into(), @@ -170,8 +260,9 @@ mod tests { years: "2020".into(), regions: "region".into(), coeff, - kind, + kind: FlowType::Fixed, cost, + is_primary_output, } } @@ -180,79 +271,191 @@ mod tests { // Valid let valid = create_process_flow_raw( EnergyPerActivity(1.0), - FlowType::Fixed, Some(MoneyPerEnergy(0.0)), + Some(false), ); assert!(valid.validate().is_ok()); // Invalid: Bad flow value let invalid = create_process_flow_raw( EnergyPerActivity(0.0), - FlowType::Fixed, Some(MoneyPerEnergy(0.0)), + Some(false), ); assert!(invalid.validate().is_err()); let invalid = create_process_flow_raw( EnergyPerActivity(f64::NAN), - FlowType::Fixed, Some(MoneyPerEnergy(0.0)), + Some(false), ); assert!(invalid.validate().is_err()); let invalid = create_process_flow_raw( EnergyPerActivity(f64::INFINITY), - FlowType::Fixed, Some(MoneyPerEnergy(0.0)), + Some(false), ); assert!(invalid.validate().is_err()); let invalid = create_process_flow_raw( EnergyPerActivity(f64::NEG_INFINITY), - FlowType::Fixed, Some(MoneyPerEnergy(0.0)), + Some(false), ); assert!(invalid.validate().is_err()); // Invalid: Bad flow cost value let invalid = create_process_flow_raw( EnergyPerActivity(1.0), - FlowType::Fixed, Some(MoneyPerEnergy(f64::NAN)), + Some(false), ); assert!(invalid.validate().is_err()); let invalid = create_process_flow_raw( EnergyPerActivity(1.0), - FlowType::Fixed, Some(MoneyPerEnergy(f64::NEG_INFINITY)), + Some(false), ); assert!(invalid.validate().is_err()); let invalid = create_process_flow_raw( EnergyPerActivity(1.0), - FlowType::Fixed, Some(MoneyPerEnergy(f64::INFINITY)), + Some(false), ); assert!(invalid.validate().is_err()); } - #[fixture] - fn commodity1() -> Commodity { - Commodity { - id: "commodity1".into(), - description: "A commodity".into(), - kind: CommodityType::ServiceDemand, - demand: DemandMap::default(), - time_slice_level: TimeSliceLevel::Annual, - levies: CommodityLevyMap::default(), - } + #[rstest] + fn single_output_explicit_primary(#[from(svd_commodity)] commodity: Commodity) { + let c1 = Rc::new(commodity); + let mut flows = IndexMap::new(); + flows.insert("commodity1".into(), flow(Rc::clone(&c1), 1.0)); + let primary_outputs = vec![("commodity1".into(), Some(true))]; + let res = validate_or_infer_primary_output(&flows, &primary_outputs).unwrap(); + assert_eq!(res, None); } - #[fixture] - fn commodity2() -> Commodity { - Commodity { - id: "commodity2".into(), - description: "Another commodity".into(), - kind: CommodityType::ServiceDemand, - demand: DemandMap::default(), - time_slice_level: TimeSliceLevel::Annual, - levies: CommodityLevyMap::default(), - } + #[rstest] + fn multiple_outputs_one_explicit_primary( + #[from(svd_commodity)] commodity1: Commodity, + #[from(svd_commodity)] commodity2: Commodity, + ) { + let c1 = Rc::new(Commodity { + id: "c1".into(), + ..commodity1 + }); + let c2 = Rc::new(Commodity { + id: "c2".into(), + ..commodity2 + }); + let mut flows = IndexMap::new(); + flows.insert("c1".into(), flow(Rc::clone(&c1), 1.0)); + flows.insert("c2".into(), flow(Rc::clone(&c2), 2.0)); + let primary_outputs = vec![("c1".into(), Some(true)), ("c2".into(), None)]; + let res = validate_or_infer_primary_output(&flows, &primary_outputs).unwrap(); + assert_eq!(res, None); + } + + #[rstest] + fn multiple_outputs_none_explicit_should_error( + #[from(svd_commodity)] commodity1: Commodity, + #[from(svd_commodity)] commodity2: Commodity, + ) { + let c1 = Rc::new(Commodity { + id: "c1".into(), + ..commodity1 + }); + let c2 = Rc::new(Commodity { + id: "c2".into(), + ..commodity2 + }); + let mut flows = IndexMap::new(); + flows.insert("c1".into(), flow(Rc::clone(&c1), 1.0)); + flows.insert("c2".into(), flow(Rc::clone(&c2), 2.0)); + let primary_outputs = vec![("c1".into(), None), ("c2".into(), None)]; + let res = validate_or_infer_primary_output(&flows, &primary_outputs); + assert!(res.is_err()); + } + + #[rstest] + fn multiple_outputs_all_explicit_false_should_error( + #[from(svd_commodity)] commodity1: Commodity, + #[from(svd_commodity)] commodity2: Commodity, + ) { + let c1 = Rc::new(Commodity { + id: "c1".into(), + ..commodity1 + }); + let c2 = Rc::new(Commodity { + id: "c2".into(), + ..commodity2 + }); + let mut flows = IndexMap::new(); + flows.insert("c1".into(), flow(Rc::clone(&c1), 1.0)); + flows.insert("c2".into(), flow(Rc::clone(&c2), 2.0)); + let primary_outputs = vec![("c1".into(), Some(false)), ("c2".into(), Some(false))]; + let res = validate_or_infer_primary_output(&flows, &primary_outputs); + assert!(res.is_err()); + } + + #[rstest] + fn all_inputs( + #[from(svd_commodity)] commodity1: Commodity, + #[from(svd_commodity)] commodity2: Commodity, + ) { + let c1 = Rc::new(Commodity { + id: "c1".into(), + ..commodity1 + }); + let c2 = Rc::new(Commodity { + id: "c2".into(), + ..commodity2 + }); + let mut flows = IndexMap::new(); + flows.insert("c1".into(), flow(Rc::clone(&c1), -1.0)); + flows.insert("c2".into(), flow(Rc::clone(&c2), -2.0)); + let primary_outputs = vec![("c1".into(), None), ("c2".into(), None)]; + let res = validate_or_infer_primary_output(&flows, &primary_outputs).unwrap(); + assert_eq!(res, None); + } + + #[rstest] + fn multiple_outputs_multiple_explicit_primaries_should_error( + #[from(svd_commodity)] commodity1: Commodity, + #[from(svd_commodity)] commodity2: Commodity, + ) { + let c1 = Rc::new(Commodity { + id: "c1".into(), + ..commodity1 + }); + let c2 = Rc::new(Commodity { + id: "c2".into(), + ..commodity2 + }); + let mut flows = IndexMap::new(); + flows.insert("c1".into(), flow(Rc::clone(&c1), 1.0)); + flows.insert("c2".into(), flow(Rc::clone(&c2), 2.0)); + let primary_outputs = vec![("c1".into(), Some(true)), ("c2".into(), Some(true))]; + let res = validate_or_infer_primary_output(&flows, &primary_outputs); + assert!(res.is_err()); + } + + #[rstest] + fn explicit_primary_on_input_should_error( + #[from(svd_commodity)] commodity1: Commodity, + #[from(svd_commodity)] commodity2: Commodity, + ) { + let c1 = Rc::new(Commodity { + id: "c1".into(), + ..commodity1 + }); + let c2 = Rc::new(Commodity { + id: "c2".into(), + ..commodity2 + }); + let mut flows = IndexMap::new(); + flows.insert("c1".into(), flow(Rc::clone(&c1), -1.0)); + flows.insert("c2".into(), flow(Rc::clone(&c2), 2.0)); + let primary_outputs = vec![("c1".into(), Some(true)), ("c2".into(), None)]; + let res = validate_or_infer_primary_output(&flows, &primary_outputs); + assert!(res.is_err()); } } diff --git a/src/process.rs b/src/process.rs index 4ae9a4a53..747068aca 100644 --- a/src/process.rs +++ b/src/process.rs @@ -69,6 +69,8 @@ pub struct ProcessFlow { /// For example, cost per unit of natural gas produced. The user can apply it to any specified /// flow. pub cost: MoneyPerEnergy, + /// Whether this flow is the primary output for the process + pub is_primary_output: bool, } impl ProcessFlow { @@ -309,6 +311,7 @@ mod tests { coeff: EnergyPerActivity(1.0), kind: FlowType::Fixed, cost: MoneyPerEnergy(5.0), + is_primary_output: false, } } @@ -335,6 +338,7 @@ mod tests { coeff: EnergyPerActivity(1.0), kind: FlowType::Fixed, cost: MoneyPerEnergy(5.0), + is_primary_output: false, } } @@ -361,6 +365,7 @@ mod tests { coeff: EnergyPerActivity(1.0), kind: FlowType::Fixed, cost: MoneyPerEnergy(5.0), + is_primary_output: false, } } @@ -375,6 +380,7 @@ mod tests { coeff: EnergyPerActivity(1.0), kind: FlowType::Fixed, cost: MoneyPerEnergy(0.0), + is_primary_output: false, }; assert_eq!( @@ -394,6 +400,7 @@ mod tests { coeff: EnergyPerActivity(1.0), kind: FlowType::Fixed, cost: MoneyPerEnergy(0.0), + is_primary_output: false, }; assert_eq!( @@ -413,6 +420,7 @@ mod tests { coeff: EnergyPerActivity(1.0), kind: FlowType::Fixed, cost: MoneyPerEnergy(0.0), + is_primary_output: false, }; assert_eq!( @@ -428,6 +436,7 @@ mod tests { coeff: EnergyPerActivity(1.0), kind: FlowType::Fixed, cost: MoneyPerEnergy(0.0), + is_primary_output: false, }; assert_eq!( @@ -447,6 +456,7 @@ mod tests { coeff: EnergyPerActivity(1.0), kind: FlowType::Fixed, cost: MoneyPerEnergy(0.0), + is_primary_output: false, }; assert_eq!( @@ -462,6 +472,7 @@ mod tests { coeff: EnergyPerActivity(1.0), kind: FlowType::Fixed, cost: MoneyPerEnergy(0.0), + is_primary_output: false, }; let different_time_slice = TimeSliceID { @@ -486,6 +497,7 @@ mod tests { coeff: EnergyPerActivity(1.0), // Positive coefficient means production kind: FlowType::Fixed, cost: MoneyPerEnergy(0.0), + is_primary_output: false, }; assert_eq!( @@ -505,6 +517,7 @@ mod tests { coeff: EnergyPerActivity(-1.0), // Negative coefficient means consumption kind: FlowType::Fixed, cost: MoneyPerEnergy(0.0), + is_primary_output: false, }; assert_eq!( @@ -524,6 +537,7 @@ mod tests { coeff: EnergyPerActivity(1.0), // Positive coefficient means production kind: FlowType::Fixed, cost: MoneyPerEnergy(0.0), + is_primary_output: false, }; assert_eq!( @@ -543,6 +557,7 @@ mod tests { coeff: EnergyPerActivity(-1.0), // Negative coefficient means consumption kind: FlowType::Fixed, cost: MoneyPerEnergy(0.0), + is_primary_output: false, }; assert_eq!( @@ -589,32 +604,26 @@ mod tests { #[rstest] fn test_get_total_cost_negative_coeff( - flow_with_cost: ProcessFlow, + mut flow_with_cost: ProcessFlow, region_id: RegionID, time_slice: TimeSliceID, ) { - let flow = ProcessFlow { - coeff: EnergyPerActivity(-2.0), - ..flow_with_cost - }; + flow_with_cost.coeff = EnergyPerActivity(-2.0); assert_eq!( - flow.get_total_cost(®ion_id, 2020, &time_slice), + flow_with_cost.get_total_cost(®ion_id, 2020, &time_slice), MoneyPerActivity(10.0) ); } #[rstest] fn test_get_total_cost_zero_coeff( - flow_with_cost: ProcessFlow, + mut flow_with_cost: ProcessFlow, region_id: RegionID, time_slice: TimeSliceID, ) { - let flow = ProcessFlow { - coeff: EnergyPerActivity(0.0), - ..flow_with_cost - }; + flow_with_cost.coeff = EnergyPerActivity(0.0); assert_eq!( - flow.get_total_cost(®ion_id, 2020, &time_slice), + flow_with_cost.get_total_cost(®ion_id, 2020, &time_slice), MoneyPerActivity(0.0) ); }