-
Notifications
You must be signed in to change notification settings - Fork 1
Use integer variables in appraisal for divisible assets #1075
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
Changes from all commits
192d999
ce77870
9ff532c
1d3ba5d
f820ab9
6998cef
acb993c
43d3b50
5491a02
c500f3f
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 |
|---|---|---|
|
|
@@ -831,11 +831,42 @@ impl Asset { | |
| *mothballed_year | ||
| } | ||
|
|
||
| /// Get the unit size for this asset's process (if any) | ||
| pub fn unit_size(&self) -> Option<Capacity> { | ||
| self.process.unit_size | ||
| } | ||
|
|
||
| /// Checks if the asset corresponds to a process that has a `unit_size` and is therefore divisible. | ||
| pub fn is_divisible(&self) -> bool { | ||
| self.process.unit_size.is_some() | ||
| } | ||
|
|
||
| /// Convert a capacity to number of units for a divisible asset. | ||
| /// | ||
| /// Divides the given capacity by the process unit size and rounds up. In other words, this is | ||
| /// the minimum number of units required to achieve at least the given capacity. | ||
| /// | ||
| /// Panics if the asset is not divisible. | ||
| pub fn capacity_to_units(&self, capacity: Capacity) -> u32 { | ||
| let unit_size = self.unit_size().expect("Asset must be divisible"); | ||
| #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] | ||
| { | ||
| (capacity / unit_size).value().ceil() as u32 | ||
| } | ||
| } | ||
|
Comment on lines
+850
to
+856
|
||
|
|
||
| /// Round a capacity up to the nearest multiple of the unit size. | ||
| /// | ||
| /// For a divisible asset, returns the minimum capacity (as a multiple of `unit_size`) | ||
| /// that is at least as large as the given capacity. | ||
| /// | ||
| /// Panics if the asset is not divisible. | ||
| pub fn round_capacity_to_unit_size(&self, capacity: Capacity) -> Capacity { | ||
| let unit_size = self.unit_size().expect("Asset must be divisible"); | ||
| let n_units = self.capacity_to_units(capacity); | ||
| Capacity(unit_size.value() * n_units as f64) | ||
| } | ||
|
Comment on lines
+864
to
+868
|
||
|
|
||
| /// Divides an asset if it is divisible and returns a vector of children | ||
| /// | ||
| /// The child assets are identical to the parent (including state) but with a capacity | ||
|
|
@@ -860,10 +891,7 @@ impl Asset { | |
| ); | ||
|
|
||
| // Calculate the number of units corresponding to the asset's capacity | ||
| // Safe because capacity and unit_size are both positive finite numbers, so their ratio | ||
| // must also be positive and finite. | ||
| #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] | ||
| let n_units = (self.capacity / unit_size).value().ceil() as usize; | ||
| let n_units = self.capacity_to_units(self.capacity) as usize; | ||
|
|
||
| // Divide the asset into `n_units` children of size `unit_size` | ||
| let child_asset = Self { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -617,12 +617,16 @@ fn get_candidate_assets<'a>( | |
| let mut asset = | ||
| Asset::new_candidate(process.clone(), region_id.clone(), Capacity(0.0), year) | ||
| .unwrap(); | ||
| asset.set_capacity(get_demand_limiting_capacity( | ||
| time_slice_info, | ||
| &asset, | ||
| commodity, | ||
| demand, | ||
| )); | ||
|
|
||
| // Set capacity based on demand | ||
| // This will serve as the upper limit when appraising the asset | ||
| // If the asset is divisible, round capacity to the nearest multiple of the unit size | ||
| let mut capacity = | ||
| get_demand_limiting_capacity(time_slice_info, &asset, commodity, demand); | ||
| if asset.is_divisible() { | ||
| capacity = asset.round_capacity_to_unit_size(capacity); | ||
| } | ||
| asset.set_capacity(capacity); | ||
|
|
||
| asset.into() | ||
| }) | ||
|
|
@@ -708,7 +712,13 @@ fn select_best_assets( | |
| let mut outputs_for_opts = Vec::new(); | ||
| for asset in &opt_assets { | ||
| let max_capacity = (!asset.is_commissioned()).then(|| { | ||
| let max_capacity = model.parameters.capacity_limit_factor * asset.capacity(); | ||
| let mut max_capacity = model.parameters.capacity_limit_factor * asset.capacity(); | ||
|
|
||
| // For divisible assets, round up to the nearest multiple of the process unit size | ||
| if asset.is_divisible() { | ||
| max_capacity = asset.round_capacity_to_unit_size(max_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. This threw me when I first read it because I assumed it was rounding the capacity of the asset and didn't notice there was an extra argument. I'm wondering if it might be clearer to have This is obvs a bit subjective, so up to you.
Collaborator
Author
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'm going to get rid of this in #1079 (if we agree to go down that route), so I'll leave it for now |
||
|
|
||
| let remaining_capacity = remaining_candidate_capacity[asset]; | ||
| max_capacity.min(remaining_capacity) | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ use crate::asset::{AssetRef, AssetState}; | |
| use crate::commodity::Commodity; | ||
| use crate::time_slice::{TimeSliceID, TimeSliceInfo}; | ||
| use crate::units::{Capacity, Flow}; | ||
| use float_cmp::approx_eq; | ||
| use highs::RowProblem as Problem; | ||
| use indexmap::IndexMap; | ||
|
|
||
|
|
@@ -19,15 +20,26 @@ pub fn add_capacity_constraint( | |
| max_capacity: Option<Capacity>, | ||
| capacity_var: Variable, | ||
| ) { | ||
| let capacity = max_capacity.unwrap_or(asset.capacity()); | ||
| let mut capacity_limit = max_capacity.unwrap_or(asset.capacity()).value(); | ||
|
|
||
| // If asset is divisible, capacity_var represents number of units, so we must divide the | ||
| // capacity bounds by the unit size. | ||
| if let Some(unit_size) = asset.unit_size() { | ||
| capacity_limit /= unit_size.value(); | ||
|
|
||
| // Sanity check: capacity_limit should be a whole number of units (i.e pre-adjusted | ||
| // capacity limit was a multiple of unit size) | ||
| assert!(approx_eq!(f64, capacity_limit, capacity_limit.round())); | ||
|
Comment on lines
+30
to
+32
|
||
| } | ||
|
|
||
| let bounds = match asset.state() { | ||
| AssetState::Commissioned { .. } => { | ||
| // Fixed capacity for commissioned assets | ||
| capacity.value()..=capacity.value() | ||
| capacity_limit..=capacity_limit | ||
| } | ||
| AssetState::Candidate => { | ||
| // Variable capacity between 0 and max for candidate assets | ||
| 0.0..=capacity.value() | ||
| 0.0..=capacity_limit | ||
| } | ||
| _ => panic!( | ||
| "add_capacity_constraint should only be called with Commissioned or Candidate assets" | ||
|
|
@@ -100,8 +112,15 @@ fn add_activity_constraints_for_candidate( | |
| time_slice_info: &TimeSliceInfo, | ||
| ) { | ||
| for (ts_selection, limits) in asset.iter_activity_per_capacity_limits() { | ||
| let upper_limit = limits.end().value(); | ||
| let lower_limit = limits.start().value(); | ||
| let mut upper_limit = limits.end().value(); | ||
| let mut lower_limit = limits.start().value(); | ||
|
|
||
| // If the asset is divisible, the capacity variable represents number of units, | ||
| // so we need to multiply the per-capacity limits by the unit size. | ||
| if let Some(unit_size) = asset.unit_size() { | ||
| upper_limit *= unit_size.value(); | ||
| lower_limit *= unit_size.value(); | ||
| } | ||
|
|
||
| // Collect capacity and activity terms | ||
| // We have a single capacity term, and activity terms for all time slices in the selection | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,15 +1,15 @@ | ||||||||||
| asset_id,process_id,region_id,agent_id,group_id,commission_year,decommission_year,capacity | ||||||||||
| 0,GASDRV,GBR,A0_GEX,,2020,,4002.26 | ||||||||||
| 1,GASPRC,GBR,A0_GPR,,2020,,3782.13 | ||||||||||
| 2,WNDFRM,GBR,A0_ELC,,2020,2040,3.964844 | ||||||||||
| 3,GASCGT,GBR,A0_ELC,,2020,2040,2.43 | ||||||||||
| 2,WNDFRM,GBR,A0_ELC,,2020,,3.964844 | ||||||||||
| 3,GASCGT,GBR,A0_ELC,,2020,,2.43 | ||||||||||
|
Comment on lines
+4
to
+5
|
||||||||||
| 2,WNDFRM,GBR,A0_ELC,,2020,,3.964844 | |
| 3,GASCGT,GBR,A0_ELC,,2020,,2.43 | |
| 2,WNDFRM,GBR,A0_ELC,,2020,2040,3.964844 | |
| 3,GASCGT,GBR,A0_ELC,,2020,2040,2.43 |
Copilot
AI
Jan 15, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assets 8 and 9 show updated capacities and different asset types/IDs compared to the original data. The change from RGASBR to RELCHP for asset 8 and the significant capacity changes (1000.0 to 255.83840587648046) should be verified as intentional outcomes of the integer optimization.
Uh oh!
There was an error while loading. Please reload this page.