Skip to content

Commit 8f1a2eb

Browse files
authored
Merge pull request #1006 from EnergySystemsModellingLab/add-warning-for-asset-deadlock
add warning for multiple assets with same metrics
2 parents ce38d1c + 9be3c66 commit 8f1a2eb

File tree

9 files changed

+358
-201
lines changed

9 files changed

+358
-201
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ If you use MUSE2 in your work, please cite us. For information on how to cite th
6969

7070
## Contributors ✨
7171

72-
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
72+
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/emoji-key)):
7373

7474
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
7575
<!-- prettier-ignore-start -->
@@ -95,7 +95,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
9595
<tr>
9696
<td align="center" size="13px" colspan="7">
9797
<img src="https://raw.githubusercontent.com/all-contributors/all-contributors-cli/1b8533af435da9854653492b1327a23a4dbd0a10/assets/logo-small.svg">
98-
<a href="https://all-contributors.js.org/docs/en/bot/usage">Add your contributions</a>
98+
<a href="https://allcontributors.org/bot/usage">Add your contributions</a>
9999
</img>
100100
</td>
101101
</tr>

src/simulation/investment.rs

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Code for performing agent investment.
22
use super::optimisation::{DispatchRun, FlowMap};
3-
use crate::agent::Agent;
3+
use crate::agent::{Agent, AgentID};
44
use crate::asset::{Asset, AssetIterator, AssetRef, AssetState};
55
use crate::commodity::{Commodity, CommodityID, CommodityMap};
66
use crate::model::ALLOW_BROKEN_OPTION_NAME;
@@ -13,13 +13,16 @@ use crate::units::{Capacity, Dimensionless, Flow, FlowPerCapacity};
1313
use anyhow::{Context, Result, bail, ensure};
1414
use indexmap::IndexMap;
1515
use itertools::{Itertools, chain};
16-
use log::debug;
16+
use log::{debug, warn};
1717
use std::collections::HashMap;
1818
use std::fmt::Display;
1919

2020
pub mod appraisal;
2121
use appraisal::coefficients::calculate_coefficients_for_assets;
22-
use appraisal::{AppraisalOutput, appraise_investment};
22+
use appraisal::{
23+
AppraisalComparisonMethod, AppraisalOutput, appraise_investment,
24+
classify_appraisal_comparison_method,
25+
};
2326

2427
/// A map of demand across time slices for a specific market
2528
type DemandMap = IndexMap<TimeSliceID, Flow>;
@@ -281,6 +284,7 @@ fn select_assets_for_single_market(
281284
opt_assets,
282285
commodity,
283286
agent,
287+
region_id,
284288
prices,
285289
demand_portion_for_market,
286290
year,
@@ -617,13 +621,45 @@ fn get_candidate_assets<'a>(
617621
})
618622
}
619623

624+
fn select_from_assets_with_equal_metric(
625+
region_id: &RegionID,
626+
agent_id: &AgentID,
627+
commodity_id: &CommodityID,
628+
equally_good_assets: Vec<AppraisalOutput>,
629+
) -> AppraisalOutput {
630+
// Format asset details for diagnostic logging
631+
let asset_details = equally_good_assets
632+
.iter()
633+
.map(|output| {
634+
format!(
635+
"Process id: '{}' (State: {}{})",
636+
output.asset.process_id(),
637+
output.asset.state(),
638+
output
639+
.asset
640+
.id()
641+
.map(|id| format!(", Asset id: {id}"))
642+
.unwrap_or_default(),
643+
)
644+
})
645+
.collect::<Vec<_>>()
646+
.join(", ");
647+
let warning_message = format!(
648+
"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.",
649+
);
650+
warn!("{warning_message}");
651+
// Select the first asset arbitrarily from the equally performing options
652+
equally_good_assets.into_iter().next().unwrap()
653+
}
654+
620655
/// Get the best assets for meeting demand for the given commodity
621656
#[allow(clippy::too_many_arguments)]
622657
fn select_best_assets(
623658
model: &Model,
624659
mut opt_assets: Vec<AssetRef>,
625660
commodity: &Commodity,
626661
agent: &Agent,
662+
region_id: &RegionID,
627663
prices: &CommodityPrices,
628664
mut demand: DemandMap,
629665
year: u32,
@@ -679,22 +715,56 @@ fn select_best_assets(
679715
&outputs_for_opts,
680716
)?;
681717

682-
// Select the best investment option according to `AppraisalOutput::compare_metric`
683-
let Some(best_output) = outputs_for_opts
718+
// Sort assets by appraisal metric
719+
let assets_sorted_by_metric: Vec<_> = outputs_for_opts
684720
.into_iter()
685-
// Investment options with zero capacity are excluded. This may happen if the asset has
686-
// zero activity limits for all time slices with demand. This can also happen due to a
687-
// known issue with the NPV objective, for which we do not currently have a solution
688-
// (see https://github.com/EnergySystemsModellingLab/MUSE2/issues/716).
689721
.filter(|output| output.capacity > Capacity(0.0))
690-
.min_by(AppraisalOutput::compare_metric)
691-
else {
692-
// If None, this means all investment options have zero capacity. In this case, we
693-
// cannot meet demand, so have to bail out.
722+
.sorted_by(AppraisalOutput::compare_metric)
723+
.collect();
724+
725+
// check if all options have zero capacity
726+
if assets_sorted_by_metric.is_empty() {
727+
// In this case, we cannot meet demand, so have to bail out.
728+
// This may happen if:
729+
// - the asset has zero activity limits for all time slices with demand.
730+
// - known issue with the NPV objective
731+
// (see https://github.com/EnergySystemsModellingLab/MUSE2/issues/716).
694732
bail!(
695733
"No feasible investment options for commodity '{}' after appraisal",
696734
&commodity.id
697735
)
736+
}
737+
738+
let appraisal_comparison_method = classify_appraisal_comparison_method(
739+
&assets_sorted_by_metric.iter().collect::<Vec<_>>(),
740+
);
741+
742+
// Determine the best asset based on whether multiple equally-good options exist
743+
let best_output = match appraisal_comparison_method {
744+
// there are multiple equally good assets by metric
745+
AppraisalComparisonMethod::EqualMetrics => {
746+
// Count how many assets have the same metric as the best one
747+
let count = assets_sorted_by_metric
748+
.iter()
749+
.take_while(|output| {
750+
AppraisalOutput::compare_metric(&assets_sorted_by_metric[0], output).is_eq()
751+
})
752+
.count();
753+
754+
// select from all equally good assets
755+
let equally_good_assets: Vec<_> =
756+
assets_sorted_by_metric.into_iter().take(count).collect();
757+
select_from_assets_with_equal_metric(
758+
region_id,
759+
&agent.id,
760+
&commodity.id,
761+
equally_good_assets,
762+
)
763+
}
764+
// there is a single best asset by metric
765+
AppraisalComparisonMethod::Metric => {
766+
assets_sorted_by_metric.into_iter().next().unwrap()
767+
}
698768
};
699769

700770
// Log the selected asset

src/simulation/investment/appraisal.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,62 @@ impl AppraisalOutput {
5454
);
5555

5656
if approx_eq!(f64, self.metric, other.metric) {
57-
Ordering::Equal
57+
self.compare_with_equal_metrics(other)
5858
} else {
5959
self.metric.partial_cmp(&other.metric).unwrap()
6060
}
6161
}
62+
63+
/// Compare this appraisal to another when the metrics are known to be equal.
64+
pub fn compare_with_equal_metrics(&self, other: &Self) -> Ordering {
65+
assert!(
66+
approx_eq!(f64, self.metric, other.metric),
67+
"Appraisal metrics must be equal"
68+
);
69+
70+
// Favour commissioned assets over non-commissioned
71+
if self.asset.is_commissioned() && !other.asset.is_commissioned() {
72+
return Ordering::Less;
73+
}
74+
if !self.asset.is_commissioned() && other.asset.is_commissioned() {
75+
return Ordering::Greater;
76+
}
77+
78+
// if both commissioned, favour newer ones
79+
if self.asset.is_commissioned() && other.asset.is_commissioned() {
80+
return self
81+
.asset
82+
.commission_year()
83+
.cmp(&other.asset.commission_year())
84+
.reverse();
85+
}
86+
87+
Ordering::Equal
88+
}
89+
}
90+
91+
/// methods used to compare multiple appraisal outputs
92+
pub enum AppraisalComparisonMethod {
93+
/// If all appraisal outputs have different metrics
94+
Metric,
95+
/// two or more appraisal outputs have equal metrics
96+
EqualMetrics,
97+
}
98+
99+
/// Classify the appropriate method to compare appraisal outputs
100+
/// given an array of appraisal outputs sorted by metric
101+
pub fn classify_appraisal_comparison_method(
102+
appraisals_sorted_by_metric: &[&AppraisalOutput],
103+
) -> AppraisalComparisonMethod {
104+
if appraisals_sorted_by_metric.len() >= 2
105+
&& appraisals_sorted_by_metric[0]
106+
.compare_metric(appraisals_sorted_by_metric[1])
107+
.is_eq()
108+
{
109+
AppraisalComparisonMethod::EqualMetrics
110+
} else {
111+
AppraisalComparisonMethod::Metric
112+
}
62113
}
63114

64115
/// Calculate LCOX for a hypothetical investment in the given asset.

tests/data/muse1_default/assets.csv

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ asset_id,process_id,region_id,agent_id,commission_year,decommission_year,capacit
77
5,gasCCGT,R1,A1_PWR,2030,,5.831988336023331
88
6,heatpump,R1,A1_RES,2035,,6.119987760024477
99
7,windturbine,R1,A1_PWR,2035,,3.5999928000144
10-
8,gasCCGT,R1,A1_PWR,2035,2050,1.119997760004481
10+
8,gasCCGT,R1,A1_PWR,2035,,1.119997760004481
1111
9,heatpump,R1,A1_RES,2040,,8.387983224033553
1212
10,windturbine,R1,A1_PWR,2040,,8.387983224033553
1313
11,heatpump,R1,A1_RES,2045,,4.787990424019152
1414
12,windturbine,R1,A1_PWR,2045,,4.787990424019152
15-
13,heatpump,R1,A1_RES,2050,,4.823990352019292
16-
14,windturbine,R1,A1_PWR,2050,,16.199967600064802
15+
13,heatpump,R1,A1_RES,2050,,5.3999892000216
16+
14,windturbine,R1,A1_PWR,2050,,5.399989200021599

tests/data/muse1_default/commodity_flows.csv

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ milestone_year,asset_id,commodity_id,time_slice,flow
513513
2050,0,gas,all-year.morning,-0.0
514514
2050,0,gas,all-year.afternoon,-0.0
515515
2050,0,gas,all-year.early-peak,-0.0
516-
2050,0,gas,all-year.late-peak,0.47494800000000054
516+
2050,0,gas,all-year.late-peak,1.6773480000000012
517517
2050,0,gas,all-year.evening,-0.0
518518
2050,2,electricity,all-year.night,-0.0
519519
2050,2,heat,all-year.night,0.0
@@ -523,8 +523,8 @@ milestone_year,asset_id,commodity_id,time_slice,flow
523523
2050,2,heat,all-year.afternoon,0.0
524524
2050,2,electricity,all-year.early-peak,-0.0
525525
2050,2,heat,all-year.early-peak,0.0
526-
2050,2,electricity,all-year.late-peak,-1.1172000000000009
527-
2050,2,heat,all-year.late-peak,2.793000000000002
526+
2050,2,electricity,all-year.late-peak,-1.0788000000000006
527+
2050,2,heat,all-year.late-peak,2.6970000000000014
528528
2050,2,electricity,all-year.evening,-0.0
529529
2050,2,heat,all-year.evening,0.0
530530
2050,3,wind,all-year.night,-1.1171999999999997
@@ -541,16 +541,16 @@ milestone_year,asset_id,commodity_id,time_slice,flow
541541
2050,3,electricity,all-year.evening,1.1171999999999997
542542
2050,4,electricity,all-year.night,-0.0
543543
2050,4,heat,all-year.night,0.0
544-
2050,4,electricity,all-year.morning,-0.19200000000000036
545-
2050,4,heat,all-year.morning,0.48000000000000087
544+
2050,4,electricity,all-year.morning,-0.15360000000000015
545+
2050,4,heat,all-year.morning,0.38400000000000034
546546
2050,4,electricity,all-year.afternoon,-0.0
547547
2050,4,heat,all-year.afternoon,0.0
548-
2050,4,electricity,all-year.early-peak,-0.19200000000000036
549-
2050,4,heat,all-year.early-peak,0.48000000000000087
548+
2050,4,electricity,all-year.early-peak,-0.15360000000000015
549+
2050,4,heat,all-year.early-peak,0.38400000000000034
550550
2050,4,electricity,all-year.late-peak,-0.8748
551551
2050,4,heat,all-year.late-peak,2.187
552-
2050,4,electricity,all-year.evening,-0.7920000000000004
553-
2050,4,heat,all-year.evening,1.9800000000000009
552+
2050,4,electricity,all-year.evening,-0.7536
553+
2050,4,heat,all-year.evening,1.884
554554
2050,5,gas,all-year.night,-0.0
555555
2050,5,electricity,all-year.night,0.0
556556
2050,5,CO2f,all-year.night,0.0
@@ -563,18 +563,18 @@ milestone_year,asset_id,commodity_id,time_slice,flow
563563
2050,5,gas,all-year.early-peak,-0.0
564564
2050,5,electricity,all-year.early-peak,0.0
565565
2050,5,CO2f,all-year.early-peak,0.0
566-
2050,5,gas,all-year.late-peak,-0.47494800000000054
567-
2050,5,electricity,all-year.late-peak,0.2844000000000003
568-
2050,5,CO2f,all-year.late-peak,26.07094800000003
566+
2050,5,gas,all-year.late-peak,-1.3967880000000008
567+
2050,5,electricity,all-year.late-peak,0.8364000000000005
568+
2050,5,CO2f,all-year.late-peak,76.67278800000004
569569
2050,5,gas,all-year.evening,-0.0
570570
2050,5,electricity,all-year.evening,0.0
571571
2050,5,CO2f,all-year.evening,0.0
572-
2050,6,electricity,all-year.night,0.0
573-
2050,6,heat,all-year.night,-0.0
572+
2050,6,electricity,all-year.night,-0.0
573+
2050,6,heat,all-year.night,0.0
574574
2050,6,electricity,all-year.morning,-0.40799999999999986
575575
2050,6,heat,all-year.morning,1.0199999999999996
576-
2050,6,electricity,all-year.afternoon,0.0
577-
2050,6,heat,all-year.afternoon,-0.0
576+
2050,6,electricity,all-year.afternoon,-0.0
577+
2050,6,heat,all-year.afternoon,0.0
578578
2050,6,electricity,all-year.early-peak,-0.40799999999999986
579579
2050,6,heat,all-year.early-peak,1.0199999999999996
580580
2050,6,electricity,all-year.late-peak,-0.40799999999999986
@@ -593,12 +593,30 @@ milestone_year,asset_id,commodity_id,time_slice,flow
593593
2050,7,electricity,all-year.late-peak,0.24000000000000005
594594
2050,7,wind,all-year.evening,-0.24000000000000005
595595
2050,7,electricity,all-year.evening,0.24000000000000005
596-
2050,9,electricity,all-year.night,-0.5592
597-
2050,9,heat,all-year.night,1.3980000000000001
596+
2050,8,gas,all-year.night,-0.0
597+
2050,8,electricity,all-year.night,0.0
598+
2050,8,CO2f,all-year.night,0.0
599+
2050,8,gas,all-year.morning,-0.0
600+
2050,8,electricity,all-year.morning,0.0
601+
2050,8,CO2f,all-year.morning,0.0
602+
2050,8,gas,all-year.afternoon,-0.0
603+
2050,8,electricity,all-year.afternoon,0.0
604+
2050,8,CO2f,all-year.afternoon,0.0
605+
2050,8,gas,all-year.early-peak,-0.0
606+
2050,8,electricity,all-year.early-peak,0.0
607+
2050,8,CO2f,all-year.early-peak,0.0
608+
2050,8,gas,all-year.late-peak,-0.28056000000000025
609+
2050,8,electricity,all-year.late-peak,0.16800000000000015
610+
2050,8,CO2f,all-year.late-peak,15.400560000000015
611+
2050,8,gas,all-year.evening,-0.0
612+
2050,8,electricity,all-year.evening,0.0
613+
2050,8,CO2f,all-year.evening,0.0
614+
2050,9,electricity,all-year.night,-0.5208
615+
2050,9,heat,all-year.night,1.302
598616
2050,9,electricity,all-year.morning,-0.5592
599617
2050,9,heat,all-year.morning,1.3980000000000001
600-
2050,9,electricity,all-year.afternoon,-0.5592
601-
2050,9,heat,all-year.afternoon,1.3980000000000001
618+
2050,9,electricity,all-year.afternoon,-0.5208
619+
2050,9,heat,all-year.afternoon,1.302
602620
2050,9,electricity,all-year.early-peak,-0.5592
603621
2050,9,heat,all-year.early-peak,1.3980000000000001
604622
2050,9,electricity,all-year.late-peak,-0.5592
@@ -641,27 +659,27 @@ milestone_year,asset_id,commodity_id,time_slice,flow
641659
2050,12,electricity,all-year.late-peak,0.31920000000000004
642660
2050,12,wind,all-year.evening,-0.31920000000000004
643661
2050,12,electricity,all-year.evening,0.31920000000000004
644-
2050,13,electricity,all-year.night,-0.3215999999999998
645-
2050,13,heat,all-year.night,0.8039999999999994
646-
2050,13,electricity,all-year.morning,-0.3215999999999998
647-
2050,13,heat,all-year.morning,0.8039999999999994
648-
2050,13,electricity,all-year.afternoon,-0.3215999999999998
649-
2050,13,heat,all-year.afternoon,0.8039999999999994
650-
2050,13,electricity,all-year.early-peak,-0.3215999999999998
651-
2050,13,heat,all-year.early-peak,0.8039999999999994
652-
2050,13,electricity,all-year.late-peak,-0.3215999999999998
653-
2050,13,heat,all-year.late-peak,0.8039999999999994
654-
2050,13,electricity,all-year.evening,-0.3215999999999998
655-
2050,13,heat,all-year.evening,0.8039999999999994
656-
2050,14,wind,all-year.night,-1.0800000000000003
657-
2050,14,electricity,all-year.night,1.0800000000000003
658-
2050,14,wind,all-year.morning,-1.0800000000000003
659-
2050,14,electricity,all-year.morning,1.0800000000000003
660-
2050,14,wind,all-year.afternoon,-1.0800000000000003
661-
2050,14,electricity,all-year.afternoon,1.0800000000000003
662-
2050,14,wind,all-year.early-peak,-1.0800000000000003
663-
2050,14,electricity,all-year.early-peak,1.0800000000000003
664-
2050,14,wind,all-year.late-peak,-1.0800000000000003
665-
2050,14,electricity,all-year.late-peak,1.0800000000000003
666-
2050,14,wind,all-year.evening,-1.0800000000000003
667-
2050,14,electricity,all-year.evening,1.0800000000000003
662+
2050,13,electricity,all-year.night,-0.36000000000000004
663+
2050,13,heat,all-year.night,0.9
664+
2050,13,electricity,all-year.morning,-0.36000000000000004
665+
2050,13,heat,all-year.morning,0.9
666+
2050,13,electricity,all-year.afternoon,-0.36000000000000004
667+
2050,13,heat,all-year.afternoon,0.9
668+
2050,13,electricity,all-year.early-peak,-0.36000000000000004
669+
2050,13,heat,all-year.early-peak,0.9
670+
2050,13,electricity,all-year.late-peak,-0.36000000000000004
671+
2050,13,heat,all-year.late-peak,0.9
672+
2050,13,electricity,all-year.evening,-0.36000000000000004
673+
2050,13,heat,all-year.evening,0.9
674+
2050,14,wind,all-year.night,-0.36
675+
2050,14,electricity,all-year.night,0.36
676+
2050,14,wind,all-year.morning,-0.36
677+
2050,14,electricity,all-year.morning,0.36
678+
2050,14,wind,all-year.afternoon,-0.36
679+
2050,14,electricity,all-year.afternoon,0.36
680+
2050,14,wind,all-year.early-peak,-0.36
681+
2050,14,electricity,all-year.early-peak,0.36
682+
2050,14,wind,all-year.late-peak,-0.36
683+
2050,14,electricity,all-year.late-peak,0.36
684+
2050,14,wind,all-year.evening,-0.36
685+
2050,14,electricity,all-year.evening,0.36

0 commit comments

Comments
 (0)