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 std::collections::{HashMap, HashSet};
12
13pub fn calculate_prices(model: &Model, solution: &Solution, year: u32) -> Result<CommodityPrices> {
29 let shadow_prices = CommodityPrices::from_iter(solution.iter_commodity_balance_duals());
31
32 let mut result = CommodityPrices::default();
34
35 let investment_order = &model.investment_order[&year];
37
38 for investment_set in investment_order.iter().rev() {
42 let mut pricing_sets = HashMap::new();
45 for (commodity_id, region_id) in investment_set.iter_markets() {
46 let commodity = &model.commodities[commodity_id];
47 if commodity.pricing_strategy == PricingStrategy::Unpriced {
48 continue;
49 }
50 pricing_sets
51 .entry(&commodity.pricing_strategy)
52 .or_insert_with(HashSet::new)
53 .insert((commodity_id.clone(), region_id.clone()));
54 }
55
56 if let Some(shadow_set) = pricing_sets.get(&PricingStrategy::Shadow) {
58 for (commodity_id, region_id, time_slice) in shadow_prices.keys() {
59 if shadow_set.contains(&(commodity_id.clone(), region_id.clone())) {
60 let price = shadow_prices
61 .get(commodity_id, region_id, time_slice)
62 .unwrap();
63 result.insert(commodity_id, region_id, time_slice, price);
64 }
65 }
66 }
67
68 if let Some(scarcity_set) = pricing_sets.get(&PricingStrategy::ScarcityAdjusted) {
70 let scarcity_prices = calculate_scarcity_adjusted_prices(
71 solution.iter_activity_duals(),
72 &shadow_prices,
73 scarcity_set,
74 );
75 result.extend(scarcity_prices);
76 }
77
78 if let Some(marginal_set) = pricing_sets.get(&PricingStrategy::MarginalCost) {
80 let marginal_cost_prices = calculate_marginal_cost_prices(
81 solution.iter_activity_keys_for_existing(),
82 solution.iter_activity_keys_for_candidates(),
83 &result,
84 year,
85 marginal_set,
86 );
87 result.extend(marginal_cost_prices);
88 }
89
90 if let Some(fullcost_set) = pricing_sets.get(&PricingStrategy::FullCost) {
92 let annual_activities =
93 calculate_annual_activities(solution.iter_activity_for_existing());
94 let full_cost_prices = calculate_full_cost_prices(
95 solution.iter_activity_keys_for_existing(),
96 solution.iter_activity_keys_for_candidates(),
97 &annual_activities,
98 &result,
99 year,
100 fullcost_set,
101 );
102 result.extend(full_cost_prices);
103 }
104 }
105
106 Ok(result)
108}
109
110#[derive(Default, Clone)]
112pub struct CommodityPrices(IndexMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>);
113
114impl CommodityPrices {
115 pub fn insert(
117 &mut self,
118 commodity_id: &CommodityID,
119 region_id: &RegionID,
120 time_slice: &TimeSliceID,
121 price: MoneyPerFlow,
122 ) {
123 let key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
124 self.0.insert(key, price);
125 }
126
127 pub fn extend<T>(&mut self, iter: T)
129 where
130 T: IntoIterator<Item = ((CommodityID, RegionID, TimeSliceID), MoneyPerFlow)>,
131 {
132 for (key, price) in iter {
133 let existing = self.0.insert(key.clone(), price).is_some();
134 assert!(!existing, "Key {key:?} already exists in the map");
135 }
136 }
137
138 pub fn iter(
144 &self,
145 ) -> impl Iterator<Item = (&CommodityID, &RegionID, &TimeSliceID, MoneyPerFlow)> {
146 self.0
147 .iter()
148 .map(|((commodity_id, region_id, ts), price)| (commodity_id, region_id, ts, *price))
149 }
150
151 pub fn get(
153 &self,
154 commodity_id: &CommodityID,
155 region_id: &RegionID,
156 time_slice: &TimeSliceID,
157 ) -> Option<MoneyPerFlow> {
158 self.0
159 .get(&(commodity_id.clone(), region_id.clone(), time_slice.clone()))
160 .copied()
161 }
162
163 pub fn keys(
165 &self,
166 ) -> indexmap::map::Keys<'_, (CommodityID, RegionID, TimeSliceID), MoneyPerFlow> {
167 self.0.keys()
168 }
169
170 fn time_slice_weighted_averages(
178 &self,
179 time_slice_info: &TimeSliceInfo,
180 ) -> HashMap<(CommodityID, RegionID), MoneyPerFlow> {
181 let mut weighted_prices = HashMap::new();
182
183 for ((commodity_id, region_id, time_slice_id), price) in &self.0 {
184 let weight = time_slice_info.time_slices[time_slice_id] / Year(1.0);
186 let key = (commodity_id.clone(), region_id.clone());
187 weighted_prices
188 .entry(key)
189 .and_modify(|v| *v += *price * weight)
190 .or_insert_with(|| *price * weight);
191 }
192
193 weighted_prices
194 }
195
196 pub fn within_tolerance_weighted(
206 &self,
207 other: &Self,
208 tolerance: Dimensionless,
209 time_slice_info: &TimeSliceInfo,
210 ) -> bool {
211 let self_averages = self.time_slice_weighted_averages(time_slice_info);
212 let other_averages = other.time_slice_weighted_averages(time_slice_info);
213
214 for (key, &price) in &self_averages {
215 let other_price = other_averages[key];
216 let abs_diff = (price - other_price).abs();
217
218 if price == MoneyPerFlow(0.0) {
220 if other_price != MoneyPerFlow(0.0) {
222 return false;
223 }
224 } else if abs_diff / price.abs() > tolerance {
226 return false;
227 }
228 }
229 true
230 }
231}
232
233impl<'a> FromIterator<(&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>
234 for CommodityPrices
235{
236 fn from_iter<I>(iter: I) -> Self
237 where
238 I: IntoIterator<Item = (&'a CommodityID, &'a RegionID, &'a TimeSliceID, MoneyPerFlow)>,
239 {
240 let map = iter
241 .into_iter()
242 .map(|(commodity_id, region_id, time_slice, price)| {
243 (
244 (commodity_id.clone(), region_id.clone(), time_slice.clone()),
245 price,
246 )
247 })
248 .collect();
249 CommodityPrices(map)
250 }
251}
252
253impl IntoIterator for CommodityPrices {
254 type Item = ((CommodityID, RegionID, TimeSliceID), MoneyPerFlow);
255 type IntoIter = indexmap::map::IntoIter<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>;
256
257 fn into_iter(self) -> Self::IntoIter {
258 self.0.into_iter()
259 }
260}
261
262fn calculate_scarcity_adjusted_prices<'a, I>(
274 activity_duals: I,
275 shadow_prices: &CommodityPrices,
276 markets_to_price: &HashSet<(CommodityID, RegionID)>,
277) -> HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>
278where
279 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID, MoneyPerActivity)>,
280{
281 let mut highest_duals = HashMap::new();
283 for (asset, time_slice, dual) in activity_duals {
284 let region_id = asset.region_id();
285
286 for flow in asset.iter_output_flows().filter(|flow| {
289 markets_to_price.contains(&(flow.commodity.id.clone(), region_id.clone()))
290 }) {
291 highest_duals
293 .entry((
294 flow.commodity.id.clone(),
295 region_id.clone(),
296 time_slice.clone(),
297 ))
298 .and_modify(|current_dual| {
299 if dual > *current_dual {
300 *current_dual = dual;
301 }
302 })
303 .or_insert(dual);
304 }
305 }
306
307 let mut scarcity_prices = HashMap::new();
309 for ((commodity, region, time_slice), highest_dual) in &highest_duals {
310 let shadow_price = shadow_prices.get(commodity, region, time_slice).unwrap();
313 let scarcity_price = shadow_price + MoneyPerFlow(highest_dual.value());
316 scarcity_prices.insert(
317 (commodity.clone(), region.clone(), time_slice.clone()),
318 scarcity_price,
319 );
320 }
321
322 scarcity_prices
323}
324
325fn calculate_marginal_cost_prices<'a, I, J>(
377 activity_keys_for_existing: I,
378 activity_keys_for_candidates: J,
379 upstream_prices: &CommodityPrices,
380 year: u32,
381 markets_to_price: &HashSet<(CommodityID, RegionID)>,
382) -> HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>
383where
384 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
385 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
386{
387 let mut prices: HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow> = HashMap::new();
388
389 let mut priced_by_existing = HashSet::new();
393 for (asset, time_slice) in activity_keys_for_existing {
394 let region_id = asset.region_id();
395
396 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
398 upstream_prices,
399 year,
400 time_slice,
401 |commodity_id: &CommodityID| {
402 markets_to_price.contains(&(commodity_id.clone(), region_id.clone()))
403 },
404 ) {
405 let key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
407 prices
408 .entry(key.clone())
409 .and_modify(|c| *c = c.max(marginal_cost))
410 .or_insert(marginal_cost);
411 priced_by_existing.insert(key);
412 }
413 }
414
415 for (asset, time_slice) in activity_keys_for_candidates {
418 let region_id = asset.region_id();
419
420 let should_process = |cid: &CommodityID| {
422 markets_to_price.contains(&(cid.clone(), region_id.clone()))
423 && !priced_by_existing.contains(&(
424 cid.clone(),
425 region_id.clone(),
426 time_slice.clone(),
427 ))
428 };
429
430 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
432 upstream_prices,
433 year,
434 time_slice,
435 |cid: &CommodityID| should_process(cid),
436 ) {
437 let key = (commodity_id.clone(), region_id.clone(), time_slice.clone());
439 prices
440 .entry(key.clone())
441 .and_modify(|c| *c = c.min(marginal_cost))
442 .or_insert(marginal_cost);
443 }
444 }
445
446 prices
448}
449
450fn calculate_annual_activities<'a, I>(activities: I) -> HashMap<AssetRef, Activity>
452where
453 I: IntoIterator<Item = (&'a AssetRef, &'a TimeSliceID, Activity)>,
454{
455 activities
456 .into_iter()
457 .map(|(asset, _ts, activity)| (asset.clone(), activity))
458 .fold(HashMap::new(), |mut acc, (asset, activity)| {
459 acc.entry(asset)
460 .and_modify(|e| *e += activity)
461 .or_insert(activity);
462 acc
463 })
464}
465
466fn calculate_full_cost_prices<'a, I, J>(
523 activity_keys_for_existing: I,
524 activity_keys_for_candidates: J,
525 annual_activities: &HashMap<AssetRef, Activity>,
526 upstream_prices: &CommodityPrices,
527 year: u32,
528 markets_to_price: &HashSet<(CommodityID, RegionID)>,
529) -> HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>
530where
531 I: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
532 J: Iterator<Item = (&'a AssetRef, &'a TimeSliceID)>,
533{
534 let mut prices: HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow> = HashMap::new();
535
536 let mut annual_fixed_costs_cache = HashMap::new();
540 let mut priced_by_existing = HashSet::new();
541 for (asset, time_slice) in activity_keys_for_existing {
542 let annual_activity = annual_activities[asset];
543 let region_id = asset.region_id();
544
545 if annual_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_fixed_costs_per_flow = *annual_fixed_costs_cache
561 .entry(asset.clone())
562 .or_insert_with(|| asset.get_annual_fixed_costs_per_flow(annual_activity));
563
564 for (commodity_id, marginal_cost) in asset.iter_marginal_costs_with_filter(
566 upstream_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_fixed_costs_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) in activity_keys_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_fixed_costs_per_flow = *annual_fixed_costs_cache
610 .entry(asset.clone())
611 .or_insert_with(|| {
612 asset.get_annual_fixed_costs_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 upstream_prices,
622 year,
623 time_slice,
624 |cid: &CommodityID| should_process(cid),
625 ) {
626 let full_cost = marginal_cost + annual_fixed_costs_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 fixed_operating_cost: MoneyPerCapacityPerYear,
681 capital_cost: MoneyPerCapacity,
682 lifetime: u32,
683 discount_rate: Dimensionless,
684 ) -> Process {
685 let mut process_flows_map = HashMap::new();
686 process_flows_map.insert((region_id.clone(), year), Rc::new(flows));
687
688 let mut process_parameter_map = HashMap::new();
689 let proc_param = ProcessParameter {
690 capital_cost,
691 fixed_operating_cost,
692 variable_operating_cost,
693 lifetime,
694 discount_rate,
695 };
696 process_parameter_map.insert((region_id.clone(), year), Rc::new(proc_param));
697
698 let mut activity_limits_map = HashMap::new();
699 activity_limits_map.insert(
700 (region_id.clone(), year),
701 Rc::new(ActivityLimits::new_with_full_availability(time_slice_info)),
702 );
703
704 let regions: IndexSet<RegionID> = IndexSet::from([region_id.clone()]);
705
706 Process {
707 id: "p1".into(),
708 description: "test process".into(),
709 years: 2010..=2020,
710 activity_limits: activity_limits_map,
711 flows: process_flows_map,
712 parameters: process_parameter_map,
713 regions,
714 primary_output: None,
715 capacity_to_activity: ActivityPerCapacity(1.0),
716 investment_constraints: HashMap::new(),
717 unit_size: None,
718 }
719 }
720
721 fn assert_price_approx(
722 prices: &HashMap<(CommodityID, RegionID, TimeSliceID), MoneyPerFlow>,
723 commodity: &CommodityID,
724 region: &RegionID,
725 time_slice: &TimeSliceID,
726 expected: MoneyPerFlow,
727 ) {
728 let p = prices[&(commodity.clone(), region.clone(), time_slice.clone())];
729 assert!((p - expected).abs() < MoneyPerFlow::EPSILON);
730 }
731
732 #[rstest]
733 #[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(
744 #[case] price1: MoneyPerFlow,
745 #[case] price2: MoneyPerFlow,
746 #[case] tolerance: Dimensionless,
747 #[case] expected: bool,
748 time_slice_info: TimeSliceInfo,
749 time_slice: TimeSliceID,
750 ) {
751 let mut prices1 = CommodityPrices::default();
752 let mut prices2 = CommodityPrices::default();
753
754 let commodity = CommodityID::new("test_commodity");
756 let region = RegionID::new("test_region");
757 prices1.insert(&commodity, ®ion, &time_slice, price1);
758 prices2.insert(&commodity, ®ion, &time_slice, price2);
759
760 assert_eq!(
761 prices1.within_tolerance_weighted(&prices2, tolerance, &time_slice_info),
762 expected
763 );
764 }
765
766 #[rstest]
767 fn time_slice_weighted_averages(
768 commodity_id: CommodityID,
769 region_id: RegionID,
770 time_slice_info: TimeSliceInfo,
771 time_slice: TimeSliceID,
772 ) {
773 let mut prices = CommodityPrices::default();
774
775 prices.insert(&commodity_id, ®ion_id, &time_slice, MoneyPerFlow(100.0));
777
778 let averages = prices.time_slice_weighted_averages(&time_slice_info);
779
780 assert_eq!(averages[&(commodity_id, region_id)], MoneyPerFlow(100.0));
782 }
783
784 #[rstest]
785 fn marginal_cost_example(
786 sed_commodity: Commodity,
787 other_commodity: Commodity,
788 region_id: RegionID,
789 time_slice_info: TimeSliceInfo,
790 time_slice: TimeSliceID,
791 ) {
792 let mut a = sed_commodity.clone();
794 a.id = "A".into();
795 let mut b = sed_commodity.clone();
796 b.id = "B".into();
797 let mut c = sed_commodity.clone();
798 c.id = "C".into();
799 let mut d = other_commodity.clone();
800 d.id = "D".into();
801
802 let mut flows = IndexMap::new();
803 flows.insert(
804 a.id.clone(),
805 build_process_flow(&a, -1.0, MoneyPerFlow(0.0)),
806 );
807 flows.insert(b.id.clone(), build_process_flow(&b, 1.0, MoneyPerFlow(0.0)));
808 flows.insert(c.id.clone(), build_process_flow(&c, 2.0, MoneyPerFlow(3.0)));
809 flows.insert(d.id.clone(), build_process_flow(&d, 1.0, MoneyPerFlow(4.0)));
810
811 let process = build_process(
812 flows,
813 ®ion_id,
814 2015u32,
815 &time_slice_info,
816 MoneyPerActivity(5.0), MoneyPerCapacityPerYear(0.0), MoneyPerCapacity(0.0), 5, Dimensionless(1.0), );
822
823 let asset =
824 Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(1.0), 2015u32)
825 .unwrap();
826 let asset_ref = AssetRef::from(asset);
827 let shadow_prices =
828 CommodityPrices::from_iter(vec![(&a.id, ®ion_id, &time_slice, MoneyPerFlow(1.0))]);
829 let mut markets = HashSet::new();
830 markets.insert((b.id.clone(), region_id.clone()));
831 markets.insert((c.id.clone(), region_id.clone()));
832
833 let existing = vec![(&asset_ref, &time_slice)];
834 let candidates = Vec::new();
835
836 let prices = calculate_marginal_cost_prices(
837 existing.into_iter(),
838 candidates.into_iter(),
839 &shadow_prices,
840 2015u32,
841 &markets,
842 );
843
844 assert_price_approx(
845 &prices,
846 &b.id,
847 ®ion_id,
848 &time_slice,
849 MoneyPerFlow(10.0 / 3.0),
850 );
851 assert_price_approx(
852 &prices,
853 &c.id,
854 ®ion_id,
855 &time_slice,
856 MoneyPerFlow(10.0 / 3.0 + 3.0),
857 );
858 }
859
860 #[rstest]
861 fn full_cost_example(
862 sed_commodity: Commodity,
863 other_commodity: Commodity,
864 region_id: RegionID,
865 time_slice_info: TimeSliceInfo,
866 time_slice: TimeSliceID,
867 ) {
868 let mut a = sed_commodity.clone();
870 a.id = "A".into();
871 let mut b = sed_commodity.clone();
872 b.id = "B".into();
873 let mut c = sed_commodity.clone();
874 c.id = "C".into();
875 let mut d = other_commodity.clone();
876 d.id = "D".into();
877
878 let mut flows = IndexMap::new();
879 flows.insert(
880 a.id.clone(),
881 build_process_flow(&a, -1.0, MoneyPerFlow(0.0)),
882 );
883 flows.insert(b.id.clone(), build_process_flow(&b, 1.0, MoneyPerFlow(0.0)));
884 flows.insert(c.id.clone(), build_process_flow(&c, 2.0, MoneyPerFlow(3.0)));
885 flows.insert(d.id.clone(), build_process_flow(&d, 1.0, MoneyPerFlow(4.0)));
886
887 let process = build_process(
888 flows,
889 ®ion_id,
890 2015u32,
891 &time_slice_info,
892 MoneyPerActivity(5.0), MoneyPerCapacityPerYear(1.0), MoneyPerCapacity(1.5), 1, Dimensionless(0.0), );
898
899 let asset =
900 Asset::new_candidate(Rc::new(process), region_id.clone(), Capacity(4.0), 2015u32)
901 .unwrap();
902 let asset_ref = AssetRef::from(asset);
903 let shadow_prices =
904 CommodityPrices::from_iter(vec![(&a.id, ®ion_id, &time_slice, MoneyPerFlow(1.0))]);
905 let mut markets = HashSet::new();
906 markets.insert((b.id.clone(), region_id.clone()));
907 markets.insert((c.id.clone(), region_id.clone()));
908
909 let existing = vec![(&asset_ref, &time_slice)];
910 let candidates = Vec::new();
911
912 let mut annual_activities = HashMap::new();
913 annual_activities.insert(asset_ref.clone(), Activity(2.0));
914
915 let prices = calculate_full_cost_prices(
916 existing.into_iter(),
917 candidates.into_iter(),
918 &annual_activities,
919 &shadow_prices,
920 2015u32,
921 &markets,
922 );
923
924 assert_price_approx(&prices, &b.id, ®ion_id, &time_slice, MoneyPerFlow(5.0));
925 assert_price_approx(&prices, &c.id, ®ion_id, &time_slice, MoneyPerFlow(8.0));
926 }
927}