use crate::commodity::CommodityID;
use crate::process::{FlowDirection, Process, ProcessFlow, ProcessID, ProcessMap};
use crate::region::RegionID;
use anyhow::Result;
use indexmap::{IndexMap, IndexSet};
use itertools::iproduct;
use petgraph::Directed;
use petgraph::dot::Dot;
use petgraph::graph::{EdgeReference, Graph};
use std::collections::HashMap;
use std::fmt::Display;
use std::fs::File;
use std::io::Write as IoWrite;
use std::path::Path;
use std::rc::Rc;
pub mod investment;
pub mod validate;
pub type CommoditiesGraph = Graph<GraphNode, GraphEdge, Directed>;
#[derive(Eq, PartialEq, Clone, Hash)]
pub enum GraphNode {
Commodity(CommodityID),
Source,
Sink,
Demand,
}
impl Display for GraphNode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GraphNode::Commodity(id) => write!(f, "{id}"),
GraphNode::Source => write!(f, "SOURCE"),
GraphNode::Sink => write!(f, "SINK"),
GraphNode::Demand => write!(f, "DEMAND"),
}
}
}
#[derive(Eq, PartialEq, Clone, Hash)]
pub enum GraphEdge {
Primary(ProcessID),
Secondary(ProcessID),
Demand,
}
impl Display for GraphEdge {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GraphEdge::Primary(process_id) | GraphEdge::Secondary(process_id) => {
write!(f, "{process_id}")
}
GraphEdge::Demand => write!(f, "DEMAND"),
}
}
}
fn get_flow_for_year(
process: &Process,
target: (RegionID, u32),
) -> Option<Rc<IndexMap<CommodityID, ProcessFlow>>> {
if process.flows.contains_key(&target) {
return process.flows.get(&target).cloned();
}
let (target_region, target_year) = target;
for ((region, year), value) in &process.flows {
if *region != target_region {
continue;
}
if year + process.parameters[&(region.clone(), *year)].lifetime >= target_year {
return Some(value.clone());
}
}
None
}
fn create_commodities_graph_for_region_year(
processes: &ProcessMap,
region_id: &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 {
continue;
};
let mut outputs: Vec<_> = flows
.values()
.filter(|flow| flow.direction() == FlowDirection::Output)
.map(|flow| GraphNode::Commodity(flow.commodity.id.clone()))
.collect();
let mut inputs: Vec<_> = flows
.values()
.filter(|flow| flow.direction() == FlowDirection::Input)
.map(|flow| GraphNode::Commodity(flow.commodity.id.clone()))
.collect();
if inputs.is_empty() {
inputs.push(GraphNode::Source);
}
if outputs.is_empty() {
outputs.push(GraphNode::Sink);
}
let primary_output = &process.primary_output;
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,
};
graph.add_edge(
source_node_index,
target_node_index,
if is_primary {
GraphEdge::Primary(process.id.clone())
} else {
GraphEdge::Secondary(process.id.clone())
},
);
}
}
graph
}
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)
})
.collect()
}
fn get_edge_attributes(_: &CommoditiesGraph, edge_ref: EdgeReference<GraphEdge>) -> String {
match edge_ref.weight() {
GraphEdge::Secondary(_) => "style=dashed".to_string(),
_ => String::new(),
}
}
pub fn save_commodity_graphs_for_model(
commodity_graphs: &IndexMap<(RegionID, u32), CommoditiesGraph>,
output_path: &Path,
) -> Result<()> {
for ((region_id, year), graph) in commodity_graphs {
let dot = Dot::with_attr_getters(
graph,
&[],
&get_edge_attributes, &|_, _| String::new(), );
let mut file = File::create(output_path.join(format!("{region_id}_{year}.dot")))?;
write!(file, "{dot}")?;
}
Ok(())
}