1use crate::asset::AssetRef;
3use crate::commodity::{CommodityID, PricingStrategy};
4use crate::model::Model;
5use crate::region::RegionID;
6use crate::simulation::optimisation::Solution;
7use crate::time_slice::{TimeSliceID, TimeSliceInfo, TimeSliceSelection};
8use crate::units::{Activity, Dimensionless, MoneyPerActivity, MoneyPerFlow, Year};
9use anyhow::Result;
10use indexmap::IndexMap;
11use itertools::iproduct;
12use std::collections::{HashMap, HashSet};
13
14type Item<'a> = (&'a AssetRef, &'a TimeSliceID, Activity);
16
17pub fn calculate_prices(model: &Model, solution: &Solution, year: u32) -> Result<CommodityPrices> {
33 let shadow_prices = CommodityPrices::from_iter(solution.iter_commodity_balance_duals());
35
36 let mut pricing_sets = HashMap::new();
39 for ((commodity_id, commodity), region_id) in
40 iproduct!(&model.commodities, model.iter_regions())
41 {
42 if commodity.pricing_strategy == PricingStrategy::Unpriced {
43 continue;
44 }
45 pricing_sets
46 .entry(&commodity.pricing_strategy)
47 .or_insert_with(HashSet::new)
48 .insert((commodity_id.clone(), region_id.clone()));
49 }
50
51 let mut result = CommodityPrices::default();
53
54 if let Some(shadow_set) = pricing_sets.get(&PricingStrategy::Shadow) {
56 for (commodity_id, region_id, time_slice) in shadow_prices.keys() {
57 if shadow_set.contains(&(commodity_id.clone(), region_id.clone())) {
58 let price = shadow_prices
59 .get(commodity_id, region_id, time_slice)
60 .unwrap();
61 result.insert(commodity_id, region_id, time_slice, price);
62 }
63 }
64 }
65
66 if let Some(scarcity_set) = pricing_sets.get(&PricingStrategy::ScarcityAdjusted) {
68 let scarcity_prices = calculate_scarcity_adjusted_prices(
69 solution.iter_activity_duals(),
70 &shadow_prices,
71 scarcity_set,
72 );
73 result.extend(scarcity_prices);
74 }
75
76 if let Some(marginal_set) = pricing_sets.get(&PricingStrategy::MarginalCost) {
78 let marginal_cost_prices = calculate_marginal_cost_prices(
79 solution.iter_activity_for_existing(),
80 solution.iter_activity_for_candidates(),
81 &shadow_prices,
82 year,
83 marginal_set,
84 );
85 result.extend(marginal_cost_prices);
86 }
87
88 if let Some(fullcost_set) = pricing_sets.get(&PricingStrategy::FullCost) {
90 let annual_activities = calculate_annual_activities(solution.iter_activity_for_existing());
91 let full_cost_prices = calculate_full_cost_prices(
92 solution.iter_activity_for_existing(),
93 solution.iter_activity_for_candidates(),
94 &annual_activities,
95 &shadow_prices,
96 year,
97 fullcost_set,
98 );
99 result.extend(full_cost_prices);
100 }
101
102 Ok(result)
104}
105
106#[derive(Default, Clone)]
108pub struct CommodityPrices(IndexMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>);
109
110impl CommodityPrices {
111 pub fn insert(
113 &mut self,
114 commodity_id: &CommodityID,
115 region_id: &RegionID,
116 time_slice: &TimeSliceID,
117 price: MoneyPerFlow,
118 ) {
119 let key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
120 self.0.insert(key, price);
121 }
122
123 pub fn extend<T>(&mut self, iter: T)
125 where
126 T: IntoIterator<Item = ((CommodityID, RegionID, TimeSliceID), MoneyPerFlow)>,
127 {
128 for (key, price) in iter {
129 let existing = self.0.insert(key.clone(), price).is_some();
130 assert!(!existing, "Key {key:?} already exists in the map");
131 }
132 }
133
134 pub fn iter(
140 &self,
141 ) -> impl Iterator<Item = (&CommodityID, &RegionID, &TimeSliceID, MoneyPerFlow)> {
142 self.0
143 .iter()
144 .map(|((commodity_id, region_id, ts), price)| (commodity_id, region_id, ts, *price))
145 }
146
147 pub fn get(
149 &self,
150 commodity_id: &CommodityID,
151 region_id: &RegionID,
152 time_slice: &TimeSliceID,
153 ) -> Option<MoneyPerFlow> {
154 self.0
155 .get(&(commodity_id.clone(), region_id.clone(), time_slice.clone()))
156 .copied()
157 }
158
159 pub fn keys(
161 &self,
162 ) -> indexmap::map::Keys<'_, (CommodityID, RegionID, TimeSliceID), MoneyPerFlow> {
163 self.0.keys()
164 }
165
166 fn time_slice_weighted_averages(
174 &self,
175 time_slice_info: &TimeSliceInfo,
176 ) -> HashMap<(CommodityID, RegionID), MoneyPerFlow> {
177 let mut weighted_prices = HashMap::new();
178
179 for ((commodity_id, region_id, time_slice_id), price) in &self.0 {
180 let weight = time_slice_info.time_slices[time_slice_id] / Year(1.0);
182 let key = (commodity_id.clone(), region_id.clone());
183 *weighted_prices.entry(key).or_default() += *price * weight;
184 }
185
186 weighted_prices
187 }
188
189 pub fn within_tolerance_weighted(
199 &self,
200 other: &Self,
201 tolerance: Dimensionless,
202 time_slice_info: &TimeSliceInfo,
203 ) -> bool {
204 let self_averages = self.time_slice_weighted_averages(time_slice_info);
205 let other_averages = other.time_slice_weighted_averages(time_slice_info);
206
207 for (key, &price) in &self_averages {
208 let other_price = other_averages[key];
209 let abs_diff = (price - other_price).abs();
210
211 if price == MoneyPerFlow(0.0) {
213 if other_price != MoneyPerFlow(0.0) {
215 return false;
216 }
217 } else if abs_diff / price.abs() > tolerance {
219 return false;
220 }
221 }
222 true
223 }
224}
225
226impl<'a> FromIterator<(&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>
227 for CommodityPrices
228{
229 fn from_iter<I>(iter: I) -> Self
230 where
231 I: IntoIterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>,
232 {
233 let map = iter
234 .into_iter()
235 .map(|(commodity_id, region_id, time_slice, price)| {
236 (
237 (commodity_id.clone(), region_id.clone(), time_slice.clone()),
238 price,
239 )
240 })
241 .collect();
242 CommodityPrices(map)
243 }
244}
245
246impl IntoIterator for CommodityPrices {
247 type Item = ((CommodityID, RegionID, TimeSliceID), MoneyPerFlow);
248 type IntoIter = indexmap::map::IntoIter<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>;
249
250 fn into_iter(self) -> Self::IntoIter {
251 self.0.into_iter()
252 }
253}
254
255fn calculate_scarcity_adjusted_prices<'a, I>(
267 activity_duals: I,
268 shadow_prices: &CommodityPrices,
269 markets_to_price: &HashSet<(CommodityID, RegionID)>,
270) -> HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>
271where
272 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
273{
274 let mut highest_duals = HashMap::new();
276 for (asset, time_slice, dual) in activity_duals {
277 let region_id = asset.region_id();
278
279 for flow in asset.iter_output_flows().filter(|flow| {
282 markets_to_price.contains(&(flow.commodity.id.clone(), region_id.clone()))
283 }) {
284 highest_duals
286 .entry((
287 flow.commodity.id.clone(),
288 region_id.clone(),
289 time_slice.clone(),
290 ))
291 .and_modify(|current_dual| {
292 if dual > *current_dual {
293 *current_dual = dual;
294 }
295 })
296 .or_insert(dual);
297 }
298 }
299
300 let mut scarcity_prices = HashMap::new();
302 for ((commodity, region, time_slice), highest_dual) in &highest_duals {
303 let shadow_price = shadow_prices.get(commodity, region, time_slice).unwrap();
306 let scarcity_price = shadow_price + MoneyPerFlow(highest_dual.value());
309 scarcity_prices.insert(
310 (commodity.clone(), region.clone(), time_slice.clone()),
311 scarcity_price,
312 );
313 }
314
315 scarcity_prices
316}
317
318fn calculate_marginal_cost_prices<'a, I, J>(
373 activity_for_existing: I,
374 activity_for_candidates: J,
375 shadow_prices: &CommodityPrices,
376 year: u32,
377 markets_to_price: &HashSet<(CommodityID, RegionID)>,
378) -> HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>
379where
380 I: Iterator<Item = Item<'a>>,
381 J: Iterator<Item = Item<'a>>,
382{
383 let mut prices: HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow> = HashMap::new();
384
385 let mut priced_by_existing = HashSet::new();
389 for (asset, time_slice, activity) in activity_for_existing {
390 let region_id = asset.region_id();
391
392 if activity < Activity::EPSILON {
394 continue;
395 }
396
397 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
399 shadow_prices,
400 year,
401 time_slice,
402 |commodity_id: &CommodityID| {
403 markets_to_price.contains(&(commodity_id.clone(), region_id.clone()))
404 },
405 ) {
406 let key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
408 prices
409 .entry(key.clone())
410 .and_modify(|c| *c = c.max(marginal_cost))
411 .or_insert(marginal_cost);
412 priced_by_existing.insert(key);
413 }
414 }
415
416 for (asset, time_slice, _activity) in activity_for_candidates {
419 let region_id = asset.region_id();
420
421 let should_process = |cid: &CommodityID| {
423 markets_to_price.contains(&(cid.clone(), region_id.clone()))
424 && !priced_by_existing.contains(&(
425 cid.clone(),
426 region_id.clone(),
427 time_slice.clone(),
428 ))
429 };
430
431 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
433 shadow_prices,
434 year,
435 time_slice,
436 |cid: &CommodityID| should_process(cid),
437 ) {
438 let key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
440 prices
441 .entry(key.clone())
442 .and_modify(|c| *c = c.min(marginal_cost))
443 .or_insert(marginal_cost);
444 }
445 }
446
447 prices
449}
450
451fn calculate_annual_activities<'a, I>(activities: I) -> HashMap<AssetRef, Activity>
453where
454 I: IntoIterator<Item = Item<'a>>,
455{
456 activities
457 .into_iter()
458 .map(|(asset, _ts, activity)| (asset.clone(), activity))
459 .fold(HashMap::new(), |mut acc, (asset, activity)| {
460 acc.entry(asset)
461 .and_modify(|e| *e += activity)
462 .or_insert(activity);
463 acc
464 })
465}
466
467fn calculate_full_cost_prices<'a, I, J>(
524 activity_for_existing: I,
525 activity_for_candidates: J,
526 annual_activities: &HashMap<AssetRef, Activity>,
527 shadow_prices: &CommodityPrices,
528 year: u32,
529 markets_to_price: &HashSet<(CommodityID, RegionID)>,
530) -> HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>
531where
532 I: Iterator<Item = Item<'a>>,
533 J: Iterator<Item = Item<'a>>,
534{
535 let mut prices: HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow> = HashMap::new();
536
537 let mut annual_capital_costs_cache = HashMap::new();
541 let mut priced_by_existing = HashSet::new();
542 for (asset, time_slice, activity) in activity_for_existing {
543 let annual_activity = annual_activities[asset];
544 let region_id = asset.region_id();
545
546 if activity < Activity::EPSILON {
548 continue;
549 }
550
551 if !asset
553 .iter_output_flows()
554 .any(|flow| markets_to_price.contains(&(flow.commodity.id.clone(), region_id.clone())))
555 {
556 continue;
557 }
558
559 let annual_capital_cost_per_flow = *annual_capital_costs_cache
561 .entry(asset.clone())
562 .or_insert_with(|| asset.get_annual_capital_cost_per_flow(annual_activity));
563
564 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
566 shadow_prices,
567 year,
568 time_slice,
569 |cid: &CommodityID| markets_to_price.contains(&(cid.clone(), region_id.clone())),
570 ) {
571 let marginal_cost = marginal_cost + annual_capital_cost_per_flow;
573
574 let key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
576 prices
577 .entry(key.clone())
578 .and_modify(|c| *c = c.max(marginal_cost))
579 .or_insert(marginal_cost);
580 priced_by_existing.insert(key);
581 }
582 }
583
584 for (asset, time_slice, _activity) in activity_for_candidates {
587 let region_id = asset.region_id();
588
589 let should_process = |cid: &CommodityID| {
591 markets_to_price.contains(&(cid.clone(), region_id.clone()))
592 && !priced_by_existing.contains(&(
593 cid.clone(),
594 region_id.clone(),
595 time_slice.clone(),
596 ))
597 };
598
599 if !asset
601 .iter_output_flows()
602 .any(|flow| should_process(&flow.commodity.id))
603 {
604 continue;
605 }
606
607 let annual_capital_cost_per_flow = *annual_capital_costs_cache
610 .entry(asset.clone())
611 .or_insert_with(|| {
612 asset.get_annual_capital_cost_per_flow(
613 *asset
614 .get_activity_limits_for_selection(&TimeSliceSelection::Annual)
615 .end(),
616 )
617 });
618
619 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
621 shadow_prices,
622 year,
623 time_slice,
624 |cid: &CommodityID| should_process(cid),
625 ) {
626 let full_cost = marginal_cost + annual_capital_cost_per_flow;
628
629 let key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
631 prices
632 .entry(key)
633 .and_modify(|c| *c = c.min(full_cost))
634 .or_insert(full_cost);
635 }
636 }
637
638 prices
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645 use crate::asset::Asset;
646 use crate::asset::AssetRef;
647 use crate::commodity::{Commodity, CommodityID};
648 use crate::fixture::{
649 commodity_id, other_commodity, region_id, sed_commodity, time_slice, time_slice_info,
650 };
651 use crate::process::{ActivityLimits, FlowType, Process, ProcessFlow, ProcessParameter};
652 use crate::region::RegionID;
653 use crate::time_slice::TimeSliceID;
654 use crate::units::ActivityPerCapacity;
655 use crate::units::{
656 Activity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity, MoneyPerCapacity,
657 MoneyPerCapacityPerYear, MoneyPerFlow,
658 };
659 use indexmap::{IndexMap, IndexSet};
660 use rstest::rstest;
661 use std::collections::{HashMap, HashSet};
662 use std::rc::Rc;
663
664 fn build_process_flow(commodity: &Commodity, coeff: f64, cost: MoneyPerFlow) -> ProcessFlow {
665 ProcessFlow {
666 commodity: Rc::new(commodity.clone()),
667 coeff: FlowPerActivity(coeff),
668 kind: FlowType::Fixed,
669 cost,
670 }
671 }
672
673 #[allow(clippy::too_many_arguments)]
674 fn build_process(
675 flows: IndexMap<CommodityID, ProcessFlow>,
676 region_id: &RegionID,
677 year: u32,
678 time_slice_info: &TimeSliceInfo,
679 variable_operating_cost: MoneyPerActivity,
680 capital_cost: MoneyPerCapacity,
681 lifetime: u32,
682 discount_rate: Dimensionless,
683 ) -> Process {
684 let mut process_flows_map = HashMap::new();
685 process_flows_map.insert((region_id.clone(), year), Rc::new(flows));
686
687 let mut process_parameter_map = HashMap::new();
688 let proc_param = ProcessParameter {
689 capital_cost,
690 fixed_operating_cost: MoneyPerCapacityPerYear(0.0),
691 variable_operating_cost,
692 lifetime,
693 discount_rate,
694 };
695 process_parameter_map.insert((region_id.clone(), year), Rc::new(proc_param));
696
697 let mut activity_limits_map = HashMap::new();
698 activity_limits_map.insert(
699 (region_id.clone(), year),
700 Rc::new(ActivityLimits::new_with_full_availability(time_slice_info)),
701 );
702
703 let regions: IndexSet<RegionID> = IndexSet::from([region_id.clone()]);
704
705 Process {
706 id: "p1".into(),
707 description: "test process".into(),
708 years: 2010..=2020,
709 activity_limits: activity_limits_map,
710 flows: process_flows_map,
711 parameters: process_parameter_map,
712 regions,
713 primary_output: None,
714 capacity_to_activity: ActivityPerCapacity(1.0),
715 investment_constraints: HashMap::new(),
716 unit_size: None,
717 }
718 }
719
720 fn assert_price_approx(
721 prices: &HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>,
722 commodity: &CommodityID,
723 region: &RegionID,
724 time_slice: &TimeSliceID,
725 expected: MoneyPerFlow,
726 ) {
727 let p = prices[&(commodity.clone(), region.clone(), time_slice.clone())];
728 assert!((p - expected).abs() < MoneyPerFlow::EPSILON);
729 }
730
731 #[rstest]
732 #[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(
743 #[case] price1: MoneyPerFlow,
744 #[case] price2: MoneyPerFlow,
745 #[case] tolerance: Dimensionless,
746 #[case] expected: bool,
747 time_slice_info: TimeSliceInfo,
748 time_slice: TimeSliceID,
749 ) {
750 let mut prices1 = CommodityPrices::default();
751 let mut prices2 = CommodityPrices::default();
752
753 let commodity = CommodityID::new("test_commodity");
755 let region = RegionID::new("test_region");
756 prices1.insert(&commodity, ®ion, &time_slice, price1);
757 prices2.insert(&commodity, ®ion, &time_slice, price2);
758
759 assert_eq!(
760 prices1.within_tolerance_weighted(&prices2, tolerance, &time_slice_info),
761 expected
762 );
763 }
764
765 #[rstest]
766 fn time_slice_weighted_averages(
767 commodity_id: CommodityID,
768 region_id: RegionID,
769 time_slice_info: TimeSliceInfo,
770 time_slice: TimeSliceID,
771 ) {
772 let mut prices = CommodityPrices::default();
773
774 prices.insert(&commodity_id, ®ion_id, &time_slice, MoneyPerFlow(100.0));
776
777 let averages = prices.time_slice_weighted_averages(&time_slice_info);
778
779 assert_eq!(averages[&(commodity_id, region_id)], MoneyPerFlow(100.0));
781 }
782
783 #[rstest]
784 fn marginal_cost_example(
785 sed_commodity: Commodity,
786 other_commodity: Commodity,
787 region_id: RegionID,
788 time_slice_info: TimeSliceInfo,
789 time_slice: TimeSliceID,
790 ) {
791 let mut a = sed_commodity.clone();
793 a.id = "A".into();
794 let mut b = sed_commodity.clone();
795 b.id = "B".into();
796 let mut c = sed_commodity.clone();
797 c.id = "C".into();
798 let mut d = other_commodity.clone();
799 d.id = "D".into();
800
801 let mut flows = IndexMap::new();
802 flows.insert(
803 a.id.clone(),
804 build_process_flow(&a, -1.0, MoneyPerFlow(0.0)),
805 );
806 flows.insert(b.id.clone(), build_process_flow(&b, 1.0, MoneyPerFlow(0.0)));
807 flows.insert(c.id.clone(), build_process_flow(&c, 2.0, MoneyPerFlow(3.0)));
808 flows.insert(d.id.clone(), build_process_flow(&d, 1.0, MoneyPerFlow(4.0)));
809
810 let process = build_process(
811 flows,
812 ®ion_id,
813 2015u32,
814 &time_slice_info,
815 MoneyPerActivity(5.0), MoneyPerCapacity(0.0), 5, Dimensionless(1.0), );
820
821 let asset =
822 Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(1.0), 2015u32)
823 .unwrap();
824 let asset_ref = AssetRef::from(asset);
825 let shadow_prices =
826 CommodityPrices::from_iter(vec![(&a.id, ®ion_id, &time_slice, MoneyPerFlow(1.0))]);
827 let mut markets = HashSet::new();
828 markets.insert((b.id.clone(), region_id.clone()));
829 markets.insert((c.id.clone(), region_id.clone()));
830
831 let existing = vec![(&asset_ref, &time_slice, Activity(1.0))];
832 let candidates = Vec::new();
833
834 let prices = calculate_marginal_cost_prices(
835 existing.into_iter(),
836 candidates.into_iter(),
837 &shadow_prices,
838 2015u32,
839 &markets,
840 );
841
842 assert_price_approx(
843 &prices,
844 &b.id,
845 ®ion_id,
846 &time_slice,
847 MoneyPerFlow(10.0 / 3.0),
848 );
849 assert_price_approx(
850 &prices,
851 &c.id,
852 ®ion_id,
853 &time_slice,
854 MoneyPerFlow(10.0 / 3.0 + 3.0),
855 );
856 }
857
858 #[rstest]
859 fn full_cost_example(
860 sed_commodity: Commodity,
861 other_commodity: Commodity,
862 region_id: RegionID,
863 time_slice_info: TimeSliceInfo,
864 time_slice: TimeSliceID,
865 ) {
866 let mut a = sed_commodity.clone();
868 a.id = "A".into();
869 let mut b = sed_commodity.clone();
870 b.id = "B".into();
871 let mut c = sed_commodity.clone();
872 c.id = "C".into();
873 let mut d = other_commodity.clone();
874 d.id = "D".into();
875
876 let mut flows = IndexMap::new();
877 flows.insert(
878 a.id.clone(),
879 build_process_flow(&a, -1.0, MoneyPerFlow(0.0)),
880 );
881 flows.insert(b.id.clone(), build_process_flow(&b, 1.0, MoneyPerFlow(0.0)));
882 flows.insert(c.id.clone(), build_process_flow(&c, 2.0, MoneyPerFlow(3.0)));
883 flows.insert(d.id.clone(), build_process_flow(&d, 1.0, MoneyPerFlow(4.0)));
884
885 let process = build_process(
886 flows,
887 ®ion_id,
888 2015u32,
889 &time_slice_info,
890 MoneyPerActivity(5.0), MoneyPerCapacity(2.5), 1, Dimensionless(0.0), );
895
896 let asset =
897 Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(4.0), 2015u32)
898 .unwrap();
899 let asset_ref = AssetRef::from(asset);
900 let shadow_prices =
901 CommodityPrices::from_iter(vec![(&a.id, ®ion_id, &time_slice, MoneyPerFlow(1.0))]);
902 let mut markets = HashSet::new();
903 markets.insert((b.id.clone(), region_id.clone()));
904 markets.insert((c.id.clone(), region_id.clone()));
905
906 let existing = vec![(&asset_ref, &time_slice, Activity(2.0))];
907 let candidates = Vec::new();
908
909 let mut annual_activities = HashMap::new();
910 annual_activities.insert(asset_ref.clone(), Activity(2.0));
911
912 let prices = calculate_full_cost_prices(
913 existing.into_iter(),
914 candidates.into_iter(),
915 &annual_activities,
916 &shadow_prices,
917 2015u32,
918 &markets,
919 );
920
921 assert_price_approx(&prices, &b.id, ®ion_id, &time_slice, MoneyPerFlow(5.0));
922 assert_price_approx(&prices, &c.id, ®ion_id, &time_slice, MoneyPerFlow(8.0));
923 }
924}