1use crate::commodity::{Commodity, CommodityID};
4use crate::id::define_id_type;
5use crate::region::RegionID;
6use crate::time_slice::{Season, TimeSliceID, TimeSliceInfo, TimeSliceLevel, TimeSliceSelection};
7use crate::units::{
8 ActivityPerCapacity, Dimensionless, FlowPerActivity, MoneyPerActivity, MoneyPerCapacity,
9 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}
67
68impl Process {
69 pub fn active_for_year(&self, year: u32) -> bool {
71 self.years.contains(&year)
72 }
73}
74
75#[derive(PartialEq, Debug, Clone)]
97pub struct ActivityLimits {
98 annual_limit: Option<RangeInclusive<Dimensionless>>,
100 seasonal_limits: IndexMap<Season, RangeInclusive<Dimensionless>>,
102 time_slice_limits: IndexMap<TimeSliceID, RangeInclusive<Dimensionless>>,
104}
105
106impl ActivityLimits {
107 pub fn new_with_full_availability(time_slice_info: &TimeSliceInfo) -> Self {
109 let mut ts_limits = IndexMap::new();
111 for (ts_id, ts_length) in time_slice_info.iter() {
112 ts_limits.insert(
113 ts_id.clone(),
114 Dimensionless(0.0)..=Dimensionless(ts_length.value()),
115 );
116 }
117
118 ActivityLimits {
119 annual_limit: None,
120 seasonal_limits: IndexMap::new(),
121 time_slice_limits: ts_limits,
122 }
123 }
124
125 pub fn new_from_limits(
148 limits: &HashMap<TimeSliceSelection, RangeInclusive<Dimensionless>>,
149 time_slice_info: &TimeSliceInfo,
150 ) -> Result<Self> {
151 let mut result = ActivityLimits::new_with_full_availability(time_slice_info);
152
153 let mut time_slices_added = IndexSet::new();
155 for (ts_selection, limit) in limits {
156 if let TimeSliceSelection::Single(ts_id) = ts_selection {
157 result.add_time_slice_limit(ts_id.clone(), limit.clone());
158 time_slices_added.insert(ts_id.clone());
159 }
160 }
161
162 if !time_slices_added.is_empty() {
164 let missing = time_slice_info
165 .iter_ids()
166 .filter(|ts_id| !time_slices_added.contains(*ts_id))
167 .collect::<Vec<_>>();
168 ensure!(
169 missing.is_empty(),
170 "Missing availability limits for time slices: [{}]. Please provide",
171 missing.iter().join(", ")
172 );
173 }
174
175 let mut seasons_added = IndexSet::new();
178 for (ts_selection, limit) in limits {
179 if let TimeSliceSelection::Season(season) = ts_selection {
180 result.add_seasonal_limit(season.clone(), limit.clone())?;
181 seasons_added.insert(season.clone());
182 }
183 }
184
185 if !seasons_added.is_empty() {
187 let missing = time_slice_info
188 .iter_seasons()
189 .filter(|season| !seasons_added.contains(*season))
190 .collect::<Vec<_>>();
191 ensure!(
192 missing.is_empty(),
193 "Missing availability limits for seasons: [{}]. Please provide",
194 missing.iter().join(", "),
195 );
196 }
197
198 if let Some(limit) = limits.get(&TimeSliceSelection::Annual) {
201 result.add_annual_limit(limit.clone())?;
202 }
203
204 Ok(result)
205 }
206
207 pub fn add_time_slice_limit(
209 &mut self,
210 ts_id: TimeSliceID,
211 limit: RangeInclusive<Dimensionless>,
212 ) {
213 self.time_slice_limits.insert(ts_id, limit);
214 }
215
216 fn add_seasonal_limit(
218 &mut self,
219 season: Season,
220 limit: RangeInclusive<Dimensionless>,
221 ) -> Result<()> {
222 let current_limit = self.get_limit_for_season(&season);
224
225 ensure!(
228 *limit.start() <= *current_limit.end() && *limit.end() >= *current_limit.start(),
229 "Availability limit for season {season} clashes with time slice limits",
230 );
231
232 if *limit.start() > *current_limit.start() || *limit.end() < *current_limit.end() {
237 self.seasonal_limits.insert(season, limit);
238 }
239
240 Ok(())
241 }
242
243 fn add_annual_limit(&mut self, limit: RangeInclusive<Dimensionless>) -> Result<()> {
245 let current_limit = self.get_limit_for_year(&TimeSliceInfo::default());
247
248 ensure!(
251 *limit.start() <= *current_limit.end() && *limit.end() >= *current_limit.start(),
252 "Annual availability limit clashes with time slice/seasonal limits",
253 );
254
255 if *limit.start() > *current_limit.start() || *limit.end() < *current_limit.end() {
259 self.annual_limit = Some(limit);
260 }
261
262 Ok(())
263 }
264
265 pub fn get_limit(
267 &self,
268 time_slice_selection: &TimeSliceSelection,
269 time_slice_info: &TimeSliceInfo,
270 ) -> RangeInclusive<Dimensionless> {
271 match time_slice_selection {
272 TimeSliceSelection::Single(ts_id) => self.get_limit_for_time_slice(ts_id),
273 TimeSliceSelection::Season(season) => self.get_limit_for_season(season),
274 TimeSliceSelection::Annual => self.get_limit_for_year(time_slice_info),
275 }
276 }
277
278 pub fn get_limit_for_time_slice(
280 &self,
281 time_slice: &TimeSliceID,
282 ) -> RangeInclusive<Dimensionless> {
283 let ts_limit = self.time_slice_limits[time_slice].clone();
285 let lower = *ts_limit.start();
286 let mut upper = *ts_limit.end();
287
288 if let Some(seasonal_limit) = self.seasonal_limits.get(&time_slice.season) {
291 upper = upper.min(*seasonal_limit.end());
292 }
293 if let Some(annual_limit) = &self.annual_limit {
294 upper = upper.min(*annual_limit.end());
295 }
296
297 lower..=upper
298 }
299
300 fn get_limit_for_season(&self, season: &Season) -> RangeInclusive<Dimensionless> {
302 let mut lower = Dimensionless(0.0);
304 let mut upper = Dimensionless(0.0);
305 for (ts, limit) in &self.time_slice_limits {
306 if &ts.season == season {
307 lower += *limit.start();
308 upper += *limit.end();
309 }
310 }
311
312 if let Some(seasonal_limit) = self.seasonal_limits.get(season) {
314 lower = lower.max(*seasonal_limit.start());
315 upper = upper.min(*seasonal_limit.end());
316 }
317
318 if let Some(annual_limit) = &self.annual_limit {
321 upper = upper.min(*annual_limit.end());
322 }
323
324 lower..=upper
325 }
326
327 fn get_limit_for_year(&self, time_slice_info: &TimeSliceInfo) -> RangeInclusive<Dimensionless> {
329 let mut total_lower = Dimensionless(0.0);
331 let mut total_upper = Dimensionless(0.0);
332 for ts_selection in time_slice_info.iter_selections_at_level(TimeSliceLevel::Season) {
333 let TimeSliceSelection::Season(season) = ts_selection else {
334 panic!("Expected season selection")
335 };
336 let season_limit = self.get_limit_for_season(&season);
337 total_lower += *season_limit.start();
338 total_upper += *season_limit.end();
339 }
340
341 if let Some(annual_limit) = &self.annual_limit {
343 total_lower = total_lower.max(*annual_limit.start());
344 total_upper = total_upper.min(*annual_limit.end());
345 }
346
347 total_lower..=total_upper
348 }
349
350 pub fn iter_limits(
355 &self,
356 ) -> impl Iterator<Item = (TimeSliceSelection, &RangeInclusive<Dimensionless>)> {
357 let time_slice_limits = self
359 .time_slice_limits
360 .iter()
361 .map(|(ts_id, limit)| (TimeSliceSelection::Single(ts_id.clone()), limit));
362
363 let seasonal_limits = self
365 .seasonal_limits
366 .iter()
367 .map(|(season, limit)| (TimeSliceSelection::Season(season.clone()), limit));
368
369 let annual_limits = self
371 .annual_limit
372 .as_ref()
373 .map(|limit| (TimeSliceSelection::Annual, limit));
374
375 time_slice_limits
377 .chain(seasonal_limits)
378 .chain(annual_limits)
379 }
380}
381
382#[derive(PartialEq, Debug, Clone)]
384pub struct ProcessFlow {
385 pub commodity: Rc<Commodity>,
387 pub coeff: FlowPerActivity,
391 pub kind: FlowType,
393 pub cost: MoneyPerFlow,
398}
399
400impl ProcessFlow {
401 pub fn get_total_cost(
405 &self,
406 region_id: &RegionID,
407 year: u32,
408 time_slice: &TimeSliceID,
409 ) -> MoneyPerActivity {
410 let cost_per_unit = self.cost + self.get_levy(region_id, year, time_slice);
411
412 self.coeff.abs() * cost_per_unit
413 }
414
415 fn get_levy(&self, region_id: &RegionID, year: u32, time_slice: &TimeSliceID) -> MoneyPerFlow {
417 match self.direction() {
418 FlowDirection::Input => *self
419 .commodity
420 .levies_cons
421 .get(&(region_id.clone(), year, time_slice.clone()))
422 .unwrap_or(&MoneyPerFlow(0.0)),
423 FlowDirection::Output => *self
424 .commodity
425 .levies_prod
426 .get(&(region_id.clone(), year, time_slice.clone()))
427 .unwrap_or(&MoneyPerFlow(0.0)),
428 FlowDirection::Zero => MoneyPerFlow(0.0),
429 }
430 }
431
432 pub fn direction(&self) -> FlowDirection {
434 match self.coeff {
435 x if x < FlowPerActivity(0.0) => FlowDirection::Input,
436 x if x > FlowPerActivity(0.0) => FlowDirection::Output,
437 _ => FlowDirection::Zero,
438 }
439 }
440}
441
442#[derive(PartialEq, Default, Debug, Clone, DeserializeLabeledStringEnum)]
444pub enum FlowType {
445 #[default]
447 #[string = "fixed"]
448 Fixed,
449 #[string = "flexible"]
452 Flexible,
453}
454
455#[derive(PartialEq, Debug)]
457pub enum FlowDirection {
458 Input,
460 Output,
462 Zero,
464}
465
466#[derive(PartialEq, Clone, Debug)]
468pub struct ProcessParameter {
469 pub capital_cost: MoneyPerCapacity,
471 pub fixed_operating_cost: MoneyPerCapacityPerYear,
473 pub variable_operating_cost: MoneyPerActivity,
475 pub lifetime: u32,
477 pub discount_rate: Dimensionless,
479}
480
481#[derive(PartialEq, Debug, Clone)]
483pub struct ProcessInvestmentConstraint {
484 pub addition_limit: Option<f64>,
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493 use crate::commodity::{CommodityLevyMap, CommodityType, DemandMap};
494 use crate::fixture::{assert_error, region_id, time_slice, time_slice_info2};
495 use crate::time_slice::TimeSliceLevel;
496 use crate::time_slice::TimeSliceSelection;
497 use float_cmp::assert_approx_eq;
498 use rstest::{fixture, rstest};
499 use std::collections::HashMap;
500 use std::rc::Rc;
501
502 #[fixture]
503 fn commodity_with_levy(region_id: RegionID, time_slice: TimeSliceID) -> Rc<Commodity> {
504 let mut levies_prod = CommodityLevyMap::new();
505 let mut levies_cons = CommodityLevyMap::new();
506
507 levies_prod.insert(
509 (region_id.clone(), 2020, time_slice.clone()),
510 MoneyPerFlow(10.0),
511 );
512 levies_cons.insert(
513 (region_id.clone(), 2020, time_slice.clone()),
514 MoneyPerFlow(-10.0),
515 );
516 levies_prod.insert(("USA".into(), 2020, time_slice.clone()), MoneyPerFlow(5.0));
518 levies_cons.insert(("USA".into(), 2020, time_slice.clone()), MoneyPerFlow(-5.0));
519 levies_prod.insert(
521 (region_id.clone(), 2030, time_slice.clone()),
522 MoneyPerFlow(7.0),
523 );
524 levies_cons.insert(
525 (region_id.clone(), 2030, time_slice.clone()),
526 MoneyPerFlow(-7.0),
527 );
528 levies_prod.insert(
530 (
531 region_id.clone(),
532 2020,
533 TimeSliceID {
534 season: "summer".into(),
535 time_of_day: "day".into(),
536 },
537 ),
538 MoneyPerFlow(3.0),
539 );
540 levies_cons.insert(
541 (
542 region_id.clone(),
543 2020,
544 TimeSliceID {
545 season: "summer".into(),
546 time_of_day: "day".into(),
547 },
548 ),
549 MoneyPerFlow(-3.0),
550 );
551
552 Rc::new(Commodity {
553 id: "test_commodity".into(),
554 description: "Test commodity".into(),
555 kind: CommodityType::ServiceDemand,
556 time_slice_level: TimeSliceLevel::Annual,
557 levies_prod: levies_prod,
558 levies_cons: levies_cons,
559 demand: DemandMap::new(),
560 })
561 }
562
563 #[fixture]
564 fn commodity_with_consumption_levy(
565 region_id: RegionID,
566 time_slice: TimeSliceID,
567 ) -> Rc<Commodity> {
568 let mut levies = CommodityLevyMap::new();
569 levies.insert((region_id, 2020, time_slice), MoneyPerFlow(10.0));
570
571 Rc::new(Commodity {
572 id: "test_commodity".into(),
573 description: "Test commodity".into(),
574 kind: CommodityType::ServiceDemand,
575 time_slice_level: TimeSliceLevel::Annual,
576 levies_prod: CommodityLevyMap::new(),
577 levies_cons: levies,
578 demand: DemandMap::new(),
579 })
580 }
581
582 #[fixture]
583 fn commodity_with_production_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 levies_prod: levies,
596 levies_cons: CommodityLevyMap::new(),
597 demand: DemandMap::new(),
598 })
599 }
600
601 #[fixture]
602 fn commodity_with_incentive(region_id: RegionID, time_slice: TimeSliceID) -> Rc<Commodity> {
603 let mut levies_prod = CommodityLevyMap::new();
604 levies_prod.insert(
605 (region_id.clone(), 2020, time_slice.clone()),
606 MoneyPerFlow(-5.0),
607 );
608 let mut levies_cons = CommodityLevyMap::new();
609 levies_cons.insert((region_id, 2020, time_slice), MoneyPerFlow(5.0));
610
611 Rc::new(Commodity {
612 id: "test_commodity".into(),
613 description: "Test commodity".into(),
614 kind: CommodityType::ServiceDemand,
615 time_slice_level: TimeSliceLevel::Annual,
616 levies_prod: levies_prod,
617 levies_cons: levies_cons,
618 demand: DemandMap::new(),
619 })
620 }
621
622 #[fixture]
623 fn commodity_no_levies() -> Rc<Commodity> {
624 Rc::new(Commodity {
625 id: "test_commodity".into(),
626 description: "Test commodity".into(),
627 kind: CommodityType::ServiceDemand,
628 time_slice_level: TimeSliceLevel::Annual,
629 levies_prod: CommodityLevyMap::new(),
630 levies_cons: CommodityLevyMap::new(),
631 demand: DemandMap::new(),
632 })
633 }
634
635 #[fixture]
636 fn flow_with_cost() -> ProcessFlow {
637 ProcessFlow {
638 commodity: Rc::new(Commodity {
639 id: "test_commodity".into(),
640 description: "Test commodity".into(),
641 kind: CommodityType::ServiceDemand,
642 time_slice_level: TimeSliceLevel::Annual,
643 levies_prod: CommodityLevyMap::new(),
644 levies_cons: CommodityLevyMap::new(),
645 demand: DemandMap::new(),
646 }),
647 coeff: FlowPerActivity(1.0),
648 kind: FlowType::Fixed,
649 cost: MoneyPerFlow(5.0),
650 }
651 }
652
653 #[fixture]
654 fn flow_with_cost_and_levy(region_id: RegionID, time_slice: TimeSliceID) -> ProcessFlow {
655 let mut levies = CommodityLevyMap::new();
656 levies.insert((region_id, 2020, time_slice), MoneyPerFlow(10.0));
657
658 ProcessFlow {
659 commodity: Rc::new(Commodity {
660 id: "test_commodity".into(),
661 description: "Test commodity".into(),
662 kind: CommodityType::ServiceDemand,
663 time_slice_level: TimeSliceLevel::Annual,
664 levies_prod: levies,
665 levies_cons: CommodityLevyMap::new(),
666 demand: DemandMap::new(),
667 }),
668 coeff: FlowPerActivity(1.0),
669 kind: FlowType::Fixed,
670 cost: MoneyPerFlow(5.0),
671 }
672 }
673
674 #[fixture]
675 fn flow_with_cost_and_incentive(region_id: RegionID, time_slice: TimeSliceID) -> ProcessFlow {
676 let mut levies = CommodityLevyMap::new();
677 levies.insert((region_id, 2020, time_slice), MoneyPerFlow(-3.0));
678
679 ProcessFlow {
680 commodity: Rc::new(Commodity {
681 id: "test_commodity".into(),
682 description: "Test commodity".into(),
683 kind: CommodityType::ServiceDemand,
684 time_slice_level: TimeSliceLevel::Annual,
685 levies_prod: levies,
686 levies_cons: CommodityLevyMap::new(),
687 demand: DemandMap::new(),
688 }),
689 coeff: FlowPerActivity(1.0),
690 kind: FlowType::Fixed,
691 cost: MoneyPerFlow(5.0),
692 }
693 }
694
695 #[rstest]
696 fn test_get_levy_no_levies(
697 commodity_no_levies: Rc<Commodity>,
698 region_id: RegionID,
699 time_slice: TimeSliceID,
700 ) {
701 let flow = ProcessFlow {
702 commodity: commodity_no_levies,
703 coeff: FlowPerActivity(1.0),
704 kind: FlowType::Fixed,
705 cost: MoneyPerFlow(0.0),
706 };
707
708 assert_eq!(
709 flow.get_levy(®ion_id, 2020, &time_slice),
710 MoneyPerFlow(0.0)
711 );
712 }
713
714 #[rstest]
715 fn test_get_levy_with_levy(
716 commodity_with_levy: Rc<Commodity>,
717 region_id: RegionID,
718 time_slice: TimeSliceID,
719 ) {
720 let flow = ProcessFlow {
721 commodity: commodity_with_levy,
722 coeff: FlowPerActivity(1.0),
723 kind: FlowType::Fixed,
724 cost: MoneyPerFlow(0.0),
725 };
726
727 assert_eq!(
728 flow.get_levy(®ion_id, 2020, &time_slice),
729 MoneyPerFlow(10.0)
730 );
731 }
732
733 #[rstest]
734 fn test_get_levy_with_incentive(
735 commodity_with_incentive: Rc<Commodity>,
736 region_id: RegionID,
737 time_slice: TimeSliceID,
738 ) {
739 let flow = ProcessFlow {
740 commodity: commodity_with_incentive,
741 coeff: FlowPerActivity(1.0),
742 kind: FlowType::Fixed,
743 cost: MoneyPerFlow(0.0),
744 };
745
746 assert_eq!(
747 flow.get_levy(®ion_id, 2020, &time_slice),
748 MoneyPerFlow(-5.0)
749 );
750 }
751
752 #[rstest]
753 fn test_get_levy_different_region(commodity_with_levy: Rc<Commodity>, time_slice: TimeSliceID) {
754 let flow = ProcessFlow {
755 commodity: commodity_with_levy,
756 coeff: FlowPerActivity(1.0),
757 kind: FlowType::Fixed,
758 cost: MoneyPerFlow(0.0),
759 };
760
761 assert_eq!(
762 flow.get_levy(&"USA".into(), 2020, &time_slice),
763 MoneyPerFlow(5.0)
764 );
765 }
766
767 #[rstest]
768 fn test_get_levy_different_year(
769 commodity_with_levy: Rc<Commodity>,
770 region_id: RegionID,
771 time_slice: TimeSliceID,
772 ) {
773 let flow = ProcessFlow {
774 commodity: commodity_with_levy,
775 coeff: FlowPerActivity(1.0),
776 kind: FlowType::Fixed,
777 cost: MoneyPerFlow(0.0),
778 };
779
780 assert_eq!(
781 flow.get_levy(®ion_id, 2030, &time_slice),
782 MoneyPerFlow(7.0)
783 );
784 }
785
786 #[rstest]
787 fn test_get_levy_different_time_slice(commodity_with_levy: Rc<Commodity>, region_id: RegionID) {
788 let flow = ProcessFlow {
789 commodity: commodity_with_levy,
790 coeff: FlowPerActivity(1.0),
791 kind: FlowType::Fixed,
792 cost: MoneyPerFlow(0.0),
793 };
794
795 let different_time_slice = TimeSliceID {
796 season: "summer".into(),
797 time_of_day: "day".into(),
798 };
799
800 assert_eq!(
801 flow.get_levy(®ion_id, 2020, &different_time_slice),
802 MoneyPerFlow(3.0)
803 );
804 }
805
806 #[rstest]
807 fn test_get_levy_consumption_positive_coeff(
808 commodity_with_consumption_levy: Rc<Commodity>,
809 region_id: RegionID,
810 time_slice: TimeSliceID,
811 ) {
812 let flow = ProcessFlow {
813 commodity: commodity_with_consumption_levy,
814 coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
816 cost: MoneyPerFlow(0.0),
817 };
818
819 assert_eq!(
820 flow.get_levy(®ion_id, 2020, &time_slice),
821 MoneyPerFlow(0.0)
822 );
823 }
824
825 #[rstest]
826 fn test_get_levy_consumption_negative_coeff(
827 commodity_with_consumption_levy: Rc<Commodity>,
828 region_id: RegionID,
829 time_slice: TimeSliceID,
830 ) {
831 let flow = ProcessFlow {
832 commodity: commodity_with_consumption_levy,
833 coeff: FlowPerActivity(-1.0), kind: FlowType::Fixed,
835 cost: MoneyPerFlow(0.0),
836 };
837
838 assert_eq!(
839 flow.get_levy(®ion_id, 2020, &time_slice),
840 MoneyPerFlow(10.0)
841 );
842 }
843
844 #[rstest]
845 fn test_get_levy_production_positive_coeff(
846 commodity_with_production_levy: Rc<Commodity>,
847 region_id: RegionID,
848 time_slice: TimeSliceID,
849 ) {
850 let flow = ProcessFlow {
851 commodity: commodity_with_production_levy,
852 coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
854 cost: MoneyPerFlow(0.0),
855 };
856
857 assert_eq!(
858 flow.get_levy(®ion_id, 2020, &time_slice),
859 MoneyPerFlow(10.0)
860 );
861 }
862
863 #[rstest]
864 fn test_get_levy_production_negative_coeff(
865 commodity_with_production_levy: Rc<Commodity>,
866 region_id: RegionID,
867 time_slice: TimeSliceID,
868 ) {
869 let flow = ProcessFlow {
870 commodity: commodity_with_production_levy,
871 coeff: FlowPerActivity(-1.0), kind: FlowType::Fixed,
873 cost: MoneyPerFlow(0.0),
874 };
875
876 assert_eq!(
877 flow.get_levy(®ion_id, 2020, &time_slice),
878 MoneyPerFlow(0.0)
879 );
880 }
881
882 #[rstest]
883 fn test_get_total_cost_base_cost(
884 flow_with_cost: ProcessFlow,
885 region_id: RegionID,
886 time_slice: TimeSliceID,
887 ) {
888 assert_eq!(
889 flow_with_cost.get_total_cost(®ion_id, 2020, &time_slice),
890 MoneyPerActivity(5.0)
891 );
892 }
893
894 #[rstest]
895 fn test_get_total_cost_with_levy(
896 flow_with_cost_and_levy: ProcessFlow,
897 region_id: RegionID,
898 time_slice: TimeSliceID,
899 ) {
900 assert_eq!(
901 flow_with_cost_and_levy.get_total_cost(®ion_id, 2020, &time_slice),
902 MoneyPerActivity(15.0)
903 );
904 }
905
906 #[rstest]
907 fn test_get_total_cost_with_incentive(
908 flow_with_cost_and_incentive: ProcessFlow,
909 region_id: RegionID,
910 time_slice: TimeSliceID,
911 ) {
912 assert_eq!(
913 flow_with_cost_and_incentive.get_total_cost(®ion_id, 2020, &time_slice),
914 MoneyPerActivity(2.0)
915 );
916 }
917
918 #[rstest]
919 fn test_get_total_cost_negative_coeff(
920 mut flow_with_cost: ProcessFlow,
921 region_id: RegionID,
922 time_slice: TimeSliceID,
923 ) {
924 flow_with_cost.coeff = FlowPerActivity(-2.0);
925 assert_eq!(
926 flow_with_cost.get_total_cost(®ion_id, 2020, &time_slice),
927 MoneyPerActivity(10.0)
928 );
929 }
930
931 #[rstest]
932 fn test_get_total_cost_zero_coeff(
933 mut flow_with_cost: ProcessFlow,
934 region_id: RegionID,
935 time_slice: TimeSliceID,
936 ) {
937 flow_with_cost.coeff = FlowPerActivity(0.0);
938 assert_eq!(
939 flow_with_cost.get_total_cost(®ion_id, 2020, &time_slice),
940 MoneyPerActivity(0.0)
941 );
942 }
943
944 #[test]
945 fn test_is_input_and_is_output() {
946 let commodity = Rc::new(Commodity {
947 id: "test_commodity".into(),
948 description: "Test commodity".into(),
949 kind: CommodityType::ServiceDemand,
950 time_slice_level: TimeSliceLevel::Annual,
951 levies_prod: CommodityLevyMap::new(),
952 levies_cons: CommodityLevyMap::new(),
953 demand: DemandMap::new(),
954 });
955
956 let flow_in = ProcessFlow {
957 commodity: Rc::clone(&commodity),
958 coeff: FlowPerActivity(-1.0),
959 kind: FlowType::Fixed,
960 cost: MoneyPerFlow(0.0),
961 };
962 let flow_out = ProcessFlow {
963 commodity: Rc::clone(&commodity),
964 coeff: FlowPerActivity(1.0),
965 kind: FlowType::Fixed,
966 cost: MoneyPerFlow(0.0),
967 };
968 let flow_zero = ProcessFlow {
969 commodity: Rc::clone(&commodity),
970 coeff: FlowPerActivity(0.0),
971 kind: FlowType::Fixed,
972 cost: MoneyPerFlow(0.0),
973 };
974
975 assert!(flow_in.direction() == FlowDirection::Input);
976 assert!(flow_out.direction() == FlowDirection::Output);
977 assert!(flow_zero.direction() == FlowDirection::Zero);
978 }
979
980 #[rstest]
981 fn test_new_with_full_availability(time_slice_info2: TimeSliceInfo) {
982 let limits = ActivityLimits::new_with_full_availability(&time_slice_info2);
983
984 for (ts_id, ts_len) in time_slice_info2.iter() {
986 let l = limits.get_limit_for_time_slice(&ts_id);
987 assert_eq!(*l.start(), Dimensionless(0.0));
989 assert_eq!(*l.end(), Dimensionless(ts_len.value()));
990 }
991
992 let annual_limit = limits.get_limit(&TimeSliceSelection::Annual, &time_slice_info2);
994 assert_approx_eq!(Dimensionless, *annual_limit.start(), Dimensionless(0.0));
995 assert_approx_eq!(Dimensionless, *annual_limit.end(), Dimensionless(1.0));
996 }
997
998 #[rstest]
999 fn test_new_from_limits_with_seasonal_limit_applied(time_slice_info2: TimeSliceInfo) {
1000 let mut limits = HashMap::new();
1001
1002 limits.insert(
1004 TimeSliceSelection::Season("winter".into()),
1005 Dimensionless(0.0)..=Dimensionless(0.01),
1006 );
1007
1008 let result = ActivityLimits::new_from_limits(&limits, &time_slice_info2).unwrap();
1009
1010 for (ts_id, _ts_len) in time_slice_info2.iter() {
1012 let ts_limit = result.get_limit_for_time_slice(&ts_id);
1013 assert_eq!(*ts_limit.end(), Dimensionless(0.01));
1014 }
1015
1016 let season_limit = result.get_limit(
1018 &TimeSliceSelection::Season("winter".into()),
1019 &time_slice_info2,
1020 );
1021 assert_eq!(*season_limit.end(), Dimensionless(0.01));
1022 }
1023
1024 #[rstest]
1025 fn test_new_from_limits_missing_timeslices_error(time_slice_info2: TimeSliceInfo) {
1026 let mut limits = HashMap::new();
1027
1028 let first_ts = time_slice_info2.iter().next().unwrap().0.clone();
1030 limits.insert(
1031 TimeSliceSelection::Single(first_ts),
1032 Dimensionless(0.0)..=Dimensionless(0.1),
1033 );
1034
1035 assert_error!(
1036 ActivityLimits::new_from_limits(&limits, &time_slice_info2),
1037 "Missing availability limits for time slices: [winter.night]. Please provide"
1038 );
1039 }
1040
1041 #[rstest]
1042 fn test_new_from_limits_incompatible_limits(time_slice_info2: TimeSliceInfo) {
1043 let mut limits = HashMap::new();
1044
1045 for (ts_id, _ts_len) in time_slice_info2.iter() {
1047 limits.insert(
1048 TimeSliceSelection::Single(ts_id.clone()),
1049 Dimensionless(0.0)..=Dimensionless(0.1),
1050 );
1051 }
1052
1053 limits.insert(
1055 TimeSliceSelection::Season("winter".into()),
1056 Dimensionless(0.99)..=Dimensionless(1.0),
1057 );
1058
1059 assert_error!(
1060 ActivityLimits::new_from_limits(&limits, &time_slice_info2),
1061 "Availability limit for season winter clashes with time slice limits"
1062 );
1063 }
1064}