1use crate::commodity::{BalanceType, Commodity, CommodityID};
4use crate::id::define_id_type;
5use crate::region::RegionID;
6use crate::time_slice::TimeSliceID;
7use crate::units::{
8 ActivityPerCapacity, Dimensionless, FlowPerActivity, MoneyPerActivity, MoneyPerCapacity,
9 MoneyPerCapacityPerYear, MoneyPerFlow,
10};
11use indexmap::{IndexMap, IndexSet};
12use serde_string_enum::DeserializeLabeledStringEnum;
13use std::collections::HashMap;
14use std::ops::RangeInclusive;
15use std::rc::Rc;
16
17define_id_type! {ProcessID}
18
19pub type ProcessMap = IndexMap<ProcessID, Rc<Process>>;
21
22pub type ProcessActivityLimitsMap =
27 HashMap<(RegionID, u32, TimeSliceID), RangeInclusive<Dimensionless>>;
28
29pub type ProcessParameterMap = HashMap<(RegionID, u32), Rc<ProcessParameter>>;
31
32pub type ProcessFlowsMap = HashMap<(RegionID, u32), IndexMap<CommodityID, ProcessFlow>>;
36
37#[derive(PartialEq, Debug)]
39pub struct Process {
40 pub id: ProcessID,
42 pub description: String,
44 pub years: Vec<u32>,
46 pub activity_limits: ProcessActivityLimitsMap,
48 pub flows: ProcessFlowsMap,
50 pub parameters: ProcessParameterMap,
52 pub regions: IndexSet<RegionID>,
54}
55
56#[derive(PartialEq, Debug, Clone)]
58pub struct ProcessFlow {
59 pub commodity: Rc<Commodity>,
61 pub coeff: FlowPerActivity,
65 pub kind: FlowType,
67 pub cost: MoneyPerFlow,
72 pub is_primary_output: bool,
74}
75
76impl ProcessFlow {
77 pub fn get_total_cost(
81 &self,
82 region_id: &RegionID,
83 year: u32,
84 time_slice: &TimeSliceID,
85 ) -> MoneyPerActivity {
86 let cost_per_unit = self.cost + self.get_levy(region_id, year, time_slice);
87
88 self.coeff.abs() * cost_per_unit
89 }
90
91 fn get_levy(&self, region_id: &RegionID, year: u32, time_slice: &TimeSliceID) -> MoneyPerFlow {
93 if self.commodity.levies.is_empty() {
94 return MoneyPerFlow(0.0);
95 }
96
97 let levy = self
98 .commodity
99 .levies
100 .get(&(region_id.clone(), year, time_slice.clone()))
101 .unwrap();
102 let apply_levy = match levy.balance_type {
103 BalanceType::Net => true,
104 BalanceType::Consumption => self.is_input(),
105 BalanceType::Production => self.is_output(),
106 };
107
108 if apply_levy {
109 levy.value
110 } else {
111 MoneyPerFlow(0.0)
112 }
113 }
114
115 pub fn is_input(&self) -> bool {
117 self.coeff < FlowPerActivity(0.0)
118 }
119
120 pub fn is_output(&self) -> bool {
122 self.coeff > FlowPerActivity(0.0)
123 }
124}
125
126#[derive(PartialEq, Default, Debug, Clone, DeserializeLabeledStringEnum)]
128pub enum FlowType {
129 #[default]
131 #[string = "fixed"]
132 Fixed,
133 #[string = "flexible"]
136 Flexible,
137}
138
139#[derive(PartialEq, Clone, Debug)]
141pub struct ProcessParameter {
142 pub capital_cost: MoneyPerCapacity,
144 pub fixed_operating_cost: MoneyPerCapacityPerYear,
146 pub variable_operating_cost: MoneyPerActivity,
148 pub lifetime: u32,
150 pub discount_rate: Dimensionless,
152 pub capacity_to_activity: ActivityPerCapacity,
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::commodity::{
164 BalanceType, CommodityLevy, CommodityLevyMap, CommodityType, DemandMap,
165 };
166 use crate::fixture::{region_id, time_slice};
167 use crate::time_slice::TimeSliceLevel;
168 use rstest::{fixture, rstest};
169 use std::rc::Rc;
170
171 #[fixture]
172 fn commodity_with_levy(region_id: RegionID, time_slice: TimeSliceID) -> Rc<Commodity> {
173 let mut levies = CommodityLevyMap::new();
174 levies.insert(
176 (region_id.clone(), 2020, time_slice.clone()),
177 CommodityLevy {
178 balance_type: BalanceType::Net,
179 value: MoneyPerFlow(10.0),
180 },
181 );
182 levies.insert(
184 ("USA".into(), 2020, time_slice.clone()),
185 CommodityLevy {
186 balance_type: BalanceType::Net,
187 value: MoneyPerFlow(5.0),
188 },
189 );
190 levies.insert(
192 (region_id.clone(), 2030, time_slice.clone()),
193 CommodityLevy {
194 balance_type: BalanceType::Net,
195 value: MoneyPerFlow(7.0),
196 },
197 );
198 levies.insert(
200 (
201 region_id.clone(),
202 2020,
203 TimeSliceID {
204 season: "summer".into(),
205 time_of_day: "day".into(),
206 },
207 ),
208 CommodityLevy {
209 balance_type: BalanceType::Net,
210 value: MoneyPerFlow(3.0),
211 },
212 );
213
214 Rc::new(Commodity {
215 id: "test_commodity".into(),
216 description: "Test commodity".into(),
217 kind: CommodityType::ServiceDemand,
218 time_slice_level: TimeSliceLevel::Annual,
219 levies,
220 demand: DemandMap::new(),
221 })
222 }
223
224 #[fixture]
225 fn commodity_with_consumption_levy(
226 region_id: RegionID,
227 time_slice: TimeSliceID,
228 ) -> Rc<Commodity> {
229 let mut levies = CommodityLevyMap::new();
230 levies.insert(
231 (region_id, 2020, time_slice),
232 CommodityLevy {
233 balance_type: BalanceType::Consumption,
234 value: MoneyPerFlow(10.0),
235 },
236 );
237
238 Rc::new(Commodity {
239 id: "test_commodity".into(),
240 description: "Test commodity".into(),
241 kind: CommodityType::ServiceDemand,
242 time_slice_level: TimeSliceLevel::Annual,
243 levies,
244 demand: DemandMap::new(),
245 })
246 }
247
248 #[fixture]
249 fn commodity_with_production_levy(
250 region_id: RegionID,
251 time_slice: TimeSliceID,
252 ) -> Rc<Commodity> {
253 let mut levies = CommodityLevyMap::new();
254 levies.insert(
255 (region_id, 2020, time_slice),
256 CommodityLevy {
257 balance_type: BalanceType::Production,
258 value: MoneyPerFlow(10.0),
259 },
260 );
261
262 Rc::new(Commodity {
263 id: "test_commodity".into(),
264 description: "Test commodity".into(),
265 kind: CommodityType::ServiceDemand,
266 time_slice_level: TimeSliceLevel::Annual,
267 levies,
268 demand: DemandMap::new(),
269 })
270 }
271
272 #[fixture]
273 fn commodity_with_incentive(region_id: RegionID, time_slice: TimeSliceID) -> Rc<Commodity> {
274 let mut levies = CommodityLevyMap::new();
275 levies.insert(
276 (region_id, 2020, time_slice),
277 CommodityLevy {
278 balance_type: BalanceType::Net,
279 value: MoneyPerFlow(-5.0),
280 },
281 );
282
283 Rc::new(Commodity {
284 id: "test_commodity".into(),
285 description: "Test commodity".into(),
286 kind: CommodityType::ServiceDemand,
287 time_slice_level: TimeSliceLevel::Annual,
288 levies,
289 demand: DemandMap::new(),
290 })
291 }
292
293 #[fixture]
294 fn commodity_no_levies() -> Rc<Commodity> {
295 Rc::new(Commodity {
296 id: "test_commodity".into(),
297 description: "Test commodity".into(),
298 kind: CommodityType::ServiceDemand,
299 time_slice_level: TimeSliceLevel::Annual,
300 levies: CommodityLevyMap::new(),
301 demand: DemandMap::new(),
302 })
303 }
304
305 #[fixture]
306 fn flow_with_cost() -> ProcessFlow {
307 ProcessFlow {
308 commodity: Rc::new(Commodity {
309 id: "test_commodity".into(),
310 description: "Test commodity".into(),
311 kind: CommodityType::ServiceDemand,
312 time_slice_level: TimeSliceLevel::Annual,
313 levies: CommodityLevyMap::new(),
314 demand: DemandMap::new(),
315 }),
316 coeff: FlowPerActivity(1.0),
317 kind: FlowType::Fixed,
318 cost: MoneyPerFlow(5.0),
319 is_primary_output: false,
320 }
321 }
322
323 #[fixture]
324 fn flow_with_cost_and_levy(region_id: RegionID, time_slice: TimeSliceID) -> ProcessFlow {
325 let mut levies = CommodityLevyMap::new();
326 levies.insert(
327 (region_id, 2020, time_slice),
328 CommodityLevy {
329 balance_type: BalanceType::Net,
330 value: MoneyPerFlow(10.0),
331 },
332 );
333
334 ProcessFlow {
335 commodity: Rc::new(Commodity {
336 id: "test_commodity".into(),
337 description: "Test commodity".into(),
338 kind: CommodityType::ServiceDemand,
339 time_slice_level: TimeSliceLevel::Annual,
340 levies,
341 demand: DemandMap::new(),
342 }),
343 coeff: FlowPerActivity(1.0),
344 kind: FlowType::Fixed,
345 cost: MoneyPerFlow(5.0),
346 is_primary_output: false,
347 }
348 }
349
350 #[fixture]
351 fn flow_with_cost_and_incentive(region_id: RegionID, time_slice: TimeSliceID) -> ProcessFlow {
352 let mut levies = CommodityLevyMap::new();
353 levies.insert(
354 (region_id, 2020, time_slice),
355 CommodityLevy {
356 balance_type: BalanceType::Net,
357 value: MoneyPerFlow(-3.0),
358 },
359 );
360
361 ProcessFlow {
362 commodity: Rc::new(Commodity {
363 id: "test_commodity".into(),
364 description: "Test commodity".into(),
365 kind: CommodityType::ServiceDemand,
366 time_slice_level: TimeSliceLevel::Annual,
367 levies,
368 demand: DemandMap::new(),
369 }),
370 coeff: FlowPerActivity(1.0),
371 kind: FlowType::Fixed,
372 cost: MoneyPerFlow(5.0),
373 is_primary_output: false,
374 }
375 }
376
377 #[rstest]
378 fn test_get_levy_no_levies(
379 commodity_no_levies: Rc<Commodity>,
380 region_id: RegionID,
381 time_slice: TimeSliceID,
382 ) {
383 let flow = ProcessFlow {
384 commodity: commodity_no_levies,
385 coeff: FlowPerActivity(1.0),
386 kind: FlowType::Fixed,
387 cost: MoneyPerFlow(0.0),
388 is_primary_output: false,
389 };
390
391 assert_eq!(
392 flow.get_levy(®ion_id, 2020, &time_slice),
393 MoneyPerFlow(0.0)
394 );
395 }
396
397 #[rstest]
398 fn test_get_levy_with_levy(
399 commodity_with_levy: Rc<Commodity>,
400 region_id: RegionID,
401 time_slice: TimeSliceID,
402 ) {
403 let flow = ProcessFlow {
404 commodity: commodity_with_levy,
405 coeff: FlowPerActivity(1.0),
406 kind: FlowType::Fixed,
407 cost: MoneyPerFlow(0.0),
408 is_primary_output: false,
409 };
410
411 assert_eq!(
412 flow.get_levy(®ion_id, 2020, &time_slice),
413 MoneyPerFlow(10.0)
414 );
415 }
416
417 #[rstest]
418 fn test_get_levy_with_incentive(
419 commodity_with_incentive: Rc<Commodity>,
420 region_id: RegionID,
421 time_slice: TimeSliceID,
422 ) {
423 let flow = ProcessFlow {
424 commodity: commodity_with_incentive,
425 coeff: FlowPerActivity(1.0),
426 kind: FlowType::Fixed,
427 cost: MoneyPerFlow(0.0),
428 is_primary_output: false,
429 };
430
431 assert_eq!(
432 flow.get_levy(®ion_id, 2020, &time_slice),
433 MoneyPerFlow(-5.0)
434 );
435 }
436
437 #[rstest]
438 fn test_get_levy_different_region(commodity_with_levy: Rc<Commodity>, time_slice: TimeSliceID) {
439 let flow = ProcessFlow {
440 commodity: commodity_with_levy,
441 coeff: FlowPerActivity(1.0),
442 kind: FlowType::Fixed,
443 cost: MoneyPerFlow(0.0),
444 is_primary_output: false,
445 };
446
447 assert_eq!(
448 flow.get_levy(&"USA".into(), 2020, &time_slice),
449 MoneyPerFlow(5.0)
450 );
451 }
452
453 #[rstest]
454 fn test_get_levy_different_year(
455 commodity_with_levy: Rc<Commodity>,
456 region_id: RegionID,
457 time_slice: TimeSliceID,
458 ) {
459 let flow = ProcessFlow {
460 commodity: commodity_with_levy,
461 coeff: FlowPerActivity(1.0),
462 kind: FlowType::Fixed,
463 cost: MoneyPerFlow(0.0),
464 is_primary_output: false,
465 };
466
467 assert_eq!(
468 flow.get_levy(®ion_id, 2030, &time_slice),
469 MoneyPerFlow(7.0)
470 );
471 }
472
473 #[rstest]
474 fn test_get_levy_different_time_slice(commodity_with_levy: Rc<Commodity>, region_id: RegionID) {
475 let flow = ProcessFlow {
476 commodity: commodity_with_levy,
477 coeff: FlowPerActivity(1.0),
478 kind: FlowType::Fixed,
479 cost: MoneyPerFlow(0.0),
480 is_primary_output: false,
481 };
482
483 let different_time_slice = TimeSliceID {
484 season: "summer".into(),
485 time_of_day: "day".into(),
486 };
487
488 assert_eq!(
489 flow.get_levy(®ion_id, 2020, &different_time_slice),
490 MoneyPerFlow(3.0)
491 );
492 }
493
494 #[rstest]
495 fn test_get_levy_consumption_positive_coeff(
496 commodity_with_consumption_levy: Rc<Commodity>,
497 region_id: RegionID,
498 time_slice: TimeSliceID,
499 ) {
500 let flow = ProcessFlow {
501 commodity: commodity_with_consumption_levy,
502 coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
504 cost: MoneyPerFlow(0.0),
505 is_primary_output: false,
506 };
507
508 assert_eq!(
509 flow.get_levy(®ion_id, 2020, &time_slice),
510 MoneyPerFlow(0.0)
511 );
512 }
513
514 #[rstest]
515 fn test_get_levy_consumption_negative_coeff(
516 commodity_with_consumption_levy: Rc<Commodity>,
517 region_id: RegionID,
518 time_slice: TimeSliceID,
519 ) {
520 let flow = ProcessFlow {
521 commodity: commodity_with_consumption_levy,
522 coeff: FlowPerActivity(-1.0), kind: FlowType::Fixed,
524 cost: MoneyPerFlow(0.0),
525 is_primary_output: false,
526 };
527
528 assert_eq!(
529 flow.get_levy(®ion_id, 2020, &time_slice),
530 MoneyPerFlow(10.0)
531 );
532 }
533
534 #[rstest]
535 fn test_get_levy_production_positive_coeff(
536 commodity_with_production_levy: Rc<Commodity>,
537 region_id: RegionID,
538 time_slice: TimeSliceID,
539 ) {
540 let flow = ProcessFlow {
541 commodity: commodity_with_production_levy,
542 coeff: FlowPerActivity(1.0), kind: FlowType::Fixed,
544 cost: MoneyPerFlow(0.0),
545 is_primary_output: false,
546 };
547
548 assert_eq!(
549 flow.get_levy(®ion_id, 2020, &time_slice),
550 MoneyPerFlow(10.0)
551 );
552 }
553
554 #[rstest]
555 fn test_get_levy_production_negative_coeff(
556 commodity_with_production_levy: Rc<Commodity>,
557 region_id: RegionID,
558 time_slice: TimeSliceID,
559 ) {
560 let flow = ProcessFlow {
561 commodity: commodity_with_production_levy,
562 coeff: FlowPerActivity(-1.0), kind: FlowType::Fixed,
564 cost: MoneyPerFlow(0.0),
565 is_primary_output: false,
566 };
567
568 assert_eq!(
569 flow.get_levy(®ion_id, 2020, &time_slice),
570 MoneyPerFlow(0.0)
571 );
572 }
573
574 #[rstest]
575 fn test_get_total_cost_base_cost(
576 flow_with_cost: ProcessFlow,
577 region_id: RegionID,
578 time_slice: TimeSliceID,
579 ) {
580 assert_eq!(
581 flow_with_cost.get_total_cost(®ion_id, 2020, &time_slice),
582 MoneyPerActivity(5.0)
583 );
584 }
585
586 #[rstest]
587 fn test_get_total_cost_with_levy(
588 flow_with_cost_and_levy: ProcessFlow,
589 region_id: RegionID,
590 time_slice: TimeSliceID,
591 ) {
592 assert_eq!(
593 flow_with_cost_and_levy.get_total_cost(®ion_id, 2020, &time_slice),
594 MoneyPerActivity(15.0)
595 );
596 }
597
598 #[rstest]
599 fn test_get_total_cost_with_incentive(
600 flow_with_cost_and_incentive: ProcessFlow,
601 region_id: RegionID,
602 time_slice: TimeSliceID,
603 ) {
604 assert_eq!(
605 flow_with_cost_and_incentive.get_total_cost(®ion_id, 2020, &time_slice),
606 MoneyPerActivity(2.0)
607 );
608 }
609
610 #[rstest]
611 fn test_get_total_cost_negative_coeff(
612 mut flow_with_cost: ProcessFlow,
613 region_id: RegionID,
614 time_slice: TimeSliceID,
615 ) {
616 flow_with_cost.coeff = FlowPerActivity(-2.0);
617 assert_eq!(
618 flow_with_cost.get_total_cost(®ion_id, 2020, &time_slice),
619 MoneyPerActivity(10.0)
620 );
621 }
622
623 #[rstest]
624 fn test_get_total_cost_zero_coeff(
625 mut flow_with_cost: ProcessFlow,
626 region_id: RegionID,
627 time_slice: TimeSliceID,
628 ) {
629 flow_with_cost.coeff = FlowPerActivity(0.0);
630 assert_eq!(
631 flow_with_cost.get_total_cost(®ion_id, 2020, &time_slice),
632 MoneyPerActivity(0.0)
633 );
634 }
635
636 #[test]
637 fn test_is_input_and_is_output() {
638 let 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: CommodityLevyMap::new(),
644 demand: DemandMap::new(),
645 });
646
647 let flow_in = ProcessFlow {
648 commodity: Rc::clone(&commodity),
649 coeff: FlowPerActivity(-1.0),
650 kind: FlowType::Fixed,
651 cost: MoneyPerFlow(0.0),
652 is_primary_output: false,
653 };
654 let flow_out = ProcessFlow {
655 commodity: Rc::clone(&commodity),
656 coeff: FlowPerActivity(1.0),
657 kind: FlowType::Fixed,
658 cost: MoneyPerFlow(0.0),
659 is_primary_output: false,
660 };
661 let flow_zero = ProcessFlow {
662 commodity: Rc::clone(&commodity),
663 coeff: FlowPerActivity(0.0),
664 kind: FlowType::Fixed,
665 cost: MoneyPerFlow(0.0),
666 is_primary_output: false,
667 };
668
669 assert!(flow_in.is_input());
670 assert!(!flow_in.is_output());
671 assert!(flow_out.is_output());
672 assert!(!flow_out.is_input());
673 assert!(!flow_zero.is_input());
674 assert!(!flow_zero.is_output());
675 }
676}