Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions examples/muse1_default/process_investment_constraints.csv
Comment thread
dc2917 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -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
115 changes: 84 additions & 31 deletions src/input/process/investment_constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,18 +24,27 @@ struct ProcessInvestmentConstraintRaw {
regions: String,
commission_years: String,
addition_limit: CapacityPerYear,
total_capacity_limit: Option<Capacity>,
}

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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good. Long run, I think we'd want to make addition_limit optional as well. We should allow either total_capacity_limit or addition_limit to be empty, but not both (if both were empty, the user should just omit the row for that process/region/year)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue created: #1418

ensure!(
limit.is_finite() && limit >= Capacity(0.0),
"Invalid value for total capacity constraint: '{limit}'; must be non-negative and finite.",
);
}

Ok(())
}
}
Expand Down Expand Up @@ -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<Capacity>,
) -> Result<()> {
let constraint = ProcessInvestmentConstraintRaw {
process_id: "test_process".into(),
regions: "ALL".into(),
commission_years: "2030".into(),
addition_limit,
total_capacity_limit,
};
constraint.validate()
}
Expand All @@ -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(
Expand Down Expand Up @@ -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)),
},
];

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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<Capacity>,
) {
// 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<Capacity>,
#[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<Capacity>,
#[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<Capacity>,
#[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);
}
}
Loading