Skip to content

Commit 6c18774

Browse files
authored
Merge pull request #949 from EnergySystemsModellingLab/topo_sort_primary_commodities
Only consider primary flows in the topological sort
2 parents 2424583 + 0a12365 commit 6c18774

File tree

2 files changed

+60
-22
lines changed

2 files changed

+60
-22
lines changed

docs/user_guide.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ This command will output a graph for each region/year in the simulation, where n
112112
and edges are processes.
113113
Graphs will be saved in [DOT format], which can be visualised locally with [Graphviz], or online
114114
with [Graphviz online].
115+
Dashed lines are used to indicate flows for non-primary outputs of a process (as defined in the
116+
`processes.csv` input file).
115117

116118
[the `muse2 save-graphs` command]: https://energysystemsmodellinglab.github.io/MUSE2/command_line_help.html#muse2-save-graphs
117119
[DOT format]: https://graphviz.org/doc/info/lang.html

src/graph.rs

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ use itertools::{Itertools, iproduct};
1010
use petgraph::Directed;
1111
use petgraph::algo::toposort;
1212
use petgraph::dot::Dot;
13-
use petgraph::graph::Graph;
13+
use petgraph::graph::{EdgeReference, Graph};
14+
use petgraph::visit::EdgeFiltered;
1415
use std::collections::HashMap;
1516
use std::fmt::Display;
1617
use std::fs::File;
@@ -48,16 +49,20 @@ impl Display for GraphNode {
4849
#[derive(Eq, PartialEq, Clone, Hash)]
4950
/// An edge in the commodity graph
5051
pub enum GraphEdge {
51-
/// An edge representing a process
52-
Process(ProcessID),
52+
/// An edge representing a primary flow of a process
53+
Primary(ProcessID),
54+
/// An edge representing a secondary (non-primary) flow of a process
55+
Secondary(ProcessID),
5356
/// An edge representing a service demand
5457
Demand,
5558
}
5659

5760
impl Display for GraphEdge {
5861
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5962
match self {
60-
GraphEdge::Process(id) => write!(f, "{id}"),
63+
GraphEdge::Primary(process_id) | GraphEdge::Secondary(process_id) => {
64+
write!(f, "{process_id}")
65+
}
6166
GraphEdge::Demand => write!(f, "DEMAND"),
6267
}
6368
}
@@ -111,6 +116,9 @@ fn create_commodities_graph_for_region_year(
111116
outputs.push(GraphNode::Sink);
112117
}
113118

119+
// Get primary output for the process
120+
let primary_output = &process.primary_output;
121+
114122
// Create edges from all inputs to all outputs
115123
// We also create nodes the first time they are encountered
116124
for (input, output) in iproduct!(inputs, outputs) {
@@ -120,10 +128,19 @@ fn create_commodities_graph_for_region_year(
120128
let target_node_index = *commodity_to_node_index
121129
.entry(output.clone())
122130
.or_insert_with(|| graph.add_node(output.clone()));
131+
let is_primary = match &output {
132+
GraphNode::Commodity(commodity_id) => primary_output.as_ref() == Some(commodity_id),
133+
_ => false,
134+
};
135+
123136
graph.add_edge(
124137
source_node_index,
125138
target_node_index,
126-
GraphEdge::Process(process.id.clone()),
139+
if is_primary {
140+
GraphEdge::Primary(process.id.clone())
141+
} else {
142+
GraphEdge::Secondary(process.id.clone())
143+
},
127144
);
128145
}
129146
}
@@ -157,13 +174,13 @@ fn prepare_commodities_graph_for_validation(
157174
let key = (region_id.clone(), year);
158175
filtered_graph.retain_edges(|graph, edge_idx| {
159176
// Get the process for the edge
160-
let GraphEdge::Process(process_id) = graph.edge_weight(edge_idx).unwrap() else {
161-
panic!("Demand edges should not be present in the base graph");
177+
let process_id = match graph.edge_weight(edge_idx).unwrap() {
178+
GraphEdge::Primary(process_id) | GraphEdge::Secondary(process_id) => process_id,
179+
GraphEdge::Demand => panic!("Demand edges should not be present in the base graph"),
162180
};
163181
let process = &processes[process_id];
164182

165183
// Check if the process has availability > 0 in any time slice in the selection
166-
167184
time_slice_selection
168185
.iter(time_slice_info)
169186
.any(|(time_slice, _)| {
@@ -242,7 +259,7 @@ fn validate_commodities_graph(
242259
// Match validation rules to commodity type
243260
match commodity.kind {
244261
CommodityType::ServiceDemand => {
245-
// Cannot have outgoing `Process` (non-`Demand`) edges
262+
// Cannot have outgoing `Primary`/`Secondary` (non-`Demand`) edges
246263
let has_non_demand_outgoing = graph
247264
.edges_directed(node_idx, petgraph::Direction::Outgoing)
248265
.any(|edge| edge.weight() != &GraphEdge::Demand);
@@ -289,8 +306,12 @@ fn topo_sort_commodities(
289306
graph: &CommoditiesGraph,
290307
commodities: &CommodityMap,
291308
) -> Result<Vec<CommodityID>> {
309+
// We only consider primary edges
310+
let primary_graph =
311+
EdgeFiltered::from_fn(graph, |edge| matches!(edge.weight(), GraphEdge::Primary(_)));
312+
292313
// Perform a topological sort on the graph
293-
let order = toposort(graph, None).map_err(|cycle| {
314+
let order = toposort(&primary_graph, None).map_err(|cycle| {
294315
let cycle_commodity = graph.node_weight(cycle.node_id()).unwrap().clone();
295316
anyhow!("Cycle detected in commodity graph for commodity {cycle_commodity}")
296317
})?;
@@ -413,6 +434,16 @@ pub fn validate_commodity_graphs_for_model(
413434
Ok(commodity_order)
414435
}
415436

437+
/// Gets custom DOT attributes for edges in a commodity graph
438+
fn get_edge_attributes(_: &CommoditiesGraph, edge_ref: EdgeReference<GraphEdge>) -> String {
439+
match edge_ref.weight() {
440+
// Use dashed lines for secondary flows
441+
GraphEdge::Secondary(_) => "style=dashed".to_string(),
442+
// Other edges use default attributes
443+
_ => String::new(),
444+
}
445+
}
446+
416447
/// Saves commodity graphs to file
417448
///
418449
/// The graphs are saved as DOT files to the specified output path
@@ -421,7 +452,12 @@ pub fn save_commodity_graphs_for_model(
421452
output_path: &Path,
422453
) -> Result<()> {
423454
for ((region_id, year), graph) in commodity_graphs {
424-
let dot = Dot::new(&graph);
455+
let dot = Dot::with_attr_getters(
456+
graph,
457+
&[],
458+
&get_edge_attributes, // Custom attributes for edges
459+
&|_, _| String::new(), // Use default attributes for nodes
460+
);
425461
let mut file = File::create(output_path.join(format!("{region_id}_{year}.dot")))?;
426462
write!(file, "{dot}")?;
427463
}
@@ -447,8 +483,8 @@ mod tests {
447483
let node_c = graph.add_node(GraphNode::Commodity("C".into()));
448484

449485
// Add edges: A -> B -> C
450-
graph.add_edge(node_a, node_b, GraphEdge::Process("process1".into()));
451-
graph.add_edge(node_b, node_c, GraphEdge::Process("process2".into()));
486+
graph.add_edge(node_a, node_b, GraphEdge::Primary("process1".into()));
487+
graph.add_edge(node_b, node_c, GraphEdge::Primary("process2".into()));
452488

453489
// Create commodities map using fixtures
454490
let mut commodities = CommodityMap::new();
@@ -474,8 +510,8 @@ mod tests {
474510
let node_b = graph.add_node(GraphNode::Commodity("B".into()));
475511

476512
// Add edges creating a cycle: A -> B -> A
477-
graph.add_edge(node_a, node_b, GraphEdge::Process("process1".into()));
478-
graph.add_edge(node_b, node_a, GraphEdge::Process("process2".into()));
513+
graph.add_edge(node_a, node_b, GraphEdge::Primary("process1".into()));
514+
graph.add_edge(node_b, node_a, GraphEdge::Primary("process2".into()));
479515

480516
// Create commodities map using fixtures
481517
let mut commodities = CommodityMap::new();
@@ -508,8 +544,8 @@ mod tests {
508544
let node_b = graph.add_node(GraphNode::Commodity("B".into()));
509545
let node_c = graph.add_node(GraphNode::Commodity("C".into()));
510546
let node_d = graph.add_node(GraphNode::Demand);
511-
graph.add_edge(node_a, node_b, GraphEdge::Process("process1".into()));
512-
graph.add_edge(node_b, node_c, GraphEdge::Process("process2".into()));
547+
graph.add_edge(node_a, node_b, GraphEdge::Primary("process1".into()));
548+
graph.add_edge(node_b, node_c, GraphEdge::Primary("process2".into()));
513549
graph.add_edge(node_c, node_d, GraphEdge::Demand);
514550

515551
// Validate the graph at DayNight level
@@ -535,8 +571,8 @@ mod tests {
535571
let node_c = graph.add_node(GraphNode::Commodity("C".into()));
536572
let node_a = graph.add_node(GraphNode::Commodity("A".into()));
537573
let node_b = graph.add_node(GraphNode::Commodity("B".into()));
538-
graph.add_edge(node_c, node_a, GraphEdge::Process("process1".into()));
539-
graph.add_edge(node_a, node_b, GraphEdge::Process("process2".into()));
574+
graph.add_edge(node_c, node_a, GraphEdge::Primary("process1".into()));
575+
graph.add_edge(node_a, node_b, GraphEdge::Primary("process2".into()));
540576

541577
// Validate the graph at DayNight level
542578
let result = validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight);
@@ -573,7 +609,7 @@ mod tests {
573609
// Build invalid graph: B(SED) -> A(SED)
574610
let node_a = graph.add_node(GraphNode::Commodity("A".into()));
575611
let node_b = graph.add_node(GraphNode::Commodity("B".into()));
576-
graph.add_edge(node_b, node_a, GraphEdge::Process("process1".into()));
612+
graph.add_edge(node_b, node_a, GraphEdge::Primary("process1".into()));
577613

578614
// Validate the graph at DayNight level
579615
let result = validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight);
@@ -600,8 +636,8 @@ mod tests {
600636
let node_a = graph.add_node(GraphNode::Commodity("A".into()));
601637
let node_b = graph.add_node(GraphNode::Commodity("B".into()));
602638
let node_c = graph.add_node(GraphNode::Commodity("C".into()));
603-
graph.add_edge(node_b, node_a, GraphEdge::Process("process1".into()));
604-
graph.add_edge(node_a, node_c, GraphEdge::Process("process2".into()));
639+
graph.add_edge(node_b, node_a, GraphEdge::Primary("process1".into()));
640+
graph.add_edge(node_a, node_c, GraphEdge::Primary("process2".into()));
605641

606642
// Validate the graph at DayNight level
607643
let result = validate_commodities_graph(&graph, &commodities, TimeSliceLevel::DayNight);

0 commit comments

Comments
 (0)