Skip to content

Commit 76fe702

Browse files
committed
Check that other commodities are either producers or consumers but not both
Closes #560.
1 parent 543f863 commit 76fe702

1 file changed

Lines changed: 142 additions & 23 deletions

File tree

src/input/process.rs

Lines changed: 142 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -157,33 +157,66 @@ fn validate_commodities(
157157
milestone_years: &[u32],
158158
time_slice_info: &TimeSliceInfo,
159159
) -> Result<()> {
160-
for (commodity, region_id, year) in iproduct!(
161-
commodities.values(),
162-
region_ids.iter(),
163-
milestone_years.iter().copied(),
164-
) {
165-
match commodity.kind {
166-
CommodityType::SupplyEqualsDemand => {
167-
validate_sed_commodity(&commodity.id, flows, region_id, year)?;
168-
}
169-
CommodityType::ServiceDemand => {
170-
for ts_selection in
171-
time_slice_info.iter_selections_for_level(commodity.time_slice_level)
172-
{
173-
validate_svd_commodity(
174-
time_slice_info,
175-
commodity,
176-
flows,
177-
availabilities,
178-
region_id,
179-
year,
180-
&ts_selection,
181-
)?;
160+
for commodity in commodities.values() {
161+
if commodity.kind == CommodityType::Other {
162+
validate_other_commodity(&commodity.id, flows)?;
163+
continue;
164+
}
165+
166+
for (region_id, year) in iproduct!(region_ids.iter(), milestone_years.iter().copied()) {
167+
match commodity.kind {
168+
CommodityType::SupplyEqualsDemand => {
169+
validate_sed_commodity(&commodity.id, flows, region_id, year)?;
182170
}
171+
CommodityType::ServiceDemand => {
172+
for ts_selection in
173+
time_slice_info.iter_selections_for_level(commodity.time_slice_level)
174+
{
175+
validate_svd_commodity(
176+
time_slice_info,
177+
commodity,
178+
flows,
179+
availabilities,
180+
region_id,
181+
year,
182+
&ts_selection,
183+
)?;
184+
}
185+
}
186+
_ => unreachable!(),
183187
}
184-
_ => {}
185188
}
186189
}
190+
191+
Ok(())
192+
}
193+
194+
/// Check that commodities of type other are either produced or consumed but not both
195+
fn validate_other_commodity(
196+
commodity_id: &CommodityID,
197+
flows: &HashMap<ProcessID, ProcessFlowsMap>,
198+
) -> Result<()> {
199+
let mut is_producer = None;
200+
for flows in flows.values().flat_map(|flows| flows.values()) {
201+
if let Some(flow) = flows.get(commodity_id) {
202+
let cur_is_producer = flow.coeff > 0.0;
203+
if let Some(is_producer) = is_producer {
204+
ensure!(
205+
is_producer == cur_is_producer,
206+
"{commodity_id} is both a producer and consumer. \
207+
Commodities of type 'other' must only be consumed or produced."
208+
);
209+
} else {
210+
is_producer = Some(cur_is_producer);
211+
}
212+
}
213+
}
214+
215+
ensure!(
216+
is_producer.is_some(),
217+
"Commodity {commodity_id} is neither produced or consumed."
218+
);
219+
187220
Ok(())
188221
}
189222

@@ -443,4 +476,90 @@ mod tests {
443476
for region GBR in year 2010 and time slice(s) winter.day"
444477
);
445478
}
479+
480+
#[fixture]
481+
fn commodity_other() -> Commodity {
482+
Commodity {
483+
id: "commodity_other".into(),
484+
description: "Other commodity".into(),
485+
kind: CommodityType::Other,
486+
time_slice_level: TimeSliceLevel::Annual,
487+
levies: CommodityLevyMap::new(),
488+
demand: DemandMap::new(),
489+
}
490+
}
491+
492+
#[fixture]
493+
fn producer_flows(commodity_other: Commodity) -> ProcessFlowsMap {
494+
ProcessFlowsMap::from_iter(vec![(
495+
("GBR".into(), 2010),
496+
indexmap! { commodity_other.id.clone() => ProcessFlow {
497+
commodity: commodity_other.into(),
498+
coeff: 10.0,
499+
kind: FlowType::Fixed,
500+
cost: 1.0
501+
}},
502+
)])
503+
}
504+
505+
#[fixture]
506+
fn consumer_flows(commodity_other: Commodity) -> ProcessFlowsMap {
507+
ProcessFlowsMap::from_iter(vec![(
508+
("GBR".into(), 2010),
509+
indexmap! { commodity_other.id.clone() => ProcessFlow {
510+
commodity: commodity_other.into(),
511+
coeff: -10.0,
512+
kind: FlowType::Fixed,
513+
cost: 1.0
514+
}},
515+
)])
516+
}
517+
518+
#[rstest]
519+
fn test_validate_other_commodity_valid_producer(
520+
commodity_other: Commodity,
521+
producer_flows: ProcessFlowsMap,
522+
) {
523+
// Valid scenario: commodity is only produced
524+
let flows = HashMap::from_iter(vec![("process1".into(), producer_flows)]);
525+
assert!(validate_other_commodity(&commodity_other.id, &flows).is_ok());
526+
}
527+
528+
#[rstest]
529+
fn test_validate_other_commodity_valid_consumer(
530+
commodity_other: Commodity,
531+
consumer_flows: ProcessFlowsMap,
532+
) {
533+
// Valid scenario: commodity is only consumed
534+
let flows = HashMap::from_iter(vec![("process1".into(), consumer_flows)]);
535+
assert!(validate_other_commodity(&commodity_other.id, &flows).is_ok());
536+
}
537+
538+
#[rstest]
539+
fn test_validate_other_commodity_invalid_both(
540+
commodity_other: Commodity,
541+
producer_flows: ProcessFlowsMap,
542+
consumer_flows: ProcessFlowsMap,
543+
) {
544+
// Invalid scenario: commodity is both produced and consumed
545+
let flows = HashMap::from_iter(vec![
546+
("process1".into(), producer_flows),
547+
("process2".into(), consumer_flows),
548+
]);
549+
assert_error!(
550+
validate_other_commodity(&commodity_other.id, &flows),
551+
"commodity_other is both a producer and consumer. \
552+
Commodities of type 'other' must only be consumed or produced."
553+
);
554+
}
555+
556+
#[rstest]
557+
fn test_validate_other_commodity_invalid_neither(commodity_other: Commodity) {
558+
// Invalid scenario: commodity is neither produced nor consumed
559+
let flows = HashMap::new();
560+
assert_error!(
561+
validate_other_commodity(&commodity_other.id, &flows),
562+
"Commodity commodity_other is neither produced or consumed."
563+
);
564+
}
446565
}

0 commit comments

Comments
 (0)