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