Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/release_notes/upcoming.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ ready to be released, carry out the following steps:
- Allow for adding both a `prod` and `cons` levy to a commodity ([#969])
- Availability limits can now be provided at multiple levels for a process ([#1018])
- Pricing strategy can now vary by commodity ([#1021])
- Users can now specify investment limits ([#1096])
- It is now required to provide unit types for every commodity; this is used for validation
([#1110])

## Experimental features

Expand All @@ -52,6 +55,7 @@ ready to be released, carry out the following steps:
- Users can now set demand to zero in `demand.csv` ([#871])
- Fix: sign for levies of type `net` was wrong for inputs ([#969])
- Fix `--overwrite` option for `save-graphs` command ([#1001])
- Skip assets with zero activity when appraising with LCOX ([#1129])

[#767]: https://github.com/EnergySystemsModellingLab/MUSE2/pull/767
[#868]: https://github.com/EnergySystemsModellingLab/MUSE2/pull/868
Expand All @@ -70,3 +74,6 @@ ready to be released, carry out the following steps:
[#1021]: https://github.com/EnergySystemsModellingLab/MUSE2/pull/1021
[#1022]: https://github.com/EnergySystemsModellingLab/MUSE2/pull/1022
[#1030]: https://github.com/EnergySystemsModellingLab/MUSE2/pull/1030
[#1096]: https://github.com/EnergySystemsModellingLab/MUSE2/pull/1096
[#1110]: https://github.com/EnergySystemsModellingLab/MUSE2/pull/1110
[#1129]: https://github.com/EnergySystemsModellingLab/MUSE2/pull/1129
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
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new test case’s inline explanation is mathematically incorrect: the fixed-cost term is still annual_fixed_cost * capacity (e.g. 100*50), so the numerator is not zero even though total activity is. Please update the comment to reflect the actual expression being avoided (division by zero leading to an invalid/infinite result).

Suggested change
None // (50*0 + 25*0) / 0 = not feasible
None // (50*100 + 0*0) / 0 -> division by zero (not feasible)

Copilot uses AI. Check for mistakes.
)]
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);
}
}
6 changes: 2 additions & 4 deletions src/fixture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,18 +382,16 @@ 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))),
}
Expand Down
7 changes: 6 additions & 1 deletion src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 @@ -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
8 changes: 5 additions & 3 deletions src/simulation/investment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -776,23 +776,25 @@ fn select_best_assets(
continue;
}

let output = appraise_investment(
if let Some(output) = appraise_investment(
model,
asset,
max_capacity,
commodity,
objective_type,
&coefficients[asset],
&demand,
)?;
outputs_for_opts.push(output);
)? {
outputs_for_opts.push(output);
}
}

// Save appraisal results
writer.write_appraisal_debug_info(
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