Skip to content

Commit 312f691

Browse files
authored
Merge pull request #1161 from EnergySystemsModellingLab/fix-equal-metrics-logging
Fix equal metrics logging
2 parents b62a7ce + 2207c82 commit 312f691

2 files changed

Lines changed: 153 additions & 8 deletions

File tree

src/simulation/investment.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ use std::fmt::Display;
1919
pub mod appraisal;
2020
use appraisal::coefficients::calculate_coefficients_for_assets;
2121
use appraisal::{
22-
AppraisalOutput, appraise_investment, sort_appraisal_outputs_by_investment_priority,
22+
AppraisalOutput, appraise_investment, count_equal_and_best_appraisal_outputs,
23+
sort_appraisal_outputs_by_investment_priority,
2324
};
2425

2526
/// A map of demand across time slices for a specific market
@@ -651,7 +652,7 @@ fn get_candidate_assets<'a>(
651652
}
652653

653654
/// Print debug message if there are multiple equally good outputs
654-
fn warn_on_equal_appraisal_outputs(
655+
fn log_on_equal_appraisal_outputs(
655656
outputs: &[AppraisalOutput],
656657
agent_id: &AgentID,
657658
commodity_id: &CommodityID,
@@ -661,11 +662,7 @@ fn warn_on_equal_appraisal_outputs(
661662
return;
662663
}
663664

664-
// Count the number of identical (or nearly identical) appraisal outputs
665-
let num_identical = outputs[1..]
666-
.iter()
667-
.take_while(|output| outputs[0].compare_metric(output).is_eq())
668-
.count();
665+
let num_identical = count_equal_and_best_appraisal_outputs(outputs);
669666

670667
if num_identical > 0 {
671668
let asset_details = outputs[..=num_identical]
@@ -829,7 +826,7 @@ fn select_best_assets(
829826
}
830827

831828
// Warn if there are multiple equally good assets
832-
warn_on_equal_appraisal_outputs(&outputs_for_opts, &agent.id, &commodity.id, region_id);
829+
log_on_equal_appraisal_outputs(&outputs_for_opts, &agent.id, &commodity.id, region_id);
833830

834831
let best_output = outputs_for_opts.into_iter().next().unwrap();
835832

src/simulation/investment/appraisal.rs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,21 @@ pub fn sort_appraisal_outputs_by_investment_priority(outputs_for_opts: &mut Vec<
378378
});
379379
}
380380

381+
/// Counts the number of top appraisal outputs in a sorted slice that are indistinguishable
382+
/// by both metric and fallback ordering. Excludes the first element from the count.
383+
pub fn count_equal_and_best_appraisal_outputs(outputs: &[AppraisalOutput]) -> usize {
384+
if outputs.is_empty() {
385+
return 0;
386+
}
387+
outputs[1..]
388+
.iter()
389+
.take_while(|output| {
390+
output.compare_metric(&outputs[0]).is_eq()
391+
&& compare_asset_fallback(&output.asset, &outputs[0].asset).is_eq()
392+
})
393+
.count()
394+
}
395+
381396
#[cfg(test)]
382397
mod tests {
383398
use super::*;
@@ -943,4 +958,137 @@ mod tests {
943958
// The invalid output should have been filtered out
944959
assert_eq!(outputs.len(), 0);
945960
}
961+
962+
/// Tests for counting number of equal metrics using identical assets so only metric values
963+
/// affect the count.
964+
#[rstest]
965+
#[case(vec![5.0], 0, "single_element")]
966+
#[case(vec![5.0, 5.0, 5.0], 2, "all_equal_returns_len_minus_one")]
967+
#[case(vec![1.0, 2.0, 3.0], 0, "none_equal_to_best")]
968+
#[case(vec![5.0, 5.0, 9.0], 1, "partial_equality_stops_at_first_difference")]
969+
#[case(vec![5.0, 5.0, 9.0, 5.0], 1, "equality_does_not_resume_after_gap")]
970+
fn count_equal_best_lcox_metric(
971+
asset: Asset,
972+
#[case] metric_values: Vec<f64>,
973+
#[case] expected_count: usize,
974+
#[case] description: &str,
975+
) {
976+
let metrics: Vec<Box<dyn MetricTrait>> = metric_values
977+
.into_iter()
978+
.map(|v| Box::new(LCOXMetric::new(MoneyPerActivity(v))) as Box<dyn MetricTrait>)
979+
.collect();
980+
981+
let outputs =
982+
appraisal_outputs_with_investment_priority_invariant_to_assets(metrics, &asset);
983+
984+
assert_eq!(
985+
count_equal_and_best_appraisal_outputs(&outputs),
986+
expected_count,
987+
"Failed for case: {description}"
988+
);
989+
}
990+
991+
/// Empty slice count should return 0.
992+
#[test]
993+
fn count_equal_best_empty_slice_returns_zero() {
994+
let outputs: Vec<AppraisalOutput> = vec![];
995+
assert_eq!(count_equal_and_best_appraisal_outputs(&outputs), 0);
996+
}
997+
998+
/// Equal metrics but differing asset fallback (commissioned vs. candidate) →
999+
/// outputs are distinguishable, so count should be 0.
1000+
#[rstest]
1001+
fn count_equal_best_equal_metric_different_fallback_returns_zero(
1002+
process: Process,
1003+
region_id: RegionID,
1004+
agent_id: AgentID,
1005+
) {
1006+
let process_rc = Rc::new(process);
1007+
let capacity = Capacity(10.0);
1008+
1009+
let commissioned = Asset::new_commissioned(
1010+
agent_id.clone(),
1011+
process_rc.clone(),
1012+
region_id.clone(),
1013+
capacity,
1014+
2020,
1015+
)
1016+
.unwrap();
1017+
let candidate =
1018+
Asset::new_candidate(process_rc.clone(), region_id.clone(), capacity, 2020).unwrap();
1019+
1020+
let metric_value = MoneyPerActivity(5.0);
1021+
let outputs = appraisal_outputs(
1022+
vec![commissioned, candidate],
1023+
vec![
1024+
Box::new(LCOXMetric::new(metric_value)),
1025+
Box::new(LCOXMetric::new(metric_value)),
1026+
],
1027+
);
1028+
1029+
assert_eq!(count_equal_and_best_appraisal_outputs(&outputs), 0);
1030+
}
1031+
1032+
/// Equal metrics and equal asset fallback (same commissioned status and commission year) →
1033+
/// the second element is indistinguishable, so count should be 1.
1034+
#[rstest]
1035+
fn count_equal_best_equal_metric_and_equal_fallback_returns_one(
1036+
process: Process,
1037+
region_id: RegionID,
1038+
agent_id: AgentID,
1039+
) {
1040+
let process_rc = Rc::new(process);
1041+
let capacity = Capacity(10.0);
1042+
let year = 2020;
1043+
1044+
let asset1 = Asset::new_commissioned(
1045+
agent_id.clone(),
1046+
process_rc.clone(),
1047+
region_id.clone(),
1048+
capacity,
1049+
year,
1050+
)
1051+
.unwrap();
1052+
let asset2 = Asset::new_commissioned(
1053+
agent_id.clone(),
1054+
process_rc.clone(),
1055+
region_id.clone(),
1056+
capacity,
1057+
year,
1058+
)
1059+
.unwrap();
1060+
1061+
let metric_value = MoneyPerActivity(5.0);
1062+
let outputs = appraisal_outputs(
1063+
vec![asset1, asset2],
1064+
vec![
1065+
Box::new(LCOXMetric::new(metric_value)),
1066+
Box::new(LCOXMetric::new(metric_value)),
1067+
],
1068+
);
1069+
1070+
assert_eq!(count_equal_and_best_appraisal_outputs(&outputs), 1);
1071+
}
1072+
1073+
/// Equal NPV metrics and identical assets → second element should be counted.
1074+
#[rstest]
1075+
fn count_equal_best_equal_npv_metrics(asset: Asset) {
1076+
let make_npv = |surplus: f64, fixed_cost: f64| {
1077+
Box::new(NPVMetric::new(ProfitabilityIndex {
1078+
total_annualised_surplus: Money(surplus),
1079+
annualised_fixed_cost: Money(fixed_cost),
1080+
})) as Box<dyn MetricTrait>
1081+
};
1082+
1083+
let metrics = vec![
1084+
make_npv(200.0, 100.0),
1085+
make_npv(200.0, 100.0), // Equal to best
1086+
make_npv(100.0, 100.0), // Worse
1087+
];
1088+
1089+
let outputs =
1090+
appraisal_outputs_with_investment_priority_invariant_to_assets(metrics, &asset);
1091+
1092+
assert_eq!(count_equal_and_best_appraisal_outputs(&outputs), 1);
1093+
}
9461094
}

0 commit comments

Comments
 (0)