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
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ documented = "0.9.2"
dirs = "6.0.0"
edit = "0.1.5"
erased-serde = "0.4.10"
logtest = "2.0.0"

[dev-dependencies]
assert_cmd = "2.2.2"
Expand Down
12 changes: 6 additions & 6 deletions examples/muse1_default/process_investment_constraints.csv
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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
process_id,regions,commission_years,addition_limit,capacity_growth_limit,total_capacity_limit
gassupply1,R1,all,10,0.5,100
gasCCGT,R1,all,10,0.5,100
windturbine,R1,all,10,0.5,100
gasboiler,R1,all,10,0.5,100
heatpump,R1,all,10,0.5,100
6 changes: 6 additions & 0 deletions schemas/input/process_investment_constraints.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ fields:
notes: |
The addition limit is allocated evenly between all agents using their proportion of the
process's primary commodity demand.
- name: capacity_growth_limit
type: number
description: |
The constraint on fractional growth per year, based on the available stock in a year, per
region and technology. E.g 0.1 -> max 10% growth allowed.
notes: This is currently unused.
- name: total_capacity_limit
type: number
description: The hard upper limit on the amount agents can invest in the process
Expand Down
90 changes: 78 additions & 12 deletions src/input/process/investment_constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ use crate::process::{
ProcessID, ProcessInvestmentConstraint, ProcessInvestmentConstraintsMap, ProcessMap,
};
use crate::region::parse_region_str;
use crate::units::{Capacity, CapacityPerYear, Year};
use crate::units::{Capacity, CapacityPerYear, Dimensionless, Year};
use anyhow::{Context, Result, ensure};
use itertools::iproduct;
use log::warn;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;
Expand All @@ -24,6 +25,7 @@ struct ProcessInvestmentConstraintRaw {
regions: String,
commission_years: String,
addition_limit: CapacityPerYear,
capacity_growth_limit: Option<Dimensionless>,
total_capacity_limit: Option<Capacity>,
}

Expand All @@ -37,6 +39,23 @@ impl ProcessInvestmentConstraintRaw {
self.addition_limit
);

// Validate capacity_growth_limit
if let Some(limit) = self.capacity_growth_limit {
// Must be in range [0, inf)
ensure!(
limit.is_finite() && limit >= Dimensionless(0.0),
"Invalid value for capacity growth constraint: '{limit}'; must be non-negative and finite.",
);

// Warn user they may have specified limit as percentage, rather than fraction
if limit > Dimensionless(1.0) {
warn!(
"Interpreting capacity growth constraint '{limit}' as {}%",
limit * Dimensionless(100.0)
);
}
}

// Validate total_capacity_limit: must be in range [0, inf)
if let Some(limit) = self.total_capacity_limit {
ensure!(
Expand Down Expand Up @@ -162,17 +181,20 @@ mod tests {
use crate::fixture::{assert_error, processes};
use crate::region::RegionID;
use crate::units::Capacity;
use logtest::Logger;
use rstest::rstest;

fn validate_raw_constraint(
addition_limit: CapacityPerYear,
capacity_growth_limit: Option<Dimensionless>,
total_capacity_limit: Option<Capacity>,
) -> Result<()> {
let constraint = ProcessInvestmentConstraintRaw {
process_id: "test_process".into(),
regions: "ALL".into(),
commission_years: "2030".into(),
addition_limit,
capacity_growth_limit,
total_capacity_limit,
};
constraint.validate()
Expand All @@ -188,6 +210,7 @@ mod tests {
regions: "GBR".into(),
commission_years: "ALL".into(), // Should apply to milestone years [2012, 2016]
addition_limit: CapacityPerYear(100.0),
capacity_growth_limit: Some(Dimensionless(0.5)),
total_capacity_limit: Some(Capacity(100.0)),
}];

Expand Down Expand Up @@ -235,20 +258,23 @@ mod tests {
regions: "GBR".into(),
commission_years: "2010".into(),
addition_limit: CapacityPerYear(100.0),
capacity_growth_limit: Some(Dimensionless(0.5)),
total_capacity_limit: Some(Capacity(100.0)),
},
ProcessInvestmentConstraintRaw {
process_id: "process1".into(),
regions: "ALL".into(),
commission_years: "2015".into(),
addition_limit: CapacityPerYear(200.0),
capacity_growth_limit: Some(Dimensionless(0.5)),
total_capacity_limit: Some(Capacity(100.0)),
},
ProcessInvestmentConstraintRaw {
process_id: "process1".into(),
regions: "USA".into(),
commission_years: "2020".into(),
addition_limit: CapacityPerYear(50.0),
capacity_growth_limit: Some(Dimensionless(0.5)),
total_capacity_limit: Some(Capacity(100.0)),
},
];
Expand Down Expand Up @@ -309,6 +335,7 @@ mod tests {
regions: "ALL".into(),
commission_years: "ALL".into(),
addition_limit: CapacityPerYear(75.0),
capacity_growth_limit: Some(Dimensionless(0.5)),
total_capacity_limit: Some(Capacity(100.0)),
}];

Expand Down Expand Up @@ -358,6 +385,7 @@ mod tests {
regions: "GBR".into(),
commission_years: "2025".into(), // Outside milestone years (2010-2020)
addition_limit: CapacityPerYear(100.0),
capacity_growth_limit: Some(Dimensionless(0.5)),
total_capacity_limit: Some(Capacity(100.0)),
}];

Expand All @@ -374,71 +402,109 @@ mod tests {
}

#[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)))]
#[case(CapacityPerYear(10.0), None, None)]
#[case(CapacityPerYear(0.0), None, None)]
#[case(CapacityPerYear(10.0), Some(Dimensionless(0.5)), None)]
#[case(CapacityPerYear(10.0), Some(Dimensionless(0.0)), None)]
#[case(CapacityPerYear(10.0), None, Some(Capacity(100.0)))]
#[case(CapacityPerYear(10.0), None, Some(Capacity(0.0)))]
fn validate_constraints_valid(
#[case] addition_limit: CapacityPerYear,
#[case] capacity_growth_limit: Option<Dimensionless>,
#[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: capacity constraints with values >= 0, and capacity_growth_limit and total_capacity_limit as None
let valid =
validate_raw_constraint(addition_limit, capacity_growth_limit, total_capacity_limit);
valid.unwrap();
}

#[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.")]
#[case(CapacityPerYear(-10.0), None, None, "Invalid value for addition constraint: '-10'; must be non-negative and finite.")]
#[case(CapacityPerYear(10.0), Some(Dimensionless(-0.5)), None, "Invalid value for capacity growth constraint: '-0.5'; must be non-negative and finite.")]
#[case(CapacityPerYear(10.0), None, 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] capacity_growth_limit: Option<Dimensionless>,
#[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);
let invalid =
validate_raw_constraint(addition_limit, capacity_growth_limit, total_capacity_limit);
assert_error!(invalid, error_msg);
}

#[rstest]
#[case(
CapacityPerYear(f64::INFINITY),
None,
None,
"Invalid value for addition constraint: 'inf'; must be non-negative and finite."
)]
#[case(
CapacityPerYear(10.0),
Some(Dimensionless(f64::INFINITY)),
None,
"Invalid value for capacity growth constraint: 'inf'; must be non-negative and finite."
)]
#[case(
CapacityPerYear(10.0),
None,
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] capacity_growth_limit: Option<Dimensionless>,
#[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);
let invalid =
validate_raw_constraint(addition_limit, capacity_growth_limit, total_capacity_limit);
assert_error!(invalid, error_msg);
}

#[rstest]
#[case(
CapacityPerYear(f64::NAN),
None,
None,
"Invalid value for addition constraint: 'NaN'; must be non-negative and finite."
)]
#[case(
CapacityPerYear(10.0),
Some(Dimensionless(f64::NAN)),
None,
"Invalid value for capacity growth constraint: 'NaN'; must be non-negative and finite."
)]
#[case(
CapacityPerYear(10.0),
None,
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] capacity_growth_limit: Option<Dimensionless>,
#[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);
let invalid =
validate_raw_constraint(addition_limit, capacity_growth_limit, total_capacity_limit);
assert_error!(invalid, error_msg);
}

#[test]
fn validate_capacity_growth_limit_warning() {
// Check warning raised if value above 1 provided
let mut logger = Logger::start();
let _ = validate_raw_constraint(CapacityPerYear(10.0), Some(Dimensionless(5.0)), None);
assert_eq!(
logger.pop().unwrap().args(),

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.

I'm surprised we've never tested log messages before. This seems like a reasonable way to do it

"Interpreting capacity growth constraint '5' as 500%"
);
}
}
Loading