1use crate::asset::AssetRef;
3use crate::commodity::{CommodityID, CommodityMap, PricingStrategy};
4use crate::input::try_insert;
5use crate::model::Model;
6use crate::region::RegionID;
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
15#[derive(Clone, Copy, Debug)]
17struct WeightedAverageAccumulator<W: UnitType> {
18 numerator: MoneyPerFlow,
20 denominator: Dimensionless,
22 _weight_type: PhantomData<W>,
24}
25
26impl<W: UnitType> Default for WeightedAverageAccumulator<W> {
27 fn default() -> Self {
28 Self {
29 numerator: MoneyPerFlow(0.0),
30 denominator: Dimensionless(0.0),
31 _weight_type: PhantomData,
32 }
33 }
34}
35
36impl<W: UnitType> WeightedAverageAccumulator<W> {
37 fn add(&mut self, value: MoneyPerFlow, weight: W) {
39 let weight = Dimensionless(weight.value());
40 self.numerator += value * weight;
41 self.denominator += weight;
42 }
43
44 fn finalise(self) -> Option<MoneyPerFlow> {
48 (self.denominator > Dimensionless::EPSILON).then(|| self.numerator / self.denominator)
49 }
50}
51
52#[derive(Clone, Copy, Debug)]
54struct WeightedAverageBackupAccumulator<W: UnitType> {
55 primary: WeightedAverageAccumulator<W>,
57 backup: WeightedAverageAccumulator<W>,
59}
60
61impl<W: UnitType> Default for WeightedAverageBackupAccumulator<W> {
62 fn default() -> Self {
63 Self {
64 primary: WeightedAverageAccumulator::<W>::default(),
65 backup: WeightedAverageAccumulator::<W>::default(),
66 }
67 }
68}
69
70impl<W: UnitType> WeightedAverageBackupAccumulator<W> {
71 fn add(&mut self, value: MoneyPerFlow, weight: W, backup_weight: W) {
73 self.primary.add(value, weight);
74 self.backup.add(value, backup_weight);
75 }
76
77 fn finalise(self) -> Option<MoneyPerFlow> {
81 self.primary.finalise().or_else(|| self.backup.finalise())
82 }
83}
84
85pub fn calculate_prices(model: &Model, solution: &Solution, year: u32) -> Result<CommodityPrices> {
101 let shadow_prices = CommodityPrices::from_iter(solution.iter_commodity_balance_duals());
103
104 let mut result = CommodityPrices::default();
106
107 let mut annual_activities: Option<HashMap<AssetRef, Activity>> = None;
109
110 let investment_order = &model.investment_order[&year];
112
113 for investment_set in investment_order.iter().rev() {
117 let mut pricing_sets = HashMap::new();
120 for (commodity_id, region_id) in investment_set.iter_markets() {
121 let commodity = &model.commodities[commodity_id];
122 if commodity.pricing_strategy == PricingStrategy::Unpriced {
123 continue;
124 }
125 pricing_sets
126 .entry(&commodity.pricing_strategy)
127 .or_insert_with(HashSet::new)
128 .insert((commodity_id.clone(), region_id.clone()));
129 }
130
131 if let Some(shadow_set) = pricing_sets.get(&PricingStrategy::Shadow) {
133 for (commodity_id, region_id, time_slice) in shadow_prices.keys() {
134 if shadow_set.contains(&(commodity_id.clone(), region_id.clone())) {
135 let price = shadow_prices
136 .get(commodity_id, region_id, time_slice)
137 .unwrap();
138 result.insert(commodity_id, region_id, time_slice, price);
139 }
140 }
141 }
142
143 if let Some(scarcity_set) = pricing_sets.get(&PricingStrategy::ScarcityAdjusted) {
145 add_scarcity_adjusted_prices(
146 solution.iter_activity_duals(),
147 &shadow_prices,
148 &mut result,
149 scarcity_set,
150 );
151 }
152
153 if let Some(marginal_set) = pricing_sets.get(&PricingStrategy::MarginalCost) {
155 add_marginal_cost_prices(
156 solution.iter_activity_for_existing(),
157 solution.iter_activity_keys_for_candidates(),
158 &mut result,
159 year,
160 marginal_set,
161 &model.commodities,
162 &model.time_slice_info,
163 );
164 }
165
166 if let Some(marginal_avg_set) = pricing_sets.get(&PricingStrategy::MarginalCostAverage) {
168 add_marginal_cost_average_prices(
169 solution.iter_activity_for_existing(),
170 solution.iter_activity_keys_for_candidates(),
171 &mut result,
172 year,
173 marginal_avg_set,
174 &model.commodities,
175 &model.time_slice_info,
176 );
177 }
178
179 if let Some(fullcost_set) = pricing_sets.get(&PricingStrategy::FullCost) {
181 let annual_activities = annual_activities.get_or_insert_with(|| {
182 calculate_annual_activities(solution.iter_activity_for_existing())
183 });
184 add_full_cost_prices(
185 solution.iter_activity_for_existing(),
186 solution.iter_activity_keys_for_candidates(),
187 annual_activities,
188 &mut result,
189 year,
190 fullcost_set,
191 &model.commodities,
192 &model.time_slice_info,
193 );
194 }
195
196 if let Some(full_avg_set) = pricing_sets.get(&PricingStrategy::FullCostAverage) {
198 let annual_activities = annual_activities.get_or_insert_with(|| {
199 calculate_annual_activities(solution.iter_activity_for_existing())
200 });
201 add_full_cost_average_prices(
202 solution.iter_activity_for_existing(),
203 solution.iter_activity_keys_for_candidates(),
204 annual_activities,
205 &mut result,
206 year,
207 full_avg_set,
208 &model.commodities,
209 &model.time_slice_info,
210 );
211 }
212 }
213
214 Ok(result)
216}
217
218#[derive(Default, Clone)]
220pub struct CommodityPrices(IndexMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>);
221
222impl CommodityPrices {
223 pub fn insert(
227 &mut self,
228 commodity_id: &CommodityID,
229 region_id: &RegionID,
230 time_slice: &TimeSliceID,
231 price: MoneyPerFlow,
232 ) {
233 let key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
234 try_insert(&mut self.0, &key, price).unwrap();
235 }
236
237 pub fn extend<T>(&mut self, iter: T)
239 where
240 T: IntoIterator<Item = ((CommodityID, RegionID, TimeSliceID), MoneyPerFlow)>,
241 {
242 for (key, price) in iter {
243 try_insert(&mut self.0, &key, price).unwrap();
244 }
245 }
246
247 fn extend_selection_prices(
252 &mut self,
253 group_prices: &IndexMap<(CommodityID, RegionID, TimeSliceSelection), MoneyPerFlow>,
254 time_slice_info: &TimeSliceInfo,
255 ) {
256 for ((commodity_id, region_id, selection), &selection_price) in group_prices {
257 for (time_slice_id, _) in selection.iter(time_slice_info) {
258 self.insert(commodity_id, region_id, time_slice_id, selection_price);
259 }
260 }
261 }
262
263 pub fn iter(
269 &self,
270 ) -> impl Iterator<Item = (&CommodityID, &RegionID, &TimeSliceID, MoneyPerFlow)> {
271 self.0
272 .iter()
273 .map(|((commodity_id, region_id, ts), price)| (commodity_id, region_id, ts, *price))
274 }
275
276 pub fn get(
278 &self,
279 commodity_id: &CommodityID,
280 region_id: &RegionID,
281 time_slice: &TimeSliceID,
282 ) -> Option<MoneyPerFlow> {
283 self.0
284 .get(&(commodity_id.clone(), region_id.clone(), time_slice.clone()))
285 .copied()
286 }
287
288 pub fn keys(
290 &self,
291 ) -> indexmap::map::Keys<'_, (CommodityID, RegionID, TimeSliceID), MoneyPerFlow> {
292 self.0.keys()
293 }
294
295 fn time_slice_weighted_averages(
303 &self,
304 time_slice_info: &TimeSliceInfo,
305 ) -> HashMap<(CommodityID, RegionID), MoneyPerFlow> {
306 let mut weighted_prices = HashMap::new();
307
308 for ((commodity_id, region_id, time_slice_id), price) in &self.0 {
309 let weight = time_slice_info.time_slices[time_slice_id] / Year(1.0);
311 let key = (commodity_id.clone(), region_id.clone());
312 weighted_prices
313 .entry(key)
314 .and_modify(|v| *v += *price * weight)
315 .or_insert_with(|| *price * weight);
316 }
317
318 weighted_prices
319 }
320
321 pub fn within_tolerance_weighted(
331 &self,
332 other: &Self,
333 tolerance: Dimensionless,
334 time_slice_info: &TimeSliceInfo,
335 ) -> bool {
336 let self_averages = self.time_slice_weighted_averages(time_slice_info);
337 let other_averages = other.time_slice_weighted_averages(time_slice_info);
338
339 for (key, &price) in &self_averages {
340 let other_price = other_averages[key];
341 let abs_diff = (price - other_price).abs();
342
343 if price == MoneyPerFlow(0.0) {
345 if other_price != MoneyPerFlow(0.0) {
347 return false;
348 }
349 } else if abs_diff / price.abs() > tolerance {
351 return false;
352 }
353 }
354 true
355 }
356}
357
358impl<'a> FromIterator<(&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>
359 for CommodityPrices
360{
361 fn from_iter<I>(iter: I) -> Self
362 where
363 I: IntoIterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>,
364 {
365 let map = iter
366 .into_iter()
367 .map(|(commodity_id, region_id, time_slice, price)| {
368 (
369 (commodity_id.clone(), region_id.clone(), time_slice.clone()),
370 price,
371 )
372 })
373 .collect();
374 CommodityPrices(map)
375 }
376}
377
378impl IntoIterator for CommodityPrices {
379 type Item = ((CommodityID, RegionID, TimeSliceID), MoneyPerFlow);
380 type IntoIter = indexmap::map::IntoIter<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>;
381
382 fn into_iter(self) -> Self::IntoIter {
383 self.0.into_iter()
384 }
385}
386
387fn add_scarcity_adjusted_prices<'a, I>(
396 activity_duals: I,
397 shadow_prices: &CommodityPrices,
398 existing_prices: &mut CommodityPrices,
399 markets_to_price: &HashSet<(CommodityID, RegionID)>,
400) where
401 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
402{
403 let mut highest_duals = IndexMap::new();
405 for (asset, time_slice, dual) in activity_duals {
406 let region_id = asset.region_id();
407
408 for flow in asset.iter_output_flows().filter(|flow| {
411 markets_to_price.contains(&(flow.commodity.id.clone(), region_id.clone()))
412 }) {
413 highest_duals
415 .entry((
416 flow.commodity.id.clone(),
417 region_id.clone(),
418 time_slice.clone(),
419 ))
420 .and_modify(|current_dual| {
421 if dual > *current_dual {
422 *current_dual = dual;
423 }
424 })
425 .or_insert(dual);
426 }
427 }
428
429 for ((commodity, region, time_slice), highest_dual) in &highest_duals {
431 let shadow_price = shadow_prices.get(commodity, region, time_slice).unwrap();
434 let scarcity_price = shadow_price + MoneyPerFlow(highest_dual.value());
437 existing_prices.insert(commodity, region, time_slice, scarcity_price);
438 }
439}
440
441fn add_marginal_cost_prices<'a, I, J>(
492 activity_for_existing: I,
493 activity_keys_for_candidates: J,
494 existing_prices: &mut CommodityPrices,
495 year: u32,
496 markets_to_price: &HashSet<(CommodityID, RegionID)>,
497 commodities: &CommodityMap,
498 time_slice_info: &TimeSliceInfo,
499) where
500 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
501 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
502{
503 let mut group_prices: IndexMap<_, _> = iter_existing_asset_max_prices(
505 activity_for_existing,
506 markets_to_price,
507 existing_prices,
508 year,
509 commodities,
510 &PricingStrategy::MarginalCost,
511 None,
512 )
513 .collect();
514 let priced_groups: HashSet<_> = group_prices.keys().cloned().collect();
515
516 let cand_group_prices = iter_candidate_asset_min_prices(
519 activity_keys_for_candidates,
520 markets_to_price,
521 existing_prices,
522 &priced_groups,
523 year,
524 commodities,
525 &PricingStrategy::MarginalCost,
526 );
527
528 group_prices.extend(cand_group_prices);
530
531 existing_prices.extend_selection_prices(&group_prices, time_slice_info);
533}
534
535fn iter_existing_asset_max_prices<'a, I>(
558 activity_for_existing: I,
559 markets_to_price: &HashSet<(CommodityID, RegionID)>,
560 existing_prices: &CommodityPrices,
561 year: u32,
562 commodities: &CommodityMap,
563 pricing_strategy: &PricingStrategy,
564 annual_activities: Option<&HashMap<AssetRef, Activity>>,
565) -> impl Iterator<Item = ((CommodityID, RegionID, TimeSliceSelection), MoneyPerFlow)> + 'a
566where
567 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
568{
569 match pricing_strategy {
571 PricingStrategy::MarginalCost => assert!(
572 annual_activities.is_none(),
573 "Cannot provide annual_activities with marginal pricing strategy"
574 ),
575 PricingStrategy::FullCost => assert!(
576 annual_activities.is_some(),
577 "annual_activities must be provided for full pricing strategy"
578 ),
579 _ => panic!("Invalid pricing strategy"),
580 }
581
582 let mut existing_accum: IndexMap<
588 (CommodityID, RegionID, TimeSliceSelection),
589 IndexMap<AssetRef, WeightedAverageBackupAccumulator<Activity>>,
590 > = IndexMap::new();
591
592 let mut annual_fixed_costs = HashMap::new();
594
595 for (asset, time_slice, activity) in activity_for_existing {
597 let region_id = asset.region_id();
598
599 let annual_activity = annual_activities.map(|activities| activities[asset]);
602 if annual_activity.is_some_and(|annual_activity| annual_activity < Activity::EPSILON) {
603 continue;
604 }
605
606 let activity_limit = *asset
608 .get_activity_limits_for_selection(&TimeSliceSelection::Single(time_slice.clone()))
609 .end();
610
611 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
613 existing_prices,
614 year,
615 time_slice,
616 |cid: &CommodityID| markets_to_price.contains(&(cid.clone(), region_id.clone())),
617 ) {
618 let ts_selection = commodities[&commodity_id]
620 .time_slice_level
621 .containing_selection(time_slice);
622
623 let total_cost = match pricing_strategy {
625 PricingStrategy::FullCost => {
626 let annual_fixed_costs_per_flow =
627 annual_fixed_costs.entry(asset.clone()).or_insert_with(|| {
628 asset.get_annual_fixed_costs_per_flow(annual_activity.unwrap())
629 });
630 marginal_cost + *annual_fixed_costs_per_flow
631 }
632 PricingStrategy::MarginalCost => marginal_cost,
633 _ => unreachable!(),
634 };
635
636 existing_accum
639 .entry((commodity_id.clone(), region_id.clone(), ts_selection))
640 .or_default()
641 .entry(asset.clone())
642 .or_default()
643 .add(total_cost, activity, activity_limit);
644 }
645 }
646
647 existing_accum.into_iter().filter_map(|(key, per_asset)| {
649 per_asset
650 .into_values()
651 .filter_map(WeightedAverageBackupAccumulator::finalise)
652 .reduce(|current, value| current.max(value))
653 .map(|v| (key, v))
654 })
655}
656
657fn iter_candidate_asset_min_prices<'a, I>(
683 activity_keys_for_candidates: I,
684 markets_to_price: &HashSet<(CommodityID, RegionID)>,
685 existing_prices: &CommodityPrices,
686 priced_groups: &HashSet<(CommodityID, RegionID, TimeSliceSelection)>,
687 year: u32,
688 commodities: &CommodityMap,
689 pricing_strategy: &PricingStrategy,
690) -> impl Iterator<Item = ((CommodityID, RegionID, TimeSliceSelection), MoneyPerFlow)>
691where
692 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
693{
694 assert!(matches!(
696 pricing_strategy,
697 PricingStrategy::MarginalCost | PricingStrategy::FullCost
698 ));
699
700 let mut annual_fixed_costs = HashMap::new();
702
703 let mut annual_activity_limits = HashMap::new();
705
706 let mut cand_accum: IndexMap<
709 (CommodityID, RegionID, TimeSliceSelection),
710 IndexMap<AssetRef, WeightedAverageAccumulator<Activity>>,
711 > = IndexMap::new();
712
713 for (asset, time_slice) in activity_keys_for_candidates {
715 let region_id = asset.region_id();
716
717 let annual_activity_limit =
720 matches!(pricing_strategy, PricingStrategy::FullCost).then(|| {
721 *annual_activity_limits
722 .entry(asset.clone())
723 .or_insert_with(|| {
724 *asset
725 .get_activity_limits_for_selection(&TimeSliceSelection::Annual)
726 .end()
727 })
728 });
729 if annual_activity_limit.is_some_and(|limit| limit < Activity::EPSILON) {
730 continue;
731 }
732
733 let activity_limit = *asset
735 .get_activity_limits_for_selection(&TimeSliceSelection::Single(time_slice.clone()))
736 .end();
737
738 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
740 existing_prices,
741 year,
742 time_slice,
743 |cid: &CommodityID| markets_to_price.contains(&(cid.clone(), region_id.clone())),
744 ) {
745 let ts_selection = commodities[&commodity_id]
747 .time_slice_level
748 .containing_selection(time_slice);
749
750 if priced_groups.contains(&(
752 commodity_id.clone(),
753 region_id.clone(),
754 ts_selection.clone(),
755 )) {
756 continue;
757 }
758
759 let total_cost = match pricing_strategy {
761 PricingStrategy::FullCost => {
762 let annual_fixed_costs_per_flow =
765 annual_fixed_costs.entry(asset.clone()).or_insert_with(|| {
766 asset.get_annual_fixed_costs_per_flow(annual_activity_limit.unwrap())
767 });
768 marginal_cost + *annual_fixed_costs_per_flow
769 }
770 PricingStrategy::MarginalCost => marginal_cost,
771 _ => unreachable!(),
772 };
773
774 cand_accum
776 .entry((commodity_id.clone(), region_id.clone(), ts_selection))
777 .or_default()
778 .entry(asset.clone())
779 .or_default()
780 .add(total_cost, activity_limit);
781 }
782 }
783
784 cand_accum.into_iter().filter_map(|(key, per_candidate)| {
786 per_candidate
787 .into_values()
788 .filter_map(WeightedAverageAccumulator::finalise)
789 .reduce(|current, value| current.min(value))
790 .map(|v| (key, v))
791 })
792}
793
794fn add_marginal_cost_average_prices<'a, I, J>(
803 activity_for_existing: I,
804 activity_keys_for_candidates: J,
805 existing_prices: &mut CommodityPrices,
806 year: u32,
807 markets_to_price: &HashSet<(CommodityID, RegionID)>,
808 commodities: &CommodityMap,
809 time_slice_info: &TimeSliceInfo,
810) where
811 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
812 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
813{
814 let mut group_prices: IndexMap<_, _> = iter_existing_asset_average_prices(
816 activity_for_existing,
817 markets_to_price,
818 existing_prices,
819 year,
820 commodities,
821 &PricingStrategy::MarginalCost,
822 None,
823 )
824 .collect();
825 let priced_groups: HashSet<_> = group_prices.keys().cloned().collect();
826
827 let cand_group_prices = iter_candidate_asset_min_prices(
830 activity_keys_for_candidates,
831 markets_to_price,
832 existing_prices,
833 &priced_groups,
834 year,
835 commodities,
836 &PricingStrategy::MarginalCost,
837 );
838
839 group_prices.extend(cand_group_prices);
841
842 existing_prices.extend_selection_prices(&group_prices, time_slice_info);
844}
845
846fn iter_existing_asset_average_prices<'a, I>(
869 activity_for_existing: I,
870 markets_to_price: &HashSet<(CommodityID, RegionID)>,
871 existing_prices: &CommodityPrices,
872 year: u32,
873 commodities: &CommodityMap,
874 pricing_strategy: &PricingStrategy,
875 annual_activities: Option<&HashMap<AssetRef, Activity>>,
876) -> impl Iterator<Item = ((CommodityID, RegionID, TimeSliceSelection), MoneyPerFlow)> + 'a
877where
878 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
879{
880 match pricing_strategy {
882 PricingStrategy::MarginalCost => assert!(
883 annual_activities.is_none(),
884 "Cannot provide annual_activities with marginal pricing strategy"
885 ),
886 PricingStrategy::FullCost => assert!(
887 annual_activities.is_some(),
888 "annual_activities must be provided for full pricing strategy"
889 ),
890 _ => panic!("Invalid pricing strategy"),
891 }
892
893 let mut existing_accum: IndexMap<
899 (CommodityID, RegionID, TimeSliceSelection),
900 WeightedAverageBackupAccumulator<Flow>,
901 > = IndexMap::new();
902
903 let mut annual_fixed_costs = HashMap::new();
905
906 for (asset, time_slice, activity) in activity_for_existing {
908 let region_id = asset.region_id();
909
910 let annual_activity = annual_activities.map(|activities| activities[asset]);
913 if annual_activity.is_some_and(|annual_activity| annual_activity < Activity::EPSILON) {
914 continue;
915 }
916
917 let activity_limit = *asset
919 .get_activity_limits_for_selection(&TimeSliceSelection::Single(time_slice.clone()))
920 .end();
921
922 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
924 existing_prices,
925 year,
926 time_slice,
927 |cid: &CommodityID| markets_to_price.contains(&(cid.clone(), region_id.clone())),
928 ) {
929 let time_slice_selection = commodities[&commodity_id]
931 .time_slice_level
932 .containing_selection(time_slice);
933
934 let total_cost = match pricing_strategy {
936 PricingStrategy::FullCost => {
937 let annual_fixed_costs_per_flow =
938 annual_fixed_costs.entry(asset.clone()).or_insert_with(|| {
939 asset.get_annual_fixed_costs_per_flow(annual_activity.unwrap())
940 });
941 marginal_cost + *annual_fixed_costs_per_flow
942 }
943 PricingStrategy::MarginalCost => marginal_cost,
944 _ => unreachable!(),
945 };
946
947 let output_coeff = asset
949 .get_flow(&commodity_id)
950 .expect("Commodity should be an output flow for this asset")
951 .coeff;
952 let output_weight = activity * output_coeff;
953 let backup_output_weight = activity_limit * output_coeff;
954
955 existing_accum
958 .entry((
959 commodity_id.clone(),
960 region_id.clone(),
961 time_slice_selection,
962 ))
963 .or_default()
964 .add(total_cost, output_weight, backup_output_weight);
965 }
966 }
967
968 existing_accum
970 .into_iter()
971 .filter_map(|(key, accum)| accum.finalise().map(|v| (key, v)))
972}
973
974fn calculate_annual_activities<'a, I>(activities: I) -> HashMap<AssetRef, Activity>
976where
977 I: IntoIterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
978{
979 activities
980 .into_iter()
981 .map(|(asset, _ts, activity)| (asset.clone(), activity))
982 .fold(HashMap::new(), |mut acc, (asset, activity)| {
983 acc.entry(asset)
984 .and_modify(|e| *e += activity)
985 .or_insert(activity);
986 acc
987 })
988}
989
990#[allow(clippy::too_many_arguments)]
1048fn add_full_cost_prices<'a, I, J>(
1049 activity_for_existing: I,
1050 activity_keys_for_candidates: J,
1051 annual_activities: &HashMap<AssetRef, Activity>,
1052 existing_prices: &mut CommodityPrices,
1053 year: u32,
1054 markets_to_price: &HashSet<(CommodityID, RegionID)>,
1055 commodities: &CommodityMap,
1056 time_slice_info: &TimeSliceInfo,
1057) where
1058 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
1059 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
1060{
1061 let mut group_prices: IndexMap<_, _> = iter_existing_asset_max_prices(
1063 activity_for_existing,
1064 markets_to_price,
1065 existing_prices,
1066 year,
1067 commodities,
1068 &PricingStrategy::FullCost,
1069 Some(annual_activities),
1070 )
1071 .collect();
1072 let priced_groups: HashSet<_> = group_prices.keys().cloned().collect();
1073
1074 let cand_group_prices = iter_candidate_asset_min_prices(
1077 activity_keys_for_candidates,
1078 markets_to_price,
1079 existing_prices,
1080 &priced_groups,
1081 year,
1082 commodities,
1083 &PricingStrategy::FullCost,
1084 );
1085
1086 group_prices.extend(cand_group_prices);
1088
1089 existing_prices.extend_selection_prices(&group_prices, time_slice_info);
1091}
1092
1093#[allow(clippy::too_many_arguments)]
1102fn add_full_cost_average_prices<'a, I, J>(
1103 activity_for_existing: I,
1104 activity_keys_for_candidates: J,
1105 annual_activities: &HashMap<AssetRef, Activity>,
1106 existing_prices: &mut CommodityPrices,
1107 year: u32,
1108 markets_to_price: &HashSet<(CommodityID, RegionID)>,
1109 commodities: &CommodityMap,
1110 time_slice_info: &TimeSliceInfo,
1111) where
1112 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
1113 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
1114{
1115 let mut group_prices: IndexMap<_, _> = iter_existing_asset_average_prices(
1117 activity_for_existing,
1118 markets_to_price,
1119 existing_prices,
1120 year,
1121 commodities,
1122 &PricingStrategy::FullCost,
1123 Some(annual_activities),
1124 )
1125 .collect();
1126 let priced_groups: HashSet<_> = group_prices.keys().cloned().collect();
1127
1128 let cand_group_prices = iter_candidate_asset_min_prices(
1130 activity_keys_for_candidates,
1131 markets_to_price,
1132 existing_prices,
1133 &priced_groups,
1134 year,
1135 commodities,
1136 &PricingStrategy::FullCost,
1137 );
1138
1139 group_prices.extend(cand_group_prices);
1141
1142 existing_prices.extend_selection_prices(&group_prices, time_slice_info);
1144}
1145
1146#[cfg(test)]
1147mod tests {
1148 use super::*;
1149 use crate::asset::Asset;
1150 use crate::asset::AssetRef;
1151 use crate::commodity::{Commodity, CommodityID, CommodityMap};
1152 use crate::fixture::{
1153 commodity_id, other_commodity, region_id, sed_commodity, time_slice, time_slice_info,
1154 };
1155 use crate::process::{ActivityLimits, FlowType, Process, ProcessFlow, ProcessParameter};
1156 use crate::region::RegionID;
1157 use crate::time_slice::TimeSliceID;
1158 use crate::units::ActivityPerCapacity;
1159 use crate::units::{
1160 Activity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity, MoneyPerCapacity,
1161 MoneyPerCapacityPerYear, MoneyPerFlow,
1162 };
1163 use float_cmp::assert_approx_eq;
1164 use indexmap::{IndexMap, IndexSet};
1165 use rstest::rstest;
1166 use std::collections::{HashMap, HashSet};
1167 use std::rc::Rc;
1168
1169 fn build_process_flow(commodity: &Commodity, coeff: f64, cost: MoneyPerFlow) -> ProcessFlow {
1170 ProcessFlow {
1171 commodity: Rc::new(commodity.clone()),
1172 coeff: FlowPerActivity(coeff),
1173 kind: FlowType::Fixed,
1174 cost,
1175 }
1176 }
1177
1178 #[allow(clippy::too_many_arguments)]
1179 fn build_process(
1180 flows: IndexMap<CommodityID, ProcessFlow>,
1181 region_id: &RegionID,
1182 year: u32,
1183 time_slice_info: &TimeSliceInfo,
1184 variable_operating_cost: MoneyPerActivity,
1185 fixed_operating_cost: MoneyPerCapacityPerYear,
1186 capital_cost: MoneyPerCapacity,
1187 lifetime: u32,
1188 discount_rate: Dimensionless,
1189 ) -> Process {
1190 let mut process_flows_map = HashMap::new();
1191 process_flows_map.insert((region_id.clone(), year), Rc::new(flows));
1192
1193 let mut process_parameter_map = HashMap::new();
1194 let proc_param = ProcessParameter {
1195 capital_cost,
1196 fixed_operating_cost,
1197 variable_operating_cost,
1198 lifetime,
1199 discount_rate,
1200 };
1201 process_parameter_map.insert((region_id.clone(), year), Rc::new(proc_param));
1202
1203 let mut activity_limits_map = HashMap::new();
1204 activity_limits_map.insert(
1205 (region_id.clone(), year),
1206 Rc::new(ActivityLimits::new_with_full_availability(time_slice_info)),
1207 );
1208
1209 let regions: IndexSet<RegionID> = IndexSet::from([region_id.clone()]);
1210
1211 Process {
1212 id: "p1".into(),
1213 description: "test process".into(),
1214 years: 2010..=2020,
1215 activity_limits: activity_limits_map,
1216 flows: process_flows_map,
1217 parameters: process_parameter_map,
1218 regions,
1219 primary_output: None,
1220 capacity_to_activity: ActivityPerCapacity(1.0),
1221 investment_constraints: HashMap::new(),
1222 unit_size: None,
1223 }
1224 }
1225
1226 fn assert_price_approx(
1227 prices: &CommodityPrices,
1228 commodity: &CommodityID,
1229 region: &RegionID,
1230 time_slice: &TimeSliceID,
1231 expected: MoneyPerFlow,
1232 ) {
1233 let p = prices.get(commodity, region, time_slice).unwrap();
1234 assert_approx_eq!(MoneyPerFlow, p, expected);
1235 }
1236
1237 #[rstest]
1238 #[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(
1249 #[case] price1: MoneyPerFlow,
1250 #[case] price2: MoneyPerFlow,
1251 #[case] tolerance: Dimensionless,
1252 #[case] expected: bool,
1253 time_slice_info: TimeSliceInfo,
1254 time_slice: TimeSliceID,
1255 ) {
1256 let mut prices1 = CommodityPrices::default();
1257 let mut prices2 = CommodityPrices::default();
1258
1259 let commodity = CommodityID::new("test_commodity");
1261 let region = RegionID::new("test_region");
1262 prices1.insert(&commodity, ®ion, &time_slice, price1);
1263 prices2.insert(&commodity, ®ion, &time_slice, price2);
1264
1265 assert_eq!(
1266 prices1.within_tolerance_weighted(&prices2, tolerance, &time_slice_info),
1267 expected
1268 );
1269 }
1270
1271 #[rstest]
1272 fn time_slice_weighted_averages(
1273 commodity_id: CommodityID,
1274 region_id: RegionID,
1275 time_slice_info: TimeSliceInfo,
1276 time_slice: TimeSliceID,
1277 ) {
1278 let mut prices = CommodityPrices::default();
1279
1280 prices.insert(&commodity_id, ®ion_id, &time_slice, MoneyPerFlow(100.0));
1282
1283 let averages = prices.time_slice_weighted_averages(&time_slice_info);
1284
1285 assert_eq!(averages[&(commodity_id, region_id)], MoneyPerFlow(100.0));
1287 }
1288
1289 #[rstest]
1290 fn marginal_cost_example(
1291 sed_commodity: Commodity,
1292 other_commodity: Commodity,
1293 region_id: RegionID,
1294 time_slice_info: TimeSliceInfo,
1295 time_slice: TimeSliceID,
1296 ) {
1297 let mut a = sed_commodity.clone();
1299 a.id = "A".into();
1300 let mut b = sed_commodity.clone();
1301 b.id = "B".into();
1302 let mut c = sed_commodity.clone();
1303 c.id = "C".into();
1304 let mut d = other_commodity.clone();
1305 d.id = "D".into();
1306
1307 let mut flows = IndexMap::new();
1308 flows.insert(
1309 a.id.clone(),
1310 build_process_flow(&a, -1.0, MoneyPerFlow(0.0)),
1311 );
1312 flows.insert(b.id.clone(), build_process_flow(&b, 1.0, MoneyPerFlow(0.0)));
1313 flows.insert(c.id.clone(), build_process_flow(&c, 2.0, MoneyPerFlow(3.0)));
1314 flows.insert(d.id.clone(), build_process_flow(&d, 1.0, MoneyPerFlow(4.0)));
1315
1316 let process = build_process(
1317 flows,
1318 ®ion_id,
1319 2015u32,
1320 &time_slice_info,
1321 MoneyPerActivity(5.0), MoneyPerCapacityPerYear(0.0), MoneyPerCapacity(0.0), 5, Dimensionless(1.0), );
1327
1328 let asset =
1329 Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(1.0), 2015u32)
1330 .unwrap();
1331 let asset_ref = AssetRef::from(asset);
1332 let existing_prices =
1333 CommodityPrices::from_iter(vec![(&a.id, ®ion_id, &time_slice, MoneyPerFlow(1.0))]);
1334 let mut markets = HashSet::new();
1335 markets.insert((b.id.clone(), region_id.clone()));
1336 markets.insert((c.id.clone(), region_id.clone()));
1337
1338 let mut commodities = CommodityMap::new();
1339 commodities.insert(b.id.clone(), Rc::new(b.clone()));
1340 commodities.insert(c.id.clone(), Rc::new(c.clone()));
1341
1342 let existing = vec![(&asset_ref, &time_slice, Activity(1.0))];
1343 let candidates = Vec::new();
1344
1345 let mut prices = existing_prices.clone();
1346 add_marginal_cost_prices(
1347 existing.into_iter(),
1348 candidates.into_iter(),
1349 &mut prices,
1350 2015u32,
1351 &markets,
1352 &commodities,
1353 &time_slice_info,
1354 );
1355
1356 assert_price_approx(
1357 &prices,
1358 &b.id,
1359 ®ion_id,
1360 &time_slice,
1361 MoneyPerFlow(10.0 / 3.0),
1362 );
1363 assert_price_approx(
1364 &prices,
1365 &c.id,
1366 ®ion_id,
1367 &time_slice,
1368 MoneyPerFlow(10.0 / 3.0 + 3.0),
1369 );
1370 }
1371
1372 #[rstest]
1373 fn full_cost_example(
1374 sed_commodity: Commodity,
1375 other_commodity: Commodity,
1376 region_id: RegionID,
1377 time_slice_info: TimeSliceInfo,
1378 time_slice: TimeSliceID,
1379 ) {
1380 let mut a = sed_commodity.clone();
1382 a.id = "A".into();
1383 let mut b = sed_commodity.clone();
1384 b.id = "B".into();
1385 let mut c = sed_commodity.clone();
1386 c.id = "C".into();
1387 let mut d = other_commodity.clone();
1388 d.id = "D".into();
1389
1390 let mut flows = IndexMap::new();
1391 flows.insert(
1392 a.id.clone(),
1393 build_process_flow(&a, -1.0, MoneyPerFlow(0.0)),
1394 );
1395 flows.insert(b.id.clone(), build_process_flow(&b, 1.0, MoneyPerFlow(0.0)));
1396 flows.insert(c.id.clone(), build_process_flow(&c, 2.0, MoneyPerFlow(3.0)));
1397 flows.insert(d.id.clone(), build_process_flow(&d, 1.0, MoneyPerFlow(4.0)));
1398
1399 let process = build_process(
1400 flows,
1401 ®ion_id,
1402 2015u32,
1403 &time_slice_info,
1404 MoneyPerActivity(5.0), MoneyPerCapacityPerYear(1.0), MoneyPerCapacity(1.5), 1, Dimensionless(0.0), );
1410
1411 let asset =
1412 Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(4.0), 2015u32)
1413 .unwrap();
1414 let asset_ref = AssetRef::from(asset);
1415 let existing_prices =
1416 CommodityPrices::from_iter(vec![(&a.id, ®ion_id, &time_slice, MoneyPerFlow(1.0))]);
1417 let mut markets = HashSet::new();
1418 markets.insert((b.id.clone(), region_id.clone()));
1419 markets.insert((c.id.clone(), region_id.clone()));
1420
1421 let mut commodities = CommodityMap::new();
1422 commodities.insert(b.id.clone(), Rc::new(b.clone()));
1423 commodities.insert(c.id.clone(), Rc::new(c.clone()));
1424
1425 let existing = vec![(&asset_ref, &time_slice, Activity(2.0))];
1426 let candidates = Vec::new();
1427
1428 let mut annual_activities = HashMap::new();
1429 annual_activities.insert(asset_ref.clone(), Activity(2.0));
1430
1431 let mut prices = existing_prices.clone();
1432 add_full_cost_prices(
1433 existing.into_iter(),
1434 candidates.into_iter(),
1435 &annual_activities,
1436 &mut prices,
1437 2015u32,
1438 &markets,
1439 &commodities,
1440 &time_slice_info,
1441 );
1442
1443 assert_price_approx(&prices, &b.id, ®ion_id, &time_slice, MoneyPerFlow(5.0));
1444 assert_price_approx(&prices, &c.id, ®ion_id, &time_slice, MoneyPerFlow(8.0));
1445 }
1446
1447 #[test]
1448 fn weighted_average_accumulator_single_value() {
1449 let mut accum = WeightedAverageAccumulator::<Dimensionless>::default();
1450 accum.add(MoneyPerFlow(100.0), Dimensionless(1.0));
1451 assert_eq!(accum.finalise(), Some(MoneyPerFlow(100.0)));
1452 }
1453
1454 #[test]
1455 fn weighted_average_accumulator_different_weights() {
1456 let mut accum = WeightedAverageAccumulator::<Dimensionless>::default();
1457 accum.add(MoneyPerFlow(100.0), Dimensionless(1.0));
1458 accum.add(MoneyPerFlow(200.0), Dimensionless(2.0));
1459 let result = accum.finalise().unwrap();
1461 assert_approx_eq!(MoneyPerFlow, result, MoneyPerFlow(500.0 / 3.0));
1462 }
1463
1464 #[test]
1465 fn weighted_average_accumulator_zero_weight() {
1466 let accum = WeightedAverageAccumulator::<Dimensionless>::default();
1467 assert_eq!(accum.finalise(), None);
1468 }
1469
1470 #[test]
1471 fn weighted_average_backup_accumulator_primary_preferred() {
1472 let mut accum = WeightedAverageBackupAccumulator::<Dimensionless>::default();
1473 accum.add(MoneyPerFlow(100.0), Dimensionless(3.0), Dimensionless(1.0));
1474 accum.add(MoneyPerFlow(200.0), Dimensionless(1.0), Dimensionless(1.0));
1475 assert_eq!(accum.finalise(), Some(MoneyPerFlow(125.0)));
1478 }
1479
1480 #[test]
1481 fn weighted_average_backup_accumulator_fallback() {
1482 let mut accum = WeightedAverageBackupAccumulator::<Dimensionless>::default();
1483 accum.add(MoneyPerFlow(100.0), Dimensionless(0.0), Dimensionless(2.0));
1484 accum.add(MoneyPerFlow(200.0), Dimensionless(0.0), Dimensionless(2.0));
1485 assert_eq!(accum.finalise(), Some(MoneyPerFlow(150.0)));
1487 }
1488
1489 #[test]
1490 fn weighted_average_backup_accumulator_both_zero() {
1491 let accum = WeightedAverageBackupAccumulator::<Dimensionless>::default();
1492 assert_eq!(accum.finalise(), None);
1493 }
1494}