Skip to content

Commit 965acca

Browse files
committed
lcox: Return None if total activity is zero
Fixes #1126.
1 parent bdfc3ad commit 965acca

2 files changed

Lines changed: 36 additions & 8 deletions

File tree

src/finance.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,14 @@ pub fn profitability_index(
7474
}
7575

7676
/// Calculates annual LCOX based on capacity and activity.
77+
///
78+
/// If the total activity is zero, then it returns `None`, otherwise `Some` LCOX value.
7779
pub fn lcox(
7880
capacity: Capacity,
7981
annual_fixed_cost: MoneyPerCapacity,
8082
activity: &IndexMap<TimeSliceID, Activity>,
8183
activity_costs: &IndexMap<TimeSliceID, MoneyPerActivity>,
82-
) -> MoneyPerActivity {
84+
) -> Option<MoneyPerActivity> {
8385
// Calculate the annualised fixed costs
8486
let annualised_fixed_cost = annual_fixed_cost * capacity;
8587

@@ -92,7 +94,8 @@ pub fn lcox(
9294
total_activity_costs += activity_cost * *activity;
9395
}
9496

95-
(annualised_fixed_cost + total_activity_costs) / total_activity
97+
(total_activity > Activity(0.0))
98+
.then(|| (annualised_fixed_cost + total_activity_costs) / total_activity)
9699
}
97100

98101
#[cfg(test)]
@@ -223,20 +226,26 @@ mod tests {
223226
100.0, 50.0,
224227
vec![("winter", "day", 10.0), ("summer", "night", 20.0)],
225228
vec![("winter", "day", 5.0), ("summer", "night", 3.0)],
226-
170.33333333333334 // (100*50 + 10*5 + 20*3) / (10+20) = 5110/30
229+
Some(170.33333333333334) // (100*50 + 10*5 + 20*3) / (10+20) = 5110/30
227230
)]
228231
#[case(
229232
50.0, 100.0,
230233
vec![("winter", "day", 25.0)],
231234
vec![("winter", "day", 0.0)],
232-
200.0 // (50*100 + 25*0) / 25 = 5000/25
235+
Some(200.0) // (50*100 + 25*0) / 25 = 5000/25
236+
)]
237+
#[case(
238+
50.0, 100.0,
239+
vec![("winter", "day", 0.0)],
240+
vec![("winter", "day", 0.0)],
241+
None // (50*0 + 25*0) / 0 = not feasible
233242
)]
234243
fn lcox_works(
235244
#[case] capacity: f64,
236245
#[case] annual_fixed_cost: f64,
237246
#[case] activity_data: Vec<(&str, &str, f64)>,
238247
#[case] cost_data: Vec<(&str, &str, f64)>,
239-
#[case] expected: f64,
248+
#[case] expected: Option<f64>,
240249
) {
241250
let activity = activity_data
242251
.into_iter()
@@ -271,7 +280,7 @@ mod tests {
271280
&activity_costs,
272281
);
273282

274-
let expected = MoneyPerActivity(expected);
275-
assert_approx_eq!(MoneyPerActivity, result, expected);
283+
let expected = expected.map(MoneyPerActivity);
284+
assert_approx_eq!(Option<MoneyPerActivity>, result, expected);
276285
}
277286
}

src/simulation/investment/appraisal.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ fn calculate_lcox(
277277
Ok(AppraisalOutput::new(
278278
asset.clone(),
279279
results,
280-
Some(LCOXMetric::new(cost_index)),
280+
cost_index.map(LCOXMetric::new),
281281
coefficients.clone(),
282282
))
283283
}
@@ -923,4 +923,23 @@ mod tests {
923923
// All zero capacity outputs should be filtered out
924924
assert_eq!(outputs.len(), 0);
925925
}
926+
927+
/// Test that appraisal outputs with an invalid metric are filtered out
928+
#[rstest]
929+
fn appraisal_sort_filters_invalid_metric(asset: Asset) {
930+
let output = AppraisalOutput {
931+
asset: AssetRef::from(asset),
932+
capacity: AssetCapacity::Continuous(Capacity(1.0)), // non-zero capacity
933+
coefficients: objective_coeffs(),
934+
activity: IndexMap::new(),
935+
unmet_demand: IndexMap::new(),
936+
metric: None,
937+
};
938+
let mut outputs = vec![output];
939+
940+
sort_appraisal_outputs_by_investment_priority(&mut outputs);
941+
942+
// The invalid output should have been filtered out
943+
assert_eq!(outputs.len(), 0);
944+
}
926945
}

0 commit comments

Comments
 (0)