@@ -10,7 +10,8 @@ use itertools::{Itertools, iproduct};
1010use petgraph:: Directed ;
1111use petgraph:: algo:: toposort;
1212use petgraph:: dot:: Dot ;
13- use petgraph:: graph:: Graph ;
13+ use petgraph:: graph:: { EdgeReference , Graph } ;
14+ use petgraph:: visit:: EdgeFiltered ;
1415use std:: collections:: HashMap ;
1516use std:: fmt:: Display ;
1617use std:: fs:: File ;
@@ -48,16 +49,20 @@ impl Display for GraphNode {
4849#[ derive( Eq , PartialEq , Clone , Hash ) ]
4950/// An edge in the commodity graph
5051pub 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
5760impl 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