Skip to content

Commit bdfc3ad

Browse files
committed
AppraisalOutput: Make metric field optional
When `None`, it indicates that a valid metric could not be computed.
1 parent ea17f45 commit bdfc3ad

3 files changed

Lines changed: 34 additions & 28 deletions

File tree

src/fixture.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ pub fn appraisal_output(asset: Asset, time_slice: TimeSliceID) -> AppraisalOutpu
393393
}),
394394
activity,
395395
unmet_demand,
396-
metric: Box::new(LCOXMetric::new(MoneyPerActivity(4.14))),
396+
metric: Some(Box::new(LCOXMetric::new(MoneyPerActivity(4.14)))),
397397
}
398398
}
399399

src/output.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ struct AppraisalResultsRow {
220220
region_id: RegionID,
221221
capacity: Capacity,
222222
capacity_coefficient: MoneyPerCapacity,
223-
metric: f64,
223+
metric: Option<f64>,
224224
}
225225

226226
/// Represents the appraisal results in a row of the appraisal results CSV file
@@ -453,7 +453,7 @@ impl DebugDataWriter {
453453
region_id: result.asset.region_id().clone(),
454454
capacity: result.capacity.total_capacity(),
455455
capacity_coefficient: result.coefficients.capacity_coefficient,
456-
metric: result.metric.value(),
456+
metric: result.metric.as_ref().map(|m| m.value()),
457457
};
458458
self.appraisal_results_writer.serialize(row)?;
459459
}
@@ -989,7 +989,7 @@ mod tests {
989989
region_id: asset.region_id().clone(),
990990
capacity: Capacity(42.0),
991991
capacity_coefficient: MoneyPerCapacity(2.14),
992-
metric: 4.14,
992+
metric: Some(4.14),
993993
};
994994
let records: Vec<AppraisalResultsRow> =
995995
csv::Reader::from_path(dir.path().join(APPRAISAL_RESULTS_FILE_NAME))

src/simulation/investment/appraisal.rs

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ pub struct AppraisalOutput {
6060
/// The hypothetical unmet demand following investment in this asset
6161
pub unmet_demand: DemandMap,
6262
/// The comparison metric to compare investment decisions
63-
pub metric: Box<dyn MetricTrait>,
63+
pub metric: Option<Box<dyn MetricTrait>>,
6464
/// Capacity and activity coefficients used in the appraisal
6565
pub coefficients: Rc<ObjectiveCoefficients>,
6666
}
@@ -70,15 +70,15 @@ impl AppraisalOutput {
7070
pub fn new<T: MetricTrait>(
7171
asset: AssetRef,
7272
results: ResultsMap,
73-
metric: T,
73+
metric: Option<T>,
7474
coefficients: Rc<ObjectiveCoefficients>,
7575
) -> Self {
7676
Self {
7777
asset,
7878
capacity: results.capacity,
7979
activity: results.activity,
8080
unmet_demand: results.unmet_demand,
81-
metric: Box::new(metric),
81+
metric: metric.map(|m| Box::new(m) as Box<dyn MetricTrait>),
8282
coefficients,
8383
}
8484
}
@@ -95,18 +95,20 @@ impl AppraisalOutput {
9595
self.is_valid() && other.is_valid(),
9696
"Cannot compare non-valid outputs"
9797
);
98-
assert!(
99-
!(self.metric.value().is_nan() || other.metric.value().is_nan()),
100-
"Appraisal metric cannot be NaN"
101-
);
102-
self.metric.compare(other.metric.as_ref())
98+
99+
// We've already checked the metrics aren't `None` in `is_valid`
100+
self.metric
101+
.as_ref()
102+
.unwrap()
103+
.compare(other.metric.as_ref().unwrap().as_ref())
103104
}
104105

105106
/// Whether this [`AppraisalOutput`] is a valid output.
106107
///
107-
/// Specifically, it checks whether the calculated capacity is greater than zero.
108+
/// Specifically, it checks whether the metric is a valid value (not `None`) and that the
109+
/// calculated capacity is greater than zero.
108110
pub fn is_valid(&self) -> bool {
109-
self.capacity.total_capacity() > Capacity(0.0)
111+
self.metric.is_some() && self.capacity.total_capacity() > Capacity(0.0)
110112
}
111113
}
112114

