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)]
501pub struct ProcessInvestmentConstraint {
502 pub addition_limit: Option<f64>,
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511 use crate::commodity::{CommodityLevyMap, CommodityType, DemandMap, PricingStrategy};
512 use crate::fixture::{assert_error, region_id, time_slice, time_slice_info2};
513 use crate::time_slice::TimeSliceLevel;
514 use crate::time_slice::TimeSliceSelection;
515 use float_cmp::assert_approx_eq;
516 use rstest::{fixture, rstest};
517 use std::collections::HashMap;
518 use std::rc::Rc;
519
520 #[fixture]
521 fn commodity_with_levy(region_id: RegionID, time_slice: TimeSliceID) -> Rc<Commodity> {
522 let mut levies_prod = CommodityLevyMap::new();
523 let mut levies_cons = CommodityLevyMap::new();
524
525 levies_prod.insert(
527 (region_id.clone(), 2020, time_slice.clone()),
528 MoneyPerFlow(10.0),
529 );
530 levies_cons.insert(
531 (region_id.clone(), 2020, time_slice.clone()),
532 MoneyPerFlow(-10.0),
533 );
534 levies_prod.insert(("USA".into(), 2020, time_slice.clone()), MoneyPerFlow(5.0));
536 levies_cons.insert(("USA".into(), 2020, time_slice.clone()), MoneyPerFlow(-5.0));
537 levies_prod.insert(
539 (region_id.clone(), 2030, time_slice.clone()),
540 MoneyPerFlow(7.0),
541 );
542 levies_cons.insert(
543 (region_id.clone(), 2030, time_slice.clone()),
544 MoneyPerFlow(-7.0),
545 );
546 levies_prod.insert(
548 (
549 region_id.clone(),
550 2020,
551 TimeSliceID {
552 season: "summer".into(),
553 time_of_day: "day".into(),
554 },
555 ),
556 MoneyPerFlow(3.0),
557 );
558 levies_cons.insert(
559 (
560 region_id.clone(),
561 2020,
562 TimeSliceID {
563 season: "summer".into(),
564 time_of_day: "day".into(),
565 },
566 ),
567 MoneyPerFlow(-3.0),
568 );
569
570 Rc::new(Commodity {
571 id: "test_commodity".into(),
572 description: "Test commodity".into(),
573 kind: CommodityType::ServiceDemand,
574 time_slice_level: TimeSliceLevel::Annual,
575 pricing_strategy: PricingStrategy::Shadow,
576 levies_prod,
577 levies_cons,
578 demand: DemandMap::new(),
579 })
580 }
581
582 #[fixture]
583 fn commodity_with_consumption_levy(
584 region_id: RegionID,
585 time_slice: TimeSliceID,
586 ) -> Rc<Commodity> {
587 let mut levies = CommodityLevyMap::new();
588 levies.insert((region_id, 2020, time_slice), MoneyPerFlow(10.0));
589
590 Rc::new(Commodity {
591 id: "test_commodity".into(),
592 description: "Test commodity".into(),
593 kind: CommodityType::ServiceDemand,
594 time_slice_level: TimeSliceLevel::Annual,
595 pricing_strategy: PricingStrategy::Shadow,
596 levies_prod: CommodityLevyMap::new(),
597 levies_cons: levies,
598 demand: DemandMap::new(),
599 })
600 }
601
602 #[fixture]
603 fn commodity_with_production_levy(
604 region_id: RegionID,
605 time_slice: TimeSliceID,
606 ) -> Rc<Commodity> {
607 let mut levies = CommodityLevyMap::new();
608 levies.insert((region_id, 2020, time_slice), MoneyPerFlow(10.0));
609
610 Rc::new(Commodity {
611 id: "test_commodity".into(),
612 description: "Test commodity".into(),
613 kind: CommodityType::ServiceDemand,
614 time_slice_level: TimeSliceLevel::Annual,
615 pricing_strategy: PricingStrategy::Shadow,
616 levies_prod: levies,
617 levies_cons: CommodityLevyMap::new(),
618 demand: DemandMap::new(),
619 })
620 }
621
622 #[fixture]
623 fn commodity_with_incentive(region_id: RegionID, time_slice: TimeSliceID) -> Rc<Commodity> {
624 let mut levies_prod = CommodityLevyMap::new();
625 levies_prod.insert(
626 (region_id.clone(), 2020, time_slice.clone()),
627 MoneyPerFlow(-5.0),
628 );
629 let mut levies_cons = CommodityLevyMap::new();
630 levies_cons.insert((region_id, 2020, time_slice), MoneyPerFlow(5.0));
631
632 Rc::new(Commodity {
633 id: "test_commodity".into(),
634 description: "Test commodity".into(),
635 kind: CommodityType::ServiceDemand,
636 time_slice_level: TimeSliceLevel::Annual,
637 pricing_strategy: PricingStrategy::Shadow,
638 levies_prod,
639 levies_cons,
640 demand: DemandMap::new(),
641 })
642 }
643
644 #[fixture]
645 fn commodity_no_levies() -> Rc<Commodity> {
646 Rc::new(Commodity {
647 id: "test_commodity".into(),
648 description: "Test commodity".into(),
649 kind: CommodityType::ServiceDemand,
650 time_slice_level: TimeSliceLevel::Annual,
651 pricing_strategy: PricingStrategy::Shadow,
652 levies_prod: CommodityLevyMap::new(),
653 levies_cons: CommodityLevyMap::new(),
654 demand: DemandMap::new(),
655 })
656 }
657
658 #[fixture]
659 fn flow_with_cost() -> ProcessFlow {
660 ProcessFlow {
661 commodity: Rc::new(Commodity {
662 id: "test_commodity".into(),
663 description: "Test commodity".into(),
664 kind: CommodityType::ServiceDemand,
665 time_slice_level: TimeSliceLevel::Annual,
666 pricing_strategy: PricingStrategy::Shadow,
667 levies_prod: CommodityLevyMap::new(),
668 levies_cons: CommodityLevyMap::new(),
669 demand: DemandMap::new(),
670 }),
671 coeff: FlowPerActivity(1.0),
672 kind: FlowType::Fixed,
673 cost: MoneyPerFlow(5.0),
674 }
675 }
676
677 #[fixture]
678 fn flow_with_cost_and_levy(region_id: RegionID, time_slice: TimeSliceID) -> ProcessFlow {
679 let mut levies = CommodityLevyMap::new();
680 levies.insert((region_id, 2020, time_slice), MoneyPerFlow(10.0));
681
682 ProcessFlow {
683 commodity: Rc::new(Commodity {
684 id: "test_commodity".into(),
685 description: "Test commodity".into(),
686 kind: CommodityType::ServiceDemand,
687 time_slice_level: TimeSliceLevel::Annual,
688 pricing_strategy: PricingStrategy::Shadow,
689 levies_prod: levies,
690 levies_cons: CommodityLevyMap::new(),
691 demand: DemandMap::new(),
692 }),
693 coeff: FlowPerActivity(1.0),
694 kind: FlowType::Fixed,
695 cost: MoneyPerFlow(5.0),
696 }
697 }
698
699 #[fixture]
700 fn flow_with_cost_and_incentive(region_id: RegionID, time_slice: TimeSliceID) -> ProcessFlow {
701 let mut levies = CommodityLevyMap::new();
702 levies.insert((region_id, 2020, time_slice), MoneyPerFlow(-3.0));
703
704 ProcessFlow {
705 commodity: Rc::new(Commodity {
706 id: "test_commodity".into(),
707 description: "Test commodity".into(),
708 kind: CommodityType::ServiceDemand,
709 time_slice_level: TimeSliceLevel::Annual,
710 pricing_strategy: PricingStrategy::Shadow,
711 levies_prod: levies,
712 levies_cons: CommodityLevyMap::new(),
713 demand: DemandMap::new(),
714 }),
715 coeff: FlowPerActivity(1.0),
716 kind: FlowType::Fixed,
717 cost: MoneyPerFlow(5.0),
718 }
719 }
720
721 #[rstest]
722 fn get_levy_no_levies(
723 commodity_no_levies: Rc<Commodity>,
724 region_id: RegionID,
725 time_slice: TimeSliceID,
726 ) {
727 let flow = ProcessFlow {
728 commodity: commodity_no_levies,
729 coeff: FlowPerActivity(1.0),
730 kind: FlowType::Fixed,
731 cost: MoneyPerFlow(0.0),
732 };
733
734 assert_eq!(
735 flow.get_levy(®ion_id, 2020, &time_slice),
736 MoneyPerFlow(0.0)
737 );
738 }
739
740 #[rstest]
741 fn get_levy_with_levy(
742 commodity_with_levy: Rc<Commodity>,
743 region_id: RegionID,
744 time_slice: TimeSliceID,
745 ) {
746 let flow = ProcessFlow {
747 commodity: commodity_with_levy,
748 coeff: FlowPerActivity(1.0),
749 kind: FlowType::Fixed,
750 cost: MoneyPerFlow(0.0),
751 };
752
753 assert_eq!(
754 flow.get_levy(®ion_id, 2020, &time_slice),
755 MoneyPerFlow(10.0)
756 );
757 }
758
759 #[rstest]
760 fn get_levy_with_incentive(
761 commodity_with_incentive: Rc<Commodity>,
762 region_id: RegionID,
763 time_slice: TimeSliceID,
764 ) {
765 let flow = ProcessFlow {
766 commodity: commodity_with_incentive,
767 coeff: FlowPerActivity(1.0),
768 kind: FlowType::Fixed,
769 cost: MoneyPerFlow(0.0),
770 };
771
772 assert_eq!(
773 flow.get_levy(®ion_id, 2020, &time_slice),
774 MoneyPerFlow(-5.0)
775 );
776 }
777
778 #[rstest]
779 fn get_levy_different_region(commodity_with_levy: Rc<Commodity>, time_slice: TimeSliceID) {
780 let flow = ProcessFlow {
781 commodity: commodity_with_levy,
782 coeff: FlowPerActivity(1.0),
783 kind: FlowType::Fixed,
784 cost: MoneyPerFlow(0.0),
785 };
786
787 assert_eq!(
788 flow.get_levy(&"USA".into(), 2020, &time_slice),
789 MoneyPerFlow(5.0)
790 );
791 }
792
793 #[rstest]
794 fn get_levy_different_year(
795 commodity_with_levy: Rc<Commodity>,
796 region_id: RegionID,
797 time_slice: TimeSliceID,
798 ) {
799 let flow = ProcessFlow {
800 commodity: commodity_with_levy,
801 coeff: FlowPerActivity(1.0),
802 kind: FlowType::Fixed,
803 cost: MoneyPerFlow(0.0),
804 };
805
806 assert_eq!(
807 flow.get_levy(®ion_id, 2030, &time_slice),
808 MoneyPerFlow(7.0)
809 );
810 }
811
812 #[rstest]
813 fn get_levy_different_time_slice(commodity_with_levy: Rc<Commodity>, region_id: RegionID) {
814 let flow = ProcessFlow {
815 commodity: commodity_with_levy,
816 coeff: FlowPerActivity(1.0),
817 kind: FlowType::Fixed,
818 cost: MoneyPerFlow(0.0),
819 };
820
821 let different_time_slice = TimeSliceID {
822 season: "summer".into(),
823 time_of_day: "day".into(),
824 };
825
826 assert_eq!(
827 flow.get_levy(®ion_id, 2020, &different_time_slice),
828 MoneyPerFlow(3.0)
829 );
830 }
831
832 #[rstest]
833 fn get_levy_consumption_positive_coeff(
834 commodity_with_consumption_levy: Rc<Commodity>,
835 region_id: RegionID,
836 time_slice: TimeSliceID,
837 ) {
838 let flow = ProcessFlow {
839 commodity: commodity_with_consumption_levy,
840 coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
842 cost: MoneyPerFlow(0.0),
843 };
844
845 assert_eq!(
846 flow.get_levy(®ion_id, 2020, &time_slice),
847 MoneyPerFlow(0.0)
848 );
849 }
850
851 #[rstest]
852 fn get_levy_consumption_negative_coeff(
853 commodity_with_consumption_levy: Rc<Commodity>,
854 region_id: RegionID,
855 time_slice: TimeSliceID,
856 ) {
857 let flow = ProcessFlow {
858 commodity: commodity_with_consumption_levy,
859 coeff: FlowPerActivity(-1.0), kind: FlowType::Fixed,
861 cost: MoneyPerFlow(0.0),
862 };
863
864 assert_eq!(
865 flow.get_levy(®ion_id, 2020, &time_slice),
866 MoneyPerFlow(10.0)
867 );
868 }
869
870 #[rstest]
871 fn get_levy_production_positive_coeff(
872 commodity_with_production_levy: Rc<Commodity>,
873 region_id: RegionID,
874 time_slice: TimeSliceID,
875 ) {
876 let flow = ProcessFlow {
877 commodity: commodity_with_production_levy,
878 coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
880 cost: MoneyPerFlow(0.0),
881 };
882
883 assert_eq!(
884 flow.get_levy(®ion_id, 2020, &time_slice),
885 MoneyPerFlow(10.0)
886 );
887 }
888
889 #[rstest]
890 fn get_levy_production_negative_coeff(
891 commodity_with_production_levy: Rc<Commodity>,
892 region_id: RegionID,
893 time_slice: TimeSliceID,
894 ) {
895 let flow = ProcessFlow {
896 commodity: commodity_with_production_levy,
897 coeff: FlowPerActivity(-1.0), kind: FlowType::Fixed,
899 cost: MoneyPerFlow(0.0),
900 };
901
902 assert_eq!(
903 flow.get_levy(®ion_id, 2020, &time_slice),
904 MoneyPerFlow(0.0)
905 );
906 }
907
908 #[rstest]
909 fn get_total_cost_base_cost(
910 flow_with_cost: ProcessFlow,
911 region_id: RegionID,
912 time_slice: TimeSliceID,
913 ) {
914 assert_eq!(
915 flow_with_cost.get_total_cost_per_activity(®ion_id, 2020, &time_slice),
916 MoneyPerActivity(5.0)
917 );
918 }
919
920 #[rstest]
921 fn get_total_cost_with_levy(
922 flow_with_cost_and_levy: ProcessFlow,
923 region_id: RegionID,
924 time_slice: TimeSliceID,
925 ) {
926 assert_eq!(
927 flow_with_cost_and_levy.get_total_cost_per_activity(®ion_id, 2020, &time_slice),
928 MoneyPerActivity(15.0)
929 );
930 }
931
932 #[rstest]
933 fn get_total_cost_with_incentive(
934 flow_with_cost_and_incentive: ProcessFlow,
935 region_id: RegionID,
936 time_slice: TimeSliceID,
937 ) {
938 assert_eq!(
939 flow_with_cost_and_incentive.get_total_cost_per_activity(®ion_id, 2020, &time_slice),
940 MoneyPerActivity(2.0)
941 );
942 }
943
944 #[rstest]
945 fn get_total_cost_negative_coeff(
946 mut flow_with_cost: ProcessFlow,
947 region_id: RegionID,
948 time_slice: TimeSliceID,
949 ) {
950 flow_with_cost.coeff = FlowPerActivity(-2.0);
951 assert_eq!(
952 flow_with_cost.get_total_cost_per_activity(®ion_id, 2020, &time_slice),
953 MoneyPerActivity(10.0)
954 );
955 }
956
957 #[rstest]
958 fn get_total_cost_zero_coeff(
959 mut flow_with_cost: ProcessFlow,
960 region_id: RegionID,
961 time_slice: TimeSliceID,
962 ) {
963 flow_with_cost.coeff = FlowPerActivity(0.0);
964 assert_eq!(
965 flow_with_cost.get_total_cost_per_activity(®ion_id, 2020, &time_slice),
966 MoneyPerActivity(0.0)
967 );
968 }
969
970 #[test]
971 fn is_input_and_is_output() {
972 let commodity = Rc::new(Commodity {
973 id: "test_commodity".into(),
974 description: "Test commodity".into(),
975 kind: CommodityType::ServiceDemand,
976 time_slice_level: TimeSliceLevel::Annual,
977 pricing_strategy: PricingStrategy::Shadow,
978 levies_prod: CommodityLevyMap::new(),
979 levies_cons: CommodityLevyMap::new(),
980 demand: DemandMap::new(),
981 });
982
983 let flow_in = ProcessFlow {
984 commodity: Rc::clone(&commodity),
985 coeff: FlowPerActivity(-1.0),
986 kind: FlowType::Fixed,
987 cost: MoneyPerFlow(0.0),
988 };
989 let flow_out = ProcessFlow {
990 commodity: Rc::clone(&commodity),
991 coeff: FlowPerActivity(1.0),
992 kind: FlowType::Fixed,
993 cost: MoneyPerFlow(0.0),
994 };
995 let flow_zero = ProcessFlow {
996 commodity: Rc::clone(&commodity),
997 coeff: FlowPerActivity(0.0),
998 kind: FlowType::Fixed,
999 cost: MoneyPerFlow(0.0),
1000 };
1001
1002 assert!(flow_in.direction() == FlowDirection::Input);
1003 assert!(flow_out.direction() == FlowDirection::Output);
1004 assert!(flow_zero.direction() == FlowDirection::Zero);
1005 }
1006
1007 #[rstest]
1008 fn new_with_full_availability(time_slice_info2: TimeSliceInfo) {
1009 let limits = ActivityLimits::new_with_full_availability(&time_slice_info2);
1010
1011 for (ts_id, ts_len) in time_slice_info2.iter() {
1013 let l = limits.get_limit_for_time_slice(ts_id);
1014 assert_eq!(*l.start(), Dimensionless(0.0));
1016 assert_eq!(*l.end(), Dimensionless(ts_len.value()));
1017 }
1018
1019 let annual_limit = limits.get_limit(&TimeSliceSelection::Annual);
1021 assert_approx_eq!(Dimensionless, *annual_limit.start(), Dimensionless(0.0));
1022 assert_approx_eq!(Dimensionless, *annual_limit.end(), Dimensionless(1.0));
1023 }
1024
1025 #[rstest]
1026 fn new_from_limits_with_seasonal_limit_applied(time_slice_info2: TimeSliceInfo) {
1027 let mut limits = HashMap::new();
1028
1029 limits.insert(
1031 TimeSliceSelection::Season("winter".into()),
1032 Dimensionless(0.0)..=Dimensionless(0.01),
1033 );
1034
1035 let result = ActivityLimits::new_from_limits(&limits, &time_slice_info2).unwrap();
1036
1037 for (ts_id, _ts_len) in time_slice_info2.iter() {
1039 let ts_limit = result.get_limit_for_time_slice(ts_id);
1040 assert_eq!(*ts_limit.end(), Dimensionless(0.01));
1041 }
1042
1043 let season_limit = result.get_limit(&TimeSliceSelection::Season("winter".into()));
1045 assert_eq!(*season_limit.end(), Dimensionless(0.01));
1046 }
1047
1048 #[rstest]
1049 fn new_from_limits_with_annual_limit_applied(time_slice_info2: TimeSliceInfo) {
1050 let mut limits = HashMap::new();
1051
1052 limits.insert(
1054 TimeSliceSelection::Annual,
1055 Dimensionless(0.0)..=Dimensionless(0.01),
1056 );
1057
1058 let result = ActivityLimits::new_from_limits(&limits, &time_slice_info2).unwrap();
1059
1060 for (ts_id, _ts_len) in time_slice_info2.iter() {
1062 let ts_limit = result.get_limit_for_time_slice(ts_id);
1063 assert_eq!(*ts_limit.end(), Dimensionless(0.01));
1064 }
1065
1066 let season_limit = result.get_limit(&TimeSliceSelection::Season("winter".into()));
1068 assert_eq!(*season_limit.end(), Dimensionless(0.01));
1069
1070 let annual_limit = result.get_limit(&TimeSliceSelection::Annual);
1072 assert_eq!(*annual_limit.end(), Dimensionless(0.01));
1073 }
1074
1075 #[rstest]
1076 fn new_from_limits_missing_timeslices_error(time_slice_info2: TimeSliceInfo) {
1077 let mut limits = HashMap::new();
1078
1079 let first_ts = time_slice_info2.iter().next().unwrap().0.clone();
1081 limits.insert(
1082 TimeSliceSelection::Single(first_ts),
1083 Dimensionless(0.0)..=Dimensionless(0.1),
1084 );
1085
1086 assert_error!(
1087 ActivityLimits::new_from_limits(&limits, &time_slice_info2),
1088 "Missing availability limits for time slices: [winter.night]. Please provide"
1089 );
1090 }
1091
1092 #[rstest]
1093 fn new_from_limits_incompatible_limits(time_slice_info2: TimeSliceInfo) {
1094 let mut limits = HashMap::new();
1095
1096 for (ts_id, _ts_len) in time_slice_info2.iter() {
1098 limits.insert(
1099 TimeSliceSelection::Single(ts_id.clone()),
1100 Dimensionless(0.0)..=Dimensionless(0.1),
1101 );
1102 }
1103
1104 limits.insert(
1106 TimeSliceSelection::Season("winter".into()),
1107 Dimensionless(0.99)..=Dimensionless(1.0),
1108 );
1109
1110 assert_error!(
1111 ActivityLimits::new_from_limits(&limits, &time_slice_info2),
1112 "Availability limit for season winter clashes with time slice limits"
1113 );
1114 }
1115}