1use crate::commodity::CommodityID;
3use crate::process::{FlowDirection, Process, ProcessFlow, ProcessID, ProcessMap};
4use crate::region::RegionID;
5use anyhow::Result;
6use indexmap::{IndexMap, IndexSet};
7use itertools::iproduct;
8use petgraph::Directed;
9use petgraph::dot::Dot;
10use petgraph::graph::{EdgeReference, Graph};
11use std::collections::HashMap;
12use std::fmt::Display;
13use std::fs::File;
14use std::io::Write as IoWrite;
15use std::path::Path;
16use std::rc::Rc;
17
18pub mod investment;
19pub mod validate;
20
21pub type CommoditiesGraph = Graph<GraphNode, GraphEdge, Directed>;
23
24#[derive(Eq, PartialEq, Clone, Hash)]
25pub enum GraphNode {
27 Commodity(CommodityID),
29 Source,
31 Sink,
33 Demand,
35}
36
37impl Display for GraphNode {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 GraphNode::Commodity(id) => write!(f, "{id}"),
41 GraphNode::Source => write!(f, "SOURCE"),
42 GraphNode::Sink => write!(f, "SINK"),
43 GraphNode::Demand => write!(f, "DEMAND"),
44 }
45 }
46}
47
48#[derive(Eq, PartialEq, Clone, Hash)]
49pub enum GraphEdge {
51 Primary(ProcessID),
53 Secondary(ProcessID),
55 Demand,
57}
58
59impl Display for GraphEdge {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 GraphEdge::Primary(process_id) | GraphEdge::Secondary(process_id) => {
63 write!(f, "{process_id}")
64 }
65 GraphEdge::Demand => write!(f, "DEMAND"),
66 }
67 }
68}
69
70fn get_flow_for_year(
79 process: &Process,
80 target: (RegionID, u32),
81) -> Option<Rc<IndexMap<CommodityID, ProcessFlow>>> {
82 if process.flows.contains_key(&target) {
84 return process.flows.get(&target).cloned();
85 }
86
87 let (target_region, target_year) = target;
90 for ((region, year), value) in &process.flows {
91 if *region != target_region {
92 continue;
93 }
94 if year
95 + process
96 .parameters
97 .get(&(region.clone(), *year))
98 .unwrap()
99 .lifetime
100 >= target_year
101 {
102 return Some(value.clone());
103 }
104 }
105 None
106}
107
108fn create_commodities_graph_for_region_year(
120 processes: &ProcessMap,
121 region_id: &RegionID,
122 year: u32,
123) -> CommoditiesGraph {
124 let mut graph = Graph::new();
125 let mut commodity_to_node_index = HashMap::new();
126
127 let key = (region_id.clone(), year);
128 for process in processes.values() {
129 let Some(flows) = get_flow_for_year(process, key.clone()) else {
130 continue;
132 };
133
134 let mut outputs: Vec<_> = flows
136 .values()
137 .filter(|flow| flow.direction() == FlowDirection::Output)
138 .map(|flow| GraphNode::Commodity(flow.commodity.id.clone()))
139 .collect();
140
141 let mut inputs: Vec<_> = flows
143 .values()
144 .filter(|flow| flow.direction() == FlowDirection::Input)
145 .map(|flow| GraphNode::Commodity(flow.commodity.id.clone()))
146 .collect();
147
148 if inputs.is_empty() {
150 inputs.push(GraphNode::Source);
151 }
152 if outputs.is_empty() {
153 outputs.push(GraphNode::Sink);
154 }
155
156 let primary_output = &process.primary_output;
158
159 for (input, output) in iproduct!(inputs, outputs) {
162 let source_node_index = *commodity_to_node_index
163 .entry(input.clone())
164 .or_insert_with(|| graph.add_node(input.clone()));
165 let target_node_index = *commodity_to_node_index
166 .entry(output.clone())
167 .or_insert_with(|| graph.add_node(output.clone()));
168 let is_primary = match &output {
169 GraphNode::Commodity(commodity_id) => primary_output.as_ref() == Some(commodity_id),
170 _ => false,
171 };
172
173 graph.add_edge(
174 source_node_index,
175 target_node_index,
176 if is_primary {
177 GraphEdge::Primary(process.id.clone())
178 } else {
179 GraphEdge::Secondary(process.id.clone())
180 },
181 );
182 }
183 }
184
185 graph
186}
187
188pub fn build_commodity_graphs_for_model(
192 processes: &ProcessMap,
193 region_ids: &IndexSet<RegionID>,
194 years: &[u32],
195) -> IndexMap<(RegionID, u32), CommoditiesGraph> {
196 iproduct!(region_ids, years.iter())
197 .map(|(region_id, year)| {
198 let graph = create_commodities_graph_for_region_year(processes, region_id, *year);
199 ((region_id.clone(), *year), graph)
200 })
201 .collect()
202}
203
204fn get_edge_attributes(_: &CommoditiesGraph, edge_ref: EdgeReference<GraphEdge>) -> String {
206 match edge_ref.weight() {
207 GraphEdge::Secondary(_) => "style=dashed".to_string(),
209 _ => String::new(),
211 }
212}
213
214pub fn save_commodity_graphs_for_model(
218 commodity_graphs: &IndexMap<(RegionID, u32), CommoditiesGraph>,
219 output_path: &Path,
220) -> Result<()> {
221 for ((region_id, year), graph) in commodity_graphs {
222 let dot = Dot::with_attr_getters(
223 graph,
224 &[],
225 &get_edge_attributes, &|_, _| String::new(), );
228 let mut file = File::create(output_path.join(format!("{region_id}_{year}.dot")))?;
229 write!(file, "{dot}")?;
230 }
231 Ok(())
232}