Skip to content

Commit 28dee31

Browse files
authored
Merge pull request #1160 from EnergySystemsModellingLab/handle-lcox-errors2
Handle LCOX errors gracefully
2 parents 2a93c16 + ab2fa47 commit 28dee31

6 files changed

Lines changed: 138 additions & 79 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/fixture.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -395,20 +395,18 @@ pub fn time_slice_info2() -> TimeSliceInfo {
395395
pub fn appraisal_output(asset: Asset, time_slice: TimeSliceID) -> AppraisalOutput {
396396
let activity_coefficients = indexmap! { time_slice.clone() => MoneyPerActivity(0.5) };
397397
let activity = indexmap! { time_slice.clone() => Activity(10.0) };
398-
let demand = indexmap! { time_slice.clone() => Flow(100.0) };
399398
let unmet_demand = indexmap! { time_slice.clone() => Flow(5.0) };
400399
AppraisalOutput {
401400
asset: AssetRef::from(asset),
402401
capacity: AssetCapacity::Continuous(Capacity(42.0)),
403-
coefficients: ObjectiveCoefficients {
402+
coefficients: Rc::new(ObjectiveCoefficients {
404403
capacity_coefficient: MoneyPerCapacity(2.14),
405404
activity_coefficients,
406405
unmet_demand_coefficient: MoneyPerFlow(10000.0),
407-
},
406+
}),
408407
activity,
409-
demand,
410408
unmet_demand,
411-
metric: Box::new(LCOXMetric::new(MoneyPerActivity(4.14))),
409+
metric: Some(Box::new(LCOXMetric::new(MoneyPerActivity(4.14)))),
412410
}
413411
}
414412

src/output.rs

Lines changed: 9 additions & 4 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
}
@@ -467,11 +467,12 @@ impl DebugDataWriter {
467467
milestone_year: u32,
468468
run_description: &str,
469469
appraisal_results: &[AppraisalOutput],
470+
demand: &IndexMap<TimeSliceID, Flow>,
470471
) -> Result<()> {
471472
for result in appraisal_results {
472473
for (time_slice, activity) in &result.activity {
473474
let activity_coefficient = result.coefficients.activity_coefficients[time_slice];
474-
let demand = result.demand[time_slice];
475+
let demand = demand[time_slice];
475476
let unmet_demand = result.unmet_demand[time_slice];
476477
let row = AppraisalResultsTimeSliceRow {
477478
milestone_year,
@@ -564,13 +565,15 @@ impl DataWriter {
564565
milestone_year: u32,
565566
run_description: &str,
566567
appraisal_results: &[AppraisalOutput],
568+
demand: &IndexMap<TimeSliceID, Flow>,
567569
) -> Result<()> {
568570
if let Some(wtr) = &mut self.debug_writer {
569571
wtr.write_appraisal_results(milestone_year, run_description, appraisal_results)?;
570572
wtr.write_appraisal_time_slice_results(
571573
milestone_year,
572574
run_description,
573575
appraisal_results,
576+
demand,
574577
)?;
575578
}
576579

@@ -986,7 +989,7 @@ mod tests {
986989
region_id: asset.region_id().clone(),
987990
capacity: Capacity(42.0),
988991
capacity_coefficient: MoneyPerCapacity(2.14),
989-
metric: 4.14,
992+
metric: Some(4.14),
990993
};
991994
let records: Vec<AppraisalResultsRow> =
992995
csv::Reader::from_path(dir.path().join(APPRAISAL_RESULTS_FILE_NAME))
@@ -1006,6 +1009,7 @@ mod tests {
10061009
let milestone_year = 2020;
10071010
let run_description = "test_run".to_string();
10081011
let dir = tempdir().unwrap();
1012+
let demand = indexmap! {time_slice.clone() => Flow(100.0) };
10091013

10101014
// Write appraisal time slice results
10111015
{
@@ -1015,6 +1019,7 @@ mod tests {
10151019
milestone_year,
10161020
&run_description,
10171021
&[appraisal_output],
1022+
&demand,
10181023
)
10191024
.unwrap();
10201025
writer.flush().unwrap();

src/simulation/investment.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ fn select_best_assets(
796796
year,
797797
&format!("{} {} round {}", &commodity.id, &agent.id, round),
798798
&outputs_for_opts,
799+
&demand,
799800
)?;
800801

801802
sort_appraisal_outputs_by_investment_priority(&mut outputs_for_opts);

0 commit comments

Comments
 (0)