1use super::super::{input_err_msg, read_csv};
3use crate::commodity::{CommodityID, CommodityMap, CommodityType};
4use crate::id::GetIDValue;
5use crate::input::parse_year_str;
6use crate::process::{
7 FlowDirection, FlowType, ProcessFlow, ProcessFlowsMap, ProcessID, ProcessMap,
8};
9use crate::region::{RegionID, parse_region_str};
10use crate::units::{FlowPerActivity, MoneyPerFlow};
11use anyhow::{Context, Result, bail, ensure};
12use indexmap::{IndexMap, IndexSet};
13use itertools::iproduct;
14use serde::Deserialize;
15use std::collections::HashMap;
16use std::path::Path;
17use std::sync::Arc;
18
19const PROCESS_FLOWS_FILE_NAME: &str = "process_flows.csv";
20
21#[derive(PartialEq, Debug, Deserialize)]
22struct ProcessFlowRaw {
23 process_id: String,
24 commodity_id: String,
25 commission_years: String,
26 regions: String,
27 coeff: FlowPerActivity,
28 #[serde(default)]
29 #[serde(rename = "type")]
30 kind: FlowType,
31 cost: Option<MoneyPerFlow>,
32}
33
34impl ProcessFlowRaw {
35 fn validate(&self) -> Result<()> {
36 ensure!(
38 self.coeff.is_finite(),
39 "Invalid value for coeff ({})",
40 self.coeff
41 );
42
43 ensure!(
45 self.kind == FlowType::Fixed,
46 "Commodity flexible assets are not currently supported"
47 );
48
49 if let Some(cost) = self.cost {
51 ensure!(
52 (cost.value() >= 0.0),
53 "Invalid value for flow cost ({cost}). Must be >=0."
54 );
55 }
56
57 Ok(())
58 }
59}
60
61pub fn read_process_flows(
74 model_dir: &Path,
75 processes: &mut ProcessMap,
76 commodities: &CommodityMap,
77 milestone_years: &[u32],
78) -> Result<HashMap<ProcessID, ProcessFlowsMap>> {
79 let file_path = model_dir.join(PROCESS_FLOWS_FILE_NAME);
80 let process_flow_csv = read_csv(&file_path)?;
81 read_process_flows_from_iter(process_flow_csv, processes, commodities, milestone_years)
82 .with_context(|| input_err_msg(&file_path))
83}
84
85fn validate_output_flows_units(flows_map: &HashMap<ProcessID, ProcessFlowsMap>) -> Result<()> {
91 let mut errors: Vec<(ProcessID, RegionID, u32, Vec<&str>)> = Vec::new();
94
95 for (process_id, process_flows) in flows_map {
96 for ((region_id, year), flows) in process_flows {
97 let sed_svd_output_units: IndexSet<&str> = flows
98 .values()
99 .filter_map(|flow| {
100 let commodity = &flow.commodity;
101 (flow.coeff.value() > 0.0
102 && matches!(
103 commodity.kind,
104 CommodityType::ServiceDemand | CommodityType::SupplyEqualsDemand
105 ))
106 .then_some(commodity.units.as_str())
107 })
108 .collect();
109
110 if sed_svd_output_units.len() > 1 {
112 errors.push((
113 process_id.clone(),
114 region_id.clone(),
115 *year,
116 sed_svd_output_units.into_iter().collect(),
117 ));
118 }
119 }
120 }
121
122 errors.sort_by_key(|(process_id, region_id, year, _)| {
124 (process_id.clone(), region_id.clone(), *year)
125 });
126
127 if let Some((process_id, region_id, year, units)) = errors.first() {
129 bail!(
130 "Process {process_id} has SED/SVD outputs with different units: [{}] \
131 in region: {region_id} and year: {year}",
132 units.join(", ")
133 );
134 }
135
136 Ok(())
137}
138
139fn read_process_flows_from_iter<I>(
152 iter: I,
153 processes: &mut ProcessMap,
154 commodities: &CommodityMap,
155 milestone_years: &[u32],
156) -> Result<HashMap<ProcessID, ProcessFlowsMap>>
157where
158 I: Iterator<Item = ProcessFlowRaw>,
159{
160 let mut flows_map: HashMap<ProcessID, ProcessFlowsMap> = HashMap::new();
161 for record in iter {
162 record.validate()?;
163
164 let (id, process) = processes.get_id_value(&record.process_id)?;
166
167 let process_regions = &process.regions;
169 let record_regions =
170 parse_region_str(&record.regions, process_regions).with_context(|| {
171 format!("Invalid region for process {id}. Valid regions are {process_regions:?}")
172 })?;
173
174 let process_years: Vec<u32> = process.years.clone().collect();
176 let record_years =
177 parse_year_str(&record.commission_years, &process_years).with_context(|| {
178 format!("Invalid year for process {id}. Valid years are {process_years:?}")
179 })?;
180
181 let (_, commodity) = commodities.get_id_value(&record.commodity_id)?;
183
184 let process_flow = ProcessFlow {
186 commodity: Arc::clone(commodity),
187 coeff: record.coeff,
188 kind: FlowType::Fixed,
189 cost: record.cost.unwrap_or(MoneyPerFlow(0.0)),
190 };
191
192 let region_year_map = flows_map.entry(id.clone()).or_default();
194 for (year, region_id) in iproduct!(record_years, record_regions.iter()) {
195 let flows_map = region_year_map
196 .entry((region_id.clone(), year))
197 .or_default();
198 let existing = Arc::get_mut(flows_map)
199 .unwrap() .insert(commodity.id.clone(), process_flow.clone())
201 .is_some();
202 ensure!(
203 !existing,
204 "Duplicate process flow entry for region {}, year {} and commodity {}",
205 region_id,
206 year,
207 commodity.id
208 );
209 }
210 }
211
212 validate_flows_and_update_primary_output(processes, &flows_map, milestone_years)?;
213 validate_secondary_flows(processes, &flows_map, milestone_years)?;
214 validate_output_flows_units(&flows_map)?;
215
216 Ok(flows_map)
217}
218
219fn validate_flows_and_update_primary_output(
220 processes: &mut ProcessMap,
221 flows_map: &HashMap<ProcessID, ProcessFlowsMap>,
222 milestone_years: &[u32],
223) -> Result<()> {
224 for (process_id, process) in processes.iter_mut() {
225 let map = flows_map
226 .get(process_id)
227 .with_context(|| format!("Missing flows map for process {process_id}"))?;
228
229 let required_years = milestone_years
231 .iter()
232 .filter(|&y| process.years.contains(y));
233 let region_year: Vec<(&RegionID, &u32)> =
234 iproduct!(process.regions.iter(), required_years).collect();
235
236 ensure!(
237 region_year
238 .iter()
239 .all(|(region_id, year)| map.contains_key(&((*region_id).clone(), **year))),
240 "Flows map for process {process_id} does not cover all regions and required years"
241 );
242
243 let primary_output = if let Some(primary_output) = &process.primary_output {
244 Some(primary_output.clone())
245 } else {
246 let (region_id, year) = region_year[0];
247 infer_primary_output(&map[&(region_id.clone(), *year)]).with_context(|| {
248 format!("Could not infer primary_output for process {process_id}")
249 })?
250 };
251
252 for (region_id, &year) in region_year {
253 let flows = &map[&(region_id.clone(), year)];
254
255 check_flows_primary_output(flows, primary_output.as_ref()).with_context(|| {
257 format!(
258 "Invalid primary output configuration for process {process_id} \
259 (region: {region_id}, year: {year})"
260 )
261 })?;
262 }
263
264 if process.primary_output != primary_output {
266 Arc::get_mut(process).unwrap().primary_output = primary_output;
268 }
269 }
270
271 Ok(())
272}
273
274fn infer_primary_output(map: &IndexMap<CommodityID, ProcessFlow>) -> Result<Option<CommodityID>> {
278 let mut iter = map.iter().filter_map(|(commodity_id, flow)| {
279 (flow.direction() == FlowDirection::Output).then_some(commodity_id)
280 });
281
282 let Some(first_output) = iter.next() else {
283 return Ok(None);
285 };
286
287 ensure!(
288 iter.next().is_none(),
289 "Need to specify primary_output explicitly if there are multiple output flows"
290 );
291
292 Ok(Some(first_output.clone()))
293}
294
295fn check_flows_primary_output(
297 flows_map: &IndexMap<CommodityID, ProcessFlow>,
298 primary_output: Option<&CommodityID>,
299) -> Result<()> {
300 if let Some(primary_output) = primary_output {
301 let flow = flows_map.get(primary_output).with_context(|| {
302 format!("Primary output commodity '{primary_output}' isn't a process flow")
303 })?;
304
305 ensure!(
306 flow.direction() == FlowDirection::Output,
307 "Primary output commodity '{primary_output}' isn't an output flow",
308 );
309 } else {
310 ensure!(
311 flows_map
312 .values()
313 .all(|x| x.direction() == FlowDirection::Input
314 || x.direction() == FlowDirection::Zero),
315 "First year is only inputs, but subsequent years have outputs, although no primary \
316 output is specified"
317 );
318 }
319
320 Ok(())
321}
322
323fn validate_secondary_flows(
326 processes: &mut ProcessMap,
327 flows_map: &HashMap<ProcessID, ProcessFlowsMap>,
328 milestone_years: &[u32],
329) -> Result<()> {
330 for (process_id, process) in processes.iter() {
331 let map = flows_map
333 .get(process_id)
334 .with_context(|| format!("Missing flows map for process {process_id}"))?;
335
336 let required_years: Vec<&u32> = milestone_years
338 .iter()
339 .filter(|&y| process.years.contains(y))
340 .collect();
341
342 let iter = iproduct!(process.years.clone(), process.regions.iter());
344 let mut flows: HashMap<(CommodityID, RegionID), Vec<&ProcessFlow>> = HashMap::new();
345 let mut number_of_years: HashMap<(CommodityID, RegionID), u32> = HashMap::new();
346 for (year, region_id) in iter {
347 if let Some(commodity_map) = map.get(&(region_id.clone(), year)) {
348 let flow = commodity_map.iter().filter_map(|(commodity_id, flow)| {
349 (Some(commodity_id) != process.primary_output.as_ref())
350 .then_some(((commodity_id.clone(), region_id.clone()), flow))
351 });
352
353 for (key, value) in flow {
354 flows.entry(key.clone()).or_default().push(value);
355 if required_years.contains(&&year) {
356 *number_of_years.entry(key).or_default() += 1;
357 }
358 }
359 }
360 }
361
362 for ((commodity_id, region_id), value) in &flows {
366 ensure!(
367 number_of_years[&(commodity_id.clone(), region_id.clone())]
368 == required_years.len().try_into().unwrap(),
369 "Flow of commodity {commodity_id} in region {region_id} for process {process_id} \
370 does not cover all milestone years within the process range of activity."
371 );
372 let input_or_zero = value
373 .iter()
374 .all(|&x| [FlowDirection::Input, FlowDirection::Zero].contains(&x.direction()));
375 let output_or_zero = value
376 .iter()
377 .all(|&x| [FlowDirection::Output, FlowDirection::Zero].contains(&x.direction()));
378 ensure!(
379 input_or_zero || output_or_zero,
380 "Flow of commodity {commodity_id} in region {region_id} for process {process_id} \
381 behaves as input or output in different years."
382 );
383 }
384 }
385
386 Ok(())
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392 use crate::commodity::{
393 Commodity, CommodityConstraintsMap, CommodityLevyMap, DemandMap, PricingStrategy,
394 };
395 use crate::fixture::{
396 assert_error, assert_validate_fails_with_simple, assert_validate_ok_simple,
397 other_commodity, process, sed_commodity, svd_commodity,
398 };
399 use crate::patch::FilePatch;
400 use crate::process::{FlowType, Process, ProcessFlow, ProcessMap};
401 use crate::time_slice::TimeSliceLevel;
402 use crate::units::{FlowPerActivity, MoneyPerFlow};
403 use indexmap::IndexMap;
404 use itertools::Itertools;
405 use map_macro::hash_map;
406 use rstest::{fixture, rstest};
407 use std::iter;
408 use std::sync::Arc;
409
410 fn flow(commodity: Arc<Commodity>, coeff: f64) -> ProcessFlow {
411 ProcessFlow {
412 commodity,
413 coeff: FlowPerActivity(coeff),
414 kind: FlowType::Fixed,
415 cost: MoneyPerFlow(0.0),
416 }
417 }
418
419 fn build_maps<I>(
420 process: Process,
421 flows: I,
422 years: Option<Vec<u32>>,
423 ) -> (ProcessMap, HashMap<ProcessID, ProcessFlowsMap>)
424 where
425 I: Clone + Iterator<Item = (CommodityID, ProcessFlow)>,
426 {
427 let years = years.unwrap_or(process.years.clone().collect());
428 let map: Arc<IndexMap<_, _>> = Arc::new(flows.collect());
429 let flows_inner = iproduct!(&process.regions, years)
430 .map(|(region_id, year)| ((region_id.clone(), year), map.clone()))
431 .collect();
432 let flows = hash_map! {process.id.clone() => flows_inner};
433 let processes = iter::once((process.id.clone(), process.into())).collect();
434
435 (processes, flows)
436 }
437
438 #[fixture]
439 pub fn sed_commodity_pj() -> Commodity {
440 Commodity {
441 id: "sed_pj".into(),
442 description: "Test SED commodity (PJ)".into(),
443 kind: CommodityType::SupplyEqualsDemand,
444 time_slice_level: TimeSliceLevel::DayNight,
445 pricing_strategy: PricingStrategy::Shadow,
446 levies_prod: CommodityLevyMap::new(),
447 levies_cons: CommodityLevyMap::new(),
448 demand: DemandMap::new(),
449 constraints: CommodityConstraintsMap::new(),
450 units: "PJ".into(),
451 }
452 }
453
454 #[fixture]
455 pub fn sed_commodity_tonnes() -> Commodity {
456 Commodity {
457 id: "sed_tonnes".into(),
458 description: "Test SED commodity (tonnes)".into(),
459 kind: CommodityType::SupplyEqualsDemand,
460 time_slice_level: TimeSliceLevel::DayNight,
461 pricing_strategy: PricingStrategy::Shadow,
462 levies_prod: CommodityLevyMap::new(),
463 levies_cons: CommodityLevyMap::new(),
464 demand: DemandMap::new(),
465 constraints: CommodityConstraintsMap::new(),
466 units: "tonnes".into(),
467 }
468 }
469
470 #[rstest]
471 fn output_flows_matching_units(
472 svd_commodity: Commodity,
473 sed_commodity: Commodity,
474 process: Process,
475 ) {
476 assert_eq!(svd_commodity.units, sed_commodity.units);
478
479 let commodity1 = Arc::new(svd_commodity);
480 let commodity2 = Arc::new(sed_commodity);
481 let (_, flows_map) = build_maps(
482 process,
483 [
484 (commodity1.id.clone(), flow(commodity1.clone(), 1.0)),
485 (commodity2.id.clone(), flow(commodity2.clone(), 2.0)),
486 ]
487 .into_iter(),
488 None,
489 );
490
491 validate_output_flows_units(&flows_map).unwrap();
493 }
494
495 #[rstest]
496 fn output_flows_mismatched_units(
497 sed_commodity_pj: Commodity,
498 sed_commodity_tonnes: Commodity,
499 process: Process,
500 ) {
501 assert_ne!(sed_commodity_pj.units, sed_commodity_tonnes.units);
503
504 let commodity1 = Arc::new(sed_commodity_pj);
505 let commodity2 = Arc::new(sed_commodity_tonnes);
506 let (_, flows_map) = build_maps(
507 process,
508 [
509 (commodity1.id.clone(), flow(commodity1.clone(), 1.0)),
510 (commodity2.id.clone(), flow(commodity2.clone(), 2.0)),
511 ]
512 .into_iter(),
513 None,
514 );
515
516 let result = validate_output_flows_units(&flows_map);
518 assert_error!(
521 result,
522 "Process process1 has SED/SVD outputs with different units: [PJ, tonnes] in region: GBR and year: 2010"
523 );
524 }
525
526 #[rstest]
527 fn output_flows_other_commodity_ignored(
528 sed_commodity_pj: Commodity,
529 other_commodity: Commodity,
530 process: Process,
531 ) {
532 let mut other_commodity = other_commodity;
534 other_commodity.units = "tonnes".into();
535 assert_ne!(sed_commodity_pj.units, other_commodity.units);
536
537 let sed_commodity = Arc::new(sed_commodity_pj);
538 let oth_commodity = Arc::new(other_commodity);
539
540 let (_, flows_map) = build_maps(
541 process,
542 [
543 (sed_commodity.id.clone(), flow(sed_commodity.clone(), 1.0)),
544 (oth_commodity.id.clone(), flow(oth_commodity.clone(), 2.0)),
545 ]
546 .into_iter(),
547 None,
548 );
549
550 validate_output_flows_units(&flows_map).unwrap();
552 }
553
554 #[rstest]
555 fn single_sed_svd_output(svd_commodity: Commodity, process: Process) {
556 let commodity = Arc::new(svd_commodity);
557 let (_, flows_map) = build_maps(
558 process,
559 std::iter::once((commodity.id.clone(), flow(commodity.clone(), 1.0))),
560 None,
561 );
562
563 validate_output_flows_units(&flows_map).unwrap();
565 }
566
567 #[rstest]
568 fn no_sed_svd_outputs(other_commodity: Commodity, process: Process) {
569 let oth_commodity_1 = Arc::new(other_commodity.clone());
570 let oth_commodity_2 = Arc::new(other_commodity.clone());
571 let (_, flows_map) = build_maps(
572 process,
573 [
574 (CommodityID("oth1".into()), flow(oth_commodity_1, 1.0)),
575 (CommodityID("oth2".into()), flow(oth_commodity_2, 2.0)),
576 ]
577 .into_iter(),
578 None,
579 );
580
581 validate_output_flows_units(&flows_map).unwrap();
583 }
584
585 #[rstest]
586 fn sed_svd_inputs_different_units_ignored(
587 sed_commodity_pj: Commodity,
588 sed_commodity_tonnes: Commodity,
589 svd_commodity: Commodity,
590 process: Process,
591 ) {
592 assert_ne!(sed_commodity_pj.units, sed_commodity_tonnes.units);
594
595 assert_eq!(svd_commodity.units, sed_commodity_pj.units);
597
598 let input1 = Arc::new(sed_commodity_pj);
599 let input2 = Arc::new(sed_commodity_tonnes);
600 let output = Arc::new(svd_commodity);
601
602 let (_, flows_map) = build_maps(
603 process,
604 [
605 (input1.id.clone(), flow(input1.clone(), -1.0)),
607 (input2.id.clone(), flow(input2.clone(), -2.0)),
608 (output.id.clone(), flow(output.clone(), 3.0)),
610 ]
611 .into_iter(),
612 None,
613 );
614
615 validate_output_flows_units(&flows_map).unwrap();
617 }
618
619 #[rstest]
620 fn single_output_infer_primary(#[from(svd_commodity)] commodity: Commodity, process: Process) {
621 let milestone_years = vec![2010, 2020];
622 let commodity = Arc::new(commodity);
623 let (mut processes, flows_map) = build_maps(
624 process,
625 std::iter::once((commodity.id.clone(), flow(commodity.clone(), 1.0))),
626 None,
627 );
628 validate_flows_and_update_primary_output(&mut processes, &flows_map, &milestone_years)
629 .unwrap();
630 assert_eq!(
631 processes.values().exactly_one().unwrap().primary_output,
632 Some(commodity.id.clone())
633 );
634 }
635
636 #[rstest]
637 fn multiple_outputs_error(
638 #[from(svd_commodity)] commodity1: Commodity,
639 #[from(sed_commodity)] commodity2: Commodity,
640 process: Process,
641 ) {
642 let milestone_years: Vec<u32> = vec![2010, 2020];
643 let commodity1 = Arc::new(commodity1);
644 let commodity2 = Arc::new(commodity2);
645 let (mut processes, flows_map) = build_maps(
646 process,
647 [
648 (commodity1.id.clone(), flow(commodity1.clone(), 1.0)),
649 (commodity2.id.clone(), flow(commodity2.clone(), 2.0)),
650 ]
651 .into_iter(),
652 None,
653 );
654 let res =
655 validate_flows_and_update_primary_output(&mut processes, &flows_map, &milestone_years);
656 assert_error!(res, "Could not infer primary_output for process process1");
657 }
658
659 #[rstest]
660 fn explicit_primary_output(
661 #[from(svd_commodity)] commodity1: Commodity,
662 #[from(sed_commodity)] commodity2: Commodity,
663 process: Process,
664 ) {
665 let milestone_years = vec![2010, 2020];
666 let commodity1 = Arc::new(commodity1);
667 let commodity2 = Arc::new(commodity2);
668 let mut process = process;
669 process.primary_output = Some(commodity2.id.clone());
670 let (mut processes, flows_map) = build_maps(
671 process,
672 [
673 (commodity1.id.clone(), flow(commodity1.clone(), 1.0)),
674 (commodity2.id.clone(), flow(commodity2.clone(), 2.0)),
675 ]
676 .into_iter(),
677 None,
678 );
679 validate_flows_and_update_primary_output(&mut processes, &flows_map, &milestone_years)
680 .unwrap();
681 assert_eq!(
682 processes.values().exactly_one().unwrap().primary_output,
683 Some(commodity2.id.clone())
684 );
685 }
686
687 #[rstest]
688 fn all_inputs_no_primary(
689 #[from(svd_commodity)] commodity1: Commodity,
690 #[from(sed_commodity)] commodity2: Commodity,
691 process: Process,
692 ) {
693 let milestone_years = vec![2010, 2020];
694 let commodity1 = Arc::new(commodity1);
695 let commodity2 = Arc::new(commodity2);
696 let (mut processes, flows_map) = build_maps(
697 process,
698 [
699 (commodity1.id.clone(), flow(commodity1.clone(), -1.0)),
700 (commodity2.id.clone(), flow(commodity2.clone(), -2.0)),
701 ]
702 .into_iter(),
703 None,
704 );
705 validate_flows_and_update_primary_output(&mut processes, &flows_map, &milestone_years)
706 .unwrap();
707 assert_eq!(
708 processes.values().exactly_one().unwrap().primary_output,
709 None
710 );
711 }
712
713 #[rstest]
714 fn flows_not_in_all_milestone_years(
715 #[from(svd_commodity)] commodity1: Commodity,
716 #[from(sed_commodity)] commodity2: Commodity,
717 process: Process,
718 ) {
719 let milestone_years = vec![2010, 2015, 2020];
720 let flow_years = vec![2010, 2020];
721 let commodity1 = Arc::new(commodity1);
722 let commodity2 = Arc::new(commodity2);
723 let (mut processes, flows_map) = build_maps(
724 process,
725 [
726 (commodity1.id.clone(), flow(commodity1.clone(), 1.0)),
727 (commodity2.id.clone(), flow(commodity2.clone(), 2.0)),
728 ]
729 .into_iter(),
730 Some(flow_years),
731 );
732 let res =
733 validate_flows_and_update_primary_output(&mut processes, &flows_map, &milestone_years);
734 assert_error!(
735 res,
736 "Flows map for process process1 does not cover all regions and required years"
737 );
738 }
739
740 #[rstest]
741 fn flows_only_milestone_years(
742 #[from(svd_commodity)] commodity1: Commodity,
743 #[from(sed_commodity)] commodity2: Commodity,
744 process: Process,
745 ) {
746 let milestone_years = vec![2010, 2015, 2020];
747 let commodity1 = Arc::new(commodity1);
748 let commodity2 = Arc::new(commodity2);
749 let (mut processes, flows_map) = build_maps(
750 process,
751 [
752 (commodity1.id.clone(), flow(commodity1.clone(), 1.0)),
753 (commodity2.id.clone(), flow(commodity2.clone(), -2.0)),
754 ]
755 .into_iter(),
756 Some(milestone_years.clone()),
757 );
758 validate_flows_and_update_primary_output(&mut processes, &flows_map, &milestone_years)
759 .unwrap();
760 }
761
762 #[test]
763 fn flows_different_direction_different_years() {
764 let patch = FilePatch::new("process_flows.csv")
765 .with_deletion("GASPRC,GASPRD,all,all,-1.05,fixed,")
766 .with_addition("GASPRC,GASPRD,all,2020;2030,-1.05,fixed,")
767 .with_addition("GASPRC,GASPRD,all,2040,1.05,fixed,");
768 assert_validate_fails_with_simple!(
769 vec![patch],
770 "Flow of commodity GASPRD in region GBR for process GASPRC behaves as input or output in different years."
771 );
772 }
773
774 #[test]
775 fn missing_flow() {
776 let patch = FilePatch::new("process_flows.csv")
777 .with_deletion("GASPRC,GASPRD,all,all,-1.05,fixed,")
778 .with_addition("GASPRC,GASPRD,all,2020;2030,-1.05,fixed,");
779 assert_validate_fails_with_simple!(
780 vec![patch],
781 "Flow of commodity GASPRD in region GBR for process GASPRC does not cover all milestone years within the process range of activity."
782 );
783 }
784
785 #[test]
786 fn coeff_zero() {
787 let patch = FilePatch::new("process_flows.csv")
788 .with_deletion("GASPRC,GASPRD,all,all,-1.05,fixed,")
789 .with_addition("GASPRC,GASPRD,all,2020;2030,-1.05,fixed,")
790 .with_addition("GASPRC,GASPRD,all,2040,0,fixed,");
791 assert_validate_ok_simple!(vec![patch]);
792 }
793
794 #[test]
795 fn flows_not_needed_before_time_horizon() {
796 let patches = vec![
801 FilePatch::new("processes.csv")
802 .with_deletion("GASDRV,Dry gas extraction,all,GASPRD,2020,2040,1.0,")
803 .with_addition("GASDRV,Dry gas extraction,all,GASPRD,1980,2040,1.0,"),
804 FilePatch::new("process_flows.csv")
805 .with_deletion("GASPRC,GASPRD,all,all,-1.05,fixed,")
806 .with_addition("GASPRC,GASPRD,all,2020;2030;2040,-1.05,fixed,"),
807 ];
808 assert_validate_ok_simple!(patches);
809 }
810}