use super::{CommoditiesGraph, GraphEdge, GraphNode};
use crate::commodity::{CommodityMap, CommodityType};
use crate::process::{Process, ProcessMap};
use crate::region::RegionID;
use crate::time_slice::{TimeSliceInfo, TimeSliceLevel, TimeSliceSelection};
use crate::units::{Dimensionless, Flow};
use anyhow::{Context, Result, ensure};
use indexmap::IndexMap;
use strum::IntoEnumIterator;
fn prepare_commodities_graph_for_validation(
base_graph: &CommoditiesGraph,
processes: &ProcessMap,
commodities: &CommodityMap,
region_id: &RegionID,
year: u32,
time_slice_selection: &TimeSliceSelection,
) -> CommoditiesGraph {
let mut filtered_graph = base_graph.clone();
let key = (region_id.clone(), year);
filtered_graph.retain_edges(|graph, edge_idx| {
let process_id = match graph.edge_weight(edge_idx).unwrap() {
GraphEdge::Primary(process_id) | GraphEdge::Secondary(process_id) => process_id,
GraphEdge::Demand => panic!("Demand edges should not be present in the base graph"),
};
let process = &processes[process_id];
can_be_active(process, &key, time_slice_selection)
});
let demand_node_index = filtered_graph.add_node(GraphNode::Demand);
for (commodity_id, commodity) in commodities {
if commodity
.demand
.get(&(region_id.clone(), year, time_slice_selection.clone()))
.is_some_and(|&v| v > Flow(0.0))
{
let commodity_node = GraphNode::Commodity(commodity_id.clone());
let commodity_node_index = filtered_graph
.node_indices()
.find(|&idx| filtered_graph.node_weight(idx) == Some(&commodity_node))
.unwrap_or_else(|| {
filtered_graph.add_node(GraphNode::Commodity(commodity_id.clone()))
});
filtered_graph.add_edge(commodity_node_index, demand_node_index, GraphEdge::Demand);
}
}
filtered_graph
}
fn can_be_active(
process: &Process,
target: &(RegionID, u32),
time_slice_selection: &TimeSliceSelection,
) -> bool {
let (target_region, target_year) = target;
for ((region, year), value) in &process.parameters {
if region != target_region {
continue;
}
if year + value.lifetime >= *target_year {
let Some(limits_map) = process.activity_limits.get(target) else {
continue;
};
if limits_map.get_limit(time_slice_selection).end() > &Dimensionless(0.0) {
return true;
}
}
}
false
}
fn validate_commodities_graph(
graph: &CommoditiesGraph,
commodities: &CommodityMap,
time_slice_level: TimeSliceLevel,
) -> Result<()> {
for node_idx in graph.node_indices() {
let graph_node = graph.node_weight(node_idx).unwrap();
let GraphNode::Commodity(commodity_id) = graph_node else {
continue;
};
let commodity = &commodities[commodity_id];
if commodity.time_slice_level != time_slice_level {
continue;
}
let has_incoming = graph
.edges_directed(node_idx, petgraph::Direction::Incoming)
.next()
.is_some();
let has_outgoing = graph
.edges_directed(node_idx, petgraph::Direction::Outgoing)
.next()
.is_some();
match commodity.kind {
CommodityType::ServiceDemand => {
let has_non_demand_outgoing = graph
.edges_directed(node_idx, petgraph::Direction::Outgoing)
.any(|edge| edge.weight() != &GraphEdge::Demand);
ensure!(
!has_non_demand_outgoing,
"SVD commodity {commodity_id} cannot be an input to a process"
);
let has_demand_edges = graph
.edges_directed(node_idx, petgraph::Direction::Outgoing)
.any(|edge| edge.weight() == &GraphEdge::Demand);
if has_demand_edges {
ensure!(
has_incoming,
"SVD commodity {commodity_id} is demanded but has no producers"
);
}
}
CommodityType::SupplyEqualsDemand => {
ensure!(
!has_outgoing || has_incoming,
"SED commodity {commodity_id} may be consumed but has no producers"
);
}
CommodityType::Other => {
ensure!(
!(has_incoming && has_outgoing),
"OTH commodity {commodity_id} cannot have both producers and consumers"
);
}
}
}
Ok(())
}
pub fn validate_commodity_graphs_for_model(
commodity_graphs: &IndexMap<(RegionID, u32), CommoditiesGraph>,
processes: &ProcessMap,
commodities: &CommodityMap,
time_slice_info: &TimeSliceInfo,
) -> Result<()> {
for ((region_id, year), base_graph) in commodity_graphs {
for ts_level in TimeSliceLevel::iter() {
for ts_selection in time_slice_info.iter_selections_at_level(ts_level) {
let graph = prepare_commodities_graph_for_validation(
base_graph,
processes,
commodities,
region_id,
*year,
&ts_selection,
);
validate_commodities_graph(&graph, commodities, ts_level).with_context(|| {
format!(
"Error validating commodity graph for \
{region_id} in {year} in {ts_selection}"
)
})?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commodity::Commodity;
use crate::fixture::{assert_error, other_commodity, sed_commodity, svd_commodity};
use petgraph::graph::Graph;
use rstest::rstest;
use std::rc::Rc;
#[rstest]
fn validate_commodities_graph_works(
other_commodity: Commodity,
sed_commodity: Commodity,
svd_commodity: Commodity,
) {
let mut graph = Graph::new();
let mut commodities = CommodityMap::new();
commodities.insert("A".into(), Rc::new(other_commodity));
commodities.insert("B".into(), Rc::new(sed_commodity));
commodities.insert("C".into(), Rc::new(svd_commodity));
let node_a = graph.add_node(GraphNode::Commodity("A".into()));
let node_b = graph.add_node(GraphNode::Commodity("B".into()));
let node_c = graph.add_node(GraphNode::Commodity("C".into()));
let node_d = graph.add_node(GraphNode::Demand);
graph.add_edge(node_a, node_b, GraphEdge::Primary("process1".into()));
graph.add_edge(node_b, node_c, GraphEdge::Primary("process2".into()));
graph.add_edge(node_c, node_d, GraphEdge::Demand);
validate_commodities_graph(&graph, &commodities, TimeSliceLevel::Annual).unwrap();
}
#[rstest]
fn validate_commodities_graph_invalid_svd_consumed(
svd_commodity: Commodity,
sed_commodity: Commodity,
other_commodity: Commodity,
) {
let mut graph = Graph::new();
let mut commodities = CommodityMap::new();
commodities.insert("A".into(), Rc::new(svd_commodity));
commodities.insert("B".into(), Rc::new(sed_commodity));
commodities.insert("C".into(), Rc::new(other_commodity));
let node_c = graph.add_node(GraphNode::Commodity("C".into()));
let node_a = graph.add_node(GraphNode::Commodity("A".into()));
let node_b = graph.add_node(GraphNode::Commodity("B".into()));
graph.add_edge(node_c, node_a, GraphEdge::Primary("process1".into()));
graph.add_edge(node_a, node_b, GraphEdge::Primary("process2".into()));
assert_error!(
validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
"SVD commodity A cannot be an input to a process"
);
}
#[rstest]
fn validate_commodities_graph_invalid_svd_not_produced(svd_commodity: Commodity) {
let mut graph = Graph::new();
let mut commodities = CommodityMap::new();
commodities.insert("A".into(), Rc::new(svd_commodity));
let node_a = graph.add_node(GraphNode::Commodity("A".into()));
let node_b = graph.add_node(GraphNode::Demand);
graph.add_edge(node_a, node_b, GraphEdge::Demand);
assert_error!(
validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
"SVD commodity A is demanded but has no producers"
);
}
#[rstest]
fn validate_commodities_graph_invalid_sed(sed_commodity: Commodity) {
let mut graph = Graph::new();
let mut commodities = CommodityMap::new();
commodities.insert("A".into(), Rc::new(sed_commodity.clone()));
commodities.insert("B".into(), Rc::new(sed_commodity));
let node_a = graph.add_node(GraphNode::Commodity("A".into()));
let node_b = graph.add_node(GraphNode::Commodity("B".into()));
graph.add_edge(node_b, node_a, GraphEdge::Primary("process1".into()));
assert_error!(
validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
"SED commodity B may be consumed but has no producers"
);
}
#[rstest]
fn validate_commodities_graph_invalid_oth(
other_commodity: Commodity,
sed_commodity: Commodity,
) {
let mut graph = Graph::new();
let mut commodities = CommodityMap::new();
commodities.insert("A".into(), Rc::new(other_commodity));
commodities.insert("B".into(), Rc::new(sed_commodity.clone()));
commodities.insert("C".into(), Rc::new(sed_commodity));
let node_a = graph.add_node(GraphNode::Commodity("A".into()));
let node_b = graph.add_node(GraphNode::Commodity("B".into()));
let node_c = graph.add_node(GraphNode::Commodity("C".into()));
graph.add_edge(node_b, node_a, GraphEdge::Primary("process1".into()));
graph.add_edge(node_a, node_c, GraphEdge::Primary("process2".into()));
assert_error!(
validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight),
"OTH commodity A cannot have both producers and consumers"
);
}
}