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 year: u32,
27 time_slice_selection: &TimeSliceSelection,
28) -> CommoditiesGraph {
29 let mut filtered_graph = base_graph.clone();
30
31 filtered_graph.retain_edges(|graph, edge_idx| {
36 let (process_id, region_id) = match graph.edge_weight(edge_idx).unwrap() {
38 GraphEdge::Primary(process_id, region_id)
39 | GraphEdge::Secondary(process_id, region_id) => (process_id, region_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, &(region_id.clone(), year), time_slice_selection)
47 });
48
49 let demand_node_index = filtered_graph.add_node(GraphNode::Demand);
54 for (commodity_id, commodity) in commodities {
55 for ((region_id, dem_year, dem_ts_sel), &flow) in &commodity.demand {
56 if *dem_year != year || dem_ts_sel != time_slice_selection || flow <= Flow(0.0) {
57 continue;
58 }
59 let commodity_node = GraphNode::Commodity(commodity_id.clone(), region_id.clone());
60 let commodity_node_index = filtered_graph
61 .node_indices()
62 .find(|&idx| filtered_graph.node_weight(idx) == Some(&commodity_node))
63 .unwrap_or_else(|| filtered_graph.add_node(commodity_node));
64 filtered_graph.add_edge(commodity_node_index, demand_node_index, GraphEdge::Demand);
65 }
66 }
67
68 filtered_graph
69}
70
71fn can_be_active(
79 process: &Process,
80 target: &(RegionID, u32),
81 time_slice_selection: &TimeSliceSelection,
82) -> bool {
83 let (target_region, target_year) = target;
84
85 for ((region, year), value) in &process.parameters {
86 if region != target_region {
87 continue;
88 }
89 if year + value.lifetime >= *target_year {
90 let Some(limits_map) = process.activity_limits.get(target) else {
91 continue;
92 };
93 if limits_map.get_limit(time_slice_selection).end() > &Dimensionless(0.0) {
94 return true;
95 }
96 }
97 }
98 false
99}
100
101fn validate_commodities_graph(
111 graph: &CommoditiesGraph,
112 commodities: &CommodityMap,
113 time_slice_level: TimeSliceLevel,
114) -> Result<()> {
115 for node_idx in graph.node_indices() {
116 let graph_node = graph.node_weight(node_idx).unwrap();
118 let GraphNode::Commodity(commodity_id, region_id) = graph_node else {
119 continue;
121 };
122
123 let commodity = &commodities[commodity_id];
125 if commodity.time_slice_level != time_slice_level {
126 continue;
127 }
128
129 let has_incoming = graph
131 .edges_directed(node_idx, petgraph::Direction::Incoming)
132 .next()
133 .is_some();
134 let has_outgoing = graph
135 .edges_directed(node_idx, petgraph::Direction::Outgoing)
136 .next()
137 .is_some();
138
139 match commodity.kind {
141 CommodityType::ServiceDemand => {
142 let has_non_demand_outgoing = graph
144 .edges_directed(node_idx, petgraph::Direction::Outgoing)
145 .any(|edge| edge.weight() != &GraphEdge::Demand);
146 ensure!(
147 !has_non_demand_outgoing,
148 "SVD commodity {commodity_id} in region {region_id} cannot be an input to a process"
149 );
150
151 let has_demand_edges = graph
153 .edges_directed(node_idx, petgraph::Direction::Outgoing)
154 .any(|edge| edge.weight() == &GraphEdge::Demand);
155 if has_demand_edges {
156 ensure!(
157 has_incoming,
158 "SVD commodity {commodity_id} in region {region_id} is demanded but has no producers"
159 );
160 }
161 }
162 CommodityType::SupplyEqualsDemand => {
163 ensure!(
165 !has_outgoing || has_incoming,
166 "SED commodity {commodity_id} in region {region_id} may be consumed but has no producers"
167 );
168 }
169 CommodityType::Other => {
170 ensure!(
172 !(has_incoming && has_outgoing),
173 "OTH commodity {commodity_id} in region {region_id} cannot have both producers and consumers"
174 );
175 }
176 }
177 }
178
179 Ok(())
180}
181
182pub fn validate_commodity_graphs_for_model(
202 commodity_graphs: &IndexMap<u32, CommoditiesGraph>,
203 processes: &ProcessMap,
204 commodities: &CommodityMap,
205 time_slice_info: &TimeSliceInfo,
206) -> Result<()> {
207 for (&year, base_graph) in commodity_graphs {
209 for ts_level in TimeSliceLevel::iter() {
210 for ts_selection in time_slice_info.iter_selections_at_level(ts_level) {
211 let graph = prepare_commodities_graph_for_validation(
212 base_graph,
213 processes,
214 commodities,
215 year,
216 &ts_selection,
217 );
218 validate_commodities_graph(&graph, commodities, ts_level).with_context(|| {
219 format!("Error validating commodity graph for {year} in {ts_selection}")
220 })?;
221 }
222 }
223 }
224 Ok(())
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::commodity::Commodity;
231 use crate::fixture::{assert_error, other_commodity, sed_commodity, svd_commodity};
232 use petgraph::graph::Graph;
233 use rstest::rstest;
234 use std::sync::Arc;
235
236 #[rstest]
237 fn validate_commodities_graph_works(
238 other_commodity: Commodity,
239 sed_commodity: Commodity,
240 svd_commodity: Commodity,
241 ) {
242 let mut graph = Graph::new();
243 let mut commodities = CommodityMap::new();
244
245 commodities.insert("A".into(), Arc::new(other_commodity));
247 commodities.insert("B".into(), Arc::new(sed_commodity));
248 commodities.insert("C".into(), Arc::new(svd_commodity));
249
250 let node_a = graph.add_node(GraphNode::Commodity("A".into(), "GBR".into()));
252 let node_b = graph.add_node(GraphNode::Commodity("B".into(), "GBR".into()));
253 let node_c = graph.add_node(GraphNode::Commodity("C".into(), "GBR".into()));
254 let node_d = graph.add_node(GraphNode::Demand);
255 graph.add_edge(
256 node_a,
257 node_b,
258 GraphEdge::Primary("process1".into(), "GBR".into()),
259 );
260 graph.add_edge(
261 node_b,
262 node_c,
263 GraphEdge::Primary("process2".into(), "GBR".into()),
264 );
265 graph.add_edge(node_c, node_d, GraphEdge::Demand);
266
267 validate_commodities_graph(&graph, &commodities, TimeSliceLevel::Annual).unwrap();
269 }
270
271 #[rstest]
272 fn validate_commodities_graph_invalid_svd_consumed(
273 svd_commodity: Commodity,
274 sed_commodity: Commodity,
275 other_commodity: Commodity,
276 ) {
277 let mut graph = Graph::new();
278 let mut commodities = CommodityMap::new();
279
280 commodities.insert("A".into(), Arc::new(svd_commodity));
282 commodities.insert("B".into(), Arc::new(sed_commodity));
283 commodities.insert("C".into(), Arc::new(other_commodity));
284
285 let node_c = graph.add_node(GraphNode::Commodity("C".into(), "GBR".into()));
287 let node_a = graph.add_node(GraphNode::Commodity("A".into(), "GBR".into()));
288 let node_b = graph.add_node(GraphNode::Commodity("B".into(), "GBR".into()));
289 graph.add_edge(
290 node_c,
291 node_a,
292 GraphEdge::Primary("process1".into(), "GBR".into()),
293 );
294 graph.add_edge(
295 node_a,
296 node_b,
297 GraphEdge::Primary("process2".into(), "GBR".into()),
298 );
299
300 assert_error!(
302 validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
303 "SVD commodity A in region GBR cannot be an input to a process"
304 );
305 }
306
307 #[rstest]
308 fn validate_commodities_graph_invalid_svd_not_produced(svd_commodity: Commodity) {
309 let mut graph = Graph::new();
310 let mut commodities = CommodityMap::new();
311
312 commodities.insert("A".into(), Arc::new(svd_commodity));
314
315 let node_a = graph.add_node(GraphNode::Commodity("A".into(), "GBR".into()));
317 let node_b = graph.add_node(GraphNode::Demand);
318 graph.add_edge(node_a, node_b, GraphEdge::Demand);
319
320 assert_error!(
322 validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
323 "SVD commodity A in region GBR is demanded but has no producers"
324 );
325 }
326
327 #[rstest]
328 fn validate_commodities_graph_invalid_sed(sed_commodity: Commodity) {
329 let mut graph = Graph::new();
330 let mut commodities = CommodityMap::new();
331
332 commodities.insert("A".into(), Arc::new(sed_commodity.clone()));
334 commodities.insert("B".into(), Arc::new(sed_commodity));
335
336 let node_a = graph.add_node(GraphNode::Commodity("A".into(), "GBR".into()));
338 let node_b = graph.add_node(GraphNode::Commodity("B".into(), "GBR".into()));
339 graph.add_edge(
340 node_b,
341 node_a,
342 GraphEdge::Primary("process1".into(), "GBR".into()),
343 );
344
345 assert_error!(
347 validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
348 "SED commodity B in region GBR may be consumed but has no producers"
349 );
350 }
351
352 #[rstest]
353 fn validate_commodities_graph_invalid_oth(
354 other_commodity: Commodity,
355 sed_commodity: Commodity,
356 ) {
357 let mut graph = Graph::new();
358 let mut commodities = CommodityMap::new();
359
360 commodities.insert("A".into(), Arc::new(other_commodity));
362 commodities.insert("B".into(), Arc::new(sed_commodity.clone()));
363 commodities.insert("C".into(), Arc::new(sed_commodity));
364
365 let node_a = graph.add_node(GraphNode::Commodity("A".into(), "GBR".into()));
367 let node_b = graph.add_node(GraphNode::Commodity("B".into(), "GBR".into()));
368 let node_c = graph.add_node(GraphNode::Commodity("C".into(), "GBR".into()));
369 graph.add_edge(
370 node_b,
371 node_a,
372 GraphEdge::Primary("process1".into(), "GBR".into()),
373 );
374 graph.add_edge(
375 node_a,
376 node_c,
377 GraphEdge::Primary("process2".into(), "GBR".into()),
378 );
379
380 assert_error!(
382 validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
383 "OTH commodity A in region GBR cannot have both producers and consumers"
384 );
385 }
386}