muse2/simulation/
prices.rs

1//! Code for updating the simulation state.
2use super::optimisation::Solution;
3use crate::asset::AssetPool;
4use crate::commodity::CommodityID;
5use crate::model::Model;
6use crate::region::RegionID;
7use crate::time_slice::{TimeSliceID, TimeSliceInfo};
8use indexmap::IndexMap;
9use log::warn;
10use std::collections::{HashMap, HashSet};
11
12/// A map relating commodity ID + region + time slice to current price (endogenous)
13#[derive(Default)]
14pub struct CommodityPrices(IndexMap<(CommodityID, RegionID, TimeSliceID), f64>);
15
16impl CommodityPrices {
17    /// Calculate commodity prices based on the result of the dispatch optimisation.
18    ///
19    /// Missing prices will be calculated directly from the input data
20    pub fn from_model_and_solution(model: &Model, solution: &Solution, assets: &AssetPool) -> Self {
21        let mut prices = CommodityPrices::default();
22        let commodity_regions_updated = prices.add_from_solution(solution, assets);
23
24        // Find commodity/region combinations not updated in last step
25        let mut remaining_commodity_regions = HashSet::new();
26        for commodity_id in model.commodities.keys() {
27            for region_id in model.regions.keys() {
28                let key = (commodity_id.clone(), region_id.clone());
29                if !commodity_regions_updated.contains(&key) {
30                    remaining_commodity_regions.insert(key);
31                }
32            }
33        }
34
35        prices.add_remaining(remaining_commodity_regions.iter(), &model.time_slice_info);
36
37        prices
38    }
39
40    /// Add commodity prices for which there are values in the solution
41    ///
42    /// Commodity prices are calculated as the sum of the commodity balance duals and the highest
43    /// capacity dual for each commodity/timeslice.
44    ///
45    /// # Arguments
46    ///
47    /// * `solution` - The solution to the dispatch optimisation
48    /// * `assets` - The asset pool
49    ///
50    /// # Returns
51    ///
52    /// The set of commodities for which prices were added.
53    fn add_from_solution(
54        &mut self,
55        solution: &Solution,
56        assets: &AssetPool,
57    ) -> HashSet<(CommodityID, RegionID)> {
58        let mut commodity_regions_updated = HashSet::new();
59
60        // Calculate highest capacity dual for each commodity/region/timeslice
61        let mut highest_duals = HashMap::new();
62        for (asset_id, time_slice, dual) in solution.iter_capacity_duals() {
63            let asset = assets.get(asset_id).unwrap();
64            let region_id = asset.region_id.clone();
65
66            // Iterate over process pacs
67            let process_pacs = asset.process.iter_pacs();
68            for pac in process_pacs {
69                let commodity = &pac.commodity;
70
71                // If the commodity flow is positive (produced PAC)
72                if pac.flow > 0.0 {
73                    // Update the highest dual for this commodity/timeslice
74                    highest_duals
75                        .entry((commodity.id.clone(), region_id.clone(), time_slice.clone()))
76                        .and_modify(|current_dual| {
77                            if dual > *current_dual {
78                                *current_dual = dual;
79                            }
80                        })
81                        .or_insert(dual);
82                }
83            }
84        }
85
86        // Add the highest capacity dual for each commodity/timeslice to each commodity balance dual
87        for (commodity_id, region_id, time_slice, dual) in solution.iter_commodity_balance_duals() {
88            let key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
89            let price = dual + highest_duals.get(&key).unwrap_or(&0.0);
90            self.insert(commodity_id, region_id, time_slice, price);
91            commodity_regions_updated.insert((commodity_id.clone(), region_id.clone()));
92        }
93
94        commodity_regions_updated
95    }
96
97    /// Add prices for any commodity not updated by the dispatch step.
98    ///
99    /// # Arguments
100    ///
101    /// * `commodity_ids` - IDs of commodities to update
102    /// * `time_slice_info` - Information about time slices
103    fn add_remaining<'a, I>(&mut self, commodity_regions: I, time_slice_info: &TimeSliceInfo)
104    where
105        I: Iterator<Item = &'a (CommodityID, RegionID)>,
106    {
107        for (commodity_id, region_id) in commodity_regions {
108            warn!("No prices calculated for commodity {commodity_id} in region {region_id}; setting to NaN");
109            for time_slice in time_slice_info.iter_ids() {
110                self.insert(commodity_id, region_id, time_slice, f64::NAN);
111            }
112        }
113    }
114
115    /// Insert a price for the given commodity, time slice and region
116    pub fn insert(
117        &mut self,
118        commodity_id: &CommodityID,
119        region_id: &RegionID,
120        time_slice: &TimeSliceID,
121        price: f64,
122    ) {
123        let key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
124        self.0.insert(key, price);
125    }
126
127    /// Iterate over the map.
128    ///
129    /// # Returns
130    ///
131    /// An iterator of tuples containing commodity ID, time slice and price.
132    pub fn iter(&self) -> impl Iterator<Item = (&CommodityID, &RegionID, &TimeSliceID, f64)> {
133        self.0
134            .iter()
135            .map(|((commodity_id, region_id, ts), price)| (commodity_id, region_id, ts, *price))
136    }
137}