From 1ae70749313b157351b25aa41012e297ece90aee Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Wed, 12 Nov 2025 09:01:26 +0000 Subject: [PATCH 1/7] :sparkles: Allow to get flow within lifetime --- src/graph.rs | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index 553ca182d..ac125d04a 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,6 +1,8 @@ //! Module for creating and analysing commodity graphs use crate::commodity::CommodityID; -use crate::process::{FlowDirection, ProcessID, ProcessMap}; +use crate::process::{ + FlowDirection, ProcessFlow, ProcessFlowsMap, ProcessID, ProcessMap, ProcessParameterMap, +}; use crate::region::RegionID; use anyhow::Result; use indexmap::{IndexMap, IndexSet}; @@ -13,6 +15,7 @@ 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; @@ -66,6 +69,35 @@ impl Display for GraphEdge { } } +/// Helper function to return a possible flow operating in the requested year +/// +/// It considers the lifetime of the process and returns the first flow that can operate in the +/// target year and region. If no flow is found fulfilling that, None is returned. +fn get_flow_for_year( + parameters: &ProcessParameterMap, + flows: ProcessFlowsMap, + target: (RegionID, u32), +) -> Option>> { + // If its already in the map, we return it + if flows.contains_key(&target) { + return flows.get(&target).cloned(); + } + + // Otherwise we try to find one that operates in the target year. It is assumed that + // parameters are defined in the same (region, year) combinations than flows, at least. + let (target_region, target_year) = target; + for ((region, year), value) in flows { + if region != target_region { + continue; + } + if year + parameters.get(&(region, year)).unwrap().lifetime >= target_year { + return Some(value); + } + return None; + } + None +} + /// Creates a directed graph of commodity flows for a given region and year. /// /// The graph contains nodes for all commodities that may be consumed/produced by processes in the @@ -87,7 +119,9 @@ fn create_commodities_graph_for_region_year( let key = (region_id.clone(), year); for process in processes.values() { - let Some(flows) = process.flows.get(&key) else { + let Some(flows) = + get_flow_for_year(&process.parameters, process.flows.clone(), key.clone()) + else { // Process doesn't operate in this region/year continue; }; From bde422a6d227ad3788e06c1d219226f75041286f Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Wed, 12 Nov 2025 09:45:15 +0000 Subject: [PATCH 2/7] Remove unnecessary return statement --- src/graph.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/graph.rs b/src/graph.rs index ac125d04a..f61bb1828 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -93,7 +93,6 @@ fn get_flow_for_year( if year + parameters.get(&(region, year)).unwrap().lifetime >= target_year { return Some(value); } - return None; } None } From b93cff296b4fdb765d700f9126eee2a5440fafa6 Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Wed, 12 Nov 2025 09:58:42 +0000 Subject: [PATCH 3/7] :bug: Update settings ton override graphs --- src/cli.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index d294be3cb..81eb01fab 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -211,12 +211,16 @@ pub fn handle_save_graphs_command( settings: Option, ) -> Result<()> { // Load program settings, if not provided - let settings = if let Some(settings) = settings { + let mut settings = if let Some(settings) = settings { settings } else { Settings::load().context("Failed to load settings.")? }; + if opts.overwrite { + settings.overwrite = true; + } + // Get path to output folder let pathbuf: PathBuf; let output_path = if let Some(p) = opts.output_dir.as_deref() { From 4c8c4b4cb18c3612b0b0ca05530ea0f76230fd7c Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Wed, 19 Nov 2025 08:13:07 +0000 Subject: [PATCH 4/7] Add more details in the docsting and use Process as input --- src/graph.rs | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/graph.rs b/src/graph.rs index f61bb1828..66f7c65e1 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,8 +1,6 @@ //! Module for creating and analysing commodity graphs use crate::commodity::CommodityID; -use crate::process::{ - FlowDirection, ProcessFlow, ProcessFlowsMap, ProcessID, ProcessMap, ProcessParameterMap, -}; +use crate::process::{FlowDirection, Process, ProcessFlow, ProcessID, ProcessMap}; use crate::region::RegionID; use anyhow::Result; use indexmap::{IndexMap, IndexSet}; @@ -71,27 +69,37 @@ impl Display for GraphEdge { /// Helper function to return a possible flow operating in the requested year /// -/// It considers the lifetime of the process and returns the first flow that can operate in the -/// target year and region. If no flow is found fulfilling that, None is returned. +/// We are interested only on the flows direction, which are always the same for all years. So this +/// function checks if the process can be operating in the target year and region and, if so, it +/// returns its flows. It considers not only when the process can be commissioned, but also the +/// lifetime of the process, since a process can be opperating many years after the commission time +/// window is over. If the process cannot be opperating in the target year and region, None is +/// returned. fn get_flow_for_year( - parameters: &ProcessParameterMap, - flows: ProcessFlowsMap, + process: &Process, target: (RegionID, u32), ) -> Option>> { // If its already in the map, we return it - if flows.contains_key(&target) { - return flows.get(&target).cloned(); + if process.flows.contains_key(&target) { + return process.flows.get(&target).cloned(); } // Otherwise we try to find one that operates in the target year. It is assumed that // parameters are defined in the same (region, year) combinations than flows, at least. let (target_region, target_year) = target; - for ((region, year), value) in flows { - if region != target_region { + for ((region, year), value) in &process.flows { + if *region != target_region { continue; } - if year + parameters.get(&(region, year)).unwrap().lifetime >= target_year { - return Some(value); + if year + + process + .parameters + .get(&(region.clone(), *year)) + .unwrap() + .lifetime + >= target_year + { + return Some(value.clone()); } } None @@ -118,9 +126,7 @@ fn create_commodities_graph_for_region_year( let key = (region_id.clone(), year); for process in processes.values() { - let Some(flows) = - get_flow_for_year(&process.parameters, process.flows.clone(), key.clone()) - else { + let Some(flows) = get_flow_for_year(process, key.clone()) else { // Process doesn't operate in this region/year continue; }; From 6a2afa149f54a9759dc80e0be2b659faa5faa203 Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Wed, 19 Nov 2025 08:48:04 +0000 Subject: [PATCH 5/7] Update checking activity limits in graph validation --- src/graph/validate.rs | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/graph/validate.rs b/src/graph/validate.rs index bf5798349..a488731ca 100644 --- a/src/graph/validate.rs +++ b/src/graph/validate.rs @@ -1,9 +1,9 @@ //! Module for validating commodity graphs use super::{CommoditiesGraph, GraphEdge, GraphNode}; use crate::commodity::{CommodityMap, CommodityType}; -use crate::process::ProcessMap; +use crate::process::{Process, ProcessMap}; use crate::region::RegionID; -use crate::time_slice::{TimeSliceInfo, TimeSliceLevel, TimeSliceSelection}; +use crate::time_slice::{TimeSliceID, TimeSliceInfo, TimeSliceLevel, TimeSliceSelection}; use crate::units::{Dimensionless, Flow}; use anyhow::{Context, Result, ensure}; use indexmap::IndexMap; @@ -44,14 +44,7 @@ fn prepare_commodities_graph_for_validation( // Check if the process has availability > 0 in any time slice in the selection time_slice_selection .iter(time_slice_info) - .any(|(time_slice, _)| { - let Some(limits_map) = process.activity_limits.get(&key) else { - return false; - }; - limits_map - .get(time_slice) - .is_some_and(|avail| *avail.end() > Dimensionless(0.0)) - }) + .any(|(time_slice, _)| can_be_active(process, &key, time_slice)) }); // Add demand edges @@ -78,6 +71,35 @@ fn prepare_commodities_graph_for_validation( filtered_graph } +/// Checks if a process can be active for a particular timeslice in a given year and region +/// +/// It considers all commission years that can lead to a running process in the target region and +/// year, accounting for the process lifetime, and then checks if, for any of those, the process +/// is active in the required timeslice. In other words, this checks if there is the _possibility_ +/// of having an active process, although there is no guarantee of that happening since it depends +/// on the investment. +fn can_be_active(process: &Process, target: &(RegionID, u32), time_slice: &TimeSliceID) -> bool { + let (target_region, target_year) = target; + + for ((region, year), value) in &process.parameters { + if region != target_region { + continue; + } + if year + value.lifetime >= *target_year { + let Some(limits_map) = process.activity_limits.get(target) else { + continue; + }; + if limits_map + .get(time_slice) + .is_some_and(|avail| *avail.end() > Dimensionless(0.0)) + { + return true; + } + } + } + false +} + /// Validates that the commodity graph follows the rules for different commodity types. /// /// It takes as input a graph created by `create_commodities_graph_for_validation`, which is built From 0a011fd91df0caa4a22d35d6c805ce6fcc83ebb7 Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Wed, 12 Nov 2025 13:43:41 +0000 Subject: [PATCH 6/7] :sparkles: Adds a configurable output directory --- .gitignore | 3 +-- schemas/settings.yaml | 5 +++++ src/cli.rs | 4 ++-- src/output.rs | 24 ++++++++++++++++++------ src/settings.rs | 7 ++++++- 5 files changed, 32 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 01f19d17e..e51b99347 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,7 @@ settings.toml !assets/settings.toml # Simulation output files -**/muse2_results -**/muse2_graphs +**/results # Generated by Cargo # will have compiled files and executables diff --git a/schemas/settings.yaml b/schemas/settings.yaml index 0dca722e3..926d7fe5b 100644 --- a/schemas/settings.yaml +++ b/schemas/settings.yaml @@ -26,3 +26,8 @@ properties: notes: | This includes raw values such as commodity balance duals, which may be useful for debugging the model or understanding results in more detail. + results_root: + type: string + description: Root directory to output results + default: "" + notes: Defaults to a "results" folder within the current working directory. diff --git a/src/cli.rs b/src/cli.rs index 81eb01fab..55d409365 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -151,7 +151,7 @@ pub fn handle_run_command( let output_path = if let Some(p) = opts.output_dir.as_deref() { p } else { - pathbuf = get_output_dir(model_path)?; + pathbuf = get_output_dir(model_path, settings.results_root)?; &pathbuf }; @@ -226,7 +226,7 @@ pub fn handle_save_graphs_command( let output_path = if let Some(p) = opts.output_dir.as_deref() { p } else { - pathbuf = get_graphs_dir(model_path)?; + pathbuf = get_graphs_dir(model_path, settings.results_root)?; &pathbuf }; diff --git a/src/output.rs b/src/output.rs index 5faa9ae33..c2b77a250 100644 --- a/src/output.rs +++ b/src/output.rs @@ -24,7 +24,7 @@ pub mod metadata; use metadata::write_metadata; /// The root folder in which model-specific output folders will be created -const OUTPUT_DIRECTORY_ROOT: &str = "muse2_results"; +const OUTPUT_DIRECTORY_ROOT: &str = "outputs"; /// The output file name for commodity flows const COMMODITY_FLOWS_FILE_NAME: &str = "commodity_flows.csv"; @@ -54,10 +54,10 @@ const APPRAISAL_RESULTS_FILE_NAME: &str = "debug_appraisal_results.csv"; const APPRAISAL_RESULTS_TIME_SLICE_FILE_NAME: &str = "debug_appraisal_results_time_slices.csv"; /// The root folder in which commodity flow graphs will be created -const GRAPHS_DIRECTORY_ROOT: &str = "muse2_graphs"; +const GRAPHS_DIRECTORY_ROOT: &str = "graphs"; /// Get the default output directory for the model -pub fn get_output_dir(model_dir: &Path) -> Result { +pub fn get_output_dir(model_dir: &Path, results_root: PathBuf) -> Result { // Get the model name from the dir path. This ends up being convoluted because we need to check // for all possible errors. Ugh. let model_dir = model_dir @@ -71,11 +71,17 @@ pub fn get_output_dir(model_dir: &Path) -> Result { .context("Invalid chars in model dir name")?; // Construct path - Ok([OUTPUT_DIRECTORY_ROOT, model_name].iter().collect()) + Ok([ + results_root, + OUTPUT_DIRECTORY_ROOT.into(), + model_name.into(), + ] + .iter() + .collect()) } /// Get the default output directory for commodity flow graphs for the model -pub fn get_graphs_dir(model_dir: &Path) -> Result { +pub fn get_graphs_dir(model_dir: &Path, results_root: PathBuf) -> Result { let model_dir = model_dir .canonicalize() // canonicalise in case the user has specified "." .context("Could not resolve path to model")?; @@ -84,7 +90,13 @@ pub fn get_graphs_dir(model_dir: &Path) -> Result { .context("Model cannot be in root folder")? .to_str() .context("Invalid chars in model dir name")?; - Ok([GRAPHS_DIRECTORY_ROOT, model_name].iter().collect()) + Ok([ + results_root, + GRAPHS_DIRECTORY_ROOT.into(), + model_name.into(), + ] + .iter() + .collect()) } /// Create a new output directory for the model, optionally overwriting existing data diff --git a/src/settings.rs b/src/settings.rs index ead537a55..0f47596f9 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -5,6 +5,7 @@ use crate::log::DEFAULT_LOG_LEVEL; use anyhow::Result; use documented::DocumentedFields; use serde::{Deserialize, Serialize}; +use std::env::current_dir; use std::fmt::Write; use std::path::{Path, PathBuf}; @@ -47,6 +48,8 @@ pub struct Settings { pub overwrite: bool, /// Whether to write additional information to CSV files pub debug_model: bool, + /// Results root path to save simulation runs. Defaults to {pwd}/results. + pub results_root: PathBuf, } impl Default for Settings { @@ -55,6 +58,7 @@ impl Default for Settings { log_level: DEFAULT_LOG_LEVEL.to_string(), overwrite: false, debug_model: false, + results_root: current_dir().unwrap().join("results"), } } } @@ -141,7 +145,8 @@ mod tests { Settings { log_level: "warn".to_string(), debug_model: false, - overwrite: false + overwrite: false, + results_root: current_dir().unwrap().join("results") } ); } From 9bea7462c3c26e8a922dc338d214ae94a0b08f70 Mon Sep 17 00:00:00 2001 From: Diego Alonso Alvarez Date: Tue, 18 Nov 2025 11:03:24 +0000 Subject: [PATCH 7/7] Use independent folders for results and graph --- .gitignore | 3 ++- schemas/settings.yaml | 9 +++++++-- src/cli.rs | 2 +- src/output.rs | 24 +++--------------------- src/settings.rs | 10 +++++++--- 5 files changed, 20 insertions(+), 28 deletions(-) diff --git a/.gitignore b/.gitignore index e51b99347..01f19d17e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,8 @@ settings.toml !assets/settings.toml # Simulation output files -**/results +**/muse2_results +**/muse2_graphs # Generated by Cargo # will have compiled files and executables diff --git a/schemas/settings.yaml b/schemas/settings.yaml index 926d7fe5b..f43602a60 100644 --- a/schemas/settings.yaml +++ b/schemas/settings.yaml @@ -28,6 +28,11 @@ properties: the model or understanding results in more detail. results_root: type: string - description: Root directory to output results + description: Results root path to save MUSE2 results default: "" - notes: Defaults to a "results" folder within the current working directory. + notes: Defaults to a "muse2_results" folder within the current working directory. + graph_results_root: + type: string + description: Results root path to save MUSE2 graph outputs + default: "" + notes: Defaults to a "muse2_graphs" folder within the current working directory. diff --git a/src/cli.rs b/src/cli.rs index 55d409365..d6a3b5c8e 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -226,7 +226,7 @@ pub fn handle_save_graphs_command( let output_path = if let Some(p) = opts.output_dir.as_deref() { p } else { - pathbuf = get_graphs_dir(model_path, settings.results_root)?; + pathbuf = get_graphs_dir(model_path, settings.graph_results_root)?; &pathbuf }; diff --git a/src/output.rs b/src/output.rs index c2b77a250..f74526b6f 100644 --- a/src/output.rs +++ b/src/output.rs @@ -23,9 +23,6 @@ use std::path::{Path, PathBuf}; pub mod metadata; use metadata::write_metadata; -/// The root folder in which model-specific output folders will be created -const OUTPUT_DIRECTORY_ROOT: &str = "outputs"; - /// The output file name for commodity flows const COMMODITY_FLOWS_FILE_NAME: &str = "commodity_flows.csv"; @@ -53,9 +50,6 @@ const APPRAISAL_RESULTS_FILE_NAME: &str = "debug_appraisal_results.csv"; /// The output file name for appraisal time slice results const APPRAISAL_RESULTS_TIME_SLICE_FILE_NAME: &str = "debug_appraisal_results_time_slices.csv"; -/// The root folder in which commodity flow graphs will be created -const GRAPHS_DIRECTORY_ROOT: &str = "graphs"; - /// Get the default output directory for the model pub fn get_output_dir(model_dir: &Path, results_root: PathBuf) -> Result { // Get the model name from the dir path. This ends up being convoluted because we need to check @@ -71,17 +65,11 @@ pub fn get_output_dir(model_dir: &Path, results_root: PathBuf) -> Result Result { +pub fn get_graphs_dir(model_dir: &Path, graph_results_root: PathBuf) -> Result { let model_dir = model_dir .canonicalize() // canonicalise in case the user has specified "." .context("Could not resolve path to model")?; @@ -90,13 +78,7 @@ pub fn get_graphs_dir(model_dir: &Path, results_root: PathBuf) -> Result