1use crate::asset::AssetRef;
3use crate::commodity::{CommodityID, CommodityMap, PricingStrategy};
4use crate::model::Model;
5use crate::region::RegionID;
6use crate::simulation::investment::InvestmentSet;
7use crate::simulation::optimisation::Solution;
8use crate::time_slice::{TimeSliceID, TimeSliceInfo, TimeSliceSelection};
9use crate::units::{Activity, Dimensionless, Flow, MoneyPerActivity, MoneyPerFlow, UnitType, Year};
10use anyhow::Result;
11use indexmap::IndexMap;
12use std::collections::{HashMap, HashSet};
13use std::marker::PhantomData;
14
15const N_CYCLE_ITERATIONS: i32 = 1;
17
18#[derive(Clone, Copy, Debug)]
20struct WeightedAverageAccumulator<W: UnitType> {
21 numerator: MoneyPerFlow,
23 denominator: Dimensionless,
25 _weight_type: PhantomData<W>,
27}
28
29impl<W: UnitType> Default for WeightedAverageAccumulator<W> {
30 fn default() -> Self {
31 Self {
32 numerator: MoneyPerFlow(0.0),
33 denominator: Dimensionless(0.0),
34 _weight_type: PhantomData,
35 }
36 }
37}
38
39impl<W: UnitType> WeightedAverageAccumulator<W> {
40 fn add(&mut self, value: MoneyPerFlow, weight: W) {
42 let weight = Dimensionless(weight.value());
43 self.numerator += value * weight;
44 self.denominator += weight;
45 }
46
47 fn finalise(self) -> Option<MoneyPerFlow> {
51 (self.denominator > Dimensionless::EPSILON).then(|| self.numerator / self.denominator)
52 }
53}
54
55#[derive(Clone, Copy, Debug)]
57struct WeightedAverageBackupAccumulator<W: UnitType> {
58 primary: WeightedAverageAccumulator<W>,
60 backup: WeightedAverageAccumulator<W>,
62}
63
64impl<W: UnitType> Default for WeightedAverageBackupAccumulator<W> {
65 fn default() -> Self {
66 Self {
67 primary: WeightedAverageAccumulator::<W>::default(),
68 backup: WeightedAverageAccumulator::<W>::default(),
69 }
70 }
71}
72
73impl<W: UnitType> WeightedAverageBackupAccumulator<W> {
74 fn add(&mut self, value: MoneyPerFlow, weight: W, backup_weight: W) {
76 self.primary.add(value, weight);
77 self.backup.add(value, backup_weight);
78 }
79
80 fn finalise(self) -> Option<MoneyPerFlow> {
84 self.primary.finalise().or_else(|| self.backup.finalise())
85 }
86}
87
88pub fn calculate_prices(model: &Model, solution: &Solution, year: u32) -> Result<CommodityPrices> {
104 let shadow_prices = CommodityPrices::from_iter(solution.iter_commodity_balance_duals());
106
107 let mut result = CommodityPrices::default();
109
110 let mut annual_activities: Option<HashMap<AssetRef, Activity>> = None;
112
113 for investment_set in model.investment_order[&year].iter().rev() {
115 price_investment_set(
116 investment_set,
117 model,
118 solution,
119 year,
120 &shadow_prices,
121 &mut annual_activities,
122 &mut result,
123 );
124 }
125
126 Ok(result)
127}
128
129fn price_investment_set(
131 investment_set: &InvestmentSet,
132 model: &Model,
133 solution: &Solution,
134 year: u32,
135 shadow_prices: &CommodityPrices,
136 annual_activities: &mut Option<HashMap<AssetRef, Activity>>,
137 result: &mut CommodityPrices,
138) {
139 match investment_set {
140 InvestmentSet::Single(market) => {
141 price_markets(
142 model,
143 solution,
144 year,
145 std::slice::from_ref(market),
146 shadow_prices,
147 annual_activities,
148 result,
149 );
150 }
151 InvestmentSet::Cycle(markets) => {
152 price_cycle(
153 model,
154 solution,
155 year,
156 markets,
157 shadow_prices,
158 annual_activities,
159 result,
160 );
161 }
162 InvestmentSet::Layer(investment_sets) => {
163 for set in investment_sets {
164 price_investment_set(
165 set,
166 model,
167 solution,
168 year,
169 shadow_prices,
170 annual_activities,
171 result,
172 );
173 }
174 }
175 }
176}
177
178fn price_markets(
196 model: &Model,
197 solution: &Solution,
198 year: u32,
199 markets: &[(CommodityID, RegionID)],
200 shadow_prices: &CommodityPrices,
201 annual_activities: &mut Option<HashMap<AssetRef, Activity>>,
202 result: &mut CommodityPrices,
203) {
204 let mut pricing_sets = HashMap::new();
207 for (commodity_id, region_id) in markets {
208 let commodity = &model.commodities[commodity_id];
209 if commodity.pricing_strategy == PricingStrategy::Unpriced {
210 continue;
211 }
212 pricing_sets
213 .entry(&commodity.pricing_strategy)
214 .or_insert_with(HashSet::new)
215 .insert((commodity_id.clone(), region_id.clone()));
216 }
217
218 if let Some(shadow_set) = pricing_sets.get(&PricingStrategy::Shadow) {
220 for (commodity_id, region_id) in shadow_set {
221 for time_slice in model.time_slice_info.iter_ids() {
222 if let Some(shadow_price) = shadow_prices.get(commodity_id, region_id, time_slice) {
223 result.insert(commodity_id, region_id, time_slice, shadow_price);
224 }
225 }
226 }
227 }
228
229 if let Some(scarcity_set) = pricing_sets.get(&PricingStrategy::ScarcityAdjusted) {
231 add_scarcity_adjusted_prices(
232 solution.iter_activity_duals(),
233 shadow_prices,
234 result,
235 scarcity_set,
236 );
237 }
238
239 if let Some(marginal_set) = pricing_sets.get(&PricingStrategy::MarginalCost) {
241 add_marginal_cost_prices(
242 solution.iter_activity_for_existing(),
243 solution.iter_activity_keys_for_candidates(),
244 result,
245 year,
246 marginal_set,
247 &model.commodities,
248 &model.time_slice_info,
249 );
250 }
251
252 if let Some(marginal_avg_set) = pricing_sets.get(&PricingStrategy::MarginalCostAverage) {
254 add_marginal_cost_average_prices(
255 solution.iter_activity_for_existing(),
256 solution.iter_activity_keys_for_candidates(),
257 result,
258 year,
259 marginal_avg_set,
260 &model.commodities,
261 &model.time_slice_info,
262 );
263 }
264
265 if let Some(fullcost_set) = pricing_sets.get(&PricingStrategy::FullCost) {
267 let annual_activities = annual_activities.get_or_insert_with(|| {
268 calculate_annual_activities(solution.iter_activity_for_existing())
269 });
270 add_full_cost_prices(
271 solution.iter_activity_for_existing(),
272 solution.iter_activity_keys_for_candidates(),
273 annual_activities,
274 result,
275 year,
276 fullcost_set,
277 &model.commodities,
278 &model.time_slice_info,
279 );
280 }
281
282 if let Some(full_avg_set) = pricing_sets.get(&PricingStrategy::FullCostAverage) {
284 let annual_activities = annual_activities.get_or_insert_with(|| {
285 calculate_annual_activities(solution.iter_activity_for_existing())
286 });
287 add_full_cost_average_prices(
288 solution.iter_activity_for_existing(),
289 solution.iter_activity_keys_for_candidates(),
290 annual_activities,
291 result,
292 year,
293 full_avg_set,
294 &model.commodities,
295 &model.time_slice_info,
296 );
297 }
298}
299
300fn price_cycle(
307 model: &Model,
308 solution: &Solution,
309 year: u32,
310 markets: &[(CommodityID, RegionID)],
311 shadow_prices: &CommodityPrices,
312 annual_activities: &mut Option<HashMap<AssetRef, Activity>>,
313 result: &mut CommodityPrices,
314) {
315 for (commodity_id, region_id) in markets {
317 for time_slice in model.time_slice_info.iter_ids() {
318 if let Some(shadow_price) = shadow_prices.get(commodity_id, region_id, time_slice) {
319 result.insert(commodity_id, region_id, time_slice, shadow_price);
320 }
321 }
322 }
323
324 for _ in 0..N_CYCLE_ITERATIONS {
326 for market in markets.iter().rev() {
328 price_markets(
329 model,
330 solution,
331 year,
332 std::slice::from_ref(market),
333 shadow_prices,
334 annual_activities,
335 result,
336 );
337 }
338 }
339}
340
341#[derive(Default, Clone)]
343pub struct CommodityPrices(IndexMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>);
344
345impl CommodityPrices {
346 pub fn insert(
348 &mut self,
349 commodity_id: &CommodityID,
350 region_id: &RegionID,
351 time_slice: &TimeSliceID,
352 price: MoneyPerFlow,
353 ) {
354 let key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
355 self.0.insert(key, price);
356 }
357
358 fn extend_selection_prices(
361 &mut self,
362 group_prices: &IndexMap<(CommodityID, RegionID, TimeSliceSelection), MoneyPerFlow>,
363 time_slice_info: &TimeSliceInfo,
364 ) {
365 for ((commodity_id, region_id, selection), &selection_price) in group_prices {
366 for (time_slice_id, _) in selection.iter(time_slice_info) {
367 self.insert(commodity_id, region_id, time_slice_id, selection_price);
368 }
369 }
370 }
371
372 pub fn iter(
378 &self,
379 ) -> impl Iterator<Item = (&CommodityID, &RegionID, &TimeSliceID, MoneyPerFlow)> {
380 self.0
381 .iter()
382 .map(|((commodity_id, region_id, ts), price)| (commodity_id, region_id, ts, *price))
383 }
384
385 pub fn get(
387 &self,
388 commodity_id: &CommodityID,
389 region_id: &RegionID,
390 time_slice: &TimeSliceID,
391 ) -> Option<MoneyPerFlow> {
392 self.0
393 .get(&(commodity_id.clone(), region_id.clone(), time_slice.clone()))
394 .copied()
395 }
396
397 pub fn keys(
399 &self,
400 ) -> indexmap::map::Keys<'_, (CommodityID, RegionID, TimeSliceID), MoneyPerFlow> {
401 self.0.keys()
402 }
403
404 fn time_slice_weighted_averages(
412 &self,
413 time_slice_info: &TimeSliceInfo,
414 ) -> HashMap<(CommodityID, RegionID), MoneyPerFlow> {
415 let mut weighted_prices = HashMap::new();
416
417 for ((commodity_id, region_id, time_slice_id), price) in &self.0 {
418 let weight = time_slice_info.time_slices[time_slice_id] / Year(1.0);
420 let key = (commodity_id.clone(), region_id.clone());
421 weighted_prices
422 .entry(key)
423 .and_modify(|v| *v += *price * weight)
424 .or_insert_with(|| *price * weight);
425 }
426
427 weighted_prices
428 }
429
430 pub fn within_tolerance_weighted(
440 &self,
441 other: &Self,
442 tolerance: Dimensionless,
443 time_slice_info: &TimeSliceInfo,
444 ) -> bool {
445 let self_averages = self.time_slice_weighted_averages(time_slice_info);
446 let other_averages = other.time_slice_weighted_averages(time_slice_info);
447
448 for (key, &price) in &self_averages {
449 let other_price = other_averages[key];
450 let abs_diff = (price - other_price).abs();
451
452 if price == MoneyPerFlow(0.0) {
454 if other_price != MoneyPerFlow(0.0) {
456 return false;
457 }
458 } else if abs_diff / price.abs() > tolerance {
460 return false;
461 }
462 }
463 true
464 }
465}
466
467impl<'a> FromIterator<(&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>
468 for CommodityPrices
469{
470 fn from_iter<I>(iter: I) -> Self
471 where
472 I: IntoIterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>,
473 {
474 let map = iter
475 .into_iter()
476 .map(|(commodity_id, region_id, time_slice, price)| {
477 (
478 (commodity_id.clone(), region_id.clone(), time_slice.clone()),
479 price,
480 )
481 })
482 .collect();
483 CommodityPrices(map)
484 }
485}
486
487impl IntoIterator for CommodityPrices {
488 type Item = ((CommodityID, RegionID, TimeSliceID), MoneyPerFlow);
489 type IntoIter = indexmap::map::IntoIter<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>;
490
491 fn into_iter(self) -> Self::IntoIter {
492 self.0.into_iter()
493 }
494}
495
496fn add_scarcity_adjusted_prices<'a, I>(
505 activity_duals: I,
506 shadow_prices: &CommodityPrices,
507 existing_prices: &mut CommodityPrices,
508 markets_to_price: &HashSet<(CommodityID, RegionID)>,
509) where
510 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
511{
512 let mut highest_duals = IndexMap::new();
514 for (asset, time_slice, dual) in activity_duals {
515 let region_id = asset.region_id();
516
517 for flow in asset.iter_output_flows().filter(|flow| {
520 markets_to_price.contains(&(flow.commodity.id.clone(), region_id.clone()))
521 }) {
522 highest_duals
524 .entry((
525 flow.commodity.id.clone(),
526 region_id.clone(),
527 time_slice.clone(),
528 ))
529 .and_modify(|current_dual| {
530 if dual > *current_dual {
531 *current_dual = dual;
532 }
533 })
534 .or_insert(dual);
535 }
536 }
537
538 for ((commodity, region, time_slice), highest_dual) in &highest_duals {
540 let shadow_price = shadow_prices.get(commodity, region, time_slice).unwrap();
543 let scarcity_price = shadow_price + MoneyPerFlow(highest_dual.value());
546 existing_prices.insert(commodity, region, time_slice, scarcity_price);
547 }
548}
549
550fn add_marginal_cost_prices<'a, I, J>(
601 activity_for_existing: I,
602 activity_keys_for_candidates: J,
603 existing_prices: &mut CommodityPrices,
604 year: u32,
605 markets_to_price: &HashSet<(CommodityID, RegionID)>,
606 commodities: &CommodityMap,
607 time_slice_info: &TimeSliceInfo,
608) where
609 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
610 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
611{
612 let mut group_prices: IndexMap<_, _> = iter_existing_asset_max_prices(
614 activity_for_existing,
615 markets_to_price,
616 existing_prices,
617 year,
618 commodities,
619 &PricingStrategy::MarginalCost,
620 None,
621 )
622 .collect();
623 let priced_groups: HashSet<_> = group_prices.keys().cloned().collect();
624
625 let cand_group_prices = iter_candidate_asset_min_prices(
628 activity_keys_for_candidates,
629 markets_to_price,
630 existing_prices,
631 &priced_groups,
632 year,
633 commodities,
634 &PricingStrategy::MarginalCost,
635 );
636
637 group_prices.extend(cand_group_prices);
639
640 existing_prices.extend_selection_prices(&group_prices, time_slice_info);
642}
643
644fn iter_existing_asset_max_prices<'a, I>(
667 activity_for_existing: I,
668 markets_to_price: &HashSet<(CommodityID, RegionID)>,
669 existing_prices: &CommodityPrices,
670 year: u32,
671 commodities: &CommodityMap,
672 pricing_strategy: &PricingStrategy,
673 annual_activities: Option<&HashMap<AssetRef, Activity>>,
674) -> impl Iterator<Item = ((CommodityID, RegionID, TimeSliceSelection), MoneyPerFlow)> + 'a
675where
676 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
677{
678 match pricing_strategy {
680 PricingStrategy::MarginalCost => assert!(
681 annual_activities.is_none(),
682 "Cannot provide annual_activities with marginal pricing strategy"
683 ),
684 PricingStrategy::FullCost => assert!(
685 annual_activities.is_some(),
686 "annual_activities must be provided for full pricing strategy"
687 ),
688 _ => panic!("Invalid pricing strategy"),
689 }
690
691 let mut existing_accum: IndexMap<
697 (CommodityID, RegionID, TimeSliceSelection),
698 IndexMap<AssetRef, WeightedAverageBackupAccumulator<Activity>>,
699 > = IndexMap::new();
700
701 let mut annual_fixed_costs = HashMap::new();
703
704 for (asset, time_slice, activity) in activity_for_existing {
706 let region_id = asset.region_id();
707
708 let annual_activity = annual_activities.map(|activities| activities[asset]);
711 if annual_activity.is_some_and(|annual_activity| annual_activity < Activity::EPSILON) {
712 continue;
713 }
714
715 let activity_limit = *asset
717 .get_activity_limits_for_selection(&TimeSliceSelection::Single(time_slice.clone()))
718 .end();
719
720 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
722 existing_prices,
723 year,
724 time_slice,
725 |cid: &CommodityID| markets_to_price.contains(&(cid.clone(), region_id.clone())),
726 ) {
727 let ts_selection = commodities[&commodity_id]
729 .time_slice_level
730 .containing_selection(time_slice);
731
732 let total_cost = match pricing_strategy {
734 PricingStrategy::FullCost => {
735 let annual_fixed_costs_per_flow =
736 annual_fixed_costs.entry(asset.clone()).or_insert_with(|| {
737 asset.get_annual_fixed_costs_per_flow(annual_activity.unwrap())
738 });
739 marginal_cost + *annual_fixed_costs_per_flow
740 }
741 PricingStrategy::MarginalCost => marginal_cost,
742 _ => unreachable!(),
743 };
744
745 existing_accum
748 .entry((commodity_id.clone(), region_id.clone(), ts_selection))
749 .or_default()
750 .entry(asset.clone())
751 .or_default()
752 .add(total_cost, activity, activity_limit);
753 }
754 }
755
756 existing_accum.into_iter().filter_map(|(key, per_asset)| {
758 per_asset
759 .into_values()
760 .filter_map(WeightedAverageBackupAccumulator::finalise)
761 .reduce(|current, value| current.max(value))
762 .map(|v| (key, v))
763 })
764}
765
766fn iter_candidate_asset_min_prices<'a, I>(
792 activity_keys_for_candidates: I,
793 markets_to_price: &HashSet<(CommodityID, RegionID)>,
794 existing_prices: &CommodityPrices,
795 priced_groups: &HashSet<(CommodityID, RegionID, TimeSliceSelection)>,
796 year: u32,
797 commodities: &CommodityMap,
798 pricing_strategy: &PricingStrategy,
799) -> impl Iterator<Item = ((CommodityID, RegionID, TimeSliceSelection), MoneyPerFlow)>
800where
801 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
802{
803 assert!(matches!(
805 pricing_strategy,
806 PricingStrategy::MarginalCost | PricingStrategy::FullCost
807 ));
808
809 let mut annual_fixed_costs = HashMap::new();
811
812 let mut annual_activity_limits = HashMap::new();
814
815 let mut cand_accum: IndexMap<
818 (CommodityID, RegionID, TimeSliceSelection),
819 IndexMap<AssetRef, WeightedAverageAccumulator<Activity>>,
820 > = IndexMap::new();
821
822 for (asset, time_slice) in activity_keys_for_candidates {
824 let region_id = asset.region_id();
825
826 let annual_activity_limit =
829 matches!(pricing_strategy, PricingStrategy::FullCost).then(|| {
830 *annual_activity_limits
831 .entry(asset.clone())
832 .or_insert_with(|| {
833 *asset
834 .get_activity_limits_for_selection(&TimeSliceSelection::Annual)
835 .end()
836 })
837 });
838 if annual_activity_limit.is_some_and(|limit| limit < Activity::EPSILON) {
839 continue;
840 }
841
842 let activity_limit = *asset
844 .get_activity_limits_for_selection(&TimeSliceSelection::Single(time_slice.clone()))
845 .end();
846
847 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
849 existing_prices,
850 year,
851 time_slice,
852 |cid: &CommodityID| markets_to_price.contains(&(cid.clone(), region_id.clone())),
853 ) {
854 let ts_selection = commodities[&commodity_id]
856 .time_slice_level
857 .containing_selection(time_slice);
858
859 if priced_groups.contains(&(
861 commodity_id.clone(),
862 region_id.clone(),
863 ts_selection.clone(),
864 )) {
865 continue;
866 }
867
868 let total_cost = match pricing_strategy {
870 PricingStrategy::FullCost => {
871 let annual_fixed_costs_per_flow =
874 annual_fixed_costs.entry(asset.clone()).or_insert_with(|| {
875 asset.get_annual_fixed_costs_per_flow(annual_activity_limit.unwrap())
876 });
877 marginal_cost + *annual_fixed_costs_per_flow
878 }
879 PricingStrategy::MarginalCost => marginal_cost,
880 _ => unreachable!(),
881 };
882
883 cand_accum
885 .entry((commodity_id.clone(), region_id.clone(), ts_selection))
886 .or_default()
887 .entry(asset.clone())
888 .or_default()
889 .add(total_cost, activity_limit);
890 }
891 }
892
893 cand_accum.into_iter().filter_map(|(key, per_candidate)| {
895 per_candidate
896 .into_values()
897 .filter_map(WeightedAverageAccumulator::finalise)
898 .reduce(|current, value| current.min(value))
899 .map(|v| (key, v))
900 })
901}
902
903fn add_marginal_cost_average_prices<'a, I, J>(
912 activity_for_existing: I,
913 activity_keys_for_candidates: J,
914 existing_prices: &mut CommodityPrices,
915 year: u32,
916 markets_to_price: &HashSet<(CommodityID, RegionID)>,
917 commodities: &CommodityMap,
918 time_slice_info: &TimeSliceInfo,
919) where
920 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
921 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
922{
923 let mut group_prices: IndexMap<_, _> = iter_existing_asset_average_prices(
925 activity_for_existing,
926 markets_to_price,
927 existing_prices,
928 year,
929 commodities,
930 &PricingStrategy::MarginalCost,
931 None,
932 )
933 .collect();
934 let priced_groups: HashSet<_> = group_prices.keys().cloned().collect();
935
936 let cand_group_prices = iter_candidate_asset_min_prices(
939 activity_keys_for_candidates,
940 markets_to_price,
941 existing_prices,
942 &priced_groups,
943 year,
944 commodities,
945 &PricingStrategy::MarginalCost,
946 );
947
948 group_prices.extend(cand_group_prices);
950
951 existing_prices.extend_selection_prices(&group_prices, time_slice_info);
953}
954
955fn iter_existing_asset_average_prices<'a, I>(
978 activity_for_existing: I,
979 markets_to_price: &HashSet<(CommodityID, RegionID)>,
980 existing_prices: &CommodityPrices,
981 year: u32,
982 commodities: &CommodityMap,
983 pricing_strategy: &PricingStrategy,
984 annual_activities: Option<&HashMap<AssetRef, Activity>>,
985) -> impl Iterator<Item = ((CommodityID, RegionID, TimeSliceSelection), MoneyPerFlow)> + 'a
986where
987 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
988{
989 match pricing_strategy {
991 PricingStrategy::MarginalCost => assert!(
992 annual_activities.is_none(),
993 "Cannot provide annual_activities with marginal pricing strategy"
994 ),
995 PricingStrategy::FullCost => assert!(
996 annual_activities.is_some(),
997 "annual_activities must be provided for full pricing strategy"
998 ),
999 _ => panic!("Invalid pricing strategy"),
1000 }
1001
1002 let mut existing_accum: IndexMap<
1008 (CommodityID, RegionID, TimeSliceSelection),
1009 WeightedAverageBackupAccumulator<Flow>,
1010 > = IndexMap::new();
1011
1012 let mut annual_fixed_costs = HashMap::new();
1014
1015 for (asset, time_slice, activity) in activity_for_existing {
1017 let region_id = asset.region_id();
1018
1019 let annual_activity = annual_activities.map(|activities| activities[asset]);
1022 if annual_activity.is_some_and(|annual_activity| annual_activity < Activity::EPSILON) {
1023 continue;
1024 }
1025
1026 let activity_limit = *asset
1028 .get_activity_limits_for_selection(&TimeSliceSelection::Single(time_slice.clone()))
1029 .end();
1030
1031 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
1033 existing_prices,
1034 year,
1035 time_slice,
1036 |cid: &CommodityID| markets_to_price.contains(&(cid.clone(), region_id.clone())),
1037 ) {
1038 let time_slice_selection = commodities[&commodity_id]
1040 .time_slice_level
1041 .containing_selection(time_slice);
1042
1043 let total_cost = match pricing_strategy {
1045 PricingStrategy::FullCost => {
1046 let annual_fixed_costs_per_flow =
1047 annual_fixed_costs.entry(asset.clone()).or_insert_with(|| {
1048 asset.get_annual_fixed_costs_per_flow(annual_activity.unwrap())
1049 });
1050 marginal_cost + *annual_fixed_costs_per_flow
1051 }
1052 PricingStrategy::MarginalCost => marginal_cost,
1053 _ => unreachable!(),
1054 };
1055
1056 let output_coeff = asset
1058 .get_flow(&commodity_id)
1059 .expect("Commodity should be an output flow for this asset")
1060 .coeff;
1061 let output_weight = activity * output_coeff;
1062 let backup_output_weight = activity_limit * output_coeff;
1063
1064 existing_accum
1067 .entry((
1068 commodity_id.clone(),
1069 region_id.clone(),
1070 time_slice_selection,
1071 ))
1072 .or_default()
1073 .add(total_cost, output_weight, backup_output_weight);
1074 }
1075 }
1076
1077 existing_accum
1079 .into_iter()
1080 .filter_map(|(key, accum)| accum.finalise().map(|v| (key, v)))
1081}
1082
1083fn calculate_annual_activities<'a, I>(activities: I) -> HashMap<AssetRef, Activity>
1085where
1086 I: IntoIterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
1087{
1088 activities
1089 .into_iter()
1090 .map(|(asset, _ts, activity)| (asset.clone(), activity))
1091 .fold(HashMap::new(), |mut acc, (asset, activity)| {
1092 acc.entry(asset)
1093 .and_modify(|e| *e += activity)
1094 .or_insert(activity);
1095 acc
1096 })
1097}
1098
1099#[allow(clippy::too_many_arguments)]
1157fn add_full_cost_prices<'a, I, J>(
1158 activity_for_existing: I,
1159 activity_keys_for_candidates: J,
1160 annual_activities: &HashMap<AssetRef, Activity>,
1161 existing_prices: &mut CommodityPrices,
1162 year: u32,
1163 markets_to_price: &HashSet<(CommodityID, RegionID)>,
1164 commodities: &CommodityMap,
1165 time_slice_info: &TimeSliceInfo,
1166) where
1167 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
1168 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
1169{
1170 let mut group_prices: IndexMap<_, _> = iter_existing_asset_max_prices(
1172 activity_for_existing,
1173 markets_to_price,
1174 existing_prices,
1175 year,
1176 commodities,
1177 &PricingStrategy::FullCost,
1178 Some(annual_activities),
1179 )
1180 .collect();
1181 let priced_groups: HashSet<_> = group_prices.keys().cloned().collect();
1182
1183 let cand_group_prices = iter_candidate_asset_min_prices(
1186 activity_keys_for_candidates,
1187 markets_to_price,
1188 existing_prices,
1189 &priced_groups,
1190 year,
1191 commodities,
1192 &PricingStrategy::FullCost,
1193 );
1194
1195 group_prices.extend(cand_group_prices);
1197
1198 existing_prices.extend_selection_prices(&group_prices, time_slice_info);
1200}
1201
1202#[allow(clippy::too_many_arguments)]
1211fn add_full_cost_average_prices<'a, I, J>(
1212 activity_for_existing: I,
1213 activity_keys_for_candidates: J,
1214 annual_activities: &HashMap<AssetRef, Activity>,
1215 existing_prices: &mut CommodityPrices,
1216 year: u32,
1217 markets_to_price: &HashSet<(CommodityID, RegionID)>,
1218 commodities: &CommodityMap,
1219 time_slice_info: &TimeSliceInfo,
1220) where
1221 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
1222 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
1223{
1224 let mut group_prices: IndexMap<_, _> = iter_existing_asset_average_prices(
1226 activity_for_existing,
1227 markets_to_price,
1228 existing_prices,
1229 year,
1230 commodities,
1231 &PricingStrategy::FullCost,
1232 Some(annual_activities),
1233 )
1234 .collect();
1235 let priced_groups: HashSet<_> = group_prices.keys().cloned().collect();
1236
1237 let cand_group_prices = iter_candidate_asset_min_prices(
1239 activity_keys_for_candidates,
1240 markets_to_price,
1241 existing_prices,
1242 &priced_groups,
1243 year,
1244 commodities,
1245 &PricingStrategy::FullCost,
1246 );
1247
1248 group_prices.extend(cand_group_prices);
1250
1251 existing_prices.extend_selection_prices(&group_prices, time_slice_info);
1253}
1254
1255#[cfg(test)]
1256mod tests {
1257 use super::*;
1258 use crate::asset::Asset;
1259 use crate::asset::AssetRef;
1260 use crate::commodity::{Commodity, CommodityID, CommodityMap};
1261 use crate::fixture::{
1262 commodity_id, other_commodity, region_id, sed_commodity, time_slice, time_slice_info,
1263 };
1264 use crate::process::{ActivityLimits, FlowType, Process, ProcessFlow, ProcessParameter};
1265 use crate::region::RegionID;
1266 use crate::time_slice::TimeSliceID;
1267 use crate::units::ActivityPerCapacity;
1268 use crate::units::{
1269 Activity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity, MoneyPerCapacity,
1270 MoneyPerCapacityPerYear, MoneyPerFlow,
1271 };
1272 use float_cmp::assert_approx_eq;
1273 use indexmap::{IndexMap, IndexSet};
1274 use rstest::rstest;
1275 use std::collections::{HashMap, HashSet};
1276 use std::rc::Rc;
1277
1278 fn build_process_flow(commodity: &Commodity, coeff: f64, cost: MoneyPerFlow) -> ProcessFlow {
1279 ProcessFlow {
1280 commodity: Rc::new(commodity.clone()),
1281 coeff: FlowPerActivity(coeff),
1282 kind: FlowType::Fixed,
1283 cost,
1284 }
1285 }
1286
1287 #[allow(clippy::too_many_arguments)]
1288 fn build_process(
1289 flows: IndexMap<CommodityID, ProcessFlow>,
1290 region_id: &RegionID,
1291 year: u32,
1292 time_slice_info: &TimeSliceInfo,
1293 variable_operating_cost: MoneyPerActivity,
1294 fixed_operating_cost: MoneyPerCapacityPerYear,
1295 capital_cost: MoneyPerCapacity,
1296 lifetime: u32,
1297 discount_rate: Dimensionless,
1298 ) -> Process {
1299 let mut process_flows_map = HashMap::new();
1300 process_flows_map.insert((region_id.clone(), year), Rc::new(flows));
1301
1302 let mut process_parameter_map = HashMap::new();
1303 let proc_param = ProcessParameter {
1304 capital_cost,
1305 fixed_operating_cost,
1306 variable_operating_cost,
1307 lifetime,
1308 discount_rate,
1309 };
1310 process_parameter_map.insert((region_id.clone(), year), Rc::new(proc_param));
1311
1312 let mut activity_limits_map = HashMap::new();
1313 activity_limits_map.insert(
1314 (region_id.clone(), year),
1315 Rc::new(ActivityLimits::new_with_full_availability(time_slice_info)),
1316 );
1317
1318 let regions: IndexSet<RegionID> = IndexSet::from([region_id.clone()]);
1319
1320 Process {
1321 id: "p1".into(),
1322 description: "test process".into(),
1323 years: 2010..=2020,
1324 activity_limits: activity_limits_map,
1325 flows: process_flows_map,
1326 parameters: process_parameter_map,
1327 regions,
1328 primary_output: None,
1329 capacity_to_activity: ActivityPerCapacity(1.0),
1330 investment_constraints: HashMap::new(),
1331 unit_size: None,
1332 }
1333 }
1334
1335 fn assert_price_approx(
1336 prices: &CommodityPrices,
1337 commodity: &CommodityID,
1338 region: &RegionID,
1339 time_slice: &TimeSliceID,
1340 expected: MoneyPerFlow,
1341 ) {
1342 let p = prices.get(commodity, region, time_slice).unwrap();
1343 assert_approx_eq!(MoneyPerFlow, p, expected);
1344 }
1345
1346 #[rstest]
1347 #[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(
1358 #[case] price1: MoneyPerFlow,
1359 #[case] price2: MoneyPerFlow,
1360 #[case] tolerance: Dimensionless,
1361 #[case] expected: bool,
1362 time_slice_info: TimeSliceInfo,
1363 time_slice: TimeSliceID,
1364 ) {
1365 let mut prices1 = CommodityPrices::default();
1366 let mut prices2 = CommodityPrices::default();
1367
1368 let commodity = CommodityID::new("test_commodity");
1370 let region = RegionID::new("test_region");
1371 prices1.insert(&commodity, ®ion, &time_slice, price1);
1372 prices2.insert(&commodity, ®ion, &time_slice, price2);
1373
1374 assert_eq!(
1375 prices1.within_tolerance_weighted(&prices2, tolerance, &time_slice_info),
1376 expected
1377 );
1378 }
1379
1380 #[rstest]
1381 fn time_slice_weighted_averages(
1382 commodity_id: CommodityID,
1383 region_id: RegionID,
1384 time_slice_info: TimeSliceInfo,
1385 time_slice: TimeSliceID,
1386 ) {
1387 let mut prices = CommodityPrices::default();
1388
1389 prices.insert(&commodity_id, ®ion_id, &time_slice, MoneyPerFlow(100.0));
1391
1392 let averages = prices.time_slice_weighted_averages(&time_slice_info);
1393
1394 assert_eq!(averages[&(commodity_id, region_id)], MoneyPerFlow(100.0));
1396 }
1397
1398 #[rstest]
1399 fn marginal_cost_example(
1400 sed_commodity: Commodity,
1401 other_commodity: Commodity,
1402 region_id: RegionID,
1403 time_slice_info: TimeSliceInfo,
1404 time_slice: TimeSliceID,
1405 ) {
1406 let mut a = sed_commodity.clone();
1408 a.id = "A".into();
1409 let mut b = sed_commodity.clone();
1410 b.id = "B".into();
1411 let mut c = sed_commodity.clone();
1412 c.id = "C".into();
1413 let mut d = other_commodity.clone();
1414 d.id = "D".into();
1415
1416 let mut flows = IndexMap::new();
1417 flows.insert(
1418 a.id.clone(),
1419 build_process_flow(&a, -1.0, MoneyPerFlow(0.0)),
1420 );
1421 flows.insert(b.id.clone(), build_process_flow(&b, 1.0, MoneyPerFlow(0.0)));
1422 flows.insert(c.id.clone(), build_process_flow(&c, 2.0, MoneyPerFlow(3.0)));
1423 flows.insert(d.id.clone(), build_process_flow(&d, 1.0, MoneyPerFlow(4.0)));
1424
1425 let process = build_process(
1426 flows,
1427 ®ion_id,
1428 2015u32,
1429 &time_slice_info,
1430 MoneyPerActivity(5.0), MoneyPerCapacityPerYear(0.0), MoneyPerCapacity(0.0), 5, Dimensionless(1.0), );
1436
1437 let asset =
1438 Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(1.0), 2015u32)
1439 .unwrap();
1440 let asset_ref = AssetRef::from(asset);
1441 let existing_prices =
1442 CommodityPrices::from_iter(vec![(&a.id, ®ion_id, &time_slice, MoneyPerFlow(1.0))]);
1443 let mut markets = HashSet::new();
1444 markets.insert((b.id.clone(), region_id.clone()));
1445 markets.insert((c.id.clone(), region_id.clone()));
1446
1447 let mut commodities = CommodityMap::new();
1448 commodities.insert(b.id.clone(), Rc::new(b.clone()));
1449 commodities.insert(c.id.clone(), Rc::new(c.clone()));
1450
1451 let existing = vec![(&asset_ref, &time_slice, Activity(1.0))];
1452 let candidates = Vec::new();
1453
1454 let mut prices = existing_prices.clone();
1455 add_marginal_cost_prices(
1456 existing.into_iter(),
1457 candidates.into_iter(),
1458 &mut prices,
1459 2015u32,
1460 &markets,
1461 &commodities,
1462 &time_slice_info,
1463 );
1464
1465 assert_price_approx(
1466 &prices,
1467 &b.id,
1468 ®ion_id,
1469 &time_slice,
1470 MoneyPerFlow(10.0 / 3.0),
1471 );
1472 assert_price_approx(
1473 &prices,
1474 &c.id,
1475 ®ion_id,
1476 &time_slice,
1477 MoneyPerFlow(10.0 / 3.0 + 3.0),
1478 );
1479 }
1480
1481 #[rstest]
1482 fn full_cost_example(
1483 sed_commodity: Commodity,
1484 other_commodity: Commodity,
1485 region_id: RegionID,
1486 time_slice_info: TimeSliceInfo,
1487 time_slice: TimeSliceID,
1488 ) {
1489 let mut a = sed_commodity.clone();
1491 a.id = "A".into();
1492 let mut b = sed_commodity.clone();
1493 b.id = "B".into();
1494 let mut c = sed_commodity.clone();
1495 c.id = "C".into();
1496 let mut d = other_commodity.clone();
1497 d.id = "D".into();
1498
1499 let mut flows = IndexMap::new();
1500 flows.insert(
1501 a.id.clone(),
1502 build_process_flow(&a, -1.0, MoneyPerFlow(0.0)),
1503 );
1504 flows.insert(b.id.clone(), build_process_flow(&b, 1.0, MoneyPerFlow(0.0)));
1505 flows.insert(c.id.clone(), build_process_flow(&c, 2.0, MoneyPerFlow(3.0)));
1506 flows.insert(d.id.clone(), build_process_flow(&d, 1.0, MoneyPerFlow(4.0)));
1507
1508 let process = build_process(
1509 flows,
1510 ®ion_id,
1511 2015u32,
1512 &time_slice_info,
1513 MoneyPerActivity(5.0), MoneyPerCapacityPerYear(1.0), MoneyPerCapacity(1.5), 1, Dimensionless(0.0), );
1519
1520 let asset =
1521 Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(4.0), 2015u32)
1522 .unwrap();
1523 let asset_ref = AssetRef::from(asset);
1524 let existing_prices =
1525 CommodityPrices::from_iter(vec![(&a.id, ®ion_id, &time_slice, MoneyPerFlow(1.0))]);
1526 let mut markets = HashSet::new();
1527 markets.insert((b.id.clone(), region_id.clone()));
1528 markets.insert((c.id.clone(), region_id.clone()));
1529
1530 let mut commodities = CommodityMap::new();
1531 commodities.insert(b.id.clone(), Rc::new(b.clone()));
1532 commodities.insert(c.id.clone(), Rc::new(c.clone()));
1533
1534 let existing = vec![(&asset_ref, &time_slice, Activity(2.0))];
1535 let candidates = Vec::new();
1536
1537 let mut annual_activities = HashMap::new();
1538 annual_activities.insert(asset_ref.clone(), Activity(2.0));
1539
1540 let mut prices = existing_prices.clone();
1541 add_full_cost_prices(
1542 existing.into_iter(),
1543 candidates.into_iter(),
1544 &annual_activities,
1545 &mut prices,
1546 2015u32,
1547 &markets,
1548 &commodities,
1549 &time_slice_info,
1550 );
1551
1552 assert_price_approx(&prices, &b.id, ®ion_id, &time_slice, MoneyPerFlow(5.0));
1553 assert_price_approx(&prices, &c.id, ®ion_id, &time_slice, MoneyPerFlow(8.0));
1554 }
1555
1556 #[test]
1557 fn weighted_average_accumulator_single_value() {
1558 let mut accum = WeightedAverageAccumulator::<Dimensionless>::default();
1559 accum.add(MoneyPerFlow(100.0), Dimensionless(1.0));
1560 assert_eq!(accum.finalise(), Some(MoneyPerFlow(100.0)));
1561 }
1562
1563 #[test]
1564 fn weighted_average_accumulator_different_weights() {
1565 let mut accum = WeightedAverageAccumulator::<Dimensionless>::default();
1566 accum.add(MoneyPerFlow(100.0), Dimensionless(1.0));
1567 accum.add(MoneyPerFlow(200.0), Dimensionless(2.0));
1568 let result = accum.finalise().unwrap();
1570 assert_approx_eq!(MoneyPerFlow, result, MoneyPerFlow(500.0 / 3.0));
1571 }
1572
1573 #[test]
1574 fn weighted_average_accumulator_zero_weight() {
1575 let accum = WeightedAverageAccumulator::<Dimensionless>::default();
1576 assert_eq!(accum.finalise(), None);
1577 }
1578
1579 #[test]
1580 fn weighted_average_backup_accumulator_primary_preferred() {
1581 let mut accum = WeightedAverageBackupAccumulator::<Dimensionless>::default();
1582 accum.add(MoneyPerFlow(100.0), Dimensionless(3.0), Dimensionless(1.0));
1583 accum.add(MoneyPerFlow(200.0), Dimensionless(1.0), Dimensionless(1.0));
1584 assert_eq!(accum.finalise(), Some(MoneyPerFlow(125.0)));
1587 }
1588
1589 #[test]
1590 fn weighted_average_backup_accumulator_fallback() {
1591 let mut accum = WeightedAverageBackupAccumulator::<Dimensionless>::default();
1592 accum.add(MoneyPerFlow(100.0), Dimensionless(0.0), Dimensionless(2.0));
1593 accum.add(MoneyPerFlow(200.0), Dimensionless(0.0), Dimensionless(2.0));
1594 assert_eq!(accum.finalise(), Some(MoneyPerFlow(150.0)));
1596 }
1597
1598 #[test]
1599 fn weighted_average_backup_accumulator_both_zero() {
1600 let accum = WeightedAverageBackupAccumulator::<Dimensionless>::default();
1601 assert_eq!(accum.finalise(), None);
1602 }
1603}