Repository navigation
Combine region graphs into single graph for each year #1569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
1eff58a
11d67b0
34895d8
4d1e0c3
5e22b4a
44a754e
7ef1093
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,9 +23,9 @@ pub type CommoditiesGraph = Graph<GraphNode, GraphEdge, Directed>; | |
| #[derive(Eq, PartialEq, Clone, Hash, derive_more::Display)] | ||
| /// A node in the commodity graph | ||
| pub enum GraphNode { | ||
| /// A node representing a commodity | ||
| #[display("{_0}")] | ||
| Commodity(CommodityID), | ||
| /// A node representing a commodity in a specific region | ||
| #[display("{_0} | {_1}")] | ||
| Commodity(CommodityID, RegionID), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be renamed to I've also just noticed that
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. Not sure why |
||
| /// A source node for processes that have no inputs | ||
| #[display("SOURCE")] | ||
| Source, | ||
|
|
@@ -40,12 +40,12 @@ pub enum GraphNode { | |
| #[derive(Eq, PartialEq, Clone, Hash, derive_more::Display)] | ||
| /// An edge in the commodity graph | ||
| pub enum GraphEdge { | ||
| /// An edge representing a primary flow of a process | ||
| #[display("{_0}")] | ||
| Primary(ProcessID), | ||
| /// An edge representing a secondary (non-primary) flow of a process | ||
| #[display("{_0}")] | ||
| Secondary(ProcessID), | ||
| /// An edge representing a primary flow of a process installed in the given region | ||
| #[display("{_0} | {_1}")] | ||
| Primary(ProcessID, RegionID), | ||
| /// An edge representing a secondary (non-primary) flow of a process installed in the given region | ||
| #[display("{_0} | {_1}")] | ||
| Secondary(ProcessID, RegionID), | ||
| /// An edge representing a service demand | ||
| #[display("DEMAND")] | ||
| Demand, | ||
|
|
@@ -81,98 +81,108 @@ fn get_flow_for_year( | |
| None | ||
| } | ||
|
|
||
| /// Creates a directed graph of commodity flows for a given region and year. | ||
| /// Creates a directed graph of commodity flows for a given year, covering all regions. | ||
| /// | ||
| /// The graph contains nodes for all commodities that may be consumed/produced by processes in the | ||
| /// specified region/year. There will be an edge from commodity A to B if there exists a process | ||
| /// that consumes A and produces B. | ||
| /// The graph contains nodes for all `(commodity, region)` pairs that may be consumed/produced | ||
| /// by processes in the specified year across all regions. There will be an edge from node | ||
| /// `(A, R)` to `(B, R)` if there exists a process in region R that consumes commodity A and | ||
| /// produces commodity B. Processes currently only consume and produce commodities in their own | ||
| /// region, so there are no edges between nodes in different regions. | ||
| /// | ||
| /// There are also special `Source` and `Sink` nodes, which are used for processes that have no | ||
| /// inputs or outputs. | ||
| /// | ||
| /// The graph does not take into account process availabilities or commodity demands, both of which | ||
| /// can vary by time slice. See `prepare_commodities_graph_for_validation`. | ||
| fn create_commodities_graph_for_region_year( | ||
| fn create_commodities_graph_for_year( | ||
| processes: &ProcessMap, | ||
| region_id: &RegionID, | ||
| region_ids: &IndexSet<RegionID>, | ||
| year: u32, | ||
| ) -> CommoditiesGraph { | ||
| let mut graph = Graph::new(); | ||
| let mut commodity_to_node_index = HashMap::new(); | ||
|
|
||
| let key = (region_id.clone(), year); | ||
| for process in processes.values() { | ||
| let Some(flows) = get_flow_for_year(process, key.clone()) else { | ||
| // Process doesn't operate in this region/year | ||
| continue; | ||
| }; | ||
|
|
||
| // Get output nodes for the process | ||
| let mut outputs: Vec<_> = flows | ||
| .values() | ||
| .filter(|flow| flow.direction() == FlowDirection::Output) | ||
| .map(|flow| GraphNode::Commodity(flow.commodity.id.clone())) | ||
| .collect(); | ||
|
|
||
| // Get input nodes for the process | ||
| let mut inputs: Vec<_> = flows | ||
| .values() | ||
| .filter(|flow| flow.direction() == FlowDirection::Input) | ||
| .map(|flow| GraphNode::Commodity(flow.commodity.id.clone())) | ||
| .collect(); | ||
|
|
||
| // Use `Source` node if no inputs, `Sink` node if no outputs | ||
| if inputs.is_empty() { | ||
| inputs.push(GraphNode::Source); | ||
| } | ||
| if outputs.is_empty() { | ||
| outputs.push(GraphNode::Sink); | ||
| } | ||
|
|
||
| // Get primary output for the process | ||
| let primary_output = &process.primary_output; | ||
|
|
||
| // Create edges from all inputs to all outputs | ||
| // We also create nodes the first time they are encountered | ||
| for (input, output) in iproduct!(inputs, outputs) { | ||
| let source_node_index = *commodity_to_node_index | ||
| .entry(input.clone()) | ||
| .or_insert_with(|| graph.add_node(input.clone())); | ||
| let target_node_index = *commodity_to_node_index | ||
| .entry(output.clone()) | ||
| .or_insert_with(|| graph.add_node(output.clone())); | ||
| let is_primary = match &output { | ||
| GraphNode::Commodity(commodity_id) => primary_output.as_ref() == Some(commodity_id), | ||
| _ => false, | ||
| let mut node_index = HashMap::new(); | ||
|
|
||
| for region_id in region_ids { | ||
| let key = (region_id.clone(), year); | ||
| for process in processes.values() { | ||
| let Some(flows) = get_flow_for_year(process, key.clone()) else { | ||
| // Process doesn't operate in this region/year | ||
| continue; | ||
| }; | ||
|
|
||
| graph.add_edge( | ||
| source_node_index, | ||
| target_node_index, | ||
| if is_primary { | ||
| GraphEdge::Primary(process.id.clone()) | ||
| } else { | ||
| GraphEdge::Secondary(process.id.clone()) | ||
| }, | ||
| ); | ||
| // Get output nodes for the process | ||
| // For now, we assume processes always output commodities in their own region | ||
| let mut outputs: Vec<_> = flows | ||
| .values() | ||
| .filter(|flow| flow.direction() == FlowDirection::Output) | ||
| .map(|flow| GraphNode::Commodity(flow.commodity.id.clone(), region_id.clone())) | ||
| .collect(); | ||
|
|
||
| // Get input nodes for the process | ||
| // For now, we assume processes always input commodities in their own region | ||
| let mut inputs: Vec<_> = flows | ||
| .values() | ||
| .filter(|flow| flow.direction() == FlowDirection::Input) | ||
| .map(|flow| GraphNode::Commodity(flow.commodity.id.clone(), region_id.clone())) | ||
| .collect(); | ||
|
|
||
| // Use `Source` node if no inputs, `Sink` node if no outputs | ||
| if inputs.is_empty() { | ||
| inputs.push(GraphNode::Source); | ||
| } | ||
| if outputs.is_empty() { | ||
| outputs.push(GraphNode::Sink); | ||
| } | ||
|
|
||
| // Get primary output for the process | ||
| let primary_output = &process.primary_output; | ||
|
|
||
| // Create edges from all inputs to all outputs. | ||
| // Edges carry the asset's installing region so the availability filter in | ||
| // validation can look up `(process_id, asset_region, year)`. | ||
| for (input, output) in iproduct!(inputs, outputs) { | ||
| let source_node_index = *node_index | ||
| .entry(input.clone()) | ||
| .or_insert_with(|| graph.add_node(input.clone())); | ||
| let target_node_index = *node_index | ||
| .entry(output.clone()) | ||
| .or_insert_with(|| graph.add_node(output.clone())); | ||
| let is_primary = match &output { | ||
| GraphNode::Commodity(commodity_id, _) => { | ||
| primary_output.as_ref() == Some(commodity_id) | ||
| } | ||
| _ => false, | ||
| }; | ||
|
|
||
| graph.add_edge( | ||
| source_node_index, | ||
| target_node_index, | ||
| if is_primary { | ||
| GraphEdge::Primary(process.id.clone(), region_id.clone()) | ||
| } else { | ||
| GraphEdge::Secondary(process.id.clone(), region_id.clone()) | ||
| }, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| graph | ||
| } | ||
|
|
||
| /// Builds base commodity graphs for each region and year | ||
| /// Builds base commodity graphs for each year, covering all regions. | ||
| /// | ||
| /// These do not take into account demand and process availability | ||
| /// These do not take into account demand and process availability. | ||
| pub fn build_commodity_graphs_for_model( | ||
| processes: &ProcessMap, | ||
| region_ids: &IndexSet<RegionID>, | ||
| years: &[u32], | ||
| ) -> IndexMap<(RegionID, u32), CommoditiesGraph> { | ||
| iproduct!(region_ids, years.iter()) | ||
| .map(|(region_id, year)| { | ||
| let graph = create_commodities_graph_for_region_year(processes, region_id, *year); | ||
| ((region_id.clone(), *year), graph) | ||
| ) -> IndexMap<u32, CommoditiesGraph> { | ||
| years | ||
| .iter() | ||
| .map(|&year| { | ||
| let graph = create_commodities_graph_for_year(processes, region_ids, year); | ||
| (year, graph) | ||
| }) | ||
| .collect() | ||
| } | ||
|
|
@@ -181,27 +191,27 @@ pub fn build_commodity_graphs_for_model( | |
| fn get_edge_attributes(_: &CommoditiesGraph, edge_ref: EdgeReference<GraphEdge>) -> String { | ||
| match edge_ref.weight() { | ||
| // Use dashed lines for secondary flows | ||
| GraphEdge::Secondary(_) => "style=dashed".to_string(), | ||
| GraphEdge::Secondary(..) => "style=dashed".to_string(), | ||
| // Other edges use default attributes | ||
| _ => String::new(), | ||
| } | ||
| } | ||
|
|
||
| /// Saves commodity graphs to file | ||
| /// | ||
| /// The graphs are saved as DOT files to the specified output path | ||
| /// The graphs are saved as DOT files to the specified output path, one file per year. | ||
| pub fn save_commodity_graphs_for_model( | ||
| commodity_graphs: &IndexMap<(RegionID, u32), CommoditiesGraph>, | ||
| commodity_graphs: &IndexMap<u32, CommoditiesGraph>, | ||
| output_path: &Path, | ||
| ) -> Result<()> { | ||
| for ((region_id, year), graph) in commodity_graphs { | ||
| for (year, graph) in commodity_graphs { | ||
| let dot = Dot::with_attr_getters( | ||
| graph, | ||
| &[], | ||
| &get_edge_attributes, // Custom attributes for edges | ||
| &|_, _| String::new(), // Use default attributes for nodes | ||
| ); | ||
| let mut file = File::create(output_path.join(format!("{region_id}_{year}.dot")))?; | ||
| let mut file = File::create(output_path.join(format!("{year}.dot")))?; | ||
| write!(file, "{dot}")?; | ||
| } | ||
| Ok(()) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.