From 45c18d52c0cf5b17b824b5421db79ee9636dd71c Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Thu, 30 Oct 2025 10:49:19 +0000 Subject: [PATCH 1/5] Allow flow coeff to be zero --- src/input/process/flow.rs | 4 ++-- src/process.rs | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/input/process/flow.rs b/src/input/process/flow.rs index b2452799e..2e2c0d26b 100644 --- a/src/input/process/flow.rs +++ b/src/input/process/flow.rs @@ -30,9 +30,9 @@ struct ProcessFlowRaw { impl ProcessFlowRaw { fn validate(&self) -> Result<()> { - // Check that flow is not infinity, nan, 0 etc. + // Check that flow is not infinity or nan. ensure!( - self.coeff.is_normal(), + self.coeff.is_finite(), "Invalid value for coeff ({})", self.coeff ); diff --git a/src/process.rs b/src/process.rs index 95667a40f..3ca34cfff 100644 --- a/src/process.rs +++ b/src/process.rs @@ -77,7 +77,8 @@ pub struct ProcessFlow { pub commodity: Rc, /// Maximum annual commodity flow quantity relative to other commodity flows. /// - /// Positive value indicates flow out and negative value indicates flow in. + /// Positive value indicates flow out and negative value indicates flow in. A value of zero is + /// both input and output. pub coeff: FlowPerActivity, /// Identifies if a flow is fixed or flexible. pub kind: FlowType, @@ -119,14 +120,14 @@ impl ProcessFlow { MoneyPerFlow(0.0) } - /// Returns true if this flow is an input (i.e., coeff < 0) + /// Returns true if this flow is an input (i.e., coeff <= 0) pub fn is_input(&self) -> bool { - self.coeff < FlowPerActivity(0.0) + self.coeff <= FlowPerActivity(0.0) } - /// Returns true if this flow is an output (i.e., coeff > 0) + /// Returns true if this flow is an output (i.e., coeff >= 0) pub fn is_output(&self) -> bool { - self.coeff > FlowPerActivity(0.0) + self.coeff >= FlowPerActivity(0.0) } } @@ -644,7 +645,7 @@ mod tests { assert!(!flow_in.is_output()); assert!(flow_out.is_output()); assert!(!flow_out.is_input()); - assert!(!flow_zero.is_input()); - assert!(!flow_zero.is_output()); + assert!(flow_zero.is_input()); + assert!(flow_zero.is_output()); } } From c5e9fdaf9d135de06cabbbdc0977cdb5f255fda9 Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Thu, 30 Oct 2025 11:18:08 +0000 Subject: [PATCH 2/5] Allow zero flow where relevant --- src/input/process/flow.rs | 18 +++++++++++------- src/process.rs | 20 ++++++++++++++------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/input/process/flow.rs b/src/input/process/flow.rs index 2e2c0d26b..85ed05787 100644 --- a/src/input/process/flow.rs +++ b/src/input/process/flow.rs @@ -189,9 +189,9 @@ fn validate_flows_and_update_primary_output( /// /// This is only possible if there is only one output flow for the process. fn infer_primary_output(map: &IndexMap) -> Result> { - let mut iter = map - .iter() - .filter_map(|(commodity_id, flow)| flow.is_output().then_some(commodity_id)); + let mut iter = map.iter().filter_map(|(commodity_id, flow)| { + (flow.is_output() || flow.is_zero()).then_some(commodity_id) + }); let Some(first_output) = iter.next() else { // If there are only input flows, then the primary output should be None @@ -217,12 +217,14 @@ fn check_flows_primary_output( })?; ensure!( - flow.is_output(), + flow.is_output() || flow.is_zero(), "Primary output commodity '{primary_output}' isn't an output flow", ); } else { ensure!( - flows_map.values().all(ProcessFlow::is_input), + flows_map + .values() + .all(|x| ProcessFlow::is_input(x) || ProcessFlow::is_zero(x)), "First year is only inputs, but subsequent years have outputs, although no primary \ output is specified" ); @@ -250,8 +252,10 @@ fn validate_secondary_flows( let flow = map[&(region_id.clone(), year)] .iter() .filter_map(|(commodity_id, flow)| { - (Some(commodity_id) != process.primary_output.as_ref()) - .then_some(((commodity_id.clone(), region_id.clone()), flow.is_input())) + (Some(commodity_id) != process.primary_output.as_ref()).then_some(( + (commodity_id.clone(), region_id.clone()), + flow.is_input() || flow.is_zero(), + )) }); for (key, value) in flow { diff --git a/src/process.rs b/src/process.rs index 3ca34cfff..5d5737698 100644 --- a/src/process.rs +++ b/src/process.rs @@ -120,14 +120,19 @@ impl ProcessFlow { MoneyPerFlow(0.0) } - /// Returns true if this flow is an input (i.e., coeff <= 0) + /// Returns true if this flow is an input (i.e., coeff < 0) pub fn is_input(&self) -> bool { - self.coeff <= FlowPerActivity(0.0) + self.coeff < FlowPerActivity(0.0) } - /// Returns true if this flow is an output (i.e., coeff >= 0) + /// Returns true if this flow is an output (i.e., coeff > 0) pub fn is_output(&self) -> bool { - self.coeff >= FlowPerActivity(0.0) + self.coeff > FlowPerActivity(0.0) + } + + /// Returns true if this flow is zero + pub fn is_zero(&self) -> bool { + self.coeff == FlowPerActivity(0.0) } } @@ -643,9 +648,12 @@ mod tests { assert!(flow_in.is_input()); assert!(!flow_in.is_output()); + assert!(!flow_in.is_zero()); assert!(flow_out.is_output()); assert!(!flow_out.is_input()); - assert!(flow_zero.is_input()); - assert!(flow_zero.is_output()); + assert!(!flow_out.is_zero()); + assert!(!flow_zero.is_input()); + assert!(!flow_zero.is_output()); + assert!(flow_zero.is_zero()); } } From 529675a83eda016b8210694ea3c86ef2da48e81e Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Thu, 30 Oct 2025 15:21:19 +0000 Subject: [PATCH 3/5] Use FlowDirection --- src/agent.rs | 6 ++-- src/asset.rs | 6 ++-- src/graph.rs | 6 ++-- src/input/process/flow.rs | 21 +++++++------- src/process.rs | 61 +++++++++++++++++++-------------------- src/simulation/prices.rs | 6 +++- 6 files changed, 57 insertions(+), 49 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index d7c0f2740..79b8d22e1 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -2,7 +2,7 @@ //! assets. use crate::commodity::CommodityID; use crate::id::define_id_type; -use crate::process::Process; +use crate::process::{FlowDirection, Process}; use crate::region::RegionID; use crate::units::{Dimensionless, Money}; use indexmap::{IndexMap, IndexSet}; @@ -63,7 +63,9 @@ impl Agent { let flows_key = (region_id.clone(), year); self.search_space[&(commodity_id.clone(), year)] .iter() - .filter(move |process| process.flows[&flows_key][commodity_id].is_output()) + .filter(move |process| { + process.flows[&flows_key][commodity_id].direction() == FlowDirection::Output + }) } } diff --git a/src/asset.rs b/src/asset.rs index 5e65da80f..e16516925 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1,7 +1,7 @@ //! Assets are instances of a process which are owned and invested in by agents. use crate::agent::AgentID; use crate::commodity::CommodityID; -use crate::process::{Process, ProcessFlow, ProcessID, ProcessParameter}; +use crate::process::{FlowDirection, Process, ProcessFlow, ProcessID, ProcessParameter}; use crate::region::RegionID; use crate::simulation::CommodityPrices; use crate::time_slice::TimeSliceID; @@ -338,7 +338,9 @@ impl Asset { input_prices: &CommodityPrices, time_slice: &TimeSliceID, ) -> MoneyPerActivity { - -self.get_revenue_from_flows_with_filter(input_prices, time_slice, ProcessFlow::is_input) + -self.get_revenue_from_flows_with_filter(input_prices, time_slice, |x| { + x.direction() == FlowDirection::Input + }) } /// Get the total revenue from a subset of flows. diff --git a/src/graph.rs b/src/graph.rs index f3672e19b..9a6327789 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,6 +1,6 @@ //! Module for creating and analysing commodity graphs use crate::commodity::{CommodityID, CommodityMap, CommodityType}; -use crate::process::{ProcessID, ProcessMap}; +use crate::process::{FlowDirection, ProcessID, ProcessMap}; use crate::region::RegionID; use crate::simulation::investment::InvestmentSet; use crate::time_slice::{TimeSliceInfo, TimeSliceLevel, TimeSliceSelection}; @@ -97,14 +97,14 @@ fn create_commodities_graph_for_region_year( // Get output nodes for the process let mut outputs: Vec<_> = flows .values() - .filter(|flow| flow.is_output()) + .filter(|flow| flow.direction() == FlowDirection::Output) .map(|flow| GraphNode::Commodity(flow.commodity.id.clone())) .collect(); // Get input nodes for the process let mut inputs: Vec<_> = flows .values() - .filter(|flow| flow.is_input()) + .filter(|flow| flow.direction() == FlowDirection::Input) .map(|flow| GraphNode::Commodity(flow.commodity.id.clone())) .collect(); diff --git a/src/input/process/flow.rs b/src/input/process/flow.rs index 85ed05787..bffec99d3 100644 --- a/src/input/process/flow.rs +++ b/src/input/process/flow.rs @@ -1,7 +1,9 @@ //! Code for reading process flows file use super::super::{input_err_msg, read_csv}; use crate::commodity::{CommodityID, CommodityMap}; -use crate::process::{FlowType, ProcessFlow, ProcessFlowsMap, ProcessID, ProcessMap}; +use crate::process::{ + FlowDirection, FlowType, ProcessFlow, ProcessFlowsMap, ProcessID, ProcessMap, +}; use crate::region::{RegionID, parse_region_str}; use crate::units::{FlowPerActivity, MoneyPerFlow}; use crate::year::parse_year_str; @@ -190,7 +192,7 @@ fn validate_flows_and_update_primary_output( /// This is only possible if there is only one output flow for the process. fn infer_primary_output(map: &IndexMap) -> Result> { let mut iter = map.iter().filter_map(|(commodity_id, flow)| { - (flow.is_output() || flow.is_zero()).then_some(commodity_id) + (flow.direction() == FlowDirection::Output).then_some(commodity_id) }); let Some(first_output) = iter.next() else { @@ -217,14 +219,15 @@ fn check_flows_primary_output( })?; ensure!( - flow.is_output() || flow.is_zero(), + flow.direction() == FlowDirection::Output, "Primary output commodity '{primary_output}' isn't an output flow", ); } else { ensure!( flows_map .values() - .all(|x| ProcessFlow::is_input(x) || ProcessFlow::is_zero(x)), + .all(|x| x.direction() == FlowDirection::Input + || x.direction() == FlowDirection::Zero), "First year is only inputs, but subsequent years have outputs, although no primary \ output is specified" ); @@ -247,15 +250,13 @@ fn validate_secondary_flows( // Get the non-primary io flows for all years, if any, arranged by (commodity, region) let iter = iproduct!(process.years.iter(), process.regions.iter()); - let mut flows: HashMap<(CommodityID, RegionID), Vec> = HashMap::new(); + let mut flows: HashMap<(CommodityID, RegionID), Vec<&ProcessFlow>> = HashMap::new(); for (&year, region_id) in iter { let flow = map[&(region_id.clone(), year)] .iter() .filter_map(|(commodity_id, flow)| { - (Some(commodity_id) != process.primary_output.as_ref()).then_some(( - (commodity_id.clone(), region_id.clone()), - flow.is_input() || flow.is_zero(), - )) + (Some(commodity_id) != process.primary_output.as_ref()) + .then_some(((commodity_id.clone(), region_id.clone()), flow)) }); for (key, value) in flow { @@ -272,7 +273,7 @@ fn validate_secondary_flows( does not cover all years" ); ensure!( - value.iter().all(|&x| x == value[0]), + value.iter().all(|&x| x.direction() == value[0].direction()), "Flow of commodity {commodity_id} in region {region_id} for process {process_id} \ behaves as input or output in different years." ); diff --git a/src/process.rs b/src/process.rs index 5d5737698..fac10e839 100644 --- a/src/process.rs +++ b/src/process.rs @@ -77,8 +77,7 @@ pub struct ProcessFlow { pub commodity: Rc, /// Maximum annual commodity flow quantity relative to other commodity flows. /// - /// Positive value indicates flow out and negative value indicates flow in. A value of zero is - /// both input and output. + /// Positive value indicates flow out and negative value indicates flow in. pub coeff: FlowPerActivity, /// Identifies if a flow is fixed or flexible. pub kind: FlowType, @@ -106,33 +105,28 @@ impl ProcessFlow { /// Get the levy/incentive for this process flow with the given parameters, if any fn get_levy(&self, region_id: &RegionID, year: u32, time_slice: &TimeSliceID) -> MoneyPerFlow { - if let Some(levy) = if self.is_input() { - self.commodity + match self.direction() { + FlowDirection::Input => *self + .commodity .levies_cons .get(&(region_id.clone(), year, time_slice.clone())) - } else { - self.commodity + .unwrap_or(&MoneyPerFlow(0.0)), + FlowDirection::Output => *self + .commodity .levies_prod .get(&(region_id.clone(), year, time_slice.clone())) - } { - return *levy; + .unwrap_or(&MoneyPerFlow(0.0)), + FlowDirection::Zero => MoneyPerFlow(0.0), } - MoneyPerFlow(0.0) } - /// Returns true if this flow is an input (i.e., coeff < 0) - pub fn is_input(&self) -> bool { - self.coeff < FlowPerActivity(0.0) - } - - /// Returns true if this flow is an output (i.e., coeff > 0) - pub fn is_output(&self) -> bool { - self.coeff > FlowPerActivity(0.0) - } - - /// Returns true if this flow is zero - pub fn is_zero(&self) -> bool { - self.coeff == FlowPerActivity(0.0) + /// Direction of the flow + pub fn direction(&self) -> FlowDirection { + match self.coeff { + x if x < FlowPerActivity(0.0) => FlowDirection::Input, + x if x > FlowPerActivity(0.0) => FlowDirection::Output, + _ => FlowDirection::Zero, + } } } @@ -149,6 +143,17 @@ pub enum FlowType { Flexible, } +/// Direction of the flow (see [`ProcessFlow`]) +#[derive(PartialEq, Debug)] +pub enum FlowDirection { + /// The flow is an input (i.e., coeff > 0) + Input, + /// The flow is an output (i.e., coeff > 0) + Output, + /// The flow is zero, neither input nor output (i.e., coeff == 0) + Zero, +} + /// Additional parameters for a process #[derive(PartialEq, Clone, Debug)] pub struct ProcessParameter { @@ -646,14 +651,8 @@ mod tests { cost: MoneyPerFlow(0.0), }; - assert!(flow_in.is_input()); - assert!(!flow_in.is_output()); - assert!(!flow_in.is_zero()); - assert!(flow_out.is_output()); - assert!(!flow_out.is_input()); - assert!(!flow_out.is_zero()); - assert!(!flow_zero.is_input()); - assert!(!flow_zero.is_output()); - assert!(flow_zero.is_zero()); + assert!(flow_in.direction() == FlowDirection::Input); + assert!(flow_out.direction() == FlowDirection::Output); + assert!(flow_zero.direction() == FlowDirection::Zero); } } diff --git a/src/simulation/prices.rs b/src/simulation/prices.rs index 6812e17f4..ebcb47bf9 100644 --- a/src/simulation/prices.rs +++ b/src/simulation/prices.rs @@ -2,6 +2,7 @@ use crate::asset::AssetRef; use crate::commodity::CommodityID; use crate::model::{Model, PricingStrategy}; +use crate::process::FlowDirection; use crate::region::RegionID; use crate::simulation::optimisation::Solution; use crate::time_slice::{TimeSliceID, TimeSliceInfo}; @@ -218,7 +219,10 @@ where let mut highest_duals = HashMap::new(); for (asset, time_slice, dual) in activity_duals { // Iterate over all output flows - for flow in asset.iter_flows().filter(|flow| flow.is_output()) { + for flow in asset + .iter_flows() + .filter(|flow| flow.direction() == FlowDirection::Output) + { // Update the highest dual for this commodity/time slice highest_duals .entry(( From e7242a4557d1bd1edab03c9eb089f5c9a62de183 Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Thu, 30 Oct 2025 15:29:46 +0000 Subject: [PATCH 4/5] Fix validation of secondaty --- src/input/process/flow.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/input/process/flow.rs b/src/input/process/flow.rs index bffec99d3..a80f2e6cb 100644 --- a/src/input/process/flow.rs +++ b/src/input/process/flow.rs @@ -272,8 +272,14 @@ fn validate_secondary_flows( "Flow of commodity {commodity_id} in region {region_id} for process {process_id} \ does not cover all years" ); + let input_or_zero = value + .iter() + .all(|&x| [FlowDirection::Input, FlowDirection::Zero].contains(&x.direction())); + let output_or_zero = value + .iter() + .all(|&x| [FlowDirection::Output, FlowDirection::Zero].contains(&x.direction())); ensure!( - value.iter().all(|&x| x.direction() == value[0].direction()), + input_or_zero || output_or_zero, "Flow of commodity {commodity_id} in region {region_id} for process {process_id} \ behaves as input or output in different years." ); From fe95ff9ea04e87e35d3fd8db2dedd015d2551c2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Alonso=20=C3=81lvarez?= <6095790+dalonsoa@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:56:06 +0000 Subject: [PATCH 5/5] Update src/process.rs Co-authored-by: Tom Bland --- src/process.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/process.rs b/src/process.rs index fac10e839..97c183eeb 100644 --- a/src/process.rs +++ b/src/process.rs @@ -146,7 +146,7 @@ pub enum FlowType { /// Direction of the flow (see [`ProcessFlow`]) #[derive(PartialEq, Debug)] pub enum FlowDirection { - /// The flow is an input (i.e., coeff > 0) + /// The flow is an input (i.e., coeff < 0) Input, /// The flow is an output (i.e., coeff > 0) Output,