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::fs::File;
13use std::io::Write as IoWrite;
14use std::path::Path;
15use std::rc::Rc;
16
17pub mod investment;
18pub mod validate;
19
20pub type CommoditiesGraph = Graph<GraphNode, GraphEdge, Directed>;
22
23#[derive(Eq, PartialEq, Clone, Hash, derive_more::Display)]
24pub enum GraphNode {
26 #[display("{_0}")]
28 Commodity(CommodityID),
29 #[display("SOURCE")]
31 Source,
32 #[display("SINK")]
34 Sink,
35 #[display("DEMAND")]
37 Demand,
38}
39
40#[derive(Eq, PartialEq, Clone, Hash, derive_more::Display)]
41pub enum GraphEdge {
43 #[display("{_0}")]
45 Primary(ProcessID),
46 #[display("{_0}")]
48 Secondary(ProcessID),
49 #[display("DEMAND")]
51 Demand,
52}
53
54fn get_flow_for_year(
62 process: &Process,
63 target: (RegionID, u32),
64) -> Option<Rc<IndexMap<CommodityID, ProcessFlow>>> {
65 if process.flows.contains_key(&target) {
67 return process.flows.get(&target).cloned();
68 }
69
70 let (target_region, target_year) = target;
73 for ((region, year), value) in &process.flows {
74 if *region != target_region {
75 continue;
76 }
77 if year + process.parameters[&(region.clone(), *year)].lifetime >= target_year {
78 return Some(value.clone());
79 }
80 }
81 None
82}
83
84fn create_commodities_graph_for_region_year(
96 processes: &ProcessMap,
97 region_id: &RegionID,
98 year: u32,
99) -> CommoditiesGraph {
100 let mut graph = Graph::new();
101 let mut commodity_to_node_index = HashMap::new();
102
103 let key = (region_id.clone(), year);
104 for process in processes.values() {
105 let Some(flows) = get_flow_for_year(process, key.clone()) else {
106 continue;
108 };
109
110 let mut outputs: Vec<_> = flows
112 .values()
113 .filter(|flow| flow.direction() == FlowDirection::Output)
114 .map(|flow| GraphNode::Commodity(flow.commodity.id.clone()))
115 .collect();
116
117 let mut inputs: Vec<_> = flows
119 .values()
120 .filter(|flow| flow.direction() == FlowDirection::Input)
121 .map(|flow| GraphNode::Commodity(flow.commodity.id.clone()))
122 .collect();
123
124 if inputs.is_empty() {
126 inputs.push(GraphNode::Source);
127 }
128 if outputs.is_empty() {
129 outputs.push(GraphNode::Sink);
130 }
131
132 let primary_output = &process.primary_output;
134
135 for (input, output) in iproduct!(inputs, outputs) {
138 let source_node_index = *commodity_to_node_index
139 .entry(input.clone())
140 .or_insert_with(|| graph.add_node(input.clone()));
141 let target_node_index = *commodity_to_node_index
142 .entry(output.clone())
143 .or_insert_with(|| graph.add_node(output.clone()));
144 let is_primary = match &output {
145 GraphNode::Commodity(commodity_id) => primary_output.as_ref() == Some(commodity_id),
146 _ => false,
147 };
148
149 graph.add_edge(
150 source_node_index,
151 target_node_index,
152 if is_primary {
153 GraphEdge::Primary(process.id.clone())
154 } else {
155 GraphEdge::Secondary(process.id.clone())
156 },
157 );
158 }
159 }
160
161 graph
162}
163
164pub fn build_commodity_graphs_for_model(
168 processes: &ProcessMap,
169 region_ids: &IndexSet<RegionID>,
170 years: &[u32],
171) -> IndexMap<(RegionID, u32), CommoditiesGraph> {
172 iproduct!(region_ids, years.iter())
173 .map(|(region_id, year)| {
174 let graph = create_commodities_graph_for_region_year(processes, region_id, *year);
175 ((region_id.clone(), *year), graph)
176 })
177 .collect()
178}
179
180fn get_edge_attributes(_: &CommoditiesGraph, edge_ref: EdgeReference<GraphEdge>) -> String {
182 match edge_ref.weight() {
183 GraphEdge::Secondary(_) => "style=dashed".to_string(),
185 _ => String::new(),
187 }
188}
189
190pub fn save_commodity_graphs_for_model(
194 commodity_graphs: &IndexMap<(RegionID, u32), CommoditiesGraph>,
195 output_path: &Path,
196) -> Result<()> {
197 for ((region_id, year), graph) in commodity_graphs {
198 let dot = Dot::with_attr_getters(
199 graph,
200 &[],
201 &get_edge_attributes, &|_, _| String::new(), );
204 let mut file = File::create(output_path.join(format!("{region_id}_{year}.dot")))?;
205 write!(file, "{dot}")?;
206 }
207 Ok(())
208}