1use crate::asset::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::{Dimensionless, MoneyPerActivity, MoneyPerFlow};
10use indexmap::IndexMap;
11use itertools::iproduct;
12use std::collections::{BTreeMap, HashMap};
13
14#[derive(Default, Clone)]
26pub struct ReducedCosts(IndexMap<(AssetRef, TimeSliceID), MoneyPerActivity>);
27
28impl ReducedCosts {
29 pub fn get(&self, asset: &AssetRef, time_slice: &TimeSliceID) -> MoneyPerActivity {
35 *self
36 .0
37 .get(&(asset.clone(), time_slice.clone()))
38 .unwrap_or_else(|| &self.0[&(asset.as_candidate(None).into(), time_slice.clone())])
39 }
40
41 pub fn extend<T>(&mut self, iter: T)
43 where
44 T: IntoIterator<Item = ((AssetRef, TimeSliceID), MoneyPerActivity)>,
45 {
46 self.0.extend(iter);
47 }
48
49 pub fn iter(&self) -> impl Iterator<Item = (&(AssetRef, TimeSliceID), &MoneyPerActivity)> {
51 self.0.iter()
52 }
53
54 pub fn iter_mut(
56 &mut self,
57 ) -> impl Iterator<Item = (&(AssetRef, TimeSliceID), &mut MoneyPerActivity)> {
58 self.0.iter_mut()
59 }
60}
61
62impl FromIterator<((AssetRef, TimeSliceID), MoneyPerActivity)> for ReducedCosts {
63 fn from_iter<T>(iter: T) -> Self
64 where
65 T: IntoIterator<Item = ((AssetRef, TimeSliceID), MoneyPerActivity)>,
66 {
67 ReducedCosts(iter.into_iter().collect())
68 }
69}
70
71impl IntoIterator for ReducedCosts {
72 type Item = ((AssetRef, TimeSliceID), MoneyPerActivity);
73 type IntoIter = indexmap::map::IntoIter<(AssetRef, TimeSliceID), MoneyPerActivity>;
74
75 fn into_iter(self) -> Self::IntoIter {
76 self.0.into_iter()
77 }
78}
79
80impl From<IndexMap<(AssetRef, TimeSliceID), MoneyPerActivity>> for ReducedCosts {
81 fn from(map: IndexMap<(AssetRef, TimeSliceID), MoneyPerActivity>) -> Self {
82 ReducedCosts(map)
83 }
84}
85
86pub fn calculate_prices_and_reduced_costs(
98 model: &Model,
99 solution: &Solution,
100 existing_assets: &[AssetRef],
101 year: u32,
102) -> (CommodityPrices, ReducedCosts) {
103 let mut prices = CommodityPrices::default();
104 let mut reduced_costs = ReducedCosts::default();
105
106 let shadow_prices = CommodityPrices::from_iter(solution.iter_commodity_balance_duals());
107 let reduced_costs_for_candidates: ReducedCosts = solution
108 .iter_reduced_costs_for_candidates()
109 .map(|(asset, time_slice, cost)| ((asset.clone(), time_slice.clone()), cost))
110 .collect();
111
112 let (new_prices, reduced_costs_for_candidates) = match model.parameters.pricing_strategy {
113 PricingStrategy::ShadowPrices => (
115 shadow_prices.with_levies(model, year),
116 reduced_costs_for_candidates,
117 ),
118 PricingStrategy::ScarcityAdjusted => {
120 let adjusted_prices = shadow_prices
121 .clone()
122 .with_scarcity_adjustment(solution.iter_activity_duals())
123 .with_levies(model, year);
124 let unadjusted_prices = shadow_prices.with_levies(model, year);
125 let mut reduced_costs_for_candidates = reduced_costs_for_candidates;
126
127 remove_scarcity_influence_from_candidate_reduced_costs(
129 &mut reduced_costs_for_candidates,
130 &adjusted_prices,
131 &unadjusted_prices,
132 );
133
134 (adjusted_prices, reduced_costs_for_candidates)
135 }
136 };
137
138 prices.extend(new_prices);
140
141 reduced_costs.extend(reduced_costs_for_candidates);
143 reduced_costs.extend(reduced_costs_for_existing(
144 &model.time_slice_info,
145 existing_assets,
146 &prices,
147 year,
148 ));
149
150 (prices, reduced_costs)
151}
152
153#[derive(Default, Clone)]
155pub struct CommodityPrices(BTreeMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>);
156
157impl CommodityPrices {
158 fn with_levies(mut self, model: &Model, year: u32) -> Self {
168 for (region_id, time_slice) in
169 iproduct!(model.iter_regions(), model.time_slice_info.iter_ids())
170 {
171 let levy_key = (region_id.clone(), year, time_slice.clone());
172 for commodity in model.commodities.values() {
173 if let Some(levy) = commodity.levies.get(&levy_key) {
174 let key = (commodity.id.clone(), region_id.clone(), time_slice.clone());
175 self.0
176 .entry(key)
177 .and_modify(|price| *price = price.max(levy.value))
178 .or_insert(levy.value);
179 }
180 }
181 }
182
183 self
184 }
185
186 fn with_scarcity_adjustment<'a, I>(mut self, activity_duals: I) -> Self
192 where
193 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
194 {
195 let highest_duals = get_highest_activity_duals(activity_duals);
196
197 for (key, highest) in highest_duals.iter() {
200 if let Some(price) = self.0.get_mut(key) {
201 *price += MoneyPerFlow(highest.value());
203 }
204 }
205
206 self
207 }
208
209 pub fn extend<T>(&mut self, iter: T)
211 where
212 T: IntoIterator<Item = ((CommodityID, RegionID, TimeSliceID), MoneyPerFlow)>,
213 {
214 self.0.extend(iter);
215 }
216
217 pub fn insert(
219 &mut self,
220 commodity_id: &CommodityID,
221 region_id: &RegionID,
222 time_slice: &TimeSliceID,
223 price: MoneyPerFlow,
224 ) {
225 let key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
226 self.0.insert(key, price);
227 }
228
229 pub fn iter(
235 &self,
236 ) -> impl Iterator<Item = (&CommodityID, &RegionID, &TimeSliceID, MoneyPerFlow)> {
237 self.0
238 .iter()
239 .map(|((commodity_id, region_id, ts), price)| (commodity_id, region_id, ts, *price))
240 }
241
242 pub fn get(
244 &self,
245 commodity_id: &CommodityID,
246 region_id: &RegionID,
247 time_slice: &TimeSliceID,
248 ) -> Option<MoneyPerFlow> {
249 self.0
250 .get(&(commodity_id.clone(), region_id.clone(), time_slice.clone()))
251 .copied()
252 }
253
254 pub fn within_tolerance(&self, other: &Self, tolerance: Dimensionless) -> bool {
258 for (key, &price) in &self.0 {
259 let other_price = other.0[key];
260 let abs_diff = (price - other_price).abs();
261
262 if price == MoneyPerFlow(0.0) {
264 if other_price != MoneyPerFlow(0.0) {
266 return false;
267 }
268 } else if abs_diff / price.abs() > tolerance {
270 return false;
271 }
272 }
273 true
274 }
275}
276
277impl<'a> FromIterator<(&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>
278 for CommodityPrices
279{
280 fn from_iter<I>(iter: I) -> Self
281 where
282 I: IntoIterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>,
283 {
284 let map = iter
285 .into_iter()
286 .map(|(commodity_id, region_id, time_slice, price)| {
287 (
288 (commodity_id.clone(), region_id.clone(), time_slice.clone()),
289 price,
290 )
291 })
292 .collect();
293 CommodityPrices(map)
294 }
295}
296
297impl IntoIterator for CommodityPrices {
298 type Item = ((CommodityID, RegionID, TimeSliceID), MoneyPerFlow);
299 type IntoIter =
300 std::collections::btree_map::IntoIter<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>;
301
302 fn into_iter(self) -> Self::IntoIter {
303 self.0.into_iter()
304 }
305}
306
307fn get_highest_activity_duals<'a, I>(
308 activity_duals: I,
309) -> HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerActivity>
310where
311 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
312{
313 let mut highest_duals = HashMap::new();
315 for (asset, time_slice, dual) in activity_duals {
316 for flow in asset.iter_flows().filter(|flow| flow.is_output()) {
318 highest_duals
320 .entry((
321 flow.commodity.id.clone(),
322 asset.region_id().clone(),
323 time_slice.clone(),
324 ))
325 .and_modify(|current_dual| {
326 if dual > *current_dual {
327 *current_dual = dual;
328 }
329 })
330 .or_insert(dual);
331 }
332 }
333
334 highest_duals
335}
336
337fn remove_scarcity_influence_from_candidate_reduced_costs(
339 reduced_costs: &mut ReducedCosts,
340 adjusted_prices: &CommodityPrices,
341 unadjusted_prices: &CommodityPrices,
342) {
343 for ((asset, time_slice), cost) in reduced_costs.iter_mut() {
344 *cost += asset
345 .iter_flows()
346 .map(|flow| {
347 get_scarcity_adjustment(
348 flow,
349 asset.region_id(),
350 time_slice,
351 adjusted_prices,
352 unadjusted_prices,
353 )
354 })
355 .sum();
356 }
357}
358
359fn get_scarcity_adjustment(
363 flow: &ProcessFlow,
364 region_id: &RegionID,
365 time_slice: &TimeSliceID,
366 adjusted_prices: &CommodityPrices,
367 unadjusted_prices: &CommodityPrices,
368) -> MoneyPerActivity {
369 let adjusted = adjusted_prices
370 .get(&flow.commodity.id, region_id, time_slice)
371 .expect("No adjusted price found");
372 let unadjusted = unadjusted_prices
373 .get(&flow.commodity.id, region_id, time_slice)
374 .expect("No unadjusted price found");
375 flow.coeff * (unadjusted - adjusted)
376}
377
378fn reduced_costs_for_existing<'a>(
380 time_slice_info: &'a TimeSliceInfo,
381 assets: &'a [AssetRef],
382 prices: &'a CommodityPrices,
383 year: u32,
384) -> impl Iterator<Item = ((AssetRef, TimeSliceID), MoneyPerActivity)> + 'a {
385 iproduct!(assets, time_slice_info.iter_ids()).map(move |(asset, time_slice)| {
386 let operating_cost = asset.get_operating_cost(year, time_slice);
387 let revenue_from_flows = asset
388 .iter_flows()
389 .map(|flow| {
390 flow.coeff
391 * prices
392 .get(&flow.commodity.id, asset.region_id(), time_slice)
393 .unwrap()
394 })
395 .sum();
396 let reduced_cost = operating_cost - revenue_from_flows;
397
398 ((asset.clone(), time_slice.clone()), reduced_cost)
399 })
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405 use crate::commodity::CommodityID;
406 use crate::fixture::{asset, assets, process, time_slice};
407 use crate::process::Process;
408 use crate::region::RegionID;
409 use crate::time_slice::TimeSliceID;
410 use indexmap::indexmap;
411 use rstest::rstest;
412
413 #[rstest]
414 fn test_get_reduced_cost(process: Process, time_slice: TimeSliceID) {
415 let asset_pool = assets(asset(process));
416 let asset = asset_pool.as_slice().first().unwrap();
417
418 let candidate = asset.as_candidate(None);
420 let mut reduced_costs = ReducedCosts::from(indexmap! {
421 (candidate.into(), time_slice.clone()) => MoneyPerActivity(42.0)
422 });
423
424 let result = reduced_costs.get(asset, &time_slice);
426 assert_eq!(result, MoneyPerActivity(42.0));
427
428 reduced_costs.extend(indexmap! {
430 (asset.clone(), time_slice.clone()) => MoneyPerActivity(100.0)
431 });
432
433 let result = reduced_costs.get(asset, &time_slice);
435 assert_eq!(result, MoneyPerActivity(100.0));
436 }
437
438 #[rstest]
439 #[case(MoneyPerFlow(100.0), MoneyPerFlow(100.0), Dimensionless(0.0), true)] #[case(MoneyPerFlow(100.0), MoneyPerFlow(105.0), Dimensionless(0.1), true)] #[case(MoneyPerFlow(-100.0), MoneyPerFlow(-105.0), Dimensionless(0.1), true)] #[case(MoneyPerFlow(0.0), MoneyPerFlow(0.0), Dimensionless(0.1), true)] #[case(MoneyPerFlow(100.0), MoneyPerFlow(105.0), Dimensionless(0.01), false)] #[case(MoneyPerFlow(100.0), MoneyPerFlow(-105.0), Dimensionless(0.1), false)] #[case(MoneyPerFlow(0.0), MoneyPerFlow(10.0), Dimensionless(0.1), false)] #[case(MoneyPerFlow(0.0), MoneyPerFlow(-10.0), Dimensionless(0.1), false)] #[case(MoneyPerFlow(10.0), MoneyPerFlow(0.0), Dimensionless(0.1), false)] #[case(MoneyPerFlow(-10.0), MoneyPerFlow(0.0), Dimensionless(0.1), false)] fn test_within_tolerance_scenarios(
450 #[case] price1: MoneyPerFlow,
451 #[case] price2: MoneyPerFlow,
452 #[case] tolerance: Dimensionless,
453 #[case] expected: bool,
454 ) {
455 let mut prices1 = CommodityPrices::default();
456 let mut prices2 = CommodityPrices::default();
457
458 let commodity = CommodityID::new("test_commodity");
459 let region = RegionID::new("test_region");
460 let time_slice: TimeSliceID = "summer.day".into();
461
462 prices1.insert(&commodity, ®ion, &time_slice, price1);
463 prices2.insert(&commodity, ®ion, &time_slice, price2);
464
465 assert_eq!(prices1.within_tolerance(&prices2, tolerance), expected);
466 }
467}