-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
b2c3488
Apply investment limits based on agent demand share
tsmbland 6dbc3e6
Scale limit based on number of years
tsmbland e2bfc4b
Update example models
tsmbland 6c28f0f
Streamline arguments to calculate_investment_limits_for_candidates
tsmbland 864b199
Small tidy ups
tsmbland 4ac9e84
Add tests
tsmbland 6c4c6c5
Pre-compute investment limits, better error message
tsmbland da9fafe
max_installable_capacity Asset function
tsmbland 4fc39e6
Use u32 for number of years
tsmbland 4afd749
Pre-scale limits
tsmbland 2a36db4
Simplify calculate_investment_limits_for_candidates
tsmbland b2eb10d
Apply constraints in circularities model
tsmbland 9da63c4
Merge branch 'main' into investment_limits
tsmbland 8f05fb6
Copilot fixes
tsmbland 25304ed
Merge branch 'investment_limits' of https://github.com/EnergySystemsM…
tsmbland 51cf7b3
Remove unnecessary clone()
alexdewar 518ea91
Tweak doc comment
alexdewar 3da198d
Add tests for `calculate_investment_limits_for_candidates`
alexdewar 8694f06
AGENTS.md: Add instructions for tests
alexdewar e3d890b
Asset::max_installable_capacity: Check `commodity_portion` is in range
alexdewar 52c7729
Remove now-stale note from schema
alexdewar 4d1132c
schemas/input/process_investment_constraints.yaml: Fixes and formatting
alexdewar 7652932
Add `CapacityPerYear` unit type
alexdewar aa045cd
Use `CapacityPerYear` type to represent unscaled addition limits
alexdewar 7ed1ab0
Merge remote-tracking branch 'origin/main' into investment_limits
alexdewar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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::{CapacityPerYear, Year}; | ||
| 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: CapacityPerYear, | ||
| } | ||
|
|
||
| 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 >= CapacityPerYear(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 * Year(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())?; | ||
| } | ||
| } | ||
|
|
@@ -134,9 +153,10 @@ mod tests { | |
| use super::*; | ||
| use crate::fixture::{assert_error, processes}; | ||
| use crate::region::RegionID; | ||
| use crate::units::Capacity; | ||
| use rstest::rstest; | ||
|
|
||
| fn validate_raw_constraint(addition_limit: f64) -> Result<()> { | ||
| fn validate_raw_constraint(addition_limit: CapacityPerYear) -> Result<()> { | ||
| let constraint = ProcessInvestmentConstraintRaw { | ||
| process_id: "test_process".into(), | ||
| regions: "ALL".into(), | ||
|
|
@@ -155,7 +175,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: CapacityPerYear(100.0), | ||
| }]; | ||
|
|
||
| let result = read_process_investment_constraints_from_iter( | ||
|
|
@@ -201,19 +221,19 @@ mod tests { | |
| process_id: "process1".into(), | ||
| regions: "GBR".into(), | ||
| commission_years: "2010".into(), | ||
| addition_limit: 100.0, | ||
| addition_limit: CapacityPerYear(100.0), | ||
| }, | ||
| ProcessInvestmentConstraintRaw { | ||
| process_id: "process1".into(), | ||
| regions: "ALL".into(), | ||
| commission_years: "2015".into(), | ||
| addition_limit: 200.0, | ||
| addition_limit: CapacityPerYear(200.0), | ||
| }, | ||
| ProcessInvestmentConstraintRaw { | ||
| process_id: "process1".into(), | ||
| regions: "USA".into(), | ||
| commission_years: "2020".into(), | ||
| addition_limit: 50.0, | ||
| addition_limit: CapacityPerYear(50.0), | ||
| }, | ||
| ]; | ||
|
|
||
|
|
@@ -234,32 +254,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 +292,7 @@ mod tests { | |
| process_id: "process1".into(), | ||
| regions: "ALL".into(), | ||
| commission_years: "ALL".into(), | ||
| addition_limit: 75.0, | ||
| addition_limit: CapacityPerYear(75.0), | ||
| }]; | ||
|
|
||
| // Read constraints into the map | ||
|
|
@@ -292,21 +312,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 +340,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: CapacityPerYear(100.0), | ||
| }]; | ||
|
|
||
| // Should fail with milestone year validation error | ||
|
|
@@ -337,15 +358,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(CapacityPerYear(10.0)); | ||
| valid.unwrap(); | ||
|
|
||
| // Valid: addition constraint with zero value | ||
| let valid = validate_raw_constraint(0.0); | ||
| let valid = validate_raw_constraint(CapacityPerYear(0.0)); | ||
| valid.unwrap(); | ||
|
|
||
| // Not valid: addition constraint with negative value | ||
| let invalid = validate_raw_constraint(-10.0); | ||
| let invalid = validate_raw_constraint(CapacityPerYear(-10.0)); | ||
| assert_error!( | ||
| invalid, | ||
| "Invalid value for addition constraint: '-10'; must be non-negative and finite." | ||
|
|
@@ -355,14 +376,14 @@ mod tests { | |
| #[test] | ||
| fn validate_addition_rejects_infinite() { | ||
| // Invalid: infinite value | ||
| let invalid = validate_raw_constraint(f64::INFINITY); | ||
| let invalid = validate_raw_constraint(CapacityPerYear(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(CapacityPerYear(f64::NAN)); | ||
| assert_error!( | ||
| invalid, | ||
| "Invalid value for addition constraint: 'NaN'; must be non-negative and finite." | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.