muse2/simulation/investment/appraisal/
coefficients.rs

1//! Calculation of cost coefficients for investment tools.
2use super::costs::annual_fixed_cost;
3use crate::agent::ObjectiveType;
4use crate::asset::AssetRef;
5use crate::model::Model;
6use crate::simulation::CommodityPrices;
7use crate::time_slice::{TimeSliceID, TimeSliceInfo};
8use crate::units::{MoneyPerActivity, MoneyPerCapacity, MoneyPerFlow};
9use indexmap::IndexMap;
10use std::collections::HashMap;
11use std::rc::Rc;
12
13/// Map storing cost coefficients for an asset.
14///
15/// These coefficients are calculated according to the agent's `ObjectiveType` and are used by
16/// the investment appraisal routines. The map contains the per-capacity and per-activity cost
17/// coefficients used in the appraisal optimisation, together with the unmet-demand penalty.
18#[derive(Clone)]
19pub struct ObjectiveCoefficients {
20    /// Cost per unit of capacity
21    pub capacity_coefficient: MoneyPerCapacity,
22    /// Cost per unit of activity in each time slice
23    pub activity_coefficients: IndexMap<TimeSliceID, MoneyPerActivity>,
24    /// Unmet demand coefficient
25    pub unmet_demand_coefficient: MoneyPerFlow,
26}
27
28/// Calculates cost coefficients for a set of assets for a given objective type.
29pub fn calculate_coefficients_for_assets(
30    model: &Model,
31    objective_type: &ObjectiveType,
32    assets: &[AssetRef],
33    prices: &CommodityPrices,
34    year: u32,
35) -> HashMap<AssetRef, Rc<ObjectiveCoefficients>> {
36    assets
37        .iter()
38        .map(|asset| {
39            let coefficient = match objective_type {
40                ObjectiveType::LevelisedCostOfX => calculate_coefficients_for_lcox(
41                    asset,
42                    &model.time_slice_info,
43                    prices,
44                    model.parameters.value_of_lost_load,
45                    year,
46                ),
47                ObjectiveType::NetPresentValue => {
48                    calculate_coefficients_for_npv(asset, &model.time_slice_info, prices, year)
49                }
50            };
51            (asset.clone(), Rc::new(coefficient))
52        })
53        .collect()
54}
55
56/// Calculates the cost coefficients for LCOX.
57///
58/// For LCOX the activity coefficient is calculated as operating cost minus revenue from
59/// non-primary flows. The unmet demand coefficient is set from the model parameter
60/// `value_of_lost_load`.
61pub fn calculate_coefficients_for_lcox(
62    asset: &AssetRef,
63    time_slice_info: &TimeSliceInfo,
64    prices: &CommodityPrices,
65    value_of_lost_load: MoneyPerFlow,
66    year: u32,
67) -> ObjectiveCoefficients {
68    // Capacity coefficient
69    let capacity_coefficient = annual_fixed_cost(asset);
70
71    // Activity coefficients
72    let mut activity_coefficients = IndexMap::new();
73    for time_slice in time_slice_info.iter_ids() {
74        let coefficient = calculate_activity_coefficient_for_lcox(asset, time_slice, prices, year);
75        activity_coefficients.insert(time_slice.clone(), coefficient);
76    }
77
78    // Unmet demand coefficient
79    let unmet_demand_coefficient = value_of_lost_load;
80
81    ObjectiveCoefficients {
82        capacity_coefficient,
83        activity_coefficients,
84        unmet_demand_coefficient,
85    }
86}
87
88/// Calculates the cost coefficients for NPV.
89///
90/// For NPV the activity coefficient is revenue (including primary output) minus operating
91/// cost; a small positive epsilon is added to activity coefficients so that assets with
92/// near-zero net value still appear in dispatch. Capacity costs and unmet-demand penalties
93/// are set to zero for the NPV objective.
94pub fn calculate_coefficients_for_npv(
95    asset: &AssetRef,
96    time_slice_info: &TimeSliceInfo,
97    prices: &CommodityPrices,
98    year: u32,
99) -> ObjectiveCoefficients {
100    // Small constant added to each activity coefficient to ensure break-even/slightly negative
101    // assets are still dispatched
102    const EPSILON_ACTIVITY_COEFFICIENT: MoneyPerActivity = MoneyPerActivity(f64::EPSILON * 100.0);
103
104    // Activity coefficients
105    let mut activity_coefficients = IndexMap::new();
106    for time_slice in time_slice_info.iter_ids() {
107        let coefficient = calculate_activity_coefficient_for_npv(asset, time_slice, prices, year);
108        activity_coefficients.insert(
109            time_slice.clone(),
110            coefficient + EPSILON_ACTIVITY_COEFFICIENT,
111        );
112    }
113
114    // Unmet demand coefficient (we don't apply a cost to unmet demand, so we set this to zero)
115    let unmet_demand_coefficient = MoneyPerFlow(0.0);
116
117    ObjectiveCoefficients {
118        capacity_coefficient: MoneyPerCapacity(0.0),
119        activity_coefficients,
120        unmet_demand_coefficient,
121    }
122}
123
124/// Calculate a single activity coefficient for the LCOX objective for a given time slice.
125fn calculate_activity_coefficient_for_lcox(
126    asset: &AssetRef,
127    time_slice: &TimeSliceID,
128    prices: &CommodityPrices,
129    year: u32,
130) -> MoneyPerActivity {
131    // Get the operating cost of the asset. This includes the variable operating cost, levies and
132    // flow costs, but excludes costs/revenues from commodity consumption/production.
133    let operating_cost = asset.get_operating_cost(year, time_slice);
134
135    // Revenue from flows excluding the primary output
136    let revenue_from_flows = asset.get_revenue_from_flows_excluding_primary(prices, time_slice);
137
138    // The activity coefficient is the operating cost minus the revenue from non-primary flows
139    operating_cost - revenue_from_flows
140}
141
142/// Calculate a single activity coefficient for the NPV objective for a given time slice.
143fn calculate_activity_coefficient_for_npv(
144    asset: &AssetRef,
145    time_slice: &TimeSliceID,
146    prices: &CommodityPrices,
147    year: u32,
148) -> MoneyPerActivity {
149    // Get the operating cost of the asset. This includes the variable operating cost, levies and
150    // flow costs, but excludes costs/revenues from commodity consumption/production.
151    let operating_cost = asset.get_operating_cost(year, time_slice);
152
153    // Revenue from flows including the primary output
154    let revenue_from_flows = asset.get_revenue_from_flows(prices, time_slice);
155
156    // The activity coefficient is the revenue from flows minus the operating cost (net revenue)
157    revenue_from_flows - operating_cost
158}