Skip to main content

muse2/simulation/investment/appraisal/
coefficients.rs

1//! Calculation of cost coefficients for investment tools.
2use crate::agent::ObjectiveType;
3use crate::asset::AssetRef;
4use crate::model::Model;
5use crate::simulation::PriceMap;
6use crate::simulation::prices::Prices;
7use crate::time_slice::{TimeSliceID, TimeSliceInfo};
8use crate::units::{MoneyPerActivity, MoneyPerFlow};
9use indexmap::IndexMap;
10use std::collections::HashMap;
11use std::rc::Rc;
12
13/// Per-time-slice cost coefficients for an asset.
14///
15/// These coefficients are calculated according to the agent's `ObjectiveType` and are used by the
16/// investment appraisal routines. They comprise the activity coefficients (revenue minus operating
17/// cost, derived from shadow prices) used in the appraisal optimisation, together with the market
18/// costs (derived from market prices).
19#[derive(Clone)]
20pub struct ObjectiveCoefficients {
21    /// Cost per unit of activity in each time slice
22    pub activity_coefficients: IndexMap<TimeSliceID, MoneyPerActivity>,
23    /// Market costs associated with asset for each time slice
24    pub market_costs: IndexMap<TimeSliceID, MoneyPerActivity>,
25}
26
27/// Calculates cost coefficients for a set of assets for a given objective type.
28///
29/// Returns a map from each asset to its [`ObjectiveCoefficients`], which holds a per-time-slice
30/// activity coefficient and market cost.
31///
32/// Activity coefficients are revenue from flows (including the primary output) minus operating
33/// cost, calculated using shadow prices. A small positive epsilon is added to each activity
34/// coefficient so that assets with near-zero net value still appear in dispatch.
35///
36/// Market costs are calculated using market prices rather than shadow prices. For NPV they use the
37/// same revenue-minus-operating-cost calculation as the activity coefficients. For LCOX the sign is
38/// inverted (as the value represents a cost) and the primary output (commodity of interest) is
39/// excluded.
40pub fn calculate_coefficients_for_assets(
41    model: &Model,
42    objective_type: &ObjectiveType,
43    assets: &[AssetRef],
44    prices: &Prices,
45    year: u32,
46) -> HashMap<AssetRef, Rc<ObjectiveCoefficients>> {
47    assets
48        .iter()
49        .map(|asset| {
50            let coefficient = calculate_coefficients_for_asset(
51                asset,
52                objective_type,
53                &model.time_slice_info,
54                prices,
55                year,
56            );
57            (asset.clone(), Rc::new(coefficient))
58        })
59        .collect()
60}
61
62/// Calculates cost coefficients for a single asset
63pub fn calculate_coefficients_for_asset(
64    asset: &AssetRef,
65    objective_type: &ObjectiveType,
66    time_slice_info: &TimeSliceInfo,
67    prices: &Prices,
68    year: u32,
69) -> ObjectiveCoefficients {
70    // Small constant added to each activity coefficient to ensure break-even/slightly negative
71    // assets are still dispatched
72    const EPSILON_ACTIVITY_COEFFICIENT: MoneyPerActivity = MoneyPerActivity(f64::EPSILON * 100.0);
73
74    // Activity coefficients
75    let mut activity_coefficients = IndexMap::new();
76    let mut market_costs = IndexMap::new();
77    let primary_output_flow = asset.primary_output().unwrap();
78    let asset_region = asset.region_id();
79    for time_slice in time_slice_info.iter_ids() {
80        // Get the operating cost of the asset. This includes the variable operating cost, levies and
81        // flow costs, but excludes costs/revenues from commodity consumption/production.
82        let operating_cost = asset.get_operating_cost(year, time_slice);
83        let net_operating_cost =
84            -calculate_asset_revenues(asset, operating_cost, time_slice, &prices.shadow);
85
86        let fallback_cost = prices
87            .fallback
88            .get(&primary_output_flow.commodity.id, asset_region, time_slice)
89            .unwrap_or(MoneyPerFlow(0.0))
90            * primary_output_flow.coeff;
91
92        activity_coefficients.insert(
93            time_slice.clone(),
94            fallback_cost - net_operating_cost + EPSILON_ACTIVITY_COEFFICIENT,
95        );
96
97        let market_cost = match objective_type {
98            ObjectiveType::LevelisedCostOfX => {
99                calculate_asset_costs_for_lcox(asset, operating_cost, time_slice, &prices.market)
100            }
101            ObjectiveType::NetPresentValue => {
102                -calculate_asset_revenues(asset, operating_cost, time_slice, &prices.market)
103            }
104        };
105        market_costs.insert(time_slice.clone(), market_cost);
106    }
107
108    ObjectiveCoefficients {
109        activity_coefficients,
110        market_costs,
111    }
112}
113
114/// Calculate the revenue from all flows minus operating cost
115fn calculate_asset_revenues(
116    asset: &AssetRef,
117    operating_cost: MoneyPerActivity,
118    time_slice: &TimeSliceID,
119    prices: &PriceMap,
120) -> MoneyPerActivity {
121    // Revenue from flows including the primary output
122    let revenue_from_flows = asset.get_revenue_from_flows(prices, time_slice);
123
124    // The activity coefficient is the revenue from flows minus the operating cost (net revenue)
125    revenue_from_flows - operating_cost
126}
127
128/// Calculate asset costs for LCOX objective.
129///
130/// Excludes revenues from the primary output (commodity of interest).
131fn calculate_asset_costs_for_lcox(
132    asset: &AssetRef,
133    operating_cost: MoneyPerActivity,
134    time_slice: &TimeSliceID,
135    prices: &PriceMap,
136) -> MoneyPerActivity {
137    // Revenue from flows excluding the primary output
138    let revenue_from_flows = asset.get_revenue_from_flows_excluding_primary(prices, time_slice);
139
140    // The activity coefficient is the operating cost minus the revenue from non-primary flows
141    operating_cost - revenue_from_flows
142}