Skip to content
Merged
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
4 changes: 4 additions & 0 deletions schemas/input/process_investment_constraints.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ fields:
notes: |
The addition limit is allocated evenly between all agents using their proportion of the
process's primary commodity demand.
- name: total_capacity_limit
type: number
description: The hard upper limit on the amount agents can invest in the process
notes: This is currently unused.
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 {
Comment thread
dc2917 marked this conversation as resolved.
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