1use super::{CommoditiesGraph, GraphEdge, GraphNode};
3use crate::commodity::{CommodityMap, CommodityType};
4use crate::process::{Process, ProcessMap};
5use crate::region::RegionID;
6use crate::time_slice::{TimeSliceInfo, TimeSliceLevel, TimeSliceSelection};
7use crate::units::{Dimensionless, Flow};
8use anyhow::{Context, Result, ensure};
9use indexmap::IndexMap;
10use strum::IntoEnumIterator;
11
12fn prepare_commodities_graph_for_validation(
23 base_graph: &CommoditiesGraph,
24 processes: &ProcessMap,
25 commodities: &CommodityMap,
26 time_slice_info: &TimeSliceInfo,
27 region_id: &RegionID,
28 year: u32,
29 time_slice_selection: &TimeSliceSelection,
30) -> CommoditiesGraph {
31 let mut filtered_graph = base_graph.clone();
32
33 let key = (region_id.clone(), year);
36 filtered_graph.retain_edges(|graph, edge_idx| {
37 let process_id = match graph.edge_weight(edge_idx).unwrap() {
39 GraphEdge::Primary(process_id) | GraphEdge::Secondary(process_id) => process_id,
40 GraphEdge::Demand => panic!("Demand edges should not be present in the base graph"),
41 };
42 let process = &processes[process_id];
43
44 can_be_active(process, &key, time_slice_selection, time_slice_info)
46 });
47
48 let demand_node_index = filtered_graph.add_node(GraphNode::Demand);
52 for (commodity_id, commodity) in commodities {
53 if commodity
54 .demand
55 .get(&(region_id.clone(), year, time_slice_selection.clone()))
56 .is_some_and(|&v| v > Flow(0.0))
57 {
58 let commodity_node = GraphNode::Commodity(commodity_id.clone());
59 let commodity_node_index = filtered_graph
60 .node_indices()
61 .find(|&idx| filtered_graph.node_weight(idx) == Some(&commodity_node))
62 .unwrap_or_else(|| {
63 filtered_graph.add_node(GraphNode::Commodity(commodity_id.clone()))
64 });
65 filtered_graph.add_edge(commodity_node_index, demand_node_index, GraphEdge::Demand);
66 }
67 }
68
69 filtered_graph
70}
71
72fn can_be_active(
80 process: &Process,
81 target: &(RegionID, u32),
82 time_slice_selection: &TimeSliceSelection,
83 time_slice_info: &TimeSliceInfo,
84) -> bool {
85 let (target_region, target_year) = target;
86
87 for ((region, year), value) in &process.parameters {
88 if region != target_region {
89 continue;
90 }
91 if year + value.lifetime >= *target_year {
92 let Some(limits_map) = process.activity_limits.get(target) else {
93 continue;
94 };
95 if limits_map
96 .get_limit(time_slice_selection, time_slice_info)
97 .end()
98 > &Dimensionless(0.0)
99 {
100 return true;
101 }
102 }
103 }
104 false
105}
106
107fn validate_commodities_graph(
117 graph: &CommoditiesGraph,
118 commodities: &CommodityMap,
119 time_slice_level: TimeSliceLevel,
120) -> Result<()> {
121 for node_idx in graph.node_indices() {
122 let graph_node = graph.node_weight(node_idx).unwrap();
124 let GraphNode::Commodity(commodity_id) = graph_node else {
125 continue;
127 };
128
129 let commodity = &commodities[commodity_id];
131 if commodity.time_slice_level != time_slice_level {
132 continue;
133 }
134
135 let has_incoming = graph
137 .edges_directed(node_idx, petgraph::Direction::Incoming)
138 .next()
139 .is_some();
140 let has_outgoing = graph
141 .edges_directed(node_idx, petgraph::Direction::Outgoing)
142 .next()
143 .is_some();
144
145 match commodity.kind {
147 CommodityType::ServiceDemand => {
148 let has_non_demand_outgoing = graph
150 .edges_directed(node_idx, petgraph::Direction::Outgoing)
151 .any(|edge| edge.weight() != &GraphEdge::Demand);
152 ensure!(
153 !has_non_demand_outgoing,
154 "SVD commodity {commodity_id} cannot be an input to a process"
155 );
156
157 let has_demand_edges = graph
159 .edges_directed(node_idx, petgraph::Direction::Outgoing)
160 .any(|edge| edge.weight() == &GraphEdge::Demand);
161 if has_demand_edges {
162 ensure!(
163 has_incoming,
164 "SVD commodity {commodity_id} is demanded but has no producers"
165 );
166 }
167 }
168 CommodityType::SupplyEqualsDemand => {
169 ensure!(
171 !has_outgoing || has_incoming,
172 "SED commodity {commodity_id} may be consumed but has no producers"
173 );
174 }
175 CommodityType::Other => {
176 ensure!(
178 !(has_incoming && has_outgoing),
179 "OTH commodity {commodity_id} cannot have both producers and consumers"
180 );
181 }
182 }
183 }
184
185 Ok(())
186}
187
188pub fn validate_commodity_graphs_for_model(
210 commodity_graphs: &IndexMap<(RegionID, u32), CommoditiesGraph>,
211 processes: &ProcessMap,
212 commodities: &CommodityMap,
213 time_slice_info: &TimeSliceInfo,
214) -> Result<()> {
215 for ((region_id, year), base_graph) in commodity_graphs {
217 for ts_level in TimeSliceLevel::iter() {
218 for ts_selection in time_slice_info.iter_selections_at_level(ts_level) {
219 let graph = prepare_commodities_graph_for_validation(
220 base_graph,
221 processes,
222 commodities,
223 time_slice_info,
224 region_id,
225 *year,
226 &ts_selection,
227 );
228 validate_commodities_graph(&graph, commodities, ts_level).with_context(|| {
229 format!(
230 "Error validating commodity graph for \
231 {region_id} in {year} in {ts_selection}"
232 )
233 })?;
234 }
235 }
236 }
237 Ok(())
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::commodity::Commodity;
244 use crate::fixture::{assert_error, other_commodity, sed_commodity, svd_commodity};
245 use petgraph::graph::Graph;
246 use rstest::rstest;
247 use std::rc::Rc;
248
249 #[rstest]
250 fn test_validate_commodities_graph(
251 other_commodity: Commodity,
252 sed_commodity: Commodity,
253 svd_commodity: Commodity,
254 ) {
255 let mut graph = Graph::new();
256 let mut commodities = CommodityMap::new();
257
258 commodities.insert("A".into(), Rc::new(other_commodity));
260 commodities.insert("B".into(), Rc::new(sed_commodity));
261 commodities.insert("C".into(), Rc::new(svd_commodity));
262
263 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
265 let node_b = graph.add_node(GraphNode::Commodity("B".into()));
266 let node_c = graph.add_node(GraphNode::Commodity("C".into()));
267 let node_d = graph.add_node(GraphNode::Demand);
268 graph.add_edge(node_a, node_b, GraphEdge::Primary("process1".into()));
269 graph.add_edge(node_b, node_c, GraphEdge::Primary("process2".into()));
270 graph.add_edge(node_c, node_d, GraphEdge::Demand);
271
272 assert!(validate_commodities_graph(&graph, &commodities, TimeSliceLevel::Annual).is_ok());
274 }
275
276 #[rstest]
277 fn test_validate_commodities_graph_invalid_svd_consumed(
278 svd_commodity: Commodity,
279 sed_commodity: Commodity,
280 other_commodity: Commodity,
281 ) {
282 let mut graph = Graph::new();
283 let mut commodities = CommodityMap::new();
284
285 commodities.insert("A".into(), Rc::new(svd_commodity));
287 commodities.insert("B".into(), Rc::new(sed_commodity));
288 commodities.insert("C".into(), Rc::new(other_commodity));
289
290 let node_c = graph.add_node(GraphNode::Commodity("C".into()));
292 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
293 let node_b = graph.add_node(GraphNode::Commodity("B".into()));
294 graph.add_edge(node_c, node_a, GraphEdge::Primary("process1".into()));
295 graph.add_edge(node_a, node_b, GraphEdge::Primary("process2".into()));
296
297 assert_error!(
299 validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
300 "SVD commodity A cannot be an input to a process"
301 );
302 }
303
304 #[rstest]
305 fn test_validate_commodities_graph_invalid_svd_not_produced(svd_commodity: Commodity) {
306 let mut graph = Graph::new();
307 let mut commodities = CommodityMap::new();
308
309 commodities.insert("A".into(), Rc::new(svd_commodity));
311
312 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
314 let node_b = graph.add_node(GraphNode::Demand);
315 graph.add_edge(node_a, node_b, GraphEdge::Demand);
316
317 assert_error!(
319 validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
320 "SVD commodity A is demanded but has no producers"
321 );
322 }
323
324 #[rstest]
325 fn test_validate_commodities_graph_invalid_sed(sed_commodity: Commodity) {
326 let mut graph = Graph::new();
327 let mut commodities = CommodityMap::new();
328
329 commodities.insert("A".into(), Rc::new(sed_commodity.clone()));
331 commodities.insert("B".into(), Rc::new(sed_commodity));
332
333 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
335 let node_b = graph.add_node(GraphNode::Commodity("B".into()));
336 graph.add_edge(node_b, node_a, GraphEdge::Primary("process1".into()));
337
338 assert_error!(
340 validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
341 "SED commodity B may be consumed but has no producers"
342 );
343 }
344
345 #[rstest]
346 fn test_validate_commodities_graph_invalid_oth(
347 other_commodity: Commodity,
348 sed_commodity: Commodity,
349 ) {
350 let mut graph = Graph::new();
351 let mut commodities = CommodityMap::new();
352
353 commodities.insert("A".into(), Rc::new(other_commodity));
355 commodities.insert("B".into(), Rc::new(sed_commodity.clone()));
356 commodities.insert("C".into(), Rc::new(sed_commodity));
357
358 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
360 let node_b = graph.add_node(GraphNode::Commodity("B".into()));
361 let node_c = graph.add_node(GraphNode::Commodity("C".into()));
362 graph.add_edge(node_b, node_a, GraphEdge::Primary("process1".into()));
363 graph.add_edge(node_a, node_c, GraphEdge::Primary("process2".into()));
364
365 assert_error!(
367 validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
368 "OTH commodity A cannot have both producers and consumers"
369 );
370 }
371}