diff --git a/examples/muse1_default/process_investment_constraints.csv b/examples/muse1_default/process_investment_constraints.csv index c8f9aeca3..e31e912b6 100644 --- a/examples/muse1_default/process_investment_constraints.csv +++ b/examples/muse1_default/process_investment_constraints.csv @@ -1,6 +1,6 @@ -process_id,regions,commission_years,addition_limit -gassupply1,R1,all,10 -gasCCGT,R1,all,10 -windturbine,R1,all,10 -gasboiler,R1,all,10 -heatpump,R1,all,10 +process_id,regions,commission_years,addition_limit,total_capacity_limit +gassupply1,R1,all,10,100 +gasCCGT,R1,all,10,100 +windturbine,R1,all,10,100 +gasboiler,R1,all,10,100 +heatpump,R1,all,10,100 diff --git a/src/input/process/investment_constraints.rs b/src/input/process/investment_constraints.rs index 4dec59c9f..d9275f655 100644 --- a/src/input/process/investment_constraints.rs +++ b/src/input/process/investment_constraints.rs @@ -7,7 +7,7 @@ use crate::process::{ ProcessID, ProcessInvestmentConstraint, ProcessInvestmentConstraintsMap, ProcessMap, }; use crate::region::parse_region_str; -use crate::units::{CapacityPerYear, Year}; +use crate::units::{Capacity, CapacityPerYear, Year}; use anyhow::{Context, Result, ensure}; use itertools::iproduct; use serde::Deserialize; @@ -24,18 +24,27 @@ struct ProcessInvestmentConstraintRaw { regions: String, commission_years: String, addition_limit: CapacityPerYear, + total_capacity_limit: Option, } impl ProcessInvestmentConstraintRaw { /// Validate the constraint record for logical consistency and required fields fn validate(&self) -> Result<()> { - // Validate that value is finite + // Validate that addition_limit is finite ensure!( self.addition_limit.is_finite() && self.addition_limit >= CapacityPerYear(0.0), "Invalid value for addition constraint: '{}'; must be non-negative and finite.", self.addition_limit ); + // Validate total_capacity_limit: must be in range [0, inf) + if let Some(limit) = self.total_capacity_limit { + ensure!( + limit.is_finite() && limit >= Capacity(0.0), + "Invalid value for total capacity constraint: '{limit}'; must be non-negative and finite.", + ); + } + Ok(()) } } @@ -155,12 +164,16 @@ mod tests { use crate::units::Capacity; use rstest::rstest; - fn validate_raw_constraint(addition_limit: CapacityPerYear) -> Result<()> { + fn validate_raw_constraint( + addition_limit: CapacityPerYear, + total_capacity_limit: Option, + ) -> Result<()> { let constraint = ProcessInvestmentConstraintRaw { process_id: "test_process".into(), regions: "ALL".into(), commission_years: "2030".into(), addition_limit, + total_capacity_limit, }; constraint.validate() } @@ -175,6 +188,7 @@ mod tests { regions: "GBR".into(), commission_years: "ALL".into(), // Should apply to milestone years [2012, 2016] addition_limit: CapacityPerYear(100.0), + total_capacity_limit: Some(Capacity(100.0)), }]; let result = read_process_investment_constraints_from_iter( @@ -221,18 +235,21 @@ mod tests { regions: "GBR".into(), commission_years: "2010".into(), addition_limit: CapacityPerYear(100.0), + total_capacity_limit: Some(Capacity(100.0)), }, ProcessInvestmentConstraintRaw { process_id: "process1".into(), regions: "ALL".into(), commission_years: "2015".into(), addition_limit: CapacityPerYear(200.0), + total_capacity_limit: Some(Capacity(100.0)), }, ProcessInvestmentConstraintRaw { process_id: "process1".into(), regions: "USA".into(), commission_years: "2020".into(), addition_limit: CapacityPerYear(50.0), + total_capacity_limit: Some(Capacity(100.0)), }, ]; @@ -292,6 +309,7 @@ mod tests { regions: "ALL".into(), commission_years: "ALL".into(), addition_limit: CapacityPerYear(75.0), + total_capacity_limit: Some(Capacity(100.0)), }]; // Read constraints into the map @@ -340,6 +358,7 @@ mod tests { regions: "GBR".into(), commission_years: "2025".into(), // Outside milestone years (2010-2020) addition_limit: CapacityPerYear(100.0), + total_capacity_limit: Some(Capacity(100.0)), }]; // Should fail with milestone year validation error @@ -354,38 +373,72 @@ mod tests { ); } - #[test] - fn validate_addition_with_finite_value() { - // Valid: addition constraint with positive value - let valid = validate_raw_constraint(CapacityPerYear(10.0)); - valid.unwrap(); - - // Valid: addition constraint with zero value - let valid = validate_raw_constraint(CapacityPerYear(0.0)); + #[rstest] + #[case(CapacityPerYear(10.0), None)] + #[case(CapacityPerYear(0.0), None)] + #[case(CapacityPerYear(10.0), Some(Capacity(100.0)))] + #[case(CapacityPerYear(10.0), Some(Capacity(0.0)))] + fn validate_constraints_valid( + #[case] addition_limit: CapacityPerYear, + #[case] total_capacity_limit: Option, + ) { + // Valid: capacity constraints with values >= 0, and total_capacity_limit as None + let valid = validate_raw_constraint(addition_limit, total_capacity_limit); valid.unwrap(); + } - // Not valid: addition constraint with negative value - let invalid = validate_raw_constraint(CapacityPerYear(-10.0)); - assert_error!( - invalid, - "Invalid value for addition constraint: '-10'; must be non-negative and finite." - ); + #[rstest] + #[case(CapacityPerYear(-10.0), None, "Invalid value for addition constraint: '-10'; must be non-negative and finite.")] + #[case(CapacityPerYear(10.0), Some(Capacity(-100.0)), "Invalid value for total capacity constraint: '-100'; must be non-negative and finite.")] + fn validate_constraints_rejects_negative( + #[case] addition_limit: CapacityPerYear, + #[case] total_capacity_limit: Option, + #[case] error_msg: &str, + ) { + // Not valid: capacity constraints with negative value + let invalid = validate_raw_constraint(addition_limit, total_capacity_limit); + assert_error!(invalid, error_msg); } - #[test] - fn validate_addition_rejects_infinite() { - // Invalid: infinite value - let invalid = validate_raw_constraint(CapacityPerYear(f64::INFINITY)); - assert_error!( - invalid, - "Invalid value for addition constraint: 'inf'; must be non-negative and finite." - ); + #[rstest] + #[case( + CapacityPerYear(f64::INFINITY), + None, + "Invalid value for addition constraint: 'inf'; must be non-negative and finite." + )] + #[case( + CapacityPerYear(10.0), + Some(Capacity(f64::INFINITY)), + "Invalid value for total capacity constraint: 'inf'; must be non-negative and finite." + )] + fn validate_constraints_rejects_infinite( + #[case] addition_limit: CapacityPerYear, + #[case] total_capacity_limit: Option, + #[case] error_msg: &str, + ) { + // Not valid: capacity constraints with infinite value + let invalid = validate_raw_constraint(addition_limit, total_capacity_limit); + assert_error!(invalid, error_msg); + } - // Invalid: NaN value - let invalid = validate_raw_constraint(CapacityPerYear(f64::NAN)); - assert_error!( - invalid, - "Invalid value for addition constraint: 'NaN'; must be non-negative and finite." - ); + #[rstest] + #[case( + CapacityPerYear(f64::NAN), + None, + "Invalid value for addition constraint: 'NaN'; must be non-negative and finite." + )] + #[case( + CapacityPerYear(10.0), + Some(Capacity(f64::NAN)), + "Invalid value for total capacity constraint: 'NaN'; must be non-negative and finite." + )] + fn validate_constraints_rejects_nan( + #[case] addition_limit: CapacityPerYear, + #[case] total_capacity_limit: Option, + #[case] error_msg: &str, + ) { + // Not valid: capacity constraints with NaN value + let invalid = validate_raw_constraint(addition_limit, total_capacity_limit); + assert_error!(invalid, error_msg); } }