-
Notifications
You must be signed in to change notification settings - Fork 2
Refactor equal metrics comparison code #1039
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 11 commits
7852273
0be0151
a65961c
0fefe9c
3a3e9a9
3cd5896
a2d8ae7
cbd50d2
b1afe73
5957fc1
f2e643c
7a6a3a8
c632d74
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 | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,16 +13,14 @@ use crate::units::{Capacity, Dimensionless, Flow, FlowPerCapacity}; | |||||||||||||||||||||
| use anyhow::{Context, Result, bail, ensure}; | ||||||||||||||||||||||
| use indexmap::IndexMap; | ||||||||||||||||||||||
| use itertools::{Itertools, chain}; | ||||||||||||||||||||||
| use log::{debug, warn}; | ||||||||||||||||||||||
| use log::debug; | ||||||||||||||||||||||
| use std::cmp::Ordering; | ||||||||||||||||||||||
| use std::collections::HashMap; | ||||||||||||||||||||||
| use std::fmt::Display; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| pub mod appraisal; | ||||||||||||||||||||||
| use appraisal::coefficients::calculate_coefficients_for_assets; | ||||||||||||||||||||||
| use appraisal::{ | ||||||||||||||||||||||
| AppraisalComparisonMethod, AppraisalOutput, appraise_investment, | ||||||||||||||||||||||
| classify_appraisal_comparison_method, | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
| use appraisal::{AppraisalOutput, appraise_investment}; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// A map of demand across time slices for a specific market | ||||||||||||||||||||||
| type DemandMap = IndexMap<TimeSliceID, Flow>; | ||||||||||||||||||||||
|
|
@@ -56,6 +54,8 @@ impl InvestmentSet { | |||||||||||||||||||||
| /// Selects assets for this investment set variant and passes through the shared | ||||||||||||||||||||||
| /// context needed by single-market, cycle, or layered selection. | ||||||||||||||||||||||
| /// | ||||||||||||||||||||||
| /// # Arguments | ||||||||||||||||||||||
| /// | ||||||||||||||||||||||
| /// * `model` – Simulation model supplying parameters, processes, and dispatch. | ||||||||||||||||||||||
| /// * `year` – Planning year being solved. | ||||||||||||||||||||||
| /// * `demand` – Net demand profiles available to all markets before selection. | ||||||||||||||||||||||
|
|
@@ -621,35 +621,39 @@ fn get_candidate_assets<'a>( | |||||||||||||||||||||
| }) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| fn select_from_assets_with_equal_metric( | ||||||||||||||||||||||
| region_id: &RegionID, | ||||||||||||||||||||||
| /// Print debug message if there are multiple equally good outputs | ||||||||||||||||||||||
| fn warn_on_equal_appraisal_outputs( | ||||||||||||||||||||||
| outputs: &[AppraisalOutput], | ||||||||||||||||||||||
| agent_id: &AgentID, | ||||||||||||||||||||||
| commodity_id: &CommodityID, | ||||||||||||||||||||||
| equally_good_assets: Vec<AppraisalOutput>, | ||||||||||||||||||||||
| ) -> AppraisalOutput { | ||||||||||||||||||||||
| // Format asset details for diagnostic logging | ||||||||||||||||||||||
| let asset_details = equally_good_assets | ||||||||||||||||||||||
| region_id: &RegionID, | ||||||||||||||||||||||
| ) { | ||||||||||||||||||||||
| // Get all the outputs which are equal to the one after | ||||||||||||||||||||||
| let asset_details = outputs | ||||||||||||||||||||||
| .iter() | ||||||||||||||||||||||
| .map(|output| { | ||||||||||||||||||||||
| .tuple_windows() | ||||||||||||||||||||||
| .take_while(|(a, b)| a.compare_metric(b).is_eq()) | ||||||||||||||||||||||
| .map(|(output, _)| { | ||||||||||||||||||||||
| let asset = &output.asset; | ||||||||||||||||||||||
| format!( | ||||||||||||||||||||||
| "Process id: '{}' (State: {}{})", | ||||||||||||||||||||||
| output.asset.process_id(), | ||||||||||||||||||||||
| output.asset.state(), | ||||||||||||||||||||||
| output | ||||||||||||||||||||||
| .asset | ||||||||||||||||||||||
| "Process id: '{}' (State: {}{}, Commission year: {})", | ||||||||||||||||||||||
| asset.process_id(), | ||||||||||||||||||||||
| asset.state(), | ||||||||||||||||||||||
| asset | ||||||||||||||||||||||
| .id() | ||||||||||||||||||||||
| .map(|id| format!(", Asset id: {id}")) | ||||||||||||||||||||||
| .unwrap_or_default(), | ||||||||||||||||||||||
| asset.commission_year() | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| }) | ||||||||||||||||||||||
| .collect::<Vec<_>>() | ||||||||||||||||||||||
| .join(", "); | ||||||||||||||||||||||
| let warning_message = format!( | ||||||||||||||||||||||
| "Could not resolve deadlock between equally good appraisals for Agent id: {agent_id}, Commodity: '{commodity_id}', Region: {region_id}. Options: [{asset_details}]. Selecting first option.", | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| warn!("{warning_message}"); | ||||||||||||||||||||||
| // Select the first asset arbitrarily from the equally performing options | ||||||||||||||||||||||
| equally_good_assets.into_iter().next().unwrap() | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if !asset_details.is_empty() { | ||||||||||||||||||||||
| debug!( | ||||||||||||||||||||||
| "Found equally good appraisals for Agent id: {agent_id}, Commodity: '{commodity_id}', \ | ||||||||||||||||||||||
| Region: {region_id}. Options: [{asset_details}]. Selecting first option.", | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Get the best assets for meeting demand for the given commodity | ||||||||||||||||||||||
|
|
@@ -716,56 +720,39 @@ fn select_best_assets( | |||||||||||||||||||||
| )?; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Sort assets by appraisal metric | ||||||||||||||||||||||
| let assets_sorted_by_metric: Vec<_> = outputs_for_opts | ||||||||||||||||||||||
| let assets_sorted_by_metric = outputs_for_opts | ||||||||||||||||||||||
| .into_iter() | ||||||||||||||||||||||
| .filter(|output| output.capacity > Capacity(0.0)) | ||||||||||||||||||||||
| .sorted_by(AppraisalOutput::compare_metric) | ||||||||||||||||||||||
| .collect(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // check if all options have zero capacity | ||||||||||||||||||||||
| if assets_sorted_by_metric.is_empty() { | ||||||||||||||||||||||
| // In this case, we cannot meet demand, so have to bail out. | ||||||||||||||||||||||
| // This may happen if: | ||||||||||||||||||||||
| // - the asset has zero activity limits for all time slices with demand. | ||||||||||||||||||||||
| // - known issue with the NPV objective | ||||||||||||||||||||||
| // (see https://github.com/EnergySystemsModellingLab/MUSE2/issues/716). | ||||||||||||||||||||||
| bail!( | ||||||||||||||||||||||
| "No feasible investment options for commodity '{}' after appraisal", | ||||||||||||||||||||||
| &commodity.id | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| .sorted_by(|output1, output2| match output1.compare_metric(output2) { | ||||||||||||||||||||||
| // If equal, we fall back on comparing asset properties | ||||||||||||||||||||||
| Ordering::Equal => compare_asset_fallback(&output1.asset, &output2.asset), | ||||||||||||||||||||||
| cmp => cmp, | ||||||||||||||||||||||
| }) | ||||||||||||||||||||||
| .collect_vec(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Check if all options have zero capacity. If so, we cannot meet demand, so have to bail | ||||||||||||||||||||||
| // out. | ||||||||||||||||||||||
| // | ||||||||||||||||||||||
| // This may happen if: | ||||||||||||||||||||||
| // - the asset has zero activity limits for all time slices with | ||||||||||||||||||||||
| // demand. | ||||||||||||||||||||||
| // - known issue with the NPV objective | ||||||||||||||||||||||
| // (see https://github.com/EnergySystemsModellingLab/MUSE2/issues/716). | ||||||||||||||||||||||
| ensure!( | ||||||||||||||||||||||
| !assets_sorted_by_metric.is_empty(), | ||||||||||||||||||||||
| "No feasible investment options for commodity '{}' after appraisal", | ||||||||||||||||||||||
| &commodity.id | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| let appraisal_comparison_method = classify_appraisal_comparison_method( | ||||||||||||||||||||||
| &assets_sorted_by_metric.iter().collect::<Vec<_>>(), | ||||||||||||||||||||||
| // Warn if there are multiple equally good assets | ||||||||||||||||||||||
| warn_on_equal_appraisal_outputs( | ||||||||||||||||||||||
| &assets_sorted_by_metric, | ||||||||||||||||||||||
| &agent.id, | ||||||||||||||||||||||
| &commodity.id, | ||||||||||||||||||||||
| region_id, | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Determine the best asset based on whether multiple equally-good options exist | ||||||||||||||||||||||
| let best_output = match appraisal_comparison_method { | ||||||||||||||||||||||
| // there are multiple equally good assets by metric | ||||||||||||||||||||||
| AppraisalComparisonMethod::EqualMetrics => { | ||||||||||||||||||||||
| // Count how many assets have the same metric as the best one | ||||||||||||||||||||||
| let count = assets_sorted_by_metric | ||||||||||||||||||||||
| .iter() | ||||||||||||||||||||||
| .take_while(|output| { | ||||||||||||||||||||||
| AppraisalOutput::compare_metric(&assets_sorted_by_metric[0], output).is_eq() | ||||||||||||||||||||||
| }) | ||||||||||||||||||||||
| .count(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // select from all equally good assets | ||||||||||||||||||||||
| let equally_good_assets: Vec<_> = | ||||||||||||||||||||||
| assets_sorted_by_metric.into_iter().take(count).collect(); | ||||||||||||||||||||||
| select_from_assets_with_equal_metric( | ||||||||||||||||||||||
| region_id, | ||||||||||||||||||||||
| &agent.id, | ||||||||||||||||||||||
| &commodity.id, | ||||||||||||||||||||||
| equally_good_assets, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| // there is a single best asset by metric | ||||||||||||||||||||||
| AppraisalComparisonMethod::Metric => { | ||||||||||||||||||||||
| assets_sorted_by_metric.into_iter().next().unwrap() | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
| let best_output = assets_sorted_by_metric.into_iter().next().unwrap(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Log the selected asset | ||||||||||||||||||||||
| debug!( | ||||||||||||||||||||||
|
|
@@ -806,6 +793,16 @@ fn is_any_remaining_demand(demand: &DemandMap) -> bool { | |||||||||||||||||||||
| demand.values().any(|flow| *flow > Flow(0.0)) | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// Compare assets as a fallback if metrics are equal. | ||||||||||||||||||||||
| /// | ||||||||||||||||||||||
| /// Commissioned assets are ordered before uncommissioned and newer before older. | ||||||||||||||||||||||
| /// | ||||||||||||||||||||||
| /// Used as a fallback to sort assets when they have equal appraisal tool outputs. | ||||||||||||||||||||||
| fn compare_asset_fallback(asset1: &Asset, asset2: &Asset) -> Ordering { | ||||||||||||||||||||||
| (asset2.is_commissioned(), asset2.commission_year()) | ||||||||||||||||||||||
| .cmp(&(asset1.is_commissioned(), asset1.commission_year())) | ||||||||||||||||||||||
|
Comment on lines
+808
to
+809
|
||||||||||||||||||||||
| (asset2.is_commissioned(), asset2.commission_year()) | |
| .cmp(&(asset1.is_commissioned(), asset1.commission_year())) | |
| // Preserve legacy behaviour: if both assets are uncommissioned (candidates), | |
| // treat them as equal regardless of their commission years. | |
| if !asset1.is_commissioned() && !asset2.is_commissioned() { | |
| Ordering::Equal | |
| } else { | |
| (asset2.is_commissioned(), asset2.commission_year()) | |
| .cmp(&(asset1.is_commissioned(), asset1.commission_year())) | |
| } |
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.
Oh, this is a good spot! I hadn't clocked that this was a functional change, although presumably the new behaviour was what was intended originally!
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.
The logic using
tuple_windows().take_while()doesn't include the last element in a sequence of equal outputs. For example, if outputs[0], outputs[1], and outputs[2] are all equal, only outputs[0] and outputs[1] will be included inasset_details, missing outputs[2]. This makes the warning message incomplete.Consider adding the final equal element after the iterator chain, or use a different approach such as:
outputs.iter().skip(1).take_while(|b| outputs[0].compare_metric(b).is_eq())and then prepend outputs[0] to the results.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.
Also a good spot... I'll rework it.