@@ -275,7 +277,7 @@ fn calculate_lcox(
275277
Ok(AppraisalOutput::new(
276278
asset.clone(),
277279
results,
278-
LCOXMetric::new(cost_index),
280+
Some(LCOXMetric::new(cost_index)),
279281
coefficients.clone(),
280282
))
281283
}
@@ -319,7 +321,7 @@ fn calculate_npv(
319321
Ok(AppraisalOutput::new(
320322
asset.clone(),
321323
results,
322-
NPVMetric::new(profitability_index),
324+
Some(NPVMetric::new(profitability_index)),
323325
coefficients.clone(),
324326
))
325327
}
@@ -582,7 +584,7 @@ mod tests {
582584
coefficients: objective_coeffs(),
583585
activity: IndexMap::new(),
584586
unmet_demand: IndexMap::new(),
585-
metric,
587+
metric: Some(metric),
586588
})
587589
.collect()
588590
}
@@ -610,9 +612,9 @@ mod tests {
610612
appraisal_outputs_with_investment_priority_invariant_to_assets(metrics, &asset);
611613
sort_appraisal_outputs_by_investment_priority(&mut outputs);
612614

613-
assert_approx_eq!(f64, outputs[0].metric.value(), 3.0); // Best (lowest)
614-
assert_approx_eq!(f64, outputs[1].metric.value(), 5.0);
615-
assert_approx_eq!(f64, outputs[2].metric.value(), 7.0); // Worst (highest)
615+
assert_approx_eq!(f64, outputs[0].metric.as_ref().unwrap().value(), 3.0); // Best (lowest)
616+
assert_approx_eq!(f64, outputs[1].metric.as_ref().unwrap().value(), 5.0);
617+
assert_approx_eq!(f64, outputs[2].metric.as_ref().unwrap().value(), 7.0); // Worst (highest)
616618
}
617619

618620
/// Test sorting by NPV profitability index when invariant to asset properties
@@ -638,9 +640,9 @@ mod tests {
638640
sort_appraisal_outputs_by_investment_priority(&mut outputs);
639641

640642
// Higher profitability index is better, so should be sorted: 3.0, 2.0, 1.5
641-
assert_approx_eq!(f64, outputs[0].metric.value(), 3.0); // Best (highest PI)
642-
assert_approx_eq!(f64, outputs[1].metric.value(), 2.0);
643-
assert_approx_eq!(f64, outputs[2].metric.value(), 1.5); // Worst (lowest PI)
643+
assert_approx_eq!(f64, outputs[0].metric.as_ref().unwrap().value(), 3.0); // Best (highest PI)
644+
assert_approx_eq!(f64, outputs[1].metric.as_ref().unwrap().value(), 2.0);
645+
assert_approx_eq!(f64, outputs[2].metric.as_ref().unwrap().value(), 1.5); // Worst (lowest PI)
644646
}
645647

646648
/// Test that NPV metrics with zero annual fixed cost are prioritised above all others
@@ -670,9 +672,9 @@ mod tests {
670672
sort_appraisal_outputs_by_investment_priority(&mut outputs);
671673

672674
// Zero AFC should be first despite lower absolute surplus value
673-
assert_approx_eq!(f64, outputs[0].metric.value(), 50.0); // Zero AFC (uses surplus)
674-
assert_approx_eq!(f64, outputs[1].metric.value(), 10.0); // PI = 1000/100
675-
assert_approx_eq!(f64, outputs[2].metric.value(), 10.0); // PI = 500/50
675+
assert_approx_eq!(f64, outputs[0].metric.as_ref().unwrap().value(), 50.0); // Zero AFC (uses surplus)
676+
assert_approx_eq!(f64, outputs[1].metric.as_ref().unwrap().value(), 10.0); // PI = 1000/100
677+
assert_approx_eq!(f64, outputs[2].metric.as_ref().unwrap().value(), 10.0); // PI = 500/50
676678
}
677679

678680
/// Test that mixing LCOX and NPV metrics causes a runtime panic during comparison
@@ -886,7 +888,11 @@ mod tests {
886888
sort_appraisal_outputs_by_investment_priority(&mut outputs);
887889

888890
// non-commissioned asset prioritised because it has a slightly better metric
889-
assert_approx_eq!(f64, outputs[0].metric.value(), best_metric_value);
891+
assert_approx_eq!(
892+
f64,
893+
outputs[0].metric.as_ref().unwrap().value(),
894+
best_metric_value
895+
);
890896
}
891897

892898
/// Test that appraisal outputs with zero capacity are filtered out during sorting.
@@ -908,7 +914,7 @@ mod tests {
908914
coefficients: objective_coeffs(),
909915
activity: IndexMap::new(),
910916
unmet_demand: IndexMap::new(),
911-
metric,
917+
metric: Some(metric),
912918
})
913919
.collect();
914920

0 commit comments

Comments
 (0)