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