1#![doc = concat!("See <", crate::docs_url!("/model/prices.html"), ">")]
4use crate::asset::AssetRef;
5use crate::commodity::{CommodityID, CommodityMap, CommodityType, PricingStrategy};
6use crate::model::Model;
7use crate::region::RegionID;
8use crate::simulation::market::MarketSet;
9use crate::simulation::optimisation::Solution;
10use crate::time_slice::{TimeSliceID, TimeSliceInfo, TimeSliceSelection};
11use crate::units::{Activity, Dimensionless, Flow, MoneyPerActivity, MoneyPerFlow, UnitType, Year};
12use anyhow::Result;
13use indexmap::IndexMap;
14use std::collections::{HashMap, HashSet};
15use std::marker::PhantomData;
16
17const N_CYCLE_ITERATIONS: i32 = 1;
19
20#[derive(Clone, Copy, Debug)]
22struct WeightedAverageAccumulator<W: UnitType> {
23 numerator: MoneyPerFlow,
25 denominator: Dimensionless,
27 _weight_type: PhantomData<W>,
29}
30
31impl<W: UnitType> Default for WeightedAverageAccumulator<W> {
32 fn default() -> Self {
33 Self {
34 numerator: MoneyPerFlow(0.0),
35 denominator: Dimensionless(0.0),
36 _weight_type: PhantomData,
37 }
38 }
39}
40
41impl<W: UnitType> WeightedAverageAccumulator<W> {
42 fn add(&mut self, value: MoneyPerFlow, weight: W) {
44 let weight = Dimensionless(weight.value());
45 self.numerator += value * weight;
46 self.denominator += weight;
47 }
48
49 fn finalise(self) -> Option<MoneyPerFlow> {
53 (self.denominator > Dimensionless::EPSILON).then(|| self.numerator / self.denominator)
54 }
55}
56
57#[derive(Default)]
59pub struct Prices {
60 pub market: PriceMap,
62 pub shadow: PriceMap,
64 pub fallback: PriceMap,
66}
67
68pub fn calculate_prices(
84 model: &Model,
85 solution_without_candidates: &Solution,
86 solution_with_candidates: &Solution,
87 year: u32,
88) -> Result<Prices> {
89 let shadow_prices =
93 PriceMap::from_iter(solution_with_candidates.iter_commodity_balance_duals());
94
95 let mut market_prices = PriceMap::default();
97
98 let mut fallback_prices = PriceMap::default();
100
101 let mut annual_activities: Option<HashMap<AssetRef, Activity>> = None;
103
104 for market_set in model.investment_order[&year].iter().rev() {
106 price_market_set(
107 market_set,
108 model,
109 solution_without_candidates,
110 solution_with_candidates,
111 year,
112 &shadow_prices,
113 &mut annual_activities,
114 &mut market_prices,
115 None,
116 );
117 if model.parameters.fallback_pricing_strategy != PricingStrategy::Unpriced {
118 price_market_set(
119 market_set,
120 model,
121 solution_without_candidates,
122 solution_with_candidates,
123 year,
124 &shadow_prices,
125 &mut annual_activities,
126 &mut fallback_prices,
127 Some(model.parameters.fallback_pricing_strategy),
128 );
129 }
130 }
131
132 Ok(Prices {
133 market: market_prices,
134 shadow: shadow_prices,
135 fallback: fallback_prices,
136 })
137}
138
139#[allow(clippy::too_many_arguments)]
141fn price_market_set(
142 market_set: &MarketSet,
143 model: &Model,
144 solution_without_candidates: &Solution,
145 solution_with_candidates: &Solution,
146 year: u32,
147 shadow_prices: &PriceMap,
148 annual_activities: &mut Option<HashMap<AssetRef, Activity>>,
149 market_prices: &mut PriceMap,
150 strategy_override: Option<PricingStrategy>,
151) {
152 match market_set {
153 MarketSet::Single(market) => {
154 price_markets(
155 model,
156 solution_without_candidates,
157 solution_with_candidates,
158 year,
159 std::slice::from_ref(market),
160 shadow_prices,
161 annual_activities,
162 market_prices,
163 strategy_override,
164 );
165 }
166 MarketSet::Cycle(markets) => {
167 price_cycle(
168 model,
169 solution_without_candidates,
170 solution_with_candidates,
171 year,
172 markets,
173 shadow_prices,
174 annual_activities,
175 market_prices,
176 strategy_override,
177 );
178 }
179 MarketSet::Layer(market_sets) => {
180 for set in market_sets {
181 price_market_set(
182 set,
183 model,
184 solution_without_candidates,
185 solution_with_candidates,
186 year,
187 shadow_prices,
188 annual_activities,
189 market_prices,
190 strategy_override,
191 );
192 }
193 }
194 }
195}
196
197#[allow(clippy::too_many_arguments)]
216fn price_markets(
217 model: &Model,
218 solution_without_candidates: &Solution,
219 solution_with_candidates: &Solution,
220 year: u32,
221 markets: &[(CommodityID, RegionID)],
222 shadow_prices: &PriceMap,
223 annual_activities: &mut Option<HashMap<AssetRef, Activity>>,
224 market_prices: &mut PriceMap,
225 strategy_override: Option<PricingStrategy>,
226) {
227 let mut pricing_sets = HashMap::new();
229 for (commodity_id, region_id) in markets {
230 let strategy = if let Some(strategy) = strategy_override.as_ref() {
232 if model.commodities[commodity_id].kind == CommodityType::Other {
234 continue;
235 }
236 strategy
237 } else {
238 let strategy = &model.commodities[commodity_id].pricing_strategy;
241 if *strategy == PricingStrategy::Unpriced {
242 continue;
243 }
244 strategy
245 };
246 pricing_sets
247 .entry(strategy)
248 .or_insert_with(HashSet::new)
249 .insert((commodity_id.clone(), region_id.clone()));
250 }
251
252 if let Some(shadow_set) = pricing_sets.get(&PricingStrategy::Shadow) {
254 for (commodity_id, region_id) in shadow_set {
255 for time_slice in model.time_slice_info.iter_ids() {
256 if let Some(shadow_price) = shadow_prices.get(commodity_id, region_id, time_slice) {
257 market_prices.insert(commodity_id, region_id, time_slice, shadow_price);
258 }
259 }
260 }
261 }
262
263 if let Some(scarcity_set) = pricing_sets.get(&PricingStrategy::ScarcityAdjusted) {
265 add_scarcity_adjusted_prices(
266 solution_with_candidates.iter_activity_duals(),
267 shadow_prices,
268 market_prices,
269 scarcity_set,
270 );
271 }
272
273 if let Some(marginal_set) = pricing_sets.get(&PricingStrategy::MarginalCost) {
275 add_marginal_cost_prices(
276 solution_without_candidates.iter_activity_for_existing(),
277 solution_with_candidates.iter_activity_keys_for_candidates(),
278 market_prices,
279 year,
280 marginal_set,
281 &model.commodities,
282 &model.time_slice_info,
283 );
284 }
285
286 if let Some(marginal_avg_set) = pricing_sets.get(&PricingStrategy::MarginalCostAverage) {
288 add_marginal_cost_average_prices(
289 solution_without_candidates.iter_activity_for_existing(),
290 solution_with_candidates.iter_activity_keys_for_candidates(),
291 market_prices,
292 year,
293 marginal_avg_set,
294 &model.commodities,
295 &model.time_slice_info,
296 );
297 }
298
299 if let Some(fullcost_set) = pricing_sets.get(&PricingStrategy::FullCost) {
301 let annual_activities = annual_activities.get_or_insert_with(|| {
302 calculate_annual_activities(solution_without_candidates.iter_activity_for_existing())
303 });
304 add_full_cost_prices(
305 solution_without_candidates.iter_activity_for_existing(),
306 solution_with_candidates.iter_activity_keys_for_candidates(),
307 annual_activities,
308 market_prices,
309 year,
310 fullcost_set,
311 &model.commodities,
312 &model.time_slice_info,
313 );
314 }
315
316 if let Some(full_avg_set) = pricing_sets.get(&PricingStrategy::FullCostAverage) {
318 let annual_activities = annual_activities.get_or_insert_with(|| {
319 calculate_annual_activities(solution_without_candidates.iter_activity_for_existing())
320 });
321 add_full_cost_average_prices(
322 solution_without_candidates.iter_activity_for_existing(),
323 solution_with_candidates.iter_activity_keys_for_candidates(),
324 annual_activities,
325 market_prices,
326 year,
327 full_avg_set,
328 &model.commodities,
329 &model.time_slice_info,
330 );
331 }
332}
333
334#[allow(clippy::too_many_arguments)]
341fn price_cycle(
342 model: &Model,
343 solution_without_candidates: &Solution,
344 solution_with_candidates: &Solution,
345 year: u32,
346 markets: &[(CommodityID, RegionID)],
347 shadow_prices: &PriceMap,
348 annual_activities: &mut Option<HashMap<AssetRef, Activity>>,
349 market_prices: &mut PriceMap,
350 strategy_override: Option<PricingStrategy>,
351) {
352 for (commodity_id, region_id) in markets {
354 for time_slice in model.time_slice_info.iter_ids() {
355 if let Some(shadow_price) = shadow_prices.get(commodity_id, region_id, time_slice) {
356 market_prices.insert(commodity_id, region_id, time_slice, shadow_price);
357 }
358 }
359 }
360
361 for _ in 0..N_CYCLE_ITERATIONS {
363 for market in markets.iter().rev() {
365 price_markets(
366 model,
367 solution_without_candidates,
368 solution_with_candidates,
369 year,
370 std::slice::from_ref(market),
371 shadow_prices,
372 annual_activities,
373 market_prices,
374 strategy_override,
375 );
376 }
377 }
378}
379
380#[derive(Default, Clone)]
382pub struct PriceMap(IndexMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>);
383
384impl PriceMap {
385 pub fn insert(
387 &mut self,
388 commodity_id: &CommodityID,
389 region_id: &RegionID,
390 time_slice: &TimeSliceID,
391 price: MoneyPerFlow,
392 ) {
393 let key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
394 self.0.insert(key, price);
395 }
396
397 fn extend_selection_prices(
400 &mut self,
401 group_prices: &IndexMap<(CommodityID, RegionID, TimeSliceSelection), MoneyPerFlow>,
402 time_slice_info: &TimeSliceInfo,
403 ) {
404 for ((commodity_id, region_id, selection), &selection_price) in group_prices {
405 for (time_slice_id, _) in selection.iter(time_slice_info) {
406 self.insert(commodity_id, region_id, time_slice_id, selection_price);
407 }
408 }
409 }
410
411 pub fn iter(
417 &self,
418 ) -> impl Iterator<Item = (&CommodityID, &RegionID, &TimeSliceID, MoneyPerFlow)> {
419 self.0
420 .iter()
421 .map(|((commodity_id, region_id, ts), price)| (commodity_id, region_id, ts, *price))
422 }
423
424 pub fn get(
426 &self,
427 commodity_id: &CommodityID,
428 region_id: &RegionID,
429 time_slice: &TimeSliceID,
430 ) -> Option<MoneyPerFlow> {
431 self.0
432 .get(&(commodity_id.clone(), region_id.clone(), time_slice.clone()))
433 .copied()
434 }
435
436 pub fn keys(
438 &self,
439 ) -> indexmap::map::Keys<'_, (CommodityID, RegionID, TimeSliceID), MoneyPerFlow> {
440 self.0.keys()
441 }
442
443 fn time_slice_weighted_averages(
451 &self,
452 time_slice_info: &TimeSliceInfo,
453 ) -> HashMap<(CommodityID, RegionID), MoneyPerFlow> {
454 let mut weighted_prices = HashMap::new();
455
456 for ((commodity_id, region_id, time_slice_id), price) in &self.0 {
457 let weight = time_slice_info.time_slices[time_slice_id] / Year(1.0);
459 let key = (commodity_id.clone(), region_id.clone());
460 weighted_prices
461 .entry(key)
462 .and_modify(|v| *v += *price * weight)
463 .or_insert_with(|| *price * weight);
464 }
465
466 weighted_prices
467 }
468
469 pub fn within_tolerance_weighted(
479 &self,
480 other: &Self,
481 tolerance: Dimensionless,
482 time_slice_info: &TimeSliceInfo,
483 ) -> bool {
484 let self_averages = self.time_slice_weighted_averages(time_slice_info);
485 let other_averages = other.time_slice_weighted_averages(time_slice_info);
486
487 for (key, &price) in &self_averages {
488 let other_price = other_averages[key];
489 let abs_diff = (price - other_price).abs();
490
491 if price == MoneyPerFlow(0.0) {
493 if other_price != MoneyPerFlow(0.0) {
495 return false;
496 }
497 } else if abs_diff / price.abs() > tolerance {
499 return false;
500 }
501 }
502 true
503 }
504}
505
506impl<'a> FromIterator<(&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)> for PriceMap {
507 fn from_iter<I>(iter: I) -> Self
508 where
509 I: IntoIterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>,
510 {
511 let map = iter
512 .into_iter()
513 .map(|(commodity_id, region_id, time_slice, price)| {
514 (
515 (commodity_id.clone(), region_id.clone(), time_slice.clone()),
516 price,
517 )
518 })
519 .collect();
520 Self(map)
521 }
522}
523
524impl IntoIterator for PriceMap {
525 type Item = ((CommodityID, RegionID, TimeSliceID), MoneyPerFlow);
526 type IntoIter = indexmap::map::IntoIter<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>;
527
528 fn into_iter(self) -> Self::IntoIter {
529 self.0.into_iter()
530 }
531}
532
533fn add_scarcity_adjusted_prices<'a, I>(
542 activity_duals: I,
543 shadow_prices: &PriceMap,
544 market_prices: &mut PriceMap,
545 markets_to_price: &HashSet<(CommodityID, RegionID)>,
546) where
547 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
548{
549 let mut highest_duals = IndexMap::new();
551 for (asset, time_slice, dual) in activity_duals {
552 let region_id = asset.region_id();
553
554 for flow in asset.iter_output_flows().filter(|flow| {
557 markets_to_price.contains(&(flow.commodity.id.clone(), region_id.clone()))
558 }) {
559 highest_duals
561 .entry((
562 flow.commodity.id.clone(),
563 region_id.clone(),
564 time_slice.clone(),
565 ))
566 .and_modify(|current_dual| {
567 if dual > *current_dual {
568 *current_dual = dual;
569 }
570 })
571 .or_insert(dual);
572 }
573 }
574
575 for ((commodity, region, time_slice), highest_dual) in &highest_duals {
577 let shadow_price = shadow_prices.get(commodity, region, time_slice).unwrap();
580 let scarcity_price = shadow_price + MoneyPerFlow(highest_dual.value());
583 market_prices.insert(commodity, region, time_slice, scarcity_price);
584 }
585}
586
587fn add_marginal_cost_prices<'a, I, J>(
638 activity_for_existing: I,
639 activity_keys_for_candidates: J,
640 market_prices: &mut PriceMap,
641 year: u32,
642 markets_to_price: &HashSet<(CommodityID, RegionID)>,
643 commodities: &CommodityMap,
644 time_slice_info: &TimeSliceInfo,
645) where
646 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
647 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
648{
649 let mut group_prices: IndexMap<_, _> = iter_existing_asset_max_prices(
651 activity_for_existing,
652 markets_to_price,
653 market_prices,
654 year,
655 commodities,
656 &PricingStrategy::MarginalCost,
657 None,
658 )
659 .collect();
660 let priced_groups: HashSet<_> = group_prices.keys().cloned().collect();
661
662 let cand_group_prices = iter_candidate_asset_min_prices(
665 activity_keys_for_candidates,
666 markets_to_price,
667 market_prices,
668 &priced_groups,
669 year,
670 commodities,
671 &PricingStrategy::MarginalCost,
672 );
673
674 group_prices.extend(cand_group_prices);
676
677 market_prices.extend_selection_prices(&group_prices, time_slice_info);
679}
680
681fn iter_existing_asset_max_prices<'a, I>(
704 activity_for_existing: I,
705 markets_to_price: &HashSet<(CommodityID, RegionID)>,
706 market_prices: &PriceMap,
707 year: u32,
708 commodities: &CommodityMap,
709 pricing_strategy: &PricingStrategy,
710 annual_activities: Option<&HashMap<AssetRef, Activity>>,
711) -> impl Iterator<Item = ((CommodityID, RegionID, TimeSliceSelection), MoneyPerFlow)> + 'a
712where
713 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
714{
715 match pricing_strategy {
717 PricingStrategy::MarginalCost => assert!(
718 annual_activities.is_none(),
719 "Cannot provide annual_activities with marginal pricing strategy"
720 ),
721 PricingStrategy::FullCost => assert!(
722 annual_activities.is_some(),
723 "annual_activities must be provided for full pricing strategy"
724 ),
725 _ => panic!("Invalid pricing strategy"),
726 }
727
728 let mut primary_accum: IndexMap<
735 (CommodityID, RegionID, TimeSliceSelection),
736 IndexMap<AssetRef, WeightedAverageAccumulator<Activity>>,
737 > = IndexMap::new();
738 let mut backup_accum: IndexMap<
739 (CommodityID, RegionID, TimeSliceSelection),
740 IndexMap<AssetRef, WeightedAverageAccumulator<Activity>>,
741 > = IndexMap::new();
742
743 let mut annual_fixed_costs = HashMap::new();
745
746 for (asset, time_slice, activity) in activity_for_existing {
748 let region_id = asset.region_id();
749
750 let annual_activity = annual_activities.map(|activities| activities[asset]);
753 if annual_activity.is_some_and(|annual_activity| annual_activity < Activity::EPSILON) {
754 continue;
755 }
756
757 let activity_limit = *asset
759 .get_activity_limits_for_selection(&TimeSliceSelection::Single(time_slice.clone()))
760 .end();
761
762 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
764 market_prices,
765 year,
766 time_slice,
767 |cid: &CommodityID| markets_to_price.contains(&(cid.clone(), region_id.clone())),
768 ) {
769 let ts_selection = commodities[&commodity_id]
771 .time_slice_level
772 .containing_selection(time_slice);
773
774 let total_cost = match pricing_strategy {
776 PricingStrategy::FullCost => {
777 let annual_fixed_costs_per_flow =
778 annual_fixed_costs.entry(asset.clone()).or_insert_with(|| {
779 asset.get_annual_fixed_costs_per_flow(annual_activity.unwrap())
780 });
781 marginal_cost + *annual_fixed_costs_per_flow
782 }
783 PricingStrategy::MarginalCost => marginal_cost,
784 _ => unreachable!(),
785 };
786
787 let key = (
790 commodity_id.clone(),
791 region_id.clone(),
792 ts_selection.clone(),
793 );
794 primary_accum
795 .entry(key.clone())
796 .or_default()
797 .entry(asset.clone())
798 .or_default()
799 .add(total_cost, activity);
800 backup_accum
801 .entry(key)
802 .or_default()
803 .entry(asset.clone())
804 .or_default()
805 .add(total_cost, activity_limit);
806 }
807 }
808
809 primary_accum.into_iter().filter_map(move |(key, primary)| {
813 let price = primary
814 .into_values()
815 .filter_map(WeightedAverageAccumulator::finalise)
816 .reduce(|a, b| a.max(b))
817 .or_else(|| {
818 backup_accum
819 .swap_remove(&key)?
820 .into_values()
821 .filter_map(WeightedAverageAccumulator::finalise)
822 .reduce(|a, b| a.min(b))
823 })?;
824 Some((key, price))
825 })
826}
827
828fn iter_candidate_asset_min_prices<'a, I>(
854 activity_keys_for_candidates: I,
855 markets_to_price: &HashSet<(CommodityID, RegionID)>,
856 market_prices: &PriceMap,
857 priced_groups: &HashSet<(CommodityID, RegionID, TimeSliceSelection)>,
858 year: u32,
859 commodities: &CommodityMap,
860 pricing_strategy: &PricingStrategy,
861) -> impl Iterator<Item = ((CommodityID, RegionID, TimeSliceSelection), MoneyPerFlow)>
862where
863 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
864{
865 assert!(matches!(
867 pricing_strategy,
868 PricingStrategy::MarginalCost | PricingStrategy::FullCost
869 ));
870
871 let mut annual_fixed_costs = HashMap::new();
873
874 let mut annual_activity_limits = HashMap::new();
876
877 let mut cand_accum: IndexMap<
880 (CommodityID, RegionID, TimeSliceSelection),
881 IndexMap<AssetRef, WeightedAverageAccumulator<Activity>>,
882 > = IndexMap::new();
883
884 for (asset, time_slice) in activity_keys_for_candidates {
886 let region_id = asset.region_id();
887
888 let annual_activity_limit =
891 matches!(pricing_strategy, PricingStrategy::FullCost).then(|| {
892 *annual_activity_limits
893 .entry(asset.clone())
894 .or_insert_with(|| {
895 *asset
896 .get_activity_limits_for_selection(&TimeSliceSelection::Annual)
897 .end()
898 })
899 });
900 if annual_activity_limit.is_some_and(|limit| limit < Activity::EPSILON) {
901 continue;
902 }
903
904 let activity_limit = *asset
906 .get_activity_limits_for_selection(&TimeSliceSelection::Single(time_slice.clone()))
907 .end();
908
909 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
911 market_prices,
912 year,
913 time_slice,
914 |cid: &CommodityID| markets_to_price.contains(&(cid.clone(), region_id.clone())),
915 ) {
916 let ts_selection = commodities[&commodity_id]
918 .time_slice_level
919 .containing_selection(time_slice);
920
921 if priced_groups.contains(&(
923 commodity_id.clone(),
924 region_id.clone(),
925 ts_selection.clone(),
926 )) {
927 continue;
928 }
929
930 let total_cost = match pricing_strategy {
932 PricingStrategy::FullCost => {
933 let annual_fixed_costs_per_flow =
936 annual_fixed_costs.entry(asset.clone()).or_insert_with(|| {
937 asset.get_annual_fixed_costs_per_flow(annual_activity_limit.unwrap())
938 });
939 marginal_cost + *annual_fixed_costs_per_flow
940 }
941 PricingStrategy::MarginalCost => marginal_cost,
942 _ => unreachable!(),
943 };
944
945 cand_accum
947 .entry((commodity_id.clone(), region_id.clone(), ts_selection))
948 .or_default()
949 .entry(asset.clone())
950 .or_default()
951 .add(total_cost, activity_limit);
952 }
953 }
954
955 cand_accum.into_iter().filter_map(|(key, per_candidate)| {
957 per_candidate
958 .into_values()
959 .filter_map(WeightedAverageAccumulator::finalise)
960 .reduce(|current, value| current.min(value))
961 .map(|v| (key, v))
962 })
963}
964
965fn add_marginal_cost_average_prices<'a, I, J>(
974 activity_for_existing: I,
975 activity_keys_for_candidates: J,
976 market_prices: &mut PriceMap,
977 year: u32,
978 markets_to_price: &HashSet<(CommodityID, RegionID)>,
979 commodities: &CommodityMap,
980 time_slice_info: &TimeSliceInfo,
981) where
982 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
983 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
984{
985 let mut group_prices: IndexMap<_, _> = iter_existing_asset_average_prices(
987 activity_for_existing,
988 markets_to_price,
989 market_prices,
990 year,
991 commodities,
992 &PricingStrategy::MarginalCost,
993 None,
994 )
995 .collect();
996 let priced_groups: HashSet<_> = group_prices.keys().cloned().collect();
997
998 let cand_group_prices = iter_candidate_asset_min_prices(
1001 activity_keys_for_candidates,
1002 markets_to_price,
1003 market_prices,
1004 &priced_groups,
1005 year,
1006 commodities,
1007 &PricingStrategy::MarginalCost,
1008 );
1009
1010 group_prices.extend(cand_group_prices);
1012
1013 market_prices.extend_selection_prices(&group_prices, time_slice_info);
1015}
1016
1017fn iter_existing_asset_average_prices<'a, I>(
1040 activity_for_existing: I,
1041 markets_to_price: &HashSet<(CommodityID, RegionID)>,
1042 market_prices: &PriceMap,
1043 year: u32,
1044 commodities: &CommodityMap,
1045 pricing_strategy: &PricingStrategy,
1046 annual_activities: Option<&HashMap<AssetRef, Activity>>,
1047) -> impl Iterator<Item = ((CommodityID, RegionID, TimeSliceSelection), MoneyPerFlow)> + 'a
1048where
1049 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
1050{
1051 match pricing_strategy {
1053 PricingStrategy::MarginalCost => assert!(
1054 annual_activities.is_none(),
1055 "Cannot provide annual_activities with marginal pricing strategy"
1056 ),
1057 PricingStrategy::FullCost => assert!(
1058 annual_activities.is_some(),
1059 "annual_activities must be provided for full pricing strategy"
1060 ),
1061 _ => panic!("Invalid pricing strategy"),
1062 }
1063
1064 let mut primary_accum: IndexMap<
1071 (CommodityID, RegionID, TimeSliceSelection),
1072 WeightedAverageAccumulator<Flow>,
1073 > = IndexMap::new();
1074 let mut backup_accum: IndexMap<
1075 (CommodityID, RegionID, TimeSliceSelection),
1076 IndexMap<AssetRef, WeightedAverageAccumulator<Activity>>,
1077 > = IndexMap::new();
1078
1079 let mut annual_fixed_costs = HashMap::new();
1081
1082 for (asset, time_slice, activity) in activity_for_existing {
1084 let region_id = asset.region_id();
1085
1086 let annual_activity = annual_activities.map(|activities| activities[asset]);
1089 if annual_activity.is_some_and(|annual_activity| annual_activity < Activity::EPSILON) {
1090 continue;
1091 }
1092
1093 let activity_limit = *asset
1095 .get_activity_limits_for_selection(&TimeSliceSelection::Single(time_slice.clone()))
1096 .end();
1097
1098 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
1100 market_prices,
1101 year,
1102 time_slice,
1103 |cid: &CommodityID| markets_to_price.contains(&(cid.clone(), region_id.clone())),
1104 ) {
1105 let time_slice_selection = commodities[&commodity_id]
1107 .time_slice_level
1108 .containing_selection(time_slice);
1109
1110 let total_cost = match pricing_strategy {
1112 PricingStrategy::FullCost => {
1113 let annual_fixed_costs_per_flow =
1114 annual_fixed_costs.entry(asset.clone()).or_insert_with(|| {
1115 asset.get_annual_fixed_costs_per_flow(annual_activity.unwrap())
1116 });
1117 marginal_cost + *annual_fixed_costs_per_flow
1118 }
1119 PricingStrategy::MarginalCost => marginal_cost,
1120 _ => unreachable!(),
1121 };
1122
1123 let output_coeff = asset
1125 .get_flow(&commodity_id)
1126 .expect("Commodity should be an output flow for this asset")
1127 .coeff;
1128 let output_weight = activity * output_coeff;
1129
1130 let key = (
1133 commodity_id.clone(),
1134 region_id.clone(),
1135 time_slice_selection.clone(),
1136 );
1137 primary_accum
1138 .entry(key.clone())
1139 .or_default()
1140 .add(total_cost, output_weight);
1141 backup_accum
1142 .entry(key)
1143 .or_default()
1144 .entry(asset.clone())
1145 .or_default()
1146 .add(total_cost, activity_limit);
1147 }
1148 }
1149
1150 primary_accum.into_iter().filter_map(move |(key, primary)| {
1154 let price = primary.finalise().or_else(|| {
1155 backup_accum
1156 .swap_remove(&key)?
1157 .into_values()
1158 .filter_map(WeightedAverageAccumulator::finalise)
1159 .reduce(|a, b| a.min(b))
1160 })?;
1161 Some((key, price))
1162 })
1163}
1164
1165fn calculate_annual_activities<'a, I>(activities: I) -> HashMap<AssetRef, Activity>
1167where
1168 I: IntoIterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
1169{
1170 activities
1171 .into_iter()
1172 .map(|(asset, _ts, activity)| (asset.clone(), activity))
1173 .fold(HashMap::new(), |mut acc, (asset, activity)| {
1174 acc.entry(asset)
1175 .and_modify(|e| *e += activity)
1176 .or_insert(activity);
1177 acc
1178 })
1179}
1180
1181#[allow(clippy::too_many_arguments)]
1239fn add_full_cost_prices<'a, I, J>(
1240 activity_for_existing: I,
1241 activity_keys_for_candidates: J,
1242 annual_activities: &HashMap<AssetRef, Activity>,
1243 market_prices: &mut PriceMap,
1244 year: u32,
1245 markets_to_price: &HashSet<(CommodityID, RegionID)>,
1246 commodities: &CommodityMap,
1247 time_slice_info: &TimeSliceInfo,
1248) where
1249 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
1250 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
1251{
1252 let mut group_prices: IndexMap<_, _> = iter_existing_asset_max_prices(
1254 activity_for_existing,
1255 markets_to_price,
1256 market_prices,
1257 year,
1258 commodities,
1259 &PricingStrategy::FullCost,
1260 Some(annual_activities),
1261 )
1262 .collect();
1263 let priced_groups: HashSet<_> = group_prices.keys().cloned().collect();
1264
1265 let cand_group_prices = iter_candidate_asset_min_prices(
1268 activity_keys_for_candidates,
1269 markets_to_price,
1270 market_prices,
1271 &priced_groups,
1272 year,
1273 commodities,
1274 &PricingStrategy::FullCost,
1275 );
1276
1277 group_prices.extend(cand_group_prices);
1279
1280 market_prices.extend_selection_prices(&group_prices, time_slice_info);
1282}
1283
1284#[allow(clippy::too_many_arguments)]
1293fn add_full_cost_average_prices<'a, I, J>(
1294 activity_for_existing: I,
1295 activity_keys_for_candidates: J,
1296 annual_activities: &HashMap<AssetRef, Activity>,
1297 market_prices: &mut PriceMap,
1298 year: u32,
1299 markets_to_price: &HashSet<(CommodityID, RegionID)>,
1300 commodities: &CommodityMap,
1301 time_slice_info: &TimeSliceInfo,
1302) where
1303 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
1304 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
1305{
1306 let mut group_prices: IndexMap<_, _> = iter_existing_asset_average_prices(
1308 activity_for_existing,
1309 markets_to_price,
1310 market_prices,
1311 year,
1312 commodities,
1313 &PricingStrategy::FullCost,
1314 Some(annual_activities),
1315 )
1316 .collect();
1317 let priced_groups: HashSet<_> = group_prices.keys().cloned().collect();
1318
1319 let cand_group_prices = iter_candidate_asset_min_prices(
1321 activity_keys_for_candidates,
1322 markets_to_price,
1323 market_prices,
1324 &priced_groups,
1325 year,
1326 commodities,
1327 &PricingStrategy::FullCost,
1328 );
1329
1330 group_prices.extend(cand_group_prices);
1332
1333 market_prices.extend_selection_prices(&group_prices, time_slice_info);
1335}
1336
1337#[cfg(test)]
1338mod tests {
1339 use super::*;
1340 use crate::asset::Asset;
1341 use crate::asset::AssetRef;
1342 use crate::commodity::{Commodity, CommodityID, CommodityMap};
1343 use crate::fixture::{
1344 commodity_id, other_commodity, region_id, sed_commodity, time_slice, time_slice_info,
1345 };
1346 use crate::process::{ActivityLimits, FlowType, Process, ProcessFlow, ProcessParameter};
1347 use crate::region::RegionID;
1348 use crate::time_slice::TimeSliceID;
1349 use crate::units::ActivityPerCapacity;
1350 use crate::units::{
1351 Activity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity, MoneyPerCapacity,
1352 MoneyPerCapacityPerYear, MoneyPerFlow,
1353 };
1354 use float_cmp::assert_approx_eq;
1355 use indexmap::{IndexMap, IndexSet};
1356 use rstest::rstest;
1357 use std::collections::{HashMap, HashSet};
1358 use std::rc::Rc;
1359
1360 fn build_process_flow(commodity: &Commodity, coeff: f64, cost: MoneyPerFlow) -> ProcessFlow {
1361 ProcessFlow {
1362 commodity: Rc::new(commodity.clone()),
1363 coeff: FlowPerActivity(coeff),
1364 kind: FlowType::Fixed,
1365 cost,
1366 }
1367 }
1368
1369 #[allow(clippy::too_many_arguments)]
1370 fn build_process(
1371 flows: IndexMap<CommodityID, ProcessFlow>,
1372 region_id: &RegionID,
1373 year: u32,
1374 time_slice_info: &TimeSliceInfo,
1375 variable_operating_cost: MoneyPerActivity,
1376 fixed_operating_cost: MoneyPerCapacityPerYear,
1377 capital_cost: MoneyPerCapacity,
1378 lifetime: u32,
1379 discount_rate: Dimensionless,
1380 ) -> Process {
1381 let mut process_flows_map = HashMap::new();
1382 process_flows_map.insert((region_id.clone(), year), Rc::new(flows));
1383
1384 let mut process_parameter_map = HashMap::new();
1385 let proc_param = ProcessParameter {
1386 capital_cost,
1387 fixed_operating_cost,
1388 variable_operating_cost,
1389 lifetime,
1390 discount_rate,
1391 };
1392 process_parameter_map.insert((region_id.clone(), year), Rc::new(proc_param));
1393
1394 let mut activity_limits_map = HashMap::new();
1395 activity_limits_map.insert(
1396 (region_id.clone(), year),
1397 Rc::new(ActivityLimits::new_with_full_availability(time_slice_info)),
1398 );
1399
1400 let regions: IndexSet<RegionID> = IndexSet::from([region_id.clone()]);
1401
1402 Process {
1403 id: "p1".into(),
1404 description: "test process".into(),
1405 years: 2010..=2020,
1406 activity_limits: activity_limits_map,
1407 flows: process_flows_map,
1408 parameters: process_parameter_map,
1409 regions,
1410 primary_output: None,
1411 capacity_to_activity: ActivityPerCapacity(1.0),
1412 investment_constraints: HashMap::new(),
1413 unit_size: None,
1414 }
1415 }
1416
1417 fn assert_price_approx(
1418 prices: &PriceMap,
1419 commodity: &CommodityID,
1420 region: &RegionID,
1421 time_slice: &TimeSliceID,
1422 expected: MoneyPerFlow,
1423 ) {
1424 let p = prices.get(commodity, region, time_slice).unwrap();
1425 assert_approx_eq!(MoneyPerFlow, p, expected);
1426 }
1427
1428 #[rstest]
1429 #[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 within_tolerance_scenarios(
1440 #[case] price1: MoneyPerFlow,
1441 #[case] price2: MoneyPerFlow,
1442 #[case] tolerance: Dimensionless,
1443 #[case] expected: bool,
1444 time_slice_info: TimeSliceInfo,
1445 time_slice: TimeSliceID,
1446 ) {
1447 let mut prices1 = PriceMap::default();
1448 let mut prices2 = PriceMap::default();
1449
1450 let commodity = CommodityID::new("test_commodity");
1452 let region = RegionID::new("test_region");
1453 prices1.insert(&commodity, ®ion, &time_slice, price1);
1454 prices2.insert(&commodity, ®ion, &time_slice, price2);
1455
1456 assert_eq!(
1457 prices1.within_tolerance_weighted(&prices2, tolerance, &time_slice_info),
1458 expected
1459 );
1460 }
1461
1462 #[rstest]
1463 fn time_slice_weighted_averages(
1464 commodity_id: CommodityID,
1465 region_id: RegionID,
1466 time_slice_info: TimeSliceInfo,
1467 time_slice: TimeSliceID,
1468 ) {
1469 let mut prices = PriceMap::default();
1470
1471 prices.insert(&commodity_id, ®ion_id, &time_slice, MoneyPerFlow(100.0));
1473
1474 let averages = prices.time_slice_weighted_averages(&time_slice_info);
1475
1476 assert_eq!(averages[&(commodity_id, region_id)], MoneyPerFlow(100.0));
1478 }
1479
1480 #[rstest]
1481 fn marginal_cost_example(
1482 sed_commodity: Commodity,
1483 other_commodity: Commodity,
1484 region_id: RegionID,
1485 time_slice_info: TimeSliceInfo,
1486 time_slice: TimeSliceID,
1487 ) {
1488 let mut a = sed_commodity.clone();
1490 a.id = "A".into();
1491 let mut b = sed_commodity.clone();
1492 b.id = "B".into();
1493 let mut c = sed_commodity.clone();
1494 c.id = "C".into();
1495 let mut d = other_commodity.clone();
1496 d.id = "D".into();
1497
1498 let mut flows = IndexMap::new();
1499 flows.insert(
1500 a.id.clone(),
1501 build_process_flow(&a, -1.0, MoneyPerFlow(0.0)),
1502 );
1503 flows.insert(b.id.clone(), build_process_flow(&b, 1.0, MoneyPerFlow(0.0)));
1504 flows.insert(c.id.clone(), build_process_flow(&c, 2.0, MoneyPerFlow(3.0)));
1505 flows.insert(d.id.clone(), build_process_flow(&d, 1.0, MoneyPerFlow(4.0)));
1506
1507 let process = build_process(
1508 flows,
1509 ®ion_id,
1510 2015u32,
1511 &time_slice_info,
1512 MoneyPerActivity(5.0), MoneyPerCapacityPerYear(0.0), MoneyPerCapacity(0.0), 5, Dimensionless(1.0), );
1518
1519 let asset =
1520 Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(1.0), 2015u32)
1521 .unwrap();
1522 let asset_ref = AssetRef::from(asset);
1523 let mut prices =
1524 PriceMap::from_iter(vec![(&a.id, ®ion_id, &time_slice, MoneyPerFlow(1.0))]);
1525 let mut markets = HashSet::new();
1526 markets.insert((b.id.clone(), region_id.clone()));
1527 markets.insert((c.id.clone(), region_id.clone()));
1528
1529 let mut commodities = CommodityMap::new();
1530 commodities.insert(b.id.clone(), Rc::new(b.clone()));
1531 commodities.insert(c.id.clone(), Rc::new(c.clone()));
1532
1533 let existing = vec![(&asset_ref, &time_slice, Activity(1.0))];
1534 let candidates = Vec::new();
1535
1536 add_marginal_cost_prices(
1537 existing.into_iter(),
1538 candidates.into_iter(),
1539 &mut prices,
1540 2015u32,
1541 &markets,
1542 &commodities,
1543 &time_slice_info,
1544 );
1545
1546 assert_price_approx(
1547 &prices,
1548 &b.id,
1549 ®ion_id,
1550 &time_slice,
1551 MoneyPerFlow(10.0 / 3.0),
1552 );
1553 assert_price_approx(
1554 &prices,
1555 &c.id,
1556 ®ion_id,
1557 &time_slice,
1558 MoneyPerFlow(10.0 / 3.0 + 3.0),
1559 );
1560 }
1561
1562 #[rstest]
1563 fn full_cost_example(
1564 sed_commodity: Commodity,
1565 other_commodity: Commodity,
1566 region_id: RegionID,
1567 time_slice_info: TimeSliceInfo,
1568 time_slice: TimeSliceID,
1569 ) {
1570 let mut a = sed_commodity.clone();
1572 a.id = "A".into();
1573 let mut b = sed_commodity.clone();
1574 b.id = "B".into();
1575 let mut c = sed_commodity.clone();
1576 c.id = "C".into();
1577 let mut d = other_commodity.clone();
1578 d.id = "D".into();
1579
1580 let mut flows = IndexMap::new();
1581 flows.insert(
1582 a.id.clone(),
1583 build_process_flow(&a, -1.0, MoneyPerFlow(0.0)),
1584 );
1585 flows.insert(b.id.clone(), build_process_flow(&b, 1.0, MoneyPerFlow(0.0)));
1586 flows.insert(c.id.clone(), build_process_flow(&c, 2.0, MoneyPerFlow(3.0)));
1587 flows.insert(d.id.clone(), build_process_flow(&d, 1.0, MoneyPerFlow(4.0)));
1588
1589 let process = build_process(
1590 flows,
1591 ®ion_id,
1592 2015u32,
1593 &time_slice_info,
1594 MoneyPerActivity(5.0), MoneyPerCapacityPerYear(1.0), MoneyPerCapacity(1.5), 1, Dimensionless(0.0), );
1600
1601 let asset =
1602 Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(4.0), 2015u32)
1603 .unwrap();
1604 let asset_ref = AssetRef::from(asset);
1605 let mut prices =
1606 PriceMap::from_iter(vec![(&a.id, ®ion_id, &time_slice, MoneyPerFlow(1.0))]);
1607 let mut markets = HashSet::new();
1608 markets.insert((b.id.clone(), region_id.clone()));
1609 markets.insert((c.id.clone(), region_id.clone()));
1610
1611 let mut commodities = CommodityMap::new();
1612 commodities.insert(b.id.clone(), Rc::new(b.clone()));
1613 commodities.insert(c.id.clone(), Rc::new(c.clone()));
1614
1615 let existing = vec![(&asset_ref, &time_slice, Activity(2.0))];
1616 let candidates = Vec::new();
1617
1618 let mut annual_activities = HashMap::new();
1619 annual_activities.insert(asset_ref.clone(), Activity(2.0));
1620
1621 add_full_cost_prices(
1622 existing.into_iter(),
1623 candidates.into_iter(),
1624 &annual_activities,
1625 &mut prices,
1626 2015u32,
1627 &markets,
1628 &commodities,
1629 &time_slice_info,
1630 );
1631
1632 assert_price_approx(&prices, &b.id, ®ion_id, &time_slice, MoneyPerFlow(5.0));
1633 assert_price_approx(&prices, &c.id, ®ion_id, &time_slice, MoneyPerFlow(8.0));
1634 }
1635
1636 #[test]
1637 fn weighted_average_accumulator_single_value() {
1638 let mut accum = WeightedAverageAccumulator::<Dimensionless>::default();
1639 accum.add(MoneyPerFlow(100.0), Dimensionless(1.0));
1640 assert_eq!(accum.finalise(), Some(MoneyPerFlow(100.0)));
1641 }
1642
1643 #[test]
1644 fn weighted_average_accumulator_different_weights() {
1645 let mut accum = WeightedAverageAccumulator::<Dimensionless>::default();
1646 accum.add(MoneyPerFlow(100.0), Dimensionless(1.0));
1647 accum.add(MoneyPerFlow(200.0), Dimensionless(2.0));
1648 let result = accum.finalise().unwrap();
1650 assert_approx_eq!(MoneyPerFlow, result, MoneyPerFlow(500.0 / 3.0));
1651 }
1652
1653 #[test]
1654 fn weighted_average_accumulator_zero_weight() {
1655 let accum = WeightedAverageAccumulator::<Dimensionless>::default();
1656 assert_eq!(accum.finalise(), None);
1657 }
1658}