1use crate::commodity::{CommodityID, CommodityMap, CommodityType};
3use crate::process::{ProcessID, ProcessMap};
4use crate::region::RegionID;
5use crate::time_slice::{TimeSliceInfo, TimeSliceLevel, TimeSliceSelection};
6use crate::units::{Dimensionless, Flow};
7use anyhow::{Context, Result, anyhow, ensure};
8use indexmap::IndexSet;
9use itertools::{Itertools, iproduct};
10use petgraph::Directed;
11use petgraph::algo::toposort;
12use petgraph::graph::Graph;
13use std::collections::HashMap;
14use std::fmt::Display;
15
16type CommoditiesGraph = Graph<GraphNode, GraphEdge, Directed>;
18
19#[derive(Eq, PartialEq, Clone, Hash)]
20enum GraphNode {
22 Commodity(CommodityID),
24 Source,
26 Sink,
28 Demand,
30}
31
32impl Display for GraphNode {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 GraphNode::Commodity(id) => write!(f, "{id}"),
36 GraphNode::Source => write!(f, "SOURCE"),
37 GraphNode::Sink => write!(f, "SINK"),
38 GraphNode::Demand => write!(f, "DEMAND"),
39 }
40 }
41}
42
43#[derive(Eq, PartialEq, Clone, Hash)]
44enum GraphEdge {
46 Process(ProcessID),
48 Demand,
50}
51
52fn create_commodities_graph_for_region_year(
64 processes: &ProcessMap,
65 region_id: &RegionID,
66 year: u32,
67) -> CommoditiesGraph {
68 let mut graph = Graph::new();
69 let mut commodity_to_node_index = HashMap::new();
70
71 let key = (region_id.clone(), year);
72 for process in processes.values() {
73 let Some(flows) = process.flows.get(&key) else {
74 continue;
76 };
77
78 let mut outputs: Vec<_> = flows
80 .values()
81 .filter(|flow| flow.is_output())
82 .map(|flow| GraphNode::Commodity(flow.commodity.id.clone()))
83 .collect();
84
85 let mut inputs: Vec<_> = flows
87 .values()
88 .filter(|flow| flow.is_input())
89 .map(|flow| GraphNode::Commodity(flow.commodity.id.clone()))
90 .collect();
91
92 if inputs.is_empty() {
94 inputs.push(GraphNode::Source);
95 }
96 if outputs.is_empty() {
97 outputs.push(GraphNode::Sink);
98 }
99
100 for (input, output) in iproduct!(inputs, outputs) {
103 let source_node_index = *commodity_to_node_index
104 .entry(input.clone())
105 .or_insert_with(|| graph.add_node(input.clone()));
106 let target_node_index = *commodity_to_node_index
107 .entry(output.clone())
108 .or_insert_with(|| graph.add_node(output.clone()));
109 graph.add_edge(
110 source_node_index,
111 target_node_index,
112 GraphEdge::Process(process.id.clone()),
113 );
114 }
115 }
116
117 graph
118}
119
120fn prepare_commodities_graph_for_validation(
131 base_graph: &CommoditiesGraph,
132 processes: &ProcessMap,
133 commodities: &CommodityMap,
134 time_slice_info: &TimeSliceInfo,
135 region_id: &RegionID,
136 year: u32,
137 time_slice_selection: &TimeSliceSelection,
138) -> CommoditiesGraph {
139 let mut filtered_graph = base_graph.clone();
140
141 filtered_graph.retain_edges(|graph, edge_idx| {
144 let GraphEdge::Process(process_id) = graph.edge_weight(edge_idx).unwrap() else {
146 panic!("Demand edges should not be present in the base graph");
147 };
148 let process = &processes[process_id];
149
150 time_slice_selection
152 .iter(time_slice_info)
153 .any(|(time_slice, _)| {
154 let key = (region_id.clone(), year, time_slice.clone());
155 process
156 .activity_limits
157 .get(&key)
158 .is_some_and(|avail| *avail.end() > Dimensionless(0.0))
159 })
160 });
161
162 let demand_node_index = filtered_graph.add_node(GraphNode::Demand);
166 for (commodity_id, commodity) in commodities {
167 if time_slice_selection.level() == commodity.time_slice_level
168 && commodity
169 .demand
170 .get(&(region_id.clone(), year, time_slice_selection.clone()))
171 .is_some_and(|&v| v > Flow(0.0))
172 {
173 let commodity_node = GraphNode::Commodity(commodity_id.clone());
174 let commodity_node_index = filtered_graph
175 .node_indices()
176 .find(|&idx| filtered_graph.node_weight(idx) == Some(&commodity_node))
177 .unwrap_or_else(|| {
178 filtered_graph.add_node(GraphNode::Commodity(commodity_id.clone()))
179 });
180 filtered_graph.add_edge(commodity_node_index, demand_node_index, GraphEdge::Demand);
181 }
182 }
183
184 filtered_graph
185}
186
187fn validate_commodities_graph(
197 graph: &CommoditiesGraph,
198 commodities: &CommodityMap,
199 time_slice_level: TimeSliceLevel,
200) -> Result<()> {
201 for node_idx in graph.node_indices() {
202 let graph_node = graph.node_weight(node_idx).unwrap();
204 let commodity_id = match graph_node {
205 GraphNode::Commodity(id) => id,
206 _ => continue,
208 };
209
210 let commodity = &commodities[commodity_id];
212 if commodity.time_slice_level != time_slice_level {
213 continue;
214 }
215
216 let has_incoming = graph
218 .edges_directed(node_idx, petgraph::Direction::Incoming)
219 .next()
220 .is_some();
221 let has_outgoing = graph
222 .edges_directed(node_idx, petgraph::Direction::Outgoing)
223 .next()
224 .is_some();
225
226 match commodity.kind {
228 CommodityType::ServiceDemand => {
229 let has_non_demand_outgoing = graph
231 .edges_directed(node_idx, petgraph::Direction::Outgoing)
232 .any(|edge| edge.weight() != &GraphEdge::Demand);
233 ensure!(
234 !has_non_demand_outgoing,
235 "SVD commodity {} cannot be an input to a process",
236 commodity_id
237 );
238
239 let has_demand_edges = graph
241 .edges_directed(node_idx, petgraph::Direction::Outgoing)
242 .any(|edge| edge.weight() == &GraphEdge::Demand);
243 if has_demand_edges {
244 ensure!(
245 has_incoming,
246 "SVD commodity {} is demanded but has no producers",
247 commodity_id
248 );
249 }
250 }
251 CommodityType::SupplyEqualsDemand => {
252 ensure!(
254 !has_outgoing || has_incoming,
255 "SED commodity {} may be consumed but has no producers",
256 commodity_id
257 );
258 }
259 CommodityType::Other => {
260 ensure!(
262 !(has_incoming && has_outgoing),
263 "OTH commodity {} cannot have both producers and consumers",
264 commodity_id
265 );
266 }
267 }
268 }
269
270 Ok(())
271}
272
273fn topo_sort_commodities(
277 graph: &CommoditiesGraph,
278 commodities: &CommodityMap,
279) -> Result<Vec<CommodityID>> {
280 let order = toposort(graph, None).map_err(|cycle| {
282 let cycle_commodity = graph.node_weight(cycle.node_id()).unwrap().clone();
283 anyhow!(
284 "Cycle detected in commodity graph for commodity {}",
285 cycle_commodity
286 )
287 })?;
288
289 let order = order
292 .iter()
293 .rev()
294 .filter_map(|node_idx| {
295 let GraphNode::Commodity(commodity_id) = graph.node_weight(*node_idx).unwrap() else {
297 return None;
299 };
300 let commodity = &commodities[commodity_id];
301
302 matches!(
304 commodity.kind,
305 CommodityType::ServiceDemand | CommodityType::SupplyEqualsDemand
306 )
307 .then(|| commodity_id.clone())
308 })
309 .collect();
310
311 Ok(order)
312}
313
314pub fn build_and_validate_commodity_graphs_for_model(
345 processes: &ProcessMap,
346 commodities: &CommodityMap,
347 region_ids: &IndexSet<RegionID>,
348 years: &[u32],
349 time_slice_info: &TimeSliceInfo,
350) -> Result<HashMap<(RegionID, u32), Vec<CommodityID>>> {
351 let commodity_graphs: HashMap<(RegionID, u32), CommoditiesGraph> =
354 iproduct!(region_ids, years.iter())
355 .map(|(region_id, year)| {
356 let graph = create_commodities_graph_for_region_year(processes, region_id, *year);
357 ((region_id.clone(), *year), graph)
358 })
359 .collect();
360
361 let commodity_order: HashMap<(RegionID, u32), Vec<CommodityID>> = commodity_graphs
363 .iter()
364 .map(|((region_id, year), graph)| -> Result<_> {
365 let order = topo_sort_commodities(graph, commodities).with_context(|| {
366 format!("Error validating commodity graph for {region_id} in {year}")
367 })?;
368 Ok(((region_id.clone(), *year), order))
369 })
370 .try_collect()?;
371
372 for ((region_id, year), base_graph) in &commodity_graphs {
374 for ts_level in [
375 TimeSliceLevel::Annual,
376 TimeSliceLevel::Season,
377 TimeSliceLevel::DayNight,
378 ] {
379 for ts_selection in time_slice_info.iter_selections_at_level(ts_level) {
380 let graph = prepare_commodities_graph_for_validation(
381 base_graph,
382 processes,
383 commodities,
384 time_slice_info,
385 region_id,
386 *year,
387 &ts_selection,
388 );
389 validate_commodities_graph(&graph, commodities, ts_level).with_context(|| {
390 format!(
391 "Error validating commodity graph for \
392 {region_id} in {year} in {ts_selection}"
393 )
394 })?;
395 }
396 }
397 }
398
399 Ok(commodity_order)
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406 use crate::commodity::Commodity;
407 use crate::fixture::{assert_error, other_commodity, sed_commodity, svd_commodity};
408 use petgraph::graph::Graph;
409 use rstest::rstest;
410 use std::rc::Rc;
411
412 #[rstest]
413 fn test_topo_sort_linear_graph(sed_commodity: Commodity, svd_commodity: Commodity) {
414 let mut graph = Graph::new();
416
417 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
418 let node_b = graph.add_node(GraphNode::Commodity("B".into()));
419 let node_c = graph.add_node(GraphNode::Commodity("C".into()));
420
421 graph.add_edge(node_a, node_b, GraphEdge::Process("process1".into()));
423 graph.add_edge(node_b, node_c, GraphEdge::Process("process2".into()));
424
425 let mut commodities = CommodityMap::new();
427 commodities.insert("A".into(), Rc::new(sed_commodity.clone()));
428 commodities.insert("B".into(), Rc::new(sed_commodity));
429 commodities.insert("C".into(), Rc::new(svd_commodity));
430
431 let result = topo_sort_commodities(&graph, &commodities).unwrap();
432
433 assert_eq!(result.len(), 3);
435 assert_eq!(result[0], "C".into());
436 assert_eq!(result[1], "B".into());
437 assert_eq!(result[2], "A".into());
438 }
439
440 #[rstest]
441 fn test_topo_sort_cyclic_graph(sed_commodity: Commodity) {
442 let mut graph = Graph::new();
444
445 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
446 let node_b = graph.add_node(GraphNode::Commodity("B".into()));
447
448 graph.add_edge(node_a, node_b, GraphEdge::Process("process1".into()));
450 graph.add_edge(node_b, node_a, GraphEdge::Process("process2".into()));
451
452 let mut commodities = CommodityMap::new();
454 commodities.insert("A".into(), Rc::new(sed_commodity.clone()));
455 commodities.insert("B".into(), Rc::new(sed_commodity));
456
457 let result = topo_sort_commodities(&graph, &commodities);
461 assert_error!(result, "Cycle detected in commodity graph for commodity B");
462 }
463
464 #[rstest]
465 fn test_validate_commodities_graph(
466 other_commodity: Commodity,
467 sed_commodity: Commodity,
468 svd_commodity: Commodity,
469 ) {
470 let mut graph = Graph::new();
471 let mut commodities = CommodityMap::new();
472
473 commodities.insert("A".into(), Rc::new(other_commodity));
475 commodities.insert("B".into(), Rc::new(sed_commodity));
476 commodities.insert("C".into(), Rc::new(svd_commodity));
477
478 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
480 let node_b = graph.add_node(GraphNode::Commodity("B".into()));
481 let node_c = graph.add_node(GraphNode::Commodity("C".into()));
482 let node_d = graph.add_node(GraphNode::Demand);
483 graph.add_edge(node_a, node_b, GraphEdge::Process("process1".into()));
484 graph.add_edge(node_b, node_c, GraphEdge::Process("process2".into()));
485 graph.add_edge(node_c, node_d, GraphEdge::Demand);
486
487 let result = validate_commodities_graph(&graph, &commodities, TimeSliceLevel::Annual);
489 assert!(result.is_ok());
490 }
491
492 #[rstest]
493 fn test_validate_commodities_graph_invalid_svd_consumed(
494 svd_commodity: Commodity,
495 sed_commodity: Commodity,
496 other_commodity: Commodity,
497 ) {
498 let mut graph = Graph::new();
499 let mut commodities = CommodityMap::new();
500
501 commodities.insert("A".into(), Rc::new(svd_commodity));
503 commodities.insert("B".into(), Rc::new(sed_commodity));
504 commodities.insert("C".into(), Rc::new(other_commodity));
505
506 let node_c = graph.add_node(GraphNode::Commodity("C".into()));
508 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
509 let node_b = graph.add_node(GraphNode::Commodity("B".into()));
510 graph.add_edge(node_c, node_a, GraphEdge::Process("process1".into()));
511 graph.add_edge(node_a, node_b, GraphEdge::Process("process2".into()));
512
513 let result = validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight);
515 assert_error!(result, "SVD commodity A cannot be an input to a process");
516 }
517
518 #[rstest]
519 fn test_validate_commodities_graph_invalid_svd_not_produced(svd_commodity: Commodity) {
520 let mut graph = Graph::new();
521 let mut commodities = CommodityMap::new();
522
523 commodities.insert("A".into(), Rc::new(svd_commodity));
525
526 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
528 let node_b = graph.add_node(GraphNode::Demand);
529 graph.add_edge(node_a, node_b, GraphEdge::Demand);
530
531 let result = validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight);
533 assert_error!(result, "SVD commodity A is demanded but has no producers");
534 }
535
536 #[rstest]
537 fn test_validate_commodities_graph_invalid_sed(sed_commodity: Commodity) {
538 let mut graph = Graph::new();
539 let mut commodities = CommodityMap::new();
540
541 commodities.insert("A".into(), Rc::new(sed_commodity.clone()));
543 commodities.insert("B".into(), Rc::new(sed_commodity));
544
545 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
547 let node_b = graph.add_node(GraphNode::Commodity("B".into()));
548 graph.add_edge(node_b, node_a, GraphEdge::Process("process1".into()));
549
550 let result = validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight);
552 assert_error!(
553 result,
554 "SED commodity B may be consumed but has no producers"
555 );
556 }
557
558 #[rstest]
559 fn test_validate_commodities_graph_invalid_oth(
560 other_commodity: Commodity,
561 sed_commodity: Commodity,
562 ) {
563 let mut graph = Graph::new();
564 let mut commodities = CommodityMap::new();
565
566 commodities.insert("A".into(), Rc::new(other_commodity));
568 commodities.insert("B".into(), Rc::new(sed_commodity.clone()));
569 commodities.insert("C".into(), Rc::new(sed_commodity));
570
571 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
573 let node_b = graph.add_node(GraphNode::Commodity("B".into()));
574 let node_c = graph.add_node(GraphNode::Commodity("C".into()));
575 graph.add_edge(node_b, node_a, GraphEdge::Process("process1".into()));
576 graph.add_edge(node_a, node_c, GraphEdge::Process("process2".into()));
577
578 let result = validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight);
580 assert_error!(
581 result,
582 "OTH commodity A cannot have both producers and consumers"
583 );
584 }
585}