muse2/simulation/
prices.rs

1//! Code for updating the simulation state.
2use crate::asset::{AssetPool, AssetRef};
3use crate::commodity::CommodityID;
4use crate::model::{Model, PricingStrategy};
5use crate::process::ProcessFlow;
6use crate::region::RegionID;
7use crate::simulation::optimisation::Solution;
8use crate::time_slice::{TimeSliceID, TimeSliceInfo};
9use crate::units::{MoneyPerActivity, MoneyPerFlow};
10use itertools::iproduct;
11use std::collections::{BTreeMap, HashMap};
12
13/// A map of reduced costs for different assets in different time slices
14pub type ReducedCosts = HashMap<(AssetRef, TimeSliceID), MoneyPerActivity>;
15
16/// Get commodity prices and reduced costs for assets
17///
18/// Note that the behaviour will be different depending on the [`PricingStrategy`] the user has
19/// selected.
20pub fn get_prices_and_reduced_costs(
21    model: &Model,
22    solution: &Solution,
23    assets: &AssetPool,
24    year: u32,
25) -> (CommodityPrices, ReducedCosts) {
26    let shadow_prices = CommodityPrices::from_iter(solution.iter_commodity_balance_duals());
27    let reduced_costs_for_candidates: HashMap<_, _> = solution
28        .iter_reduced_costs_for_candidates()
29        .map(|(asset, time_slice, cost)| ((asset.clone(), time_slice.clone()), cost))
30        .collect();
31
32    let (prices, reduced_costs_for_candidates) = match model.parameters.pricing_strategy {
33        // Use raw shadow prices and reduced costs
34        PricingStrategy::ShadowPrices => (
35            shadow_prices.with_levies(model, year),
36            reduced_costs_for_candidates,
37        ),
38        // Adjust prices for scarcity and then remove this adjustment from reduced costs
39        PricingStrategy::ScarcityAdjusted => {
40            let adjusted_prices = shadow_prices
41                .clone()
42                .with_scarcity_adjustment(solution.iter_activity_duals())
43                .with_levies(model, year);
44            let unadjusted_prices = shadow_prices.with_levies(model, year);
45            let mut reduced_costs_for_candidates = reduced_costs_for_candidates;
46
47            // Remove adjustment
48            remove_scarcity_influence_from_candidate_reduced_costs(
49                &mut reduced_costs_for_candidates,
50                &adjusted_prices,
51                &unadjusted_prices,
52            );
53
54            (adjusted_prices, reduced_costs_for_candidates)
55        }
56    };
57
58    // Add reduced costs for existing assets
59    let mut reduced_costs = reduced_costs_for_candidates;
60    reduced_costs.extend(reduced_costs_for_existing(
61        &model.time_slice_info,
62        assets,
63        &prices,
64        year,
65    ));
66
67    (prices, reduced_costs)
68}
69
70/// A map relating commodity ID + region + time slice to current price (endogenous)
71#[derive(Default, Clone)]
72pub struct CommodityPrices(BTreeMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>);
73
74impl CommodityPrices {
75    /// Add prices based on levies/incentives.
76    ///
77    /// If a commodity already has a price based on the previous dual-based calculation, we choose
78    /// the higher of the two.
79    ///
80    /// # Arguments
81    ///
82    /// * `model` - The model
83    /// * `year` - The milestone year of interest
84    fn with_levies(mut self, model: &Model, year: u32) -> Self {
85        for (region_id, time_slice) in
86            iproduct!(model.iter_regions(), model.time_slice_info.iter_ids())
87        {
88            let levy_key = (region_id.clone(), year, time_slice.clone());
89            for commodity in model.commodities.values() {
90                if let Some(levy) = commodity.levies.get(&levy_key) {
91                    let key = (commodity.id.clone(), region_id.clone(), time_slice.clone());
92                    self.0
93                        .entry(key)
94                        .and_modify(|price| *price = price.max(levy.value))
95                        .or_insert(levy.value);
96                }
97            }
98        }
99
100        self
101    }
102
103    /// Remove the impact of scarcity on prices.
104    ///
105    /// # Arguments
106    ///
107    /// * `activity_duals` - Value of activity duals from solution
108    fn with_scarcity_adjustment<'a, I>(mut self, activity_duals: I) -> Self
109    where
110        I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
111    {
112        let highest_duals = get_highest_activity_duals(activity_duals);
113
114        // Add the highest activity dual for each commodity/region/timeslice to each commodity
115        // balance dual
116        for (key, highest) in highest_duals.iter() {
117            if let Some(price) = self.0.get_mut(key) {
118                // highest is in units of MoneyPerActivity, but this is correct according to Adam
119                *price += MoneyPerFlow(highest.value());
120            }
121        }
122
123        self
124    }
125
126    /// Insert a price for the given commodity, region and time slice
127    pub fn insert(
128        &mut self,
129        commodity_id: &CommodityID,
130        region_id: &RegionID,
131        time_slice: &TimeSliceID,
132        price: MoneyPerFlow,
133    ) {
134        let key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
135        self.0.insert(key, price);
136    }
137
138    /// Iterate over the map.
139    ///
140    /// # Returns
141    ///
142    /// An iterator of tuples containing commodity ID, region ID, time slice and price.
143    pub fn iter(
144        &self,
145    ) -> impl Iterator<Item = (&CommodityID, &RegionID, &TimeSliceID, MoneyPerFlow)> {
146        self.0
147            .iter()
148            .map(|((commodity_id, region_id, ts), price)| (commodity_id, region_id, ts, *price))
149    }
150
151    /// Get the price for the specified commodity for a given region and time slice
152    pub fn get(
153        &self,
154        commodity_id: &CommodityID,
155        region_id: &RegionID,
156        time_slice: &TimeSliceID,
157    ) -> Option<MoneyPerFlow> {
158        self.0
159            .get(&(commodity_id.clone(), region_id.clone(), time_slice.clone()))
160            .copied()
161    }
162}
163
164impl<'a> FromIterator<(&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>
165    for CommodityPrices
166{
167    fn from_iter<I>(iter: I) -> Self
168    where
169        I: IntoIterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>,
170    {
171        let map = iter
172            .into_iter()
173            .map(|(commodity_id, region_id, time_slice, price)| {
174                (
175                    (commodity_id.clone(), region_id.clone(), time_slice.clone()),
176                    price,
177                )
178            })
179            .collect();
180        CommodityPrices(map)
181    }
182}
183
184fn get_highest_activity_duals<'a, I>(
185    activity_duals: I,
186) -> HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerActivity>
187where
188    I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
189{
190    // Calculate highest activity dual for each commodity/region/timeslice
191    let mut highest_duals = HashMap::new();
192    for (asset, time_slice, dual) in activity_duals {
193        // Iterate over all output flows
194        for flow in asset.iter_flows().filter(|flow| flow.is_output()) {
195            // Update the highest dual for this commodity/timeslice
196            highest_duals
197                .entry((
198                    flow.commodity.id.clone(),
199                    asset.region_id.clone(),
200                    time_slice.clone(),
201                ))
202                .and_modify(|current_dual| {
203                    if dual > *current_dual {
204                        *current_dual = dual;
205                    }
206                })
207                .or_insert(dual);
208        }
209    }
210
211    highest_duals
212}
213
214/// Remove the effect of scarcity on candidate assets' reduced costs
215fn remove_scarcity_influence_from_candidate_reduced_costs(
216    reduced_costs: &mut ReducedCosts,
217    adjusted_prices: &CommodityPrices,
218    unadjusted_prices: &CommodityPrices,
219) {
220    for ((asset, time_slice), cost) in reduced_costs.iter_mut() {
221        *cost += asset
222            .iter_flows()
223            .map(|flow| {
224                get_scarcity_adjustment(
225                    flow,
226                    &asset.region_id,
227                    time_slice,
228                    adjusted_prices,
229                    unadjusted_prices,
230                )
231            })
232            .sum();
233    }
234}
235
236/// Get the scarcity adjustment for the given flow/region/time slice combination.
237///
238/// The return value may be negative.
239fn get_scarcity_adjustment(
240    flow: &ProcessFlow,
241    region_id: &RegionID,
242    time_slice: &TimeSliceID,
243    adjusted_prices: &CommodityPrices,
244    unadjusted_prices: &CommodityPrices,
245) -> MoneyPerActivity {
246    let adjusted = adjusted_prices
247        .get(&flow.commodity.id, region_id, time_slice)
248        .expect("No adjusted price found");
249    let unadjusted = unadjusted_prices
250        .get(&flow.commodity.id, region_id, time_slice)
251        .expect("No unadjusted price found");
252    flow.coeff * (unadjusted - adjusted)
253}
254
255/// Calculate reduced costs for existing assets
256fn reduced_costs_for_existing<'a>(
257    time_slice_info: &'a TimeSliceInfo,
258    assets: &'a AssetPool,
259    prices: &'a CommodityPrices,
260    year: u32,
261) -> impl Iterator<Item = ((AssetRef, TimeSliceID), MoneyPerActivity)> + 'a {
262    iproduct!(assets.iter(), time_slice_info.iter_ids()).map(move |(asset, time_slice)| {
263        let operating_cost = asset.get_operating_cost(year, time_slice);
264        let revenue_from_flows = asset
265            .iter_flows()
266            .map(|flow| {
267                flow.coeff
268                    * prices
269                        .get(&flow.commodity.id, &asset.region_id, time_slice)
270                        .unwrap()
271            })
272            .sum();
273        let reduced_cost = operating_cost - revenue_from_flows;
274
275        ((asset.clone(), time_slice.clone()), reduced_cost)
276    })
277}