1use crate::commodity::{Commodity, CommodityID};
4use crate::id::define_id_type;
5use crate::region::RegionID;
6use crate::time_slice::{Season, TimeSliceID, TimeSliceInfo, TimeSliceSelection};
7use crate::units::{
8 ActivityPerCapacity, Capacity, Dimensionless, FlowPerActivity, MoneyPerActivity,
9 MoneyPerCapacity, MoneyPerCapacityPerYear, MoneyPerFlow,
10};
11use anyhow::{Result, ensure};
12use indexmap::{IndexMap, IndexSet};
13use itertools::Itertools;
14use serde_string_enum::DeserializeLabeledStringEnum;
15use std::collections::HashMap;
16use std::ops::RangeInclusive;
17use std::rc::Rc;
18
19define_id_type! {ProcessID}
20
21pub type ProcessMap = IndexMap<ProcessID, Rc<Process>>;
23
24pub type ProcessActivityLimitsMap = HashMap<(RegionID, u32), Rc<ActivityLimits>>;
26
27pub type ProcessParameterMap = HashMap<(RegionID, u32), Rc<ProcessParameter>>;
29
30pub type ProcessFlowsMap = HashMap<(RegionID, u32), Rc<IndexMap<CommodityID, ProcessFlow>>>;
34
35pub type ProcessInvestmentConstraintsMap =
37 HashMap<(RegionID, u32), Rc<ProcessInvestmentConstraint>>;
38
39#[derive(PartialEq, Debug)]
41pub struct Process {
42 pub id: ProcessID,
44 pub description: String,
46 pub years: RangeInclusive<u32>,
48 pub activity_limits: ProcessActivityLimitsMap,
50 pub flows: ProcessFlowsMap,
52 pub parameters: ProcessParameterMap,
54 pub regions: IndexSet<RegionID>,
56 pub primary_output: Option<CommodityID>,
58 pub capacity_to_activity: ActivityPerCapacity,
64 pub investment_constraints: ProcessInvestmentConstraintsMap,
66 pub unit_size: Option<Capacity>,
72}
73
74impl Process {
75 pub fn active_for_year(&self, year: u32) -> bool {
77 self.years.contains(&year)
78 }
79}
80
81#[derive(PartialEq, Debug, Clone)]
103pub struct ActivityLimits {
104 annual_limit: Option<RangeInclusive<Dimensionless>>,
106 seasonal_limits: IndexMap<Season, RangeInclusive<Dimensionless>>,
108 time_slice_limits: IndexMap<TimeSliceID, RangeInclusive<Dimensionless>>,
110}
111
112impl ActivityLimits {
113 pub fn new_with_full_availability(time_slice_info: &TimeSliceInfo) -> Self {
115 let mut ts_limits = IndexMap::new();
117 for (ts_id, ts_length) in time_slice_info.iter() {
118 ts_limits.insert(
119 ts_id.clone(),
120 Dimensionless(0.0)..=Dimensionless(ts_length.value()),
121 );
122 }
123
124 ActivityLimits {
125 annual_limit: None,
126 seasonal_limits: IndexMap::new(),
127 time_slice_limits: ts_limits,
128 }
129 }
130
131 pub fn new_from_limits(
154 limits: &HashMap<TimeSliceSelection, RangeInclusive<Dimensionless>>,
155 time_slice_info: &TimeSliceInfo,
156 ) -> Result<Self> {
157 let mut result = ActivityLimits::new_with_full_availability(time_slice_info);
158
159 let mut time_slices_added = IndexSet::new();
161 for (ts_selection, limit) in limits {
162 if let TimeSliceSelection::Single(ts_id) = ts_selection {
163 result.add_time_slice_limit(ts_id.clone(), limit.clone());
164 time_slices_added.insert(ts_id.clone());
165 }
166 }
167
168 if !time_slices_added.is_empty() {
170 let missing = time_slice_info
171 .iter_ids()
172 .filter(|ts_id| !time_slices_added.contains(*ts_id))
173 .collect::<Vec<_>>();
174 ensure!(
175 missing.is_empty(),
176 "Missing availability limits for time slices: [{}]. Please provide",
177 missing.iter().join(", ")
178 );
179 }
180
181 let mut seasons_added = IndexSet::new();
184 for (ts_selection, limit) in limits {
185 if let TimeSliceSelection::Season(season) = ts_selection {
186 result.add_seasonal_limit(season.clone(), limit.clone())?;
187 seasons_added.insert(season.clone());
188 }
189 }
190
191 if !seasons_added.is_empty() {
193 let missing = time_slice_info
194 .iter_seasons()
195 .filter(|season| !seasons_added.contains(*season))
196 .collect::<Vec<_>>();
197 ensure!(
198 missing.is_empty(),
199 "Missing availability limits for seasons: [{}]. Please provide",
200 missing.iter().join(", "),
201 );
202 }
203
204 if let Some(limit) = limits.get(&TimeSliceSelection::Annual) {
207 result.add_annual_limit(limit.clone())?;
208 }
209
210 Ok(result)
211 }
212
213 pub fn add_time_slice_limit(
215 &mut self,
216 ts_id: TimeSliceID,
217 limit: RangeInclusive<Dimensionless>,
218 ) {
219 self.time_slice_limits.insert(ts_id, limit);
220 }
221
222 fn add_seasonal_limit(
224 &mut self,
225 season: Season,
226 limit: RangeInclusive<Dimensionless>,
227 ) -> Result<()> {
228 let current_limit = self.get_limit_for_season(&season);
230
231 ensure!(
234 *limit.start() <= *current_limit.end() && *limit.end() >= *current_limit.start(),
235 "Availability limit for season {season} clashes with time slice limits",
236 );
237
238 if *limit.start() > *current_limit.start() || *limit.end() < *current_limit.end() {
243 self.seasonal_limits.insert(season, limit);
244 }
245
246 Ok(())
247 }
248
249 fn add_annual_limit(&mut self, limit: RangeInclusive<Dimensionless>) -> Result<()> {
251 let current_limit = self.get_limit_for_year();
253
254 ensure!(
257 *limit.start() <= *current_limit.end() && *limit.end() >= *current_limit.start(),
258 "Annual availability limit clashes with time slice/seasonal limits",
259 );
260
261 if *limit.start() > *current_limit.start() || *limit.end() < *current_limit.end() {
265 self.annual_limit = Some(limit);
266 }
267
268 Ok(())
269 }
270
271 pub fn get_limit(
273 &self,
274 time_slice_selection: &TimeSliceSelection,
275 ) -> RangeInclusive<Dimensionless> {
276 match time_slice_selection {
277 TimeSliceSelection::Single(ts_id) => self.get_limit_for_time_slice(ts_id),
278 TimeSliceSelection::Season(season) => self.get_limit_for_season(season),
279 TimeSliceSelection::Annual => self.get_limit_for_year(),
280 }
281 }
282
283 pub fn get_limit_for_time_slice(
285 &self,
286 time_slice: &TimeSliceID,
287 ) -> RangeInclusive<Dimensionless> {
288 let ts_limit = self.time_slice_limits[time_slice].clone();
290 let lower = *ts_limit.start();
291 let mut upper = *ts_limit.end();
292
293 if let Some(seasonal_limit) = self.seasonal_limits.get(&time_slice.season) {
296 upper = upper.min(*seasonal_limit.end());
297 }
298 if let Some(annual_limit) = &self.annual_limit {
299 upper = upper.min(*annual_limit.end());
300 }
301
302 lower..=upper
303 }
304
305 fn get_limit_for_season(&self, season: &Season) -> RangeInclusive<Dimensionless> {
307 let mut lower = Dimensionless(0.0);
309 let mut upper = Dimensionless(0.0);
310 for (ts, limit) in &self.time_slice_limits {
311 if &ts.season == season {
312 lower += *limit.start();
313 upper += *limit.end();
314 }
315 }
316
317 if let Some(seasonal_limit) = self.seasonal_limits.get(season) {
319 lower = lower.max(*seasonal_limit.start());
320 upper = upper.min(*seasonal_limit.end());
321 }
322
323 if let Some(annual_limit) = &self.annual_limit {
326 upper = upper.min(*annual_limit.end());
327 }
328
329 lower..=upper
330 }
331
332 fn get_limit_for_year(&self) -> RangeInclusive<Dimensionless> {
334 let mut total_lower = Dimensionless(0.0);
336 let mut total_upper = Dimensionless(0.0);
337 let seasons = self
338 .time_slice_limits
339 .keys()
340 .map(|ts_id| ts_id.season.clone())
341 .unique();
342 for season in seasons {
343 let season_limit = self.get_limit_for_season(&season);
344 total_lower += *season_limit.start();
345 total_upper += *season_limit.end();
346 }
347
348 if let Some(annual_limit) = &self.annual_limit {
350 total_lower = total_lower.max(*annual_limit.start());
351 total_upper = total_upper.min(*annual_limit.end());
352 }
353
354 total_lower..=total_upper
355 }
356
357 pub fn iter_limits(
362 &self,
363 ) -> impl Iterator<Item = (TimeSliceSelection, &RangeInclusive<Dimensionless>)> {
364 let time_slice_limits = self
366 .time_slice_limits
367 .iter()
368 .map(|(ts_id, limit)| (TimeSliceSelection::Single(ts_id.clone()), limit));
369
370 let seasonal_limits = self
372 .seasonal_limits
373 .iter()
374 .map(|(season, limit)| (TimeSliceSelection::Season(season.clone()), limit));
375
376 let annual_limits = self
378 .annual_limit
379 .as_ref()
380 .map(|limit| (TimeSliceSelection::Annual, limit));
381
382 time_slice_limits
384 .chain(seasonal_limits)
385 .chain(annual_limits)
386 }
387}
388
389#[derive(PartialEq, Debug, Clone)]
391pub struct ProcessFlow {
392 pub commodity: Rc<Commodity>,
394 pub coeff: FlowPerActivity,
398 pub kind: FlowType,
400 pub cost: MoneyPerFlow,
405}
406
407impl ProcessFlow {
408 pub fn get_total_cost_per_flow(
412 &self,
413 region_id: &RegionID,
414 year: u32,
415 time_slice: &TimeSliceID,
416 ) -> MoneyPerFlow {
417 self.cost + self.get_levy(region_id, year, time_slice)
418 }
419
420 pub fn get_total_cost_per_activity(
424 &self,
425 region_id: &RegionID,
426 year: u32,
427 time_slice: &TimeSliceID,
428 ) -> MoneyPerActivity {
429 let cost_per_unit = self.get_total_cost_per_flow(region_id, year, time_slice);
430 self.coeff.abs() * cost_per_unit
431 }
432
433 fn get_levy(&self, region_id: &RegionID, year: u32, time_slice: &TimeSliceID) -> MoneyPerFlow {
435 match self.direction() {
436 FlowDirection::Input => *self
437 .commodity
438 .levies_cons
439 .get(&(region_id.clone(), year, time_slice.clone()))
440 .unwrap_or(&MoneyPerFlow(0.0)),
441 FlowDirection::Output => *self
442 .commodity
443 .levies_prod
444 .get(&(region_id.clone(), year, time_slice.clone()))
445 .unwrap_or(&MoneyPerFlow(0.0)),
446 FlowDirection::Zero => MoneyPerFlow(0.0),
447 }
448 }
449
450 pub fn direction(&self) -> FlowDirection {
452 match self.coeff {
453 x if x < FlowPerActivity(0.0) => FlowDirection::Input,
454 x if x > FlowPerActivity(0.0) => FlowDirection::Output,
455 _ => FlowDirection::Zero,
456 }
457 }
458}
459
460#[derive(PartialEq, Default, Debug, Clone, DeserializeLabeledStringEnum)]
462pub enum FlowType {
463 #[default]
465 #[string = "fixed"]
466 Fixed,
467 #[string = "flexible"]
470 Flexible,
471}
472
473#[derive(PartialEq, Debug)]
475pub enum FlowDirection {
476 Input,
478 Output,
480 Zero,
482}
483
484#[derive(PartialEq, Clone, Debug)]
486pub struct ProcessParameter {
487 pub capital_cost: MoneyPerCapacity,
489 pub fixed_operating_cost: MoneyPerCapacityPerYear,
491 pub variable_operating_cost: MoneyPerActivity,
493 pub lifetime: u32,
495 pub discount_rate: Dimensionless,
497}
498
499#[derive(PartialEq, Debug, Clone)]
504pub struct ProcessInvestmentConstraint {
505 pub addition_limit: Option<Capacity>,
508}
509
510impl ProcessInvestmentConstraint {
511 pub fn get_addition_limit(&self) -> Option<Capacity> {
517 self.addition_limit
518 }
519}
520
521#[cfg(test)]
522mod tests {
523 use super::*;
524 use crate::commodity::{CommodityLevyMap, CommodityType, DemandMap, PricingStrategy};
525 use crate::fixture::{assert_error, region_id, time_slice, time_slice_info2};
526 use crate::time_slice::TimeSliceLevel;
527 use crate::time_slice::TimeSliceSelection;
528 use float_cmp::assert_approx_eq;
529 use rstest::{fixture, rstest};
530 use std::collections::HashMap;
531 use std::rc::Rc;
532
533 #[fixture]
534 fn commodity_with_levy(region_id: RegionID, time_slice: TimeSliceID) -> Rc<Commodity> {
535 let mut levies_prod = CommodityLevyMap::new();
536 let mut levies_cons = CommodityLevyMap::new();
537
538 levies_prod.insert(
540 (region_id.clone(), 2020, time_slice.clone()),
541 MoneyPerFlow(10.0),
542 );
543 levies_cons.insert(
544 (region_id.clone(), 2020, time_slice.clone()),
545 MoneyPerFlow(-10.0),
546 );
547 levies_prod.insert(("USA".into(), 2020, time_slice.clone()), MoneyPerFlow(5.0));
549 levies_cons.insert(("USA".into(), 2020, time_slice.clone()), MoneyPerFlow(-5.0));
550 levies_prod.insert(
552 (region_id.clone(), 2030, time_slice.clone()),
553 MoneyPerFlow(7.0),
554 );
555 levies_cons.insert(
556 (region_id.clone(), 2030, time_slice.clone()),
557 MoneyPerFlow(-7.0),
558 );
559 levies_prod.insert(
561 (
562 region_id.clone(),
563 2020,
564 TimeSliceID {
565 season: "summer".into(),
566 time_of_day: "day".into(),
567 },
568 ),
569 MoneyPerFlow(3.0),
570 );
571 levies_cons.insert(
572 (
573 region_id.clone(),
574 2020,
575 TimeSliceID {
576 season: "summer".into(),
577 time_of_day: "day".into(),
578 },
579 ),
580 MoneyPerFlow(-3.0),
581 );
582
583 Rc::new(Commodity {
584 id: "test_commodity".into(),
585 description: "Test commodity".into(),
586 kind: CommodityType::ServiceDemand,
587 time_slice_level: TimeSliceLevel::Annual,
588 pricing_strategy: PricingStrategy::Shadow,
589 levies_prod,
590 levies_cons,
591 demand: DemandMap::new(),
592 units: "PJ".into(),
593 })
594 }
595
596 #[fixture]
597 fn commodity_with_consumption_levy(
598 region_id: RegionID,
599 time_slice: TimeSliceID,
600 ) -> Rc<Commodity> {
601 let mut levies = CommodityLevyMap::new();
602 levies.insert((region_id, 2020, time_slice), MoneyPerFlow(10.0));
603
604 Rc::new(Commodity {
605 id: "test_commodity".into(),
606 description: "Test commodity".into(),
607 kind: CommodityType::ServiceDemand,
608 time_slice_level: TimeSliceLevel::Annual,
609 pricing_strategy: PricingStrategy::Shadow,
610 levies_prod: CommodityLevyMap::new(),
611 levies_cons: levies,
612 demand: DemandMap::new(),
613 units: "PJ".into(),
614 })
615 }
616
617 #[fixture]
618 fn commodity_with_production_levy(
619 region_id: RegionID,
620 time_slice: TimeSliceID,
621 ) -> Rc<Commodity> {
622 let mut levies = CommodityLevyMap::new();
623 levies.insert((region_id, 2020, time_slice), MoneyPerFlow(10.0));
624
625 Rc::new(Commodity {
626 id: "test_commodity".into(),
627 description: "Test commodity".into(),
628 kind: CommodityType::ServiceDemand,
629 time_slice_level: TimeSliceLevel::Annual,
630 pricing_strategy: PricingStrategy::Shadow,
631 levies_prod: levies,
632 levies_cons: CommodityLevyMap::new(),
633 demand: DemandMap::new(),
634 units: "PJ".into(),
635 })
636 }
637
638 #[fixture]
639 fn commodity_with_incentive(region_id: RegionID, time_slice: TimeSliceID) -> Rc<Commodity> {
640 let mut levies_prod = CommodityLevyMap::new();
641 levies_prod.insert(
642 (region_id.clone(), 2020, time_slice.clone()),
643 MoneyPerFlow(-5.0),
644 );
645 let mut levies_cons = CommodityLevyMap::new();
646 levies_cons.insert((region_id, 2020, time_slice), MoneyPerFlow(5.0));
647
648 Rc::new(Commodity {
649 id: "test_commodity".into(),
650 description: "Test commodity".into(),
651 kind: CommodityType::ServiceDemand,
652 time_slice_level: TimeSliceLevel::Annual,
653 pricing_strategy: PricingStrategy::Shadow,
654 levies_prod,
655 levies_cons,
656 demand: DemandMap::new(),
657 units: "PJ".into(),
658 })
659 }
660
661 #[fixture]
662 fn commodity_no_levies() -> Rc<Commodity> {
663 Rc::new(Commodity {
664 id: "test_commodity".into(),
665 description: "Test commodity".into(),
666 kind: CommodityType::ServiceDemand,
667 time_slice_level: TimeSliceLevel::Annual,
668 pricing_strategy: PricingStrategy::Shadow,
669 levies_prod: CommodityLevyMap::new(),
670 levies_cons: CommodityLevyMap::new(),
671 demand: DemandMap::new(),
672 units: "PJ".into(),
673 })
674 }
675
676 #[fixture]
677 fn flow_with_cost() -> ProcessFlow {
678 ProcessFlow {
679 commodity: Rc::new(Commodity {
680 id: "test_commodity".into(),
681 description: "Test commodity".into(),
682 kind: CommodityType::ServiceDemand,
683 time_slice_level: TimeSliceLevel::Annual,
684 pricing_strategy: PricingStrategy::Shadow,
685 levies_prod: CommodityLevyMap::new(),
686 levies_cons: CommodityLevyMap::new(),
687 demand: DemandMap::new(),
688 units: "PJ".into(),
689 }),
690 coeff: FlowPerActivity(1.0),
691 kind: FlowType::Fixed,
692 cost: MoneyPerFlow(5.0),
693 }
694 }
695
696 #[fixture]
697 fn flow_with_cost_and_levy(region_id: RegionID, time_slice: TimeSliceID) -> ProcessFlow {
698 let mut levies = CommodityLevyMap::new();
699 levies.insert((region_id, 2020, time_slice), MoneyPerFlow(10.0));
700
701 ProcessFlow {
702 commodity: Rc::new(Commodity {
703 id: "test_commodity".into(),
704 description: "Test commodity".into(),
705 kind: CommodityType::ServiceDemand,
706 time_slice_level: TimeSliceLevel::Annual,
707 pricing_strategy: PricingStrategy::Shadow,
708 levies_prod: levies,
709 levies_cons: CommodityLevyMap::new(),
710 demand: DemandMap::new(),
711 units: "PJ".into(),
712 }),
713 coeff: FlowPerActivity(1.0),
714 kind: FlowType::Fixed,
715 cost: MoneyPerFlow(5.0),
716 }
717 }
718
719 #[fixture]
720 fn flow_with_cost_and_incentive(region_id: RegionID, time_slice: TimeSliceID) -> ProcessFlow {
721 let mut levies = CommodityLevyMap::new();
722 levies.insert((region_id, 2020, time_slice), MoneyPerFlow(-3.0));
723
724 ProcessFlow {
725 commodity: Rc::new(Commodity {
726 id: "test_commodity".into(),
727 description: "Test commodity".into(),
728 kind: CommodityType::ServiceDemand,
729 time_slice_level: TimeSliceLevel::Annual,
730 pricing_strategy: PricingStrategy::Shadow,
731 levies_prod: levies,
732 levies_cons: CommodityLevyMap::new(),
733 demand: DemandMap::new(),
734 units: "PJ".into(),
735 }),
736 coeff: FlowPerActivity(1.0),
737 kind: FlowType::Fixed,
738 cost: MoneyPerFlow(5.0),
739 }
740 }
741
742 #[rstest]
743 fn get_levy_no_levies(
744 commodity_no_levies: Rc<Commodity>,
745 region_id: RegionID,
746 time_slice: TimeSliceID,
747 ) {
748 let flow = ProcessFlow {
749 commodity: commodity_no_levies,
750 coeff: FlowPerActivity(1.0),
751 kind: FlowType::Fixed,
752 cost: MoneyPerFlow(0.0),
753 };
754
755 assert_eq!(
756 flow.get_levy(®ion_id, 2020, &time_slice),
757 MoneyPerFlow(0.0)
758 );
759 }
760
761 #[rstest]
762 fn get_levy_with_levy(
763 commodity_with_levy: Rc<Commodity>,
764 region_id: RegionID,
765 time_slice: TimeSliceID,
766 ) {
767 let flow = ProcessFlow {
768 commodity: commodity_with_levy,
769 coeff: FlowPerActivity(1.0),
770 kind: FlowType::Fixed,
771 cost: MoneyPerFlow(0.0),
772 };
773
774 assert_eq!(
775 flow.get_levy(®ion_id, 2020, &time_slice),
776 MoneyPerFlow(10.0)
777 );
778 }
779
780 #[rstest]
781 fn get_levy_with_incentive(
782 commodity_with_incentive: Rc<Commodity>,
783 region_id: RegionID,
784 time_slice: TimeSliceID,
785 ) {
786 let flow = ProcessFlow {
787 commodity: commodity_with_incentive,
788 coeff: FlowPerActivity(1.0),
789 kind: FlowType::Fixed,
790 cost: MoneyPerFlow(0.0),
791 };
792
793 assert_eq!(
794 flow.get_levy(®ion_id, 2020, &time_slice),
795 MoneyPerFlow(-5.0)
796 );
797 }
798
799 #[rstest]
800 fn get_levy_different_region(commodity_with_levy: Rc<Commodity>, time_slice: TimeSliceID) {
801 let flow = ProcessFlow {
802 commodity: commodity_with_levy,
803 coeff: FlowPerActivity(1.0),
804 kind: FlowType::Fixed,
805 cost: MoneyPerFlow(0.0),
806 };
807
808 assert_eq!(
809 flow.get_levy(&"USA".into(), 2020, &time_slice),
810 MoneyPerFlow(5.0)
811 );
812 }
813
814 #[rstest]
815 fn get_levy_different_year(
816 commodity_with_levy: Rc<Commodity>,
817 region_id: RegionID,
818 time_slice: TimeSliceID,
819 ) {
820 let flow = ProcessFlow {
821 commodity: commodity_with_levy,
822 coeff: FlowPerActivity(1.0),
823 kind: FlowType::Fixed,
824 cost: MoneyPerFlow(0.0),
825 };
826
827 assert_eq!(
828 flow.get_levy(®ion_id, 2030, &time_slice),
829 MoneyPerFlow(7.0)
830 );
831 }
832
833 #[rstest]
834 fn get_levy_different_time_slice(commodity_with_levy: Rc<Commodity>, region_id: RegionID) {
835 let flow = ProcessFlow {
836 commodity: commodity_with_levy,
837 coeff: FlowPerActivity(1.0),
838 kind: FlowType::Fixed,
839 cost: MoneyPerFlow(0.0),
840 };
841
842 let different_time_slice = TimeSliceID {
843 season: "summer".into(),
844 time_of_day: "day".into(),
845 };
846
847 assert_eq!(
848 flow.get_levy(®ion_id, 2020, &different_time_slice),
849 MoneyPerFlow(3.0)
850 );
851 }
852
853 #[rstest]
854 fn get_levy_consumption_positive_coeff(
855 commodity_with_consumption_levy: Rc<Commodity>,
856 region_id: RegionID,
857 time_slice: TimeSliceID,
858 ) {
859 let flow = ProcessFlow {
860 commodity: commodity_with_consumption_levy,
861 coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
863 cost: MoneyPerFlow(0.0),
864 };
865
866 assert_eq!(
867 flow.get_levy(®ion_id, 2020, &time_slice),
868 MoneyPerFlow(0.0)
869 );
870 }
871
872 #[rstest]
873 fn get_levy_consumption_negative_coeff(
874 commodity_with_consumption_levy: Rc<Commodity>,
875 region_id: RegionID,
876 time_slice: TimeSliceID,
877 ) {
878 let flow = ProcessFlow {
879 commodity: commodity_with_consumption_levy,
880 coeff: FlowPerActivity(-1.0), kind: FlowType::Fixed,
882 cost: MoneyPerFlow(0.0),
883 };
884
885 assert_eq!(
886 flow.get_levy(®ion_id, 2020, &time_slice),
887 MoneyPerFlow(10.0)
888 );
889 }
890
891 #[rstest]
892 fn get_levy_production_positive_coeff(
893 commodity_with_production_levy: Rc<Commodity>,
894 region_id: RegionID,
895 time_slice: TimeSliceID,
896 ) {
897 let flow = ProcessFlow {
898 commodity: commodity_with_production_levy,
899 coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
901 cost: MoneyPerFlow(0.0),
902 };
903
904 assert_eq!(
905 flow.get_levy(®ion_id, 2020, &time_slice),
906 MoneyPerFlow(10.0)
907 );
908 }
909
910 #[rstest]
911 fn get_levy_production_negative_coeff(
912 commodity_with_production_levy: Rc<Commodity>,
913 region_id: RegionID,
914 time_slice: TimeSliceID,
915 ) {
916 let flow = ProcessFlow {
917 commodity: commodity_with_production_levy,
918 coeff: FlowPerActivity(-1.0), kind: FlowType::Fixed,
920 cost: MoneyPerFlow(0.0),
921 };
922
923 assert_eq!(
924 flow.get_levy(®ion_id, 2020, &time_slice),
925 MoneyPerFlow(0.0)
926 );
927 }
928
929 #[rstest]
930 fn get_total_cost_base_cost(
931 flow_with_cost: ProcessFlow,
932 region_id: RegionID,
933 time_slice: TimeSliceID,
934 ) {
935 assert_eq!(
936 flow_with_cost.get_total_cost_per_activity(®ion_id, 2020, &time_slice),
937 MoneyPerActivity(5.0)
938 );
939 }
940
941 #[rstest]
942 fn get_total_cost_with_levy(
943 flow_with_cost_and_levy: ProcessFlow,
944 region_id: RegionID,
945 time_slice: TimeSliceID,
946 ) {
947 assert_eq!(
948 flow_with_cost_and_levy.get_total_cost_per_activity(®ion_id, 2020, &time_slice),
949 MoneyPerActivity(15.0)
950 );
951 }
952
953 #[rstest]
954 fn get_total_cost_with_incentive(
955 flow_with_cost_and_incentive: ProcessFlow,
956 region_id: RegionID,
957 time_slice: TimeSliceID,
958 ) {
959 assert_eq!(
960 flow_with_cost_and_incentive.get_total_cost_per_activity(®ion_id, 2020, &time_slice),
961 MoneyPerActivity(2.0)
962 );
963 }
964
965 #[rstest]
966 fn get_total_cost_negative_coeff(
967 mut flow_with_cost: ProcessFlow,
968 region_id: RegionID,
969 time_slice: TimeSliceID,
970 ) {
971 flow_with_cost.coeff = FlowPerActivity(-2.0);
972 assert_eq!(
973 flow_with_cost.get_total_cost_per_activity(®ion_id, 2020, &time_slice),
974 MoneyPerActivity(10.0)
975 );
976 }
977
978 #[rstest]
979 fn get_total_cost_zero_coeff(
980 mut flow_with_cost: ProcessFlow,
981 region_id: RegionID,
982 time_slice: TimeSliceID,
983 ) {
984 flow_with_cost.coeff = FlowPerActivity(0.0);
985 assert_eq!(
986 flow_with_cost.get_total_cost_per_activity(®ion_id, 2020, &time_slice),
987 MoneyPerActivity(0.0)
988 );
989 }
990
991 #[test]
992 fn is_input_and_is_output() {
993 let commodity = Rc::new(Commodity {
994 id: "test_commodity".into(),
995 description: "Test commodity".into(),
996 kind: CommodityType::ServiceDemand,
997 time_slice_level: TimeSliceLevel::Annual,
998 pricing_strategy: PricingStrategy::Shadow,
999 levies_prod: CommodityLevyMap::new(),
1000 levies_cons: CommodityLevyMap::new(),
1001 demand: DemandMap::new(),
1002 units: "PJ".into(),
1003 });
1004
1005 let flow_in = ProcessFlow {
1006 commodity: Rc::clone(&commodity),
1007 coeff: FlowPerActivity(-1.0),
1008 kind: FlowType::Fixed,
1009 cost: MoneyPerFlow(0.0),
1010 };
1011 let flow_out = ProcessFlow {
1012 commodity: Rc::clone(&commodity),
1013 coeff: FlowPerActivity(1.0),
1014 kind: FlowType::Fixed,
1015 cost: MoneyPerFlow(0.0),
1016 };
1017 let flow_zero = ProcessFlow {
1018 commodity: Rc::clone(&commodity),
1019 coeff: FlowPerActivity(0.0),
1020 kind: FlowType::Fixed,
1021 cost: MoneyPerFlow(0.0),
1022 };
1023
1024 assert!(flow_in.direction() == FlowDirection::Input);
1025 assert!(flow_out.direction() == FlowDirection::Output);
1026 assert!(flow_zero.direction() == FlowDirection::Zero);
1027 }
1028
1029 #[rstest]
1030 fn new_with_full_availability(time_slice_info2: TimeSliceInfo) {
1031 let limits = ActivityLimits::new_with_full_availability(&time_slice_info2);
1032
1033 for (ts_id, ts_len) in time_slice_info2.iter() {
1035 let l = limits.get_limit_for_time_slice(ts_id);
1036 assert_eq!(*l.start(), Dimensionless(0.0));
1038 assert_eq!(*l.end(), Dimensionless(ts_len.value()));
1039 }
1040
1041 let annual_limit = limits.get_limit(&TimeSliceSelection::Annual);
1043 assert_approx_eq!(Dimensionless, *annual_limit.start(), Dimensionless(0.0));
1044 assert_approx_eq!(Dimensionless, *annual_limit.end(), Dimensionless(1.0));
1045 }
1046
1047 #[rstest]
1048 fn new_from_limits_with_seasonal_limit_applied(time_slice_info2: TimeSliceInfo) {
1049 let mut limits = HashMap::new();
1050
1051 limits.insert(
1053 TimeSliceSelection::Season("winter".into()),
1054 Dimensionless(0.0)..=Dimensionless(0.01),
1055 );
1056
1057 let result = ActivityLimits::new_from_limits(&limits, &time_slice_info2).unwrap();
1058
1059 for (ts_id, _ts_len) in time_slice_info2.iter() {
1061 let ts_limit = result.get_limit_for_time_slice(ts_id);
1062 assert_eq!(*ts_limit.end(), Dimensionless(0.01));
1063 }
1064
1065 let season_limit = result.get_limit(&TimeSliceSelection::Season("winter".into()));
1067 assert_eq!(*season_limit.end(), Dimensionless(0.01));
1068 }
1069
1070 #[rstest]
1071 fn new_from_limits_with_annual_limit_applied(time_slice_info2: TimeSliceInfo) {
1072 let mut limits = HashMap::new();
1073
1074 limits.insert(
1076 TimeSliceSelection::Annual,
1077 Dimensionless(0.0)..=Dimensionless(0.01),
1078 );
1079
1080 let result = ActivityLimits::new_from_limits(&limits, &time_slice_info2).unwrap();
1081
1082 for (ts_id, _ts_len) in time_slice_info2.iter() {
1084 let ts_limit = result.get_limit_for_time_slice(ts_id);
1085 assert_eq!(*ts_limit.end(), Dimensionless(0.01));
1086 }
1087
1088 let season_limit = result.get_limit(&TimeSliceSelection::Season("winter".into()));
1090 assert_eq!(*season_limit.end(), Dimensionless(0.01));
1091
1092 let annual_limit = result.get_limit(&TimeSliceSelection::Annual);
1094 assert_eq!(*annual_limit.end(), Dimensionless(0.01));
1095 }
1096
1097 #[rstest]
1098 fn new_from_limits_missing_timeslices_error(time_slice_info2: TimeSliceInfo) {
1099 let mut limits = HashMap::new();
1100
1101 let first_ts = time_slice_info2.iter().next().unwrap().0.clone();
1103 limits.insert(
1104 TimeSliceSelection::Single(first_ts),
1105 Dimensionless(0.0)..=Dimensionless(0.1),
1106 );
1107
1108 assert_error!(
1109 ActivityLimits::new_from_limits(&limits, &time_slice_info2),
1110 "Missing availability limits for time slices: [winter.night]. Please provide"
1111 );
1112 }
1113
1114 #[rstest]
1115 fn new_from_limits_incompatible_limits(time_slice_info2: TimeSliceInfo) {
1116 let mut limits = HashMap::new();
1117
1118 for (ts_id, _ts_len) in time_slice_info2.iter() {
1120 limits.insert(
1121 TimeSliceSelection::Single(ts_id.clone()),
1122 Dimensionless(0.0)..=Dimensionless(0.1),
1123 );
1124 }
1125
1126 limits.insert(
1128 TimeSliceSelection::Season("winter".into()),
1129 Dimensionless(0.99)..=Dimensionless(1.0),
1130 );
1131
1132 assert_error!(
1133 ActivityLimits::new_from_limits(&limits, &time_slice_info2),
1134 "Availability limit for season winter clashes with time slice limits"
1135 );
1136 }
1137}