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