use super::DemandMap;
use crate::agent::ObjectiveType;
use crate::asset::{Asset, AssetCapacity, AssetRef};
use crate::commodity::Commodity;
use crate::finance::{ProfitabilityIndex, lcox, profitability_index};
use crate::model::Model;
use crate::time_slice::TimeSliceID;
use crate::units::{Activity, Capacity, Money, MoneyPerActivity, MoneyPerCapacity};
use anyhow::Result;
use costs::annual_fixed_cost;
use erased_serde::Serialize as ErasedSerialize;
use indexmap::IndexMap;
use optimisation::ResultsMap;
use serde::Serialize;
use std::any::Any;
use std::cmp::Ordering;
use std::rc::Rc;
pub mod coefficients;
mod constraints;
mod costs;
mod optimisation;
use coefficients::ObjectiveCoefficients;
use float_cmp::approx_eq;
use float_cmp::{ApproxEq, F64Margin};
use optimisation::perform_optimisation;
fn compare_approx<T>(a: T, b: T) -> Ordering
where
T: Copy + PartialOrd + ApproxEq<Margin = F64Margin>,
{
if a.approx_eq(b, F64Margin::default()) {
Ordering::Equal
} else {
a.partial_cmp(&b).expect("Cannot compare NaN values")
}
}
pub struct AppraisalOutput {
pub asset: AssetRef,
pub capacity: AssetCapacity,
pub activity: IndexMap<TimeSliceID, Activity>,
pub unmet_demand: DemandMap,
pub metric: Option<Box<dyn MetricTrait>>,
pub coefficients: Rc<ObjectiveCoefficients>,
}
impl AppraisalOutput {
fn new<T: MetricTrait>(
asset: AssetRef,
results: ResultsMap,
metric: Option<T>,
coefficients: Rc<ObjectiveCoefficients>,
) -> Self {
Self {
asset,
capacity: results.capacity,
activity: results.activity,
unmet_demand: results.unmet_demand,
metric: metric.map(|m| Box::new(m) as Box<dyn MetricTrait>),
coefficients,
}
}
pub fn compare_metric(&self, other: &Self) -> Ordering {
assert!(
self.is_valid() && other.is_valid(),
"Cannot compare non-valid outputs"
);
self.metric
.as_ref()
.unwrap()
.compare(other.metric.as_ref().unwrap().as_ref())
}
pub fn is_valid(&self) -> bool {
self.metric.is_some() && self.capacity.total_capacity() > Capacity(0.0)
}
}
pub trait MetricTrait: ComparableMetric + ErasedSerialize {}
erased_serde::serialize_trait_object!(MetricTrait);
pub trait ComparableMetric: Any + Send + Sync {
fn value(&self) -> f64;
fn compare(&self, other: &dyn ComparableMetric) -> Ordering;
fn as_any(&self) -> &dyn Any;
}
#[derive(Debug, Clone, Serialize)]
pub struct LCOXMetric {
pub cost: MoneyPerActivity,
}
impl LCOXMetric {
pub fn new(cost: MoneyPerActivity) -> Self {
Self { cost }
}
}
impl ComparableMetric for LCOXMetric {
fn value(&self) -> f64 {
self.cost.value()
}
fn compare(&self, other: &dyn ComparableMetric) -> Ordering {
let other = other
.as_any()
.downcast_ref::<Self>()
.expect("Cannot compare metrics of different types");
compare_approx(self.cost, other.cost)
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl MetricTrait for LCOXMetric {}
#[derive(Debug, Clone, Serialize)]
pub struct NPVMetric(ProfitabilityIndex);
impl NPVMetric {
pub fn new(profitability_index: ProfitabilityIndex) -> Self {
Self(profitability_index)
}
fn is_zero_fixed_cost(&self) -> bool {
approx_eq!(Money, self.0.annualised_fixed_cost, Money(0.0))
}
}
impl ComparableMetric for NPVMetric {
fn value(&self) -> f64 {
if self.is_zero_fixed_cost() {
self.0.total_annualised_surplus.value()
} else {
self.0.value().value()
}
}
fn compare(&self, other: &dyn ComparableMetric) -> Ordering {
let other = other
.as_any()
.downcast_ref::<Self>()
.expect("Cannot compare metrics of different types");
match (self.is_zero_fixed_cost(), other.is_zero_fixed_cost()) {
(true, true) => {
let self_surplus = self.0.total_annualised_surplus;
let other_surplus = other.0.total_annualised_surplus;
compare_approx(other_surplus, self_surplus)
}
(false, false) => {
let self_pi = self.0.value();
let other_pi = other.0.value();
compare_approx(other_pi, self_pi)
}
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
}
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl MetricTrait for NPVMetric {}
fn calculate_lcox(
model: &Model,
asset: &AssetRef,
max_capacity: Option<AssetCapacity>,
commodity: &Commodity,
coefficients: &Rc<ObjectiveCoefficients>,
demand: &DemandMap,
) -> Result<AppraisalOutput> {
let results = perform_optimisation(
asset,
max_capacity,
commodity,
coefficients,
demand,
&model.time_slice_info,
highs::Sense::Minimise,
)?;
let cost_index = lcox(
results.capacity.total_capacity(),
coefficients.capacity_coefficient,
&results.activity,
&coefficients.activity_coefficients,
);
Ok(AppraisalOutput::new(
asset.clone(),
results,
cost_index.map(LCOXMetric::new),
coefficients.clone(),
))
}
fn calculate_npv(
model: &Model,
asset: &AssetRef,
max_capacity: Option<AssetCapacity>,
commodity: &Commodity,
coefficients: &Rc<ObjectiveCoefficients>,
demand: &DemandMap,
) -> Result<AppraisalOutput> {
let results = perform_optimisation(
asset,
max_capacity,
commodity,
coefficients,
demand,
&model.time_slice_info,
highs::Sense::Maximise,
)?;
let annual_fixed_cost = annual_fixed_cost(asset);
assert!(
annual_fixed_cost >= MoneyPerCapacity(0.0),
"The current NPV calculation does not support negative annual fixed costs"
);
let profitability_index = profitability_index(
results.capacity.total_capacity(),
annual_fixed_cost,
&results.activity,
&coefficients.activity_coefficients,
);
Ok(AppraisalOutput::new(
asset.clone(),
results,
Some(NPVMetric::new(profitability_index)),
coefficients.clone(),
))
}
pub fn appraise_investment(
model: &Model,
asset: &AssetRef,
max_capacity: Option<AssetCapacity>,
commodity: &Commodity,
objective_type: &ObjectiveType,
coefficients: &Rc<ObjectiveCoefficients>,
demand: &DemandMap,
) -> Result<AppraisalOutput> {
let appraisal_method = match objective_type {
ObjectiveType::LevelisedCostOfX => calculate_lcox,
ObjectiveType::NetPresentValue => calculate_npv,
};
appraisal_method(model, asset, max_capacity, commodity, coefficients, demand)
}
fn compare_asset_fallback(asset1: &Asset, asset2: &Asset) -> Ordering {
(asset2.is_commissioned(), asset2.commission_year())
.cmp(&(asset1.is_commissioned(), asset1.commission_year()))
}
pub fn sort_and_filter_appraisal_outputs(outputs_for_opts: &mut Vec<AppraisalOutput>) {
outputs_for_opts.retain(AppraisalOutput::is_valid);
outputs_for_opts.sort_by(|output1, output2| match output1.compare_metric(output2) {
Ordering::Equal => compare_asset_fallback(&output1.asset, &output2.asset),
cmp => cmp,
});
}
pub fn count_equal_and_best_appraisal_outputs(outputs: &[AppraisalOutput]) -> usize {
if outputs.is_empty() {
return 0;
}
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::*;
use crate::agent::AgentID;
use crate::finance::ProfitabilityIndex;
use crate::fixture::{agent_id, asset, process, region_id};
use crate::process::Process;
use crate::region::RegionID;
use crate::units::{Money, MoneyPerActivity, MoneyPerFlow};
use float_cmp::assert_approx_eq;
use rstest::rstest;
use std::rc::Rc;
#[rstest]
#[case(10.0, 10.0, Ordering::Equal, "equal_costs")]
#[case(5.0, 10.0, Ordering::Less, "first_lower_cost_is_better")]
#[case(10.0, 5.0, Ordering::Greater, "second_lower_cost_is_better")]
fn lcox_metric_comparison(
#[case] cost1: f64,
#[case] cost2: f64,
#[case] expected: Ordering,
#[case] description: &str,
) {
let metric1 = LCOXMetric::new(MoneyPerActivity(cost1));
let metric2 = LCOXMetric::new(MoneyPerActivity(cost2));
assert_eq!(
metric1.compare(&metric2),
expected,
"Failed comparison for case: {description}"
);
}
#[rstest]
#[case(100.0, 0.0, 50.0, 0.0, Ordering::Less, "both_zero_afc_first_better")]
#[case(
50.0,
0.0,
100.0,
0.0,
Ordering::Greater,
"both_zero_afc_second_better"
)]
#[case(100.0, 0.0, 100.0, 0.0, Ordering::Equal, "both_zero_afc_equal")]
#[case(
100.0,
1e-10,
50.0,
1e-10,
Ordering::Less,
"both_approx_zero_afc_first_better"
)]
#[case(
100.0,
1e-10,
200.0,
50.0,
Ordering::Less,
"approx_zero_afc_beats_nonzero"
)]
#[case(
200.0,
50.0,
100.0,
1e-10,
Ordering::Greater,
"nonzero_afc_loses_to_approx_zero"
)]
#[case(
200.0,
100.0,
150.0,
100.0,
Ordering::Less,
"both_nonzero_afc_first_better"
)]
#[case(
150.0,
100.0,
200.0,
100.0,
Ordering::Greater,
"both_nonzero_afc_second_better"
)]
#[case(200.0, 100.0, 200.0, 100.0, Ordering::Equal, "both_nonzero_afc_equal")]
#[case(
10.0,
0.0,
1000.0,
100.0,
Ordering::Less,
"first_zero_afc_beats_second_nonzero_afc"
)]
#[case(
10.0,
1e-10,
1000.0,
100.0,
Ordering::Less,
"first_approx_zero_afc_beats_second_nonzero_afc"
)]
#[case(
1000.0,
100.0,
10.0,
0.0,
Ordering::Greater,
"second_zero_afc_beats_first_nonzero_afc"
)]
#[case(
1000.0,
100.0,
10.0,
1e-10,
Ordering::Greater,
"second_nonzero_afc_beats_first_approx_zero_afc"
)]
fn npv_metric_comparison(
#[case] surplus1: f64,
#[case] fixed_cost1: f64,
#[case] surplus2: f64,
#[case] fixed_cost2: f64,
#[case] expected: Ordering,
#[case] description: &str,
) {
let metric1 = NPVMetric::new(ProfitabilityIndex {
total_annualised_surplus: Money(surplus1),
annualised_fixed_cost: Money(fixed_cost1),
});
let metric2 = NPVMetric::new(ProfitabilityIndex {
total_annualised_surplus: Money(surplus2),
annualised_fixed_cost: Money(fixed_cost2),
});
assert_eq!(
metric1.compare(&metric2),
expected,
"Failed comparison for case: {description}"
);
}
#[rstest]
fn compare_assets_fallback(process: Process, region_id: RegionID, agent_id: AgentID) {
let process = Rc::new(process);
let capacity = Capacity(2.0);
let asset1 = Asset::new_commissioned(
agent_id.clone(),
process.clone(),
region_id.clone(),
capacity,
2015,
)
.unwrap();
let asset2 =
Asset::new_candidate(process.clone(), region_id.clone(), capacity, 2015).unwrap();
let asset3 =
Asset::new_commissioned(agent_id, process, region_id.clone(), capacity, 2010).unwrap();
assert!(compare_asset_fallback(&asset1, &asset1).is_eq());
assert!(compare_asset_fallback(&asset2, &asset2).is_eq());
assert!(compare_asset_fallback(&asset3, &asset3).is_eq());
assert!(compare_asset_fallback(&asset1, &asset2).is_lt());
assert!(compare_asset_fallback(&asset2, &asset1).is_gt());
assert!(compare_asset_fallback(&asset1, &asset3).is_lt());
assert!(compare_asset_fallback(&asset3, &asset1).is_gt());
assert!(compare_asset_fallback(&asset3, &asset2).is_lt());
assert!(compare_asset_fallback(&asset2, &asset3).is_gt());
}
fn objective_coeffs() -> Rc<ObjectiveCoefficients> {
Rc::new(ObjectiveCoefficients {
capacity_coefficient: MoneyPerCapacity(0.0),
activity_coefficients: IndexMap::new(),
unmet_demand_coefficient: MoneyPerFlow(0.0),
})
}
fn appraisal_outputs(
assets: Vec<Asset>,
metrics: Vec<Box<dyn MetricTrait>>,
) -> Vec<AppraisalOutput> {
assert_eq!(
assets.len(),
metrics.len(),
"assets and metrics must have the same length"
);
assets
.into_iter()
.zip(metrics)
.map(|(asset, metric)| AppraisalOutput {
asset: AssetRef::from(asset),
capacity: AssetCapacity::Continuous(Capacity(10.0)),
coefficients: objective_coeffs(),
activity: IndexMap::new(),
unmet_demand: IndexMap::new(),
metric: Some(metric),
})
.collect()
}
fn appraisal_outputs_with_investment_priority_invariant_to_assets(
metrics: Vec<Box<dyn MetricTrait>>,
asset: &Asset,
) -> Vec<AppraisalOutput> {
let assets = vec![asset.clone(); metrics.len()];
appraisal_outputs(assets, metrics)
}
#[rstest]
fn appraisal_sort_by_lcox_metric(asset: Asset) {
let metrics: Vec<Box<dyn MetricTrait>> = vec![
Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
Box::new(LCOXMetric::new(MoneyPerActivity(3.0))),
Box::new(LCOXMetric::new(MoneyPerActivity(7.0))),
];
let mut outputs =
appraisal_outputs_with_investment_priority_invariant_to_assets(metrics, &asset);
sort_and_filter_appraisal_outputs(&mut outputs);
assert_approx_eq!(f64, outputs[0].metric.as_ref().unwrap().value(), 3.0); assert_approx_eq!(f64, outputs[1].metric.as_ref().unwrap().value(), 5.0);
assert_approx_eq!(f64, outputs[2].metric.as_ref().unwrap().value(), 7.0); }
#[rstest]
fn appraisal_sort_by_npv_metric(asset: Asset) {
let metrics: Vec<Box<dyn MetricTrait>> = vec![
Box::new(NPVMetric::new(ProfitabilityIndex {
total_annualised_surplus: Money(200.0),
annualised_fixed_cost: Money(100.0),
})),
Box::new(NPVMetric::new(ProfitabilityIndex {
total_annualised_surplus: Money(300.0),
annualised_fixed_cost: Money(100.0),
})),
Box::new(NPVMetric::new(ProfitabilityIndex {
total_annualised_surplus: Money(150.0),
annualised_fixed_cost: Money(100.0),
})),
];
let mut outputs =
appraisal_outputs_with_investment_priority_invariant_to_assets(metrics, &asset);
sort_and_filter_appraisal_outputs(&mut outputs);
assert_approx_eq!(f64, outputs[0].metric.as_ref().unwrap().value(), 3.0); assert_approx_eq!(f64, outputs[1].metric.as_ref().unwrap().value(), 2.0);
assert_approx_eq!(f64, outputs[2].metric.as_ref().unwrap().value(), 1.5); }
#[rstest]
fn appraisal_sort_by_npv_metric_zero_afc_prioritised(asset: Asset) {
let metrics: Vec<Box<dyn MetricTrait>> = vec![
Box::new(NPVMetric::new(ProfitabilityIndex {
total_annualised_surplus: Money(1000.0),
annualised_fixed_cost: Money(100.0),
})),
Box::new(NPVMetric::new(ProfitabilityIndex {
total_annualised_surplus: Money(50.0),
annualised_fixed_cost: Money(0.0),
})),
Box::new(NPVMetric::new(ProfitabilityIndex {
total_annualised_surplus: Money(500.0),
annualised_fixed_cost: Money(50.0),
})),
];
let mut outputs =
appraisal_outputs_with_investment_priority_invariant_to_assets(metrics, &asset);
sort_and_filter_appraisal_outputs(&mut outputs);
assert_approx_eq!(f64, outputs[0].metric.as_ref().unwrap().value(), 50.0); assert_approx_eq!(f64, outputs[1].metric.as_ref().unwrap().value(), 10.0); assert_approx_eq!(f64, outputs[2].metric.as_ref().unwrap().value(), 10.0); }
#[rstest]
#[should_panic(expected = "Cannot compare metrics of different types")]
fn appraisal_sort_by_mixed_metrics_panics(asset: Asset) {
let metrics: Vec<Box<dyn MetricTrait>> = vec![
Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
Box::new(NPVMetric::new(ProfitabilityIndex {
total_annualised_surplus: Money(200.0),
annualised_fixed_cost: Money(100.0),
})),
Box::new(LCOXMetric::new(MoneyPerActivity(3.0))),
];
let mut outputs =
appraisal_outputs_with_investment_priority_invariant_to_assets(metrics, &asset);
sort_and_filter_appraisal_outputs(&mut outputs);
}
#[rstest]
fn appraisal_sort_by_commission_year_when_metrics_equal(
process: Process,
region_id: RegionID,
agent_id: AgentID,
) {
let process_rc = Rc::new(process);
let capacity = Capacity(10.0);
let commission_years = [2015, 2020, 2010];
let assets: Vec<_> = commission_years
.iter()
.map(|&year| {
Asset::new_commissioned(
agent_id.clone(),
process_rc.clone(),
region_id.clone(),
capacity,
year,
)
.unwrap()
})
.collect();
let metrics: Vec<Box<dyn MetricTrait>> = vec![
Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
];
let mut outputs = appraisal_outputs(assets, metrics);
sort_and_filter_appraisal_outputs(&mut outputs);
assert_eq!(outputs[0].asset.commission_year(), 2020);
assert_eq!(outputs[1].asset.commission_year(), 2015);
assert_eq!(outputs[2].asset.commission_year(), 2010);
}
#[rstest]
fn appraisal_sort_maintains_order_when_all_equal(process: Process, region_id: RegionID) {
let process_rc = Rc::new(process);
let capacity = Capacity(10.0);
let commission_year = 2015;
let agent_ids = ["agent1", "agent2", "agent3"];
let assets: Vec<_> = agent_ids
.iter()
.map(|&id| {
Asset::new_commissioned(
AgentID(id.into()),
process_rc.clone(),
region_id.clone(),
capacity,
commission_year,
)
.unwrap()
})
.collect();
let metrics: Vec<Box<dyn MetricTrait>> = vec![
Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
];
let mut outputs = appraisal_outputs(assets.clone(), metrics);
sort_and_filter_appraisal_outputs(&mut outputs);
for (&expected_id, output) in agent_ids.iter().zip(outputs) {
assert_eq!(output.asset.agent_id(), Some(&AgentID(expected_id.into())));
}
}
#[rstest]
fn appraisal_sort_commissioned_before_uncommissioned_when_metrics_equal(
process: Process,
region_id: RegionID,
agent_id: AgentID,
) {
let process_rc = Rc::new(process);
let capacity = Capacity(10.0);
let commissioned_asset_newer = Asset::new_commissioned(
agent_id.clone(),
process_rc.clone(),
region_id.clone(),
capacity,
2020,
)
.unwrap();
let commissioned_asset_older = Asset::new_commissioned(
agent_id.clone(),
process_rc.clone(),
region_id.clone(),
capacity,
2015,
)
.unwrap();
let candidate_asset =
Asset::new_candidate(process_rc.clone(), region_id.clone(), capacity, 2020).unwrap();
let assets = vec![
candidate_asset.clone(),
commissioned_asset_older.clone(),
candidate_asset.clone(),
commissioned_asset_newer.clone(),
];
let metrics: Vec<Box<dyn MetricTrait>> = vec![
Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
Box::new(LCOXMetric::new(MoneyPerActivity(5.0))),
];
let mut outputs = appraisal_outputs(assets, metrics);
sort_and_filter_appraisal_outputs(&mut outputs);
assert!(outputs[0].asset.is_commissioned());
assert!(outputs[0].asset.commission_year() == 2020);
assert!(outputs[1].asset.is_commissioned());
assert!(outputs[1].asset.commission_year() == 2015);
assert!(!outputs[2].asset.is_commissioned());
assert!(!outputs[3].asset.is_commissioned());
}
#[rstest]
fn appraisal_metric_is_prioritised_over_asset_properties(
process: Process,
region_id: RegionID,
agent_id: AgentID,
) {
let process_rc = Rc::new(process);
let capacity = Capacity(10.0);
let commissioned_asset_newer = Asset::new_commissioned(
agent_id.clone(),
process_rc.clone(),
region_id.clone(),
capacity,
2020,
)
.unwrap();
let commissioned_asset_older = Asset::new_commissioned(
agent_id.clone(),
process_rc.clone(),
region_id.clone(),
capacity,
2015,
)
.unwrap();
let candidate_asset =
Asset::new_candidate(process_rc.clone(), region_id.clone(), capacity, 2020).unwrap();
let assets = vec![
candidate_asset.clone(),
commissioned_asset_older.clone(),
candidate_asset.clone(),
commissioned_asset_newer.clone(),
];
let baseline_metric_value = 5.0;
let best_metric_value = baseline_metric_value - 1e-5;
let metrics: Vec<Box<dyn MetricTrait>> = vec![
Box::new(LCOXMetric::new(MoneyPerActivity(best_metric_value))),
Box::new(LCOXMetric::new(MoneyPerActivity(baseline_metric_value))),
Box::new(LCOXMetric::new(MoneyPerActivity(baseline_metric_value))),
Box::new(LCOXMetric::new(MoneyPerActivity(baseline_metric_value))),
];
let mut outputs = appraisal_outputs(assets, metrics);
sort_and_filter_appraisal_outputs(&mut outputs);
assert_approx_eq!(
f64,
outputs[0].metric.as_ref().unwrap().value(),
best_metric_value
);
}
#[rstest]
fn appraisal_sort_filters_zero_capacity_outputs(asset: Asset) {
let metric = LCOXMetric::new(MoneyPerActivity(1.0));
let metrics = [
Box::new(metric.clone()),
Box::new(metric.clone()),
Box::new(metric),
];
let mut outputs: Vec<AppraisalOutput> = metrics
.into_iter()
.map(|metric| AppraisalOutput {
asset: AssetRef::from(asset.clone()),
capacity: AssetCapacity::Continuous(Capacity(0.0)),
coefficients: objective_coeffs(),
activity: IndexMap::new(),
unmet_demand: IndexMap::new(),
metric: Some(metric),
})
.collect();
sort_and_filter_appraisal_outputs(&mut outputs);
assert_eq!(outputs.len(), 0);
}
#[rstest]
fn appraisal_sort_filters_invalid_metric(asset: Asset) {
let output = AppraisalOutput {
asset: AssetRef::from(asset),
capacity: AssetCapacity::Continuous(Capacity(1.0)), coefficients: objective_coeffs(),
activity: IndexMap::new(),
unmet_demand: IndexMap::new(),
metric: None,
};
let mut outputs = vec![output];
sort_and_filter_appraisal_outputs(&mut outputs);
assert_eq!(outputs.len(), 0);
}
#[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<f64>,
#[case] expected_count: usize,
#[case] description: &str,
) {
let metrics: Vec<Box<dyn MetricTrait>> = metric_values
.into_iter()
.map(|v| Box::new(LCOXMetric::new(MoneyPerActivity(v))) as Box<dyn MetricTrait>)
.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}"
);
}
#[test]
fn count_equal_best_empty_slice_returns_zero() {
let outputs: Vec<AppraisalOutput> = vec![];
assert_eq!(count_equal_and_best_appraisal_outputs(&outputs), 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);
}
#[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);
}
#[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<dyn MetricTrait>
};
let metrics = vec![
make_npv(200.0, 100.0),
make_npv(200.0, 100.0), make_npv(100.0, 100.0), ];
let outputs =
appraisal_outputs_with_investment_priority_invariant_to_assets(metrics, &asset);
assert_eq!(count_equal_and_best_appraisal_outputs(&outputs), 1);
}
}