-
Notifications
You must be signed in to change notification settings - Fork 2
Implement investment limits #1096
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b2c3488
6dbc3e6
e2bfc4b
6c28f0f
864b199
4ac9e84
6c4c6c5
da9fafe
4fc39e6
4afd749
2a36db4
b2eb10d
9da63c4
8f05fb6
25304ed
51cf7b3
518ea91
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| process_id,regions,commission_years,addition_limit | ||
| gassupply1,R1;R2,all,10 | ||
| gasCCGT,R1;R2,all,10 | ||
| windturbine,R1;R2,all,10 | ||
| gasboiler,R1;R2,all,10 | ||
| heatpump,R1;R2,all,10 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ use crate::process::{ | |
| ProcessID, ProcessInvestmentConstraint, ProcessInvestmentConstraintsMap, ProcessMap, | ||
| }; | ||
| use crate::region::parse_region_str; | ||
| use crate::units::{Capacity, Dimensionless}; | ||
| use crate::year::parse_year_str; | ||
| use anyhow::{Context, Result, ensure}; | ||
| use itertools::iproduct; | ||
|
|
@@ -21,15 +22,15 @@ struct ProcessInvestmentConstraintRaw { | |
| process_id: String, | ||
| regions: String, | ||
| commission_years: String, | ||
| addition_limit: f64, | ||
| addition_limit: Capacity, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should actually be |
||
| } | ||
|
|
||
| impl ProcessInvestmentConstraintRaw { | ||
| /// Validate the constraint record for logical consistency and required fields | ||
| fn validate(&self) -> Result<()> { | ||
| // Validate that value is finite | ||
| ensure!( | ||
| self.addition_limit.is_finite() && self.addition_limit >= 0.0, | ||
| self.addition_limit.is_finite() && self.addition_limit >= Capacity(0.0), | ||
| "Invalid value for addition constraint: '{}'; must be non-negative and finite.", | ||
| self.addition_limit | ||
| ); | ||
|
|
@@ -118,11 +119,29 @@ where | |
| })?; | ||
|
|
||
| // Create constraints for each region and year combination | ||
| let constraint = Rc::new(ProcessInvestmentConstraint { | ||
| addition_limit: Some(record.addition_limit), | ||
| }); | ||
| // For a given milestone year, the addition limit should be multiplied | ||
| // by the number of years since the previous milestone year. Any | ||
| // addition limits specified for the first milestone year are ignored. | ||
| let process_map = map.entry(process_id.clone()).or_default(); | ||
| for (region, &year) in iproduct!(&record_regions, &constraint_years) { | ||
| // Calculate years since previous milestone year | ||
| // We can ignore constraints in the first milestone year as no investments are performed then | ||
| let idx = milestone_years.iter().position(|y| *y == year).expect( | ||
| "Year should be in milestone_years since it was validated by parse_year_str", | ||
| ); | ||
| if idx == 0 { | ||
| continue; | ||
| } | ||
| let prev_year = milestone_years[idx - 1]; | ||
| let years_since_prev = year - prev_year; | ||
|
|
||
| // Multiply the addition limit by the number of years since previous milestone. | ||
| let scaled_limit = record.addition_limit * Dimensionless(years_since_prev as f64); | ||
|
|
||
| let constraint = Rc::new(ProcessInvestmentConstraint { | ||
| addition_limit: Some(scaled_limit), | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Aurashk Do you know if there was a particular motivation in making |
||
| }); | ||
|
|
||
| try_insert(process_map, &(region.clone(), year), constraint.clone())?; | ||
| } | ||
| } | ||
|
|
@@ -136,7 +155,7 @@ mod tests { | |
| use crate::region::RegionID; | ||
| use rstest::rstest; | ||
|
|
||
| fn validate_raw_constraint(addition_limit: f64) -> Result<()> { | ||
| fn validate_raw_constraint(addition_limit: Capacity) -> Result<()> { | ||
| let constraint = ProcessInvestmentConstraintRaw { | ||
| process_id: "test_process".into(), | ||
| regions: "ALL".into(), | ||
|
|
@@ -155,7 +174,7 @@ mod tests { | |
| process_id: "process1".into(), | ||
| regions: "GBR".into(), | ||
| commission_years: "ALL".into(), // Should apply to milestone years [2012, 2016] | ||
| addition_limit: 100.0, | ||
| addition_limit: Capacity(100.0), | ||
| }]; | ||
|
|
||
| let result = read_process_investment_constraints_from_iter( | ||
|
|
@@ -201,19 +220,19 @@ mod tests { | |
| process_id: "process1".into(), | ||
| regions: "GBR".into(), | ||
| commission_years: "2010".into(), | ||
| addition_limit: 100.0, | ||
| addition_limit: Capacity(100.0), | ||
| }, | ||
| ProcessInvestmentConstraintRaw { | ||
| process_id: "process1".into(), | ||
| regions: "ALL".into(), | ||
| commission_years: "2015".into(), | ||
| addition_limit: 200.0, | ||
| addition_limit: Capacity(200.0), | ||
| }, | ||
| ProcessInvestmentConstraintRaw { | ||
| process_id: "process1".into(), | ||
| regions: "USA".into(), | ||
| commission_years: "2020".into(), | ||
| addition_limit: 50.0, | ||
| addition_limit: Capacity(50.0), | ||
| }, | ||
| ]; | ||
|
|
||
|
|
@@ -234,32 +253,32 @@ mod tests { | |
| let gbr_region: RegionID = "GBR".into(); | ||
| let usa_region: RegionID = "USA".into(); | ||
|
|
||
| // Check GBR 2010 constraint | ||
| let gbr_2010 = process_constraints | ||
| .get(&(gbr_region.clone(), 2010)) | ||
| .expect("GBR 2010 constraint should exist"); | ||
| assert_eq!(gbr_2010.addition_limit, Some(100.0)); | ||
| // GBR 2010 constraint is for the first milestone year and should be ignored | ||
| assert!( | ||
| !process_constraints.contains_key(&(gbr_region.clone(), 2010)), | ||
| "GBR 2010 constraint should not exist" | ||
| ); | ||
|
|
||
| // Check GBR 2015 constraint (from ALL regions) | ||
| // Check GBR 2015 constraint (from ALL regions), scaled by years since previous milestone (5 years) | ||
| let gbr_2015 = process_constraints | ||
| .get(&(gbr_region, 2015)) | ||
| .expect("GBR 2015 constraint should exist"); | ||
| assert_eq!(gbr_2015.addition_limit, Some(200.0)); | ||
| assert_eq!(gbr_2015.addition_limit, Some(Capacity(200.0 * 5.0))); | ||
|
|
||
| // Check USA 2015 constraint (from ALL regions) | ||
| // Check USA 2015 constraint (from ALL regions), scaled by 5 years | ||
| let usa_2015 = process_constraints | ||
| .get(&(usa_region.clone(), 2015)) | ||
| .expect("USA 2015 constraint should exist"); | ||
| assert_eq!(usa_2015.addition_limit, Some(200.0)); | ||
| assert_eq!(usa_2015.addition_limit, Some(Capacity(200.0 * 5.0))); | ||
|
|
||
| // Check USA 2020 constraint | ||
| // Check USA 2020 constraint, scaled by years since previous milestone (5 years) | ||
| let usa_2020 = process_constraints | ||
| .get(&(usa_region, 2020)) | ||
| .expect("USA 2020 constraint should exist"); | ||
| assert_eq!(usa_2020.addition_limit, Some(50.0)); | ||
| assert_eq!(usa_2020.addition_limit, Some(Capacity(50.0 * 5.0))); | ||
|
|
||
| // Verify total number of constraints (2 GBR + 2 USA = 4) | ||
| assert_eq!(process_constraints.len(), 4); | ||
| // Verify total number of constraints (GBR 2015, USA 2015, USA 2020 = 3) | ||
| assert_eq!(process_constraints.len(), 3); | ||
| } | ||
|
|
||
| #[rstest] | ||
|
|
@@ -272,7 +291,7 @@ mod tests { | |
| process_id: "process1".into(), | ||
| regions: "ALL".into(), | ||
| commission_years: "ALL".into(), | ||
| addition_limit: 75.0, | ||
| addition_limit: Capacity(75.0), | ||
| }]; | ||
|
|
||
| // Read constraints into the map | ||
|
|
@@ -292,21 +311,22 @@ mod tests { | |
| let gbr_region: RegionID = "GBR".into(); | ||
| let usa_region: RegionID = "USA".into(); | ||
|
|
||
| // Verify constraint exists for all region-year combinations | ||
| for &year in &milestone_years { | ||
| // Verify constraint exists for all region-year combinations except the first milestone year | ||
| for &year in &milestone_years[1..] { | ||
| let gbr_constraint = process_constraints | ||
| .get(&(gbr_region.clone(), year)) | ||
| .unwrap_or_else(|| panic!("GBR {year} constraint should exist")); | ||
| assert_eq!(gbr_constraint.addition_limit, Some(75.0)); | ||
| // scaled by years since previous milestone (5 years) | ||
| assert_eq!(gbr_constraint.addition_limit, Some(Capacity(75.0 * 5.0))); | ||
|
|
||
| let usa_constraint = process_constraints | ||
| .get(&(usa_region.clone(), year)) | ||
| .unwrap_or_else(|| panic!("USA {year} constraint should exist")); | ||
| assert_eq!(usa_constraint.addition_limit, Some(75.0)); | ||
| assert_eq!(usa_constraint.addition_limit, Some(Capacity(75.0 * 5.0))); | ||
| } | ||
|
|
||
| // Verify total number of constraints (2 regions × 3 years = 6) | ||
| assert_eq!(process_constraints.len(), 6); | ||
| // Verify total number of constraints (2 regions × 2 years = 4) | ||
| assert_eq!(process_constraints.len(), 4); | ||
| } | ||
|
|
||
| #[rstest] | ||
|
|
@@ -319,7 +339,7 @@ mod tests { | |
| process_id: "process1".into(), | ||
| regions: "GBR".into(), | ||
| commission_years: "2025".into(), // Outside milestone years (2010-2020) | ||
| addition_limit: 100.0, | ||
| addition_limit: Capacity(100.0), | ||
| }]; | ||
|
|
||
| // Should fail with milestone year validation error | ||
|
|
@@ -337,15 +357,15 @@ mod tests { | |
| #[test] | ||
| fn validate_addition_with_finite_value() { | ||
| // Valid: addition constraint with positive value | ||
| let valid = validate_raw_constraint(10.0); | ||
| let valid = validate_raw_constraint(Capacity(10.0)); | ||
| valid.unwrap(); | ||
|
|
||
| // Valid: addition constraint with zero value | ||
| let valid = validate_raw_constraint(0.0); | ||
| let valid = validate_raw_constraint(Capacity(0.0)); | ||
| valid.unwrap(); | ||
|
|
||
| // Not valid: addition constraint with negative value | ||
| let invalid = validate_raw_constraint(-10.0); | ||
| let invalid = validate_raw_constraint(Capacity(-10.0)); | ||
| assert_error!( | ||
| invalid, | ||
| "Invalid value for addition constraint: '-10'; must be non-negative and finite." | ||
|
|
@@ -355,14 +375,14 @@ mod tests { | |
| #[test] | ||
| fn validate_addition_rejects_infinite() { | ||
| // Invalid: infinite value | ||
| let invalid = validate_raw_constraint(f64::INFINITY); | ||
| let invalid = validate_raw_constraint(Capacity(f64::INFINITY)); | ||
| assert_error!( | ||
| invalid, | ||
| "Invalid value for addition constraint: 'inf'; must be non-negative and finite." | ||
| ); | ||
|
|
||
| // Invalid: NaN value | ||
| let invalid = validate_raw_constraint(f64::NAN); | ||
| let invalid = validate_raw_constraint(Capacity(f64::NAN)); | ||
| assert_error!( | ||
| invalid, | ||
| "Invalid value for addition constraint: 'NaN'; must be non-negative and finite." | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.