Skip to content
Draft
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
14 changes: 11 additions & 3 deletions src/simulation/investment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -759,14 +759,22 @@ fn select_best_assets(
// Appraise all options
let mut outputs_for_opts = Vec::new();
for asset in &opt_assets {
// For candidates, determine the maximum capacity that can be invested in this round,
// according to the tranche size and remaining capacity limits.
// For candidates, determine the maximum capacity that can be invested in this round.
// This is whichever is the smallest of the tranche size (based on demand limiting
// capacity before investment), the remaining available capacity for the candidate and
// the demand limiting capacity recalculated based on demand unserved by the other
// selected assets.
let max_capacity = (!asset.is_commissioned()).then(|| {
let tranche_capacity = asset
.capacity()
.apply_limit_factor(model.parameters.capacity_limit_factor);
let dlc = AssetCapacity::from_capacity(
get_demand_limiting_capacity(&model.time_slice_info, asset, commodity, &demand),
asset.unit_size(),
);
let remaining_capacity = remaining_candidate_capacity[asset];
tranche_capacity.min(remaining_capacity)

tranche_capacity.min(dlc).min(remaining_capacity)
});
Comment on lines 761 to 778
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

max_capacity (including the new demand-limiting-capacity recomputation) is calculated before the group-id de-duplication check. If there are many identical candidates in the same group, this recomputes DLC/tranche/remaining for assets that will be skipped anyway. Consider moving the group check before computing max_capacity (or computing max_capacity only for the representative asset) to avoid unnecessary work in large models.

Copilot uses AI. Check for mistakes.

// Skip any assets from groups we've already seen
Expand Down
12 changes: 8 additions & 4 deletions src/simulation/investment/appraisal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,14 @@ impl AppraisalOutput {
/// Create a new `AppraisalOutput`
fn new<T: MetricTrait>(
asset: AssetRef,
capacity: AssetCapacity,
results: ResultsMap,
metric: Option<T>,
coefficients: Rc<ObjectiveCoefficients>,
) -> Self {
Self {
asset,
capacity: results.capacity,
capacity,
activity: results.activity,
unmet_demand: results.unmet_demand,
metric: metric.map(|m| Box::new(m) as Box<dyn MetricTrait>),
Expand Down Expand Up @@ -252,7 +253,7 @@ impl MetricTrait for NPVMetric {}
fn calculate_lcox(
model: &Model,
asset: &AssetRef,
max_capacity: Option<AssetCapacity>,
max_capacity: AssetCapacity,
commodity: &Commodity,
coefficients: &Rc<ObjectiveCoefficients>,
demand: &DemandMap,
Expand All @@ -276,6 +277,7 @@ fn calculate_lcox(

Ok(AppraisalOutput::new(
asset.clone(),
results.capacity,
results,
cost_index.map(LCOXMetric::new),
coefficients.clone(),
Expand All @@ -290,7 +292,7 @@ fn calculate_lcox(
fn calculate_npv(
model: &Model,
asset: &AssetRef,
max_capacity: Option<AssetCapacity>,
max_capacity: AssetCapacity,
commodity: &Commodity,
coefficients: &Rc<ObjectiveCoefficients>,
demand: &DemandMap,
Expand Down Expand Up @@ -320,13 +322,14 @@ fn calculate_npv(

Ok(AppraisalOutput::new(
asset.clone(),
max_capacity,
results,
Some(NPVMetric::new(profitability_index)),
coefficients.clone(),
))
Comment on lines 323 to 329
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

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

In calculate_npv, AppraisalOutput::new is being given max_capacity rather than the optimiser's chosen results.capacity. This makes the output capacity inconsistent with the activity/unmet_demand/metric (which are all based on results), and can also cause AppraisalOutput::is_valid() to treat a solution with results.capacity == 0 as valid if max_capacity > 0 (potentially selecting an asset that doesn't actually serve demand). Use results.capacity for the output capacity, or alternatively change the optimisation formulation so results.capacity is forced to equal max_capacity if that is the intended installed capacity for NPV appraisal.

Copilot uses AI. Check for mistakes.
}

/// Appraise the given investment with the specified objective type
/// Appraise the given investment with the specified objective type.
///
/// # Returns
///
Expand All @@ -341,6 +344,7 @@ pub fn appraise_investment(
coefficients: &Rc<ObjectiveCoefficients>,
demand: &DemandMap,
) -> Result<AppraisalOutput> {
let max_capacity = max_capacity.unwrap_or(asset.capacity());
let appraisal_method = match objective_type {
ObjectiveType::LevelisedCostOfX => calculate_lcox,
ObjectiveType::NetPresentValue => calculate_npv,
Expand Down
5 changes: 2 additions & 3 deletions src/simulation/investment/appraisal/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ use indexmap::IndexMap;
pub fn add_capacity_constraint(
problem: &mut Problem,
asset: &AssetRef,
max_capacity: Option<AssetCapacity>,
max_capacity: AssetCapacity,
capacity_var: Variable,
) {
let capacity_limit = max_capacity.unwrap_or(asset.capacity());
let capacity_limit = match capacity_limit {
let capacity_limit = match max_capacity {
AssetCapacity::Continuous(cap) => cap.value(),
AssetCapacity::Discrete(units, _) => units as f64,
};
Expand Down
4 changes: 2 additions & 2 deletions src/simulation/investment/appraisal/optimisation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ pub struct ResultsMap {
fn add_constraints(
problem: &mut Problem,
asset: &AssetRef,
max_capacity: Option<AssetCapacity>,
max_capacity: AssetCapacity,
commodity: &Commodity,
variables: &VariableMap,
demand: &DemandMap,
Expand Down Expand Up @@ -127,7 +127,7 @@ fn add_constraints(
/// asset has a defined unit size.
pub fn perform_optimisation(
asset: &AssetRef,
max_capacity: Option<AssetCapacity>,
max_capacity: AssetCapacity,
commodity: &Commodity,
coefficients: &ObjectiveCoefficients,
demand: &DemandMap,
Expand Down
12 changes: 6 additions & 6 deletions tests/data/simple_npv/assets.csv
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ asset_id,process_id,region_id,agent_id,group_id,commission_year,decommission_yea
3,GASCGT,GBR,A0_ELC,,2020,,2.43
4,RGASBR,GBR,A0_RES,,2020,2030,2900.0
5,RELCHP,GBR,A0_RES,,2020,2035,399.98
6,RELCHP,GBR,A0_RES,,2030,,3290.2365652888325
6,RELCHP,GBR,A0_RES,,2030,,3255.838405876481
7,GASCGT,GBR,A0_ELC,,2030,,33.820477802912976
8,GASPRC,GBR,A0_GPR,,2030,,879.1648830751317
9,GASDRV,GBR,A0_GEX,,2030,,923.1231272288879
10,RGASBR,GBR,A0_RES,,2040,,4011.65737547648
11,RELCHP,GBR,A0_RES,,2040,,802.3314750952961
12,GASCGT,GBR,A0_ELC,,2040,,3.7231090668357614
13,GASPRC,GBR,A0_GPR,,2040,,94.9477829022087
14,GASDRV,GBR,A0_GEX,,2040,,99.6951720473196
10,RGASBR,GBR,A0_RES,,2040,,4011.6573754764804
11,RELCHP,GBR,A0_RES,,2040,,755.8189695999995
12,GASCGT,GBR,A0_ELC,,2040,,3.7231090668357543
13,GASPRC,GBR,A0_GPR,,2040,,94.94778290220857
14,GASDRV,GBR,A0_GEX,,2040,,99.69517204731937
Loading
Loading