Skip to content
23 changes: 16 additions & 7 deletions src/finance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,14 @@ pub fn profitability_index(
}

/// Calculates annual LCOX based on capacity and activity.
///
/// If the total activity is zero, then it returns `None`, otherwise `Some` LCOX value.
pub fn lcox(
capacity: Capacity,
annual_fixed_cost: MoneyPerCapacity,
activity: &IndexMap<TimeSliceID, Activity>,
activity_costs: &IndexMap<TimeSliceID, MoneyPerActivity>,
) -> MoneyPerActivity {
) -> Option<MoneyPerActivity> {
// Calculate the annualised fixed costs
let annualised_fixed_cost = annual_fixed_cost * capacity;

Expand All @@ -92,7 +94,8 @@ pub fn lcox(
total_activity_costs += activity_cost * *activity;
}

(annualised_fixed_cost + total_activity_costs) / total_activity
(total_activity > Activity(0.0))
.then(|| (annualised_fixed_cost + total_activity_costs) / total_activity)
}

#[cfg(test)]
Expand Down Expand Up @@ -223,20 +226,26 @@ mod tests {
100.0, 50.0,
vec![("winter", "day", 10.0), ("summer", "night", 20.0)],
vec![("winter", "day", 5.0), ("summer", "night", 3.0)],
170.33333333333334 // (100*50 + 10*5 + 20*3) / (10+20) = 5110/30
Some(170.33333333333334) // (100*50 + 10*5 + 20*3) / (10+20) = 5110/30
)]
#[case(
50.0, 100.0,
vec![("winter", "day", 25.0)],
vec![("winter", "day", 0.0)],
200.0 // (50*100 + 25*0) / 25 = 5000/25
Some(200.0) // (50*100 + 25*0) / 25 = 5000/25
)]
#[case(
50.0, 100.0,
vec![("winter", "day", 0.0)],
vec![("winter", "day", 0.0)],
None // (50*0 + 25*0) / 0 = not feasible
)]
fn lcox_works(
#[case] capacity: f64,
#[case] annual_fixed_cost: f64,
#[case] activity_data: Vec<(&str, &str, f64)>,
#[case] cost_data: Vec<(&str, &str, f64)>,
#[case] expected: f64,
#[case] expected: Option<f64>,
) {
let activity = activity_data
.into_iter()
Expand Down Expand Up @@ -271,7 +280,7 @@ mod tests {
&activity_costs,
);

let expected = MoneyPerActivity(expected);
assert_approx_eq!(MoneyPerActivity, result, expected);
let expected = expected.map(MoneyPerActivity);
assert_approx_eq!(Option<MoneyPerActivity>, result, expected);
}
}
8 changes: 3 additions & 5 deletions src/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,20 +382,18 @@ pub fn time_slice_info2() -> TimeSliceInfo {
pub fn appraisal_output(asset: Asset, time_slice: TimeSliceID) -> AppraisalOutput {
let activity_coefficients = indexmap! { time_slice.clone() => MoneyPerActivity(0.5) };
let activity = indexmap! { time_slice.clone() => Activity(10.0) };
let demand = indexmap! { time_slice.clone() => Flow(100.0) };
let unmet_demand = indexmap! { time_slice.clone() => Flow(5.0) };
AppraisalOutput {
asset: AssetRef::from(asset),
capacity: AssetCapacity::Continuous(Capacity(42.0)),
coefficients: ObjectiveCoefficients {
coefficients: Rc::new(ObjectiveCoefficients {
capacity_coefficient: MoneyPerCapacity(2.14),
activity_coefficients,
unmet_demand_coefficient: MoneyPerFlow(10000.0),
},
}),
activity,
demand,
unmet_demand,
metric: Box::new(LCOXMetric::new(MoneyPerActivity(4.14))),
metric: Some(Box::new(LCOXMetric::new(MoneyPerActivity(4.14)))),
}
}

Expand Down
13 changes: 9 additions & 4 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ struct AppraisalResultsRow {
region_id: RegionID,
capacity: Capacity,
capacity_coefficient: MoneyPerCapacity,
metric: f64,
metric: Option<f64>,
}

/// Represents the appraisal results in a row of the appraisal results CSV file
Expand Down Expand Up @@ -453,7 +453,7 @@ impl DebugDataWriter {
region_id: result.asset.region_id().clone(),
capacity: result.capacity.total_capacity(),
capacity_coefficient: result.coefficients.capacity_coefficient,
metric: result.metric.value(),
metric: result.metric.as_ref().map(|m| m.value()),
};
self.appraisal_results_writer.serialize(row)?;
}
Expand All @@ -467,11 +467,12 @@ impl DebugDataWriter {
milestone_year: u32,
run_description: &str,
appraisal_results: &[AppraisalOutput],
demand: &IndexMap<TimeSliceID, Flow>,
) -> Result<()> {
for result in appraisal_results {
for (time_slice, activity) in &result.activity {
let activity_coefficient = result.coefficients.activity_coefficients[time_slice];
let demand = result.demand[time_slice];
let demand = demand[time_slice];
let unmet_demand = result.unmet_demand[time_slice];
let row = AppraisalResultsTimeSliceRow {
milestone_year,
Expand Down Expand Up @@ -564,13 +565,15 @@ impl DataWriter {
milestone_year: u32,
run_description: &str,
appraisal_results: &[AppraisalOutput],
demand: &IndexMap<TimeSliceID, Flow>,
) -> Result<()> {
if let Some(wtr) = &mut self.debug_writer {
wtr.write_appraisal_results(milestone_year, run_description, appraisal_results)?;
wtr.write_appraisal_time_slice_results(
milestone_year,
run_description,
appraisal_results,
demand,
)?;
}

Expand Down Expand Up @@ -986,7 +989,7 @@ mod tests {
region_id: asset.region_id().clone(),
capacity: Capacity(42.0),
capacity_coefficient: MoneyPerCapacity(2.14),
metric: 4.14,
metric: Some(4.14),
};
let records: Vec<AppraisalResultsRow> =
csv::Reader::from_path(dir.path().join(APPRAISAL_RESULTS_FILE_NAME))
Expand All @@ -1006,6 +1009,7 @@ mod tests {
let milestone_year = 2020;
let run_description = "test_run".to_string();
let dir = tempdir().unwrap();
let demand = indexmap! {time_slice.clone() => Flow(100.0) };

// Write appraisal time slice results
{
Expand All @@ -1015,6 +1019,7 @@ mod tests {
milestone_year,
&run_description,
&[appraisal_output],
&demand,
)
.unwrap();
writer.flush().unwrap();
Expand Down
1 change: 1 addition & 0 deletions src/simulation/investment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@ fn select_best_assets(
year,
&format!("{} {} round {}", &commodity.id, &agent.id, round),
&outputs_for_opts,
&demand,
)?;

sort_appraisal_outputs_by_investment_priority(&mut outputs_for_opts);
Expand Down
Loading
Loading