From 514ddc144e6ee082c09e30827ee42c706380363b Mon Sep 17 00:00:00 2001 From: Aurash Karimi Date: Tue, 3 Mar 2026 12:53:23 +0000 Subject: [PATCH 1/2] add fallback to equal-metrics comparison --- src/simulation/investment.rs | 9 +++------ src/simulation/investment/appraisal.rs | 12 ++++++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/simulation/investment.rs b/src/simulation/investment.rs index 688bd524..8fa3688c 100644 --- a/src/simulation/investment.rs +++ b/src/simulation/investment.rs @@ -19,7 +19,8 @@ use std::fmt::Display; pub mod appraisal; use appraisal::coefficients::calculate_coefficients_for_assets; use appraisal::{ - AppraisalOutput, appraise_investment, sort_appraisal_outputs_by_investment_priority, + AppraisalOutput, appraise_investment, count_equal_and_best_appraisal_outputs, + sort_appraisal_outputs_by_investment_priority, }; /// A map of demand across time slices for a specific market @@ -661,11 +662,7 @@ fn warn_on_equal_appraisal_outputs( return; } - // Count the number of identical (or nearly identical) appraisal outputs - let num_identical = outputs[1..] - .iter() - .take_while(|output| outputs[0].compare_metric(output).is_eq()) - .count(); + let num_identical = count_equal_and_best_appraisal_outputs(outputs); if num_identical > 0 { let asset_details = outputs[..=num_identical] diff --git a/src/simulation/investment/appraisal.rs b/src/simulation/investment/appraisal.rs index 7fbf42d6..82643d04 100644 --- a/src/simulation/investment/appraisal.rs +++ b/src/simulation/investment/appraisal.rs @@ -378,6 +378,18 @@ pub fn sort_appraisal_outputs_by_investment_priority(outputs_for_opts: &mut Vec< }); } +/// Counts the number of top appraisal outputs in a sorted slice that are indistinguishable +/// by both metric and fallback ordering. +pub fn count_equal_and_best_appraisal_outputs(outputs: &[AppraisalOutput]) -> usize { + outputs[1..] + .iter() + .take_while(|output| { + output.compare_metric(&outputs[0]).is_eq() + && compare_asset_fallback(&output.asset, &outputs[0].asset).is_eq() + }) + .count() +} + #[cfg(test)] mod tests { use super::*; From 2207c82fcfafb9ce12dc3e113b7b56ea8848ca09 Mon Sep 17 00:00:00 2001 From: Aurash Karimi Date: Wed, 4 Mar 2026 12:44:08 +0000 Subject: [PATCH 2/2] add tests for equal metric count --- src/simulation/investment.rs | 4 +- src/simulation/investment/appraisal.rs | 138 ++++++++++++++++++++++++- 2 files changed, 139 insertions(+), 3 deletions(-) diff --git a/src/simulation/investment.rs b/src/simulation/investment.rs index 8fa3688c..5e9716a7 100644 --- a/src/simulation/investment.rs +++ b/src/simulation/investment.rs @@ -652,7 +652,7 @@ fn get_candidate_assets<'a>( } /// Print debug message if there are multiple equally good outputs -fn warn_on_equal_appraisal_outputs( +fn log_on_equal_appraisal_outputs( outputs: &[AppraisalOutput], agent_id: &AgentID, commodity_id: &CommodityID, @@ -826,7 +826,7 @@ fn select_best_assets( } // Warn if there are multiple equally good assets - warn_on_equal_appraisal_outputs(&outputs_for_opts, &agent.id, &commodity.id, region_id); + log_on_equal_appraisal_outputs(&outputs_for_opts, &agent.id, &commodity.id, region_id); let best_output = outputs_for_opts.into_iter().next().unwrap(); diff --git a/src/simulation/investment/appraisal.rs b/src/simulation/investment/appraisal.rs index 82643d04..acbb303c 100644 --- a/src/simulation/investment/appraisal.rs +++ b/src/simulation/investment/appraisal.rs @@ -379,8 +379,11 @@ pub fn sort_appraisal_outputs_by_investment_priority(outputs_for_opts: &mut Vec< } /// Counts the number of top appraisal outputs in a sorted slice that are indistinguishable -/// by both metric and fallback ordering. +/// by both metric and fallback ordering. Excludes the first element from the count. pub fn count_equal_and_best_appraisal_outputs(outputs: &[AppraisalOutput]) -> usize { + if outputs.is_empty() { + return 0; + } outputs[1..] .iter() .take_while(|output| { @@ -955,4 +958,137 @@ mod tests { // The invalid output should have been filtered out assert_eq!(outputs.len(), 0); } + + /// Tests for counting number of equal metrics using identical assets so only metric values + /// affect the count. + #[rstest] + #[case(vec![5.0], 0, "single_element")] + #[case(vec![5.0, 5.0, 5.0], 2, "all_equal_returns_len_minus_one")] + #[case(vec![1.0, 2.0, 3.0], 0, "none_equal_to_best")] + #[case(vec![5.0, 5.0, 9.0], 1, "partial_equality_stops_at_first_difference")] + #[case(vec![5.0, 5.0, 9.0, 5.0], 1, "equality_does_not_resume_after_gap")] + fn count_equal_best_lcox_metric( + asset: Asset, + #[case] metric_values: Vec, + #[case] expected_count: usize, + #[case] description: &str, + ) { + let metrics: Vec> = metric_values + .into_iter() + .map(|v| Box::new(LCOXMetric::new(MoneyPerActivity(v))) as Box) + .collect(); + + let outputs = + appraisal_outputs_with_investment_priority_invariant_to_assets(metrics, &asset); + + assert_eq!( + count_equal_and_best_appraisal_outputs(&outputs), + expected_count, + "Failed for case: {description}" + ); + } + + /// Empty slice count should return 0. + #[test] + fn count_equal_best_empty_slice_returns_zero() { + let outputs: Vec = vec![]; + assert_eq!(count_equal_and_best_appraisal_outputs(&outputs), 0); + } + + /// Equal metrics but differing asset fallback (commissioned vs. candidate) → + /// outputs are distinguishable, so count should be 0. + #[rstest] + fn count_equal_best_equal_metric_different_fallback_returns_zero( + process: Process, + region_id: RegionID, + agent_id: AgentID, + ) { + let process_rc = Rc::new(process); + let capacity = Capacity(10.0); + + let commissioned = Asset::new_commissioned( + agent_id.clone(), + process_rc.clone(), + region_id.clone(), + capacity, + 2020, + ) + .unwrap(); + let candidate = + Asset::new_candidate(process_rc.clone(), region_id.clone(), capacity, 2020).unwrap(); + + let metric_value = MoneyPerActivity(5.0); + let outputs = appraisal_outputs( + vec![commissioned, candidate], + vec![ + Box::new(LCOXMetric::new(metric_value)), + Box::new(LCOXMetric::new(metric_value)), + ], + ); + + assert_eq!(count_equal_and_best_appraisal_outputs(&outputs), 0); + } + + /// Equal metrics and equal asset fallback (same commissioned status and commission year) → + /// the second element is indistinguishable, so count should be 1. + #[rstest] + fn count_equal_best_equal_metric_and_equal_fallback_returns_one( + process: Process, + region_id: RegionID, + agent_id: AgentID, + ) { + let process_rc = Rc::new(process); + let capacity = Capacity(10.0); + let year = 2020; + + let asset1 = Asset::new_commissioned( + agent_id.clone(), + process_rc.clone(), + region_id.clone(), + capacity, + year, + ) + .unwrap(); + let asset2 = Asset::new_commissioned( + agent_id.clone(), + process_rc.clone(), + region_id.clone(), + capacity, + year, + ) + .unwrap(); + + let metric_value = MoneyPerActivity(5.0); + let outputs = appraisal_outputs( + vec![asset1, asset2], + vec![ + Box::new(LCOXMetric::new(metric_value)), + Box::new(LCOXMetric::new(metric_value)), + ], + ); + + assert_eq!(count_equal_and_best_appraisal_outputs(&outputs), 1); + } + + /// Equal NPV metrics and identical assets → second element should be counted. + #[rstest] + fn count_equal_best_equal_npv_metrics(asset: Asset) { + let make_npv = |surplus: f64, fixed_cost: f64| { + Box::new(NPVMetric::new(ProfitabilityIndex { + total_annualised_surplus: Money(surplus), + annualised_fixed_cost: Money(fixed_cost), + })) as Box + }; + + let metrics = vec![ + make_npv(200.0, 100.0), + make_npv(200.0, 100.0), // Equal to best + make_npv(100.0, 100.0), // Worse + ]; + + let outputs = + appraisal_outputs_with_investment_priority_invariant_to_assets(metrics, &asset); + + assert_eq!(count_equal_and_best_appraisal_outputs(&outputs), 1); + } }