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 region_id: &RegionID,
27 year: u32,
28 time_slice_selection: &TimeSliceSelection,
29) -> CommoditiesGraph {
30 let mut filtered_graph = base_graph.clone();
31
32 let key = (region_id.clone(), year);
35 filtered_graph.retain_edges(|graph, edge_idx| {
36 let process_id = match graph.edge_weight(edge_idx).unwrap() {
38 GraphEdge::Primary(process_id) | GraphEdge::Secondary(process_id) => process_id,
39 GraphEdge::Demand => panic!("Demand edges should not be present in the base graph"),
40 };
41 let process = &processes[process_id];
42
43 can_be_active(process, &key, time_slice_selection)
45 });
46
47 let demand_node_index = filtered_graph.add_node(GraphNode::Demand);
51 for (commodity_id, commodity) in commodities {
52 if commodity
53 .demand
54 .get(&(region_id.clone(), year, time_slice_selection.clone()))
55 .is_some_and(|&v| v > Flow(0.0))
56 {
57 let commodity_node = GraphNode::Commodity(commodity_id.clone());
58 let commodity_node_index = filtered_graph
59 .node_indices()
60 .find(|&idx| filtered_graph.node_weight(idx) == Some(&commodity_node))
61 .unwrap_or_else(|| {
62 filtered_graph.add_node(GraphNode::Commodity(commodity_id.clone()))
63 });
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) = 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} 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} is demanded but has no producers"
159 );
160 }
161 }
162 CommodityType::SupplyEqualsDemand => {
163 ensure!(
165 !has_outgoing || has_incoming,
166 "SED commodity {commodity_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} cannot have both producers and consumers"
174 );
175 }
176 }
177 }
178
179 Ok(())
180}
181
182pub fn validate_commodity_graphs_for_model(
204 commodity_graphs: &IndexMap<(RegionID, u32), CommoditiesGraph>,
205 processes: &ProcessMap,
206 commodities: &CommodityMap,
207 time_slice_info: &TimeSliceInfo,
208) -> Result<()> {
209 for ((region_id, year), base_graph) in commodity_graphs {
211 for ts_level in TimeSliceLevel::iter() {
212 for ts_selection in time_slice_info.iter_selections_at_level(ts_level) {
213 let graph = prepare_commodities_graph_for_validation(
214 base_graph,
215 processes,
216 commodities,
217 region_id,
218 *year,
219 &ts_selection,
220 );
221 validate_commodities_graph(&graph, commodities, ts_level).with_context(|| {
222 format!(
223 "Error validating commodity graph for \
224 {region_id} in {year} in {ts_selection}"
225 )
226 })?;
227 }
228 }
229 }
230 Ok(())
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::commodity::Commodity;
237 use crate::fixture::{assert_error, other_commodity, sed_commodity, svd_commodity};
238 use petgraph::graph::Graph;
239 use rstest::rstest;
240 use std::rc::Rc;
241
242 #[rstest]
243 fn validate_commodities_graph_works(
244 other_commodity: Commodity,
245 sed_commodity: Commodity,
246 svd_commodity: Commodity,
247 ) {
248 let mut graph = Graph::new();
249 let mut commodities = CommodityMap::new();
250
251 commodities.insert("A".into(), Rc::new(other_commodity));
253 commodities.insert("B".into(), Rc::new(sed_commodity));
254 commodities.insert("C".into(), Rc::new(svd_commodity));
255
256 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
258 let node_b = graph.add_node(GraphNode::Commodity("B".into()));
259 let node_c = graph.add_node(GraphNode::Commodity("C".into()));
260 let node_d = graph.add_node(GraphNode::Demand);
261 graph.add_edge(node_a, node_b, GraphEdge::Primary("process1".into()));
262 graph.add_edge(node_b, node_c, GraphEdge::Primary("process2".into()));
263 graph.add_edge(node_c, node_d, GraphEdge::Demand);
264
265 validate_commodities_graph(&graph, &commodities, TimeSliceLevel::Annual).unwrap();
267 }
268
269 #[rstest]
270 fn validate_commodities_graph_invalid_svd_consumed(
271 svd_commodity: Commodity,
272 sed_commodity: Commodity,
273 other_commodity: Commodity,
274 ) {
275 let mut graph = Graph::new();
276 let mut commodities = CommodityMap::new();
277
278 commodities.insert("A".into(), Rc::new(svd_commodity));
280 commodities.insert("B".into(), Rc::new(sed_commodity));
281 commodities.insert("C".into(), Rc::new(other_commodity));
282
283 let node_c = graph.add_node(GraphNode::Commodity("C".into()));
285 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
286 let node_b = graph.add_node(GraphNode::Commodity("B".into()));
287 graph.add_edge(node_c, node_a, GraphEdge::Primary("process1".into()));
288 graph.add_edge(node_a, node_b, GraphEdge::Primary("process2".into()));
289
290 assert_error!(
292 validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
293 "SVD commodity A cannot be an input to a process"
294 );
295 }
296
297 #[rstest]
298 fn validate_commodities_graph_invalid_svd_not_produced(svd_commodity: Commodity) {
299 let mut graph = Graph::new();
300 let mut commodities = CommodityMap::new();
301
302 commodities.insert("A".into(), Rc::new(svd_commodity));
304
305 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
307 let node_b = graph.add_node(GraphNode::Demand);
308 graph.add_edge(node_a, node_b, GraphEdge::Demand);
309
310 assert_error!(
312 validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
313 "SVD commodity A is demanded but has no producers"
314 );
315 }
316
317 #[rstest]
318 fn validate_commodities_graph_invalid_sed(sed_commodity: Commodity) {
319 let mut graph = Graph::new();
320 let mut commodities = CommodityMap::new();
321
322 commodities.insert("A".into(), Rc::new(sed_commodity.clone()));
324 commodities.insert("B".into(), Rc::new(sed_commodity));
325
326 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
328 let node_b = graph.add_node(GraphNode::Commodity("B".into()));
329 graph.add_edge(node_b, node_a, GraphEdge::Primary("process1".into()));
330
331 assert_error!(
333 validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
334 "SED commodity B may be consumed but has no producers"
335 );
336 }
337
338 #[rstest]
339 fn validate_commodities_graph_invalid_oth(
340 other_commodity: Commodity,
341 sed_commodity: Commodity,
342 ) {
343 let mut graph = Graph::new();
344 let mut commodities = CommodityMap::new();
345
346 commodities.insert("A".into(), Rc::new(other_commodity));
348 commodities.insert("B".into(), Rc::new(sed_commodity.clone()));
349 commodities.insert("C".into(), Rc::new(sed_commodity));
350
351 let node_a = graph.add_node(GraphNode::Commodity("A".into()));
353 let node_b = graph.add_node(GraphNode::Commodity("B".into()));
354 let node_c = graph.add_node(GraphNode::Commodity("C".into()));
355 graph.add_edge(node_b, node_a, GraphEdge::Primary("process1".into()));
356 graph.add_edge(node_a, node_c, GraphEdge::Primary("process2".into()));
357
358 assert_error!(
360 validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
361 "OTH commodity A cannot have both producers and consumers"
362 );
363 }
364}