Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions schemas/settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,13 @@ 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: Results root path to save MUSE2 results
default: ""
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.
10 changes: 7 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

Expand Down Expand Up @@ -211,18 +211,22 @@ pub fn handle_save_graphs_command(
settings: Option<Settings>,
) -> 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() {
p
} else {
pathbuf = get_graphs_dir(model_path)?;
pathbuf = get_graphs_dir(model_path, settings.graph_results_root)?;
&pathbuf
};

Expand Down
43 changes: 41 additions & 2 deletions src/graph.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Module for creating and analysing commodity graphs
use crate::commodity::CommodityID;
use crate::process::{FlowDirection, ProcessID, ProcessMap};
use crate::process::{FlowDirection, Process, ProcessFlow, ProcessID, ProcessMap};
use crate::region::RegionID;
use anyhow::Result;
use indexmap::{IndexMap, IndexSet};
Expand All @@ -13,6 +13,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;
Expand Down Expand Up @@ -66,6 +67,44 @@ impl Display for GraphEdge {
}
}

/// Helper function to return a possible flow operating in the requested year
///
/// 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(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think probably cleaner for this to take process: &Process

process: &Process,
target: (RegionID, u32),
) -> Option<Rc<IndexMap<CommodityID, ProcessFlow>>> {
// If its already in the map, we return it
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 &process.flows {
if *region != target_region {
continue;
}
if year
+ process
.parameters
.get(&(region.clone(), *year))
.unwrap()
.lifetime
>= target_year
{
return Some(value.clone());
}
}
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
Expand All @@ -87,7 +126,7 @@ 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, key.clone()) else {
// Process doesn't operate in this region/year
continue;
};
Expand Down
42 changes: 32 additions & 10 deletions src/graph/validate.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
14 changes: 4 additions & 10 deletions src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "muse2_results";

/// The output file name for commodity flows
const COMMODITY_FLOWS_FILE_NAME: &str = "commodity_flows.csv";

Expand Down Expand Up @@ -53,11 +50,8 @@ 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 = "muse2_graphs";

/// Get the default output directory for the model
pub fn get_output_dir(model_dir: &Path) -> Result<PathBuf> {
pub fn get_output_dir(model_dir: &Path, results_root: PathBuf) -> Result<PathBuf> {
// 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
Expand All @@ -71,11 +65,11 @@ pub fn get_output_dir(model_dir: &Path) -> Result<PathBuf> {
.context("Invalid chars in model dir name")?;

// Construct path
Ok([OUTPUT_DIRECTORY_ROOT, model_name].iter().collect())
Ok([results_root, 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<PathBuf> {
pub fn get_graphs_dir(model_dir: &Path, graph_results_root: PathBuf) -> Result<PathBuf> {
let model_dir = model_dir
.canonicalize() // canonicalise in case the user has specified "."
.context("Could not resolve path to model")?;
Expand All @@ -84,7 +78,7 @@ pub fn get_graphs_dir(model_dir: &Path) -> Result<PathBuf> {
.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([graph_results_root, model_name.into()].iter().collect())
}

/// Create a new output directory for the model, optionally overwriting existing data
Expand Down
11 changes: 10 additions & 1 deletion src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -47,6 +48,10 @@ pub struct Settings {
pub overwrite: bool,
/// Whether to write additional information to CSV files
pub debug_model: bool,
/// Results root path to save MUSE2 results. Defaults to `{pwd}/muse2_results`.
pub results_root: PathBuf,
/// Results root path to save MUSE2 graph outputs. Defaults to `{pwd}/muse2_graphs`.
pub graph_results_root: PathBuf,
}

impl Default for Settings {
Expand All @@ -55,6 +60,8 @@ impl Default for Settings {
log_level: DEFAULT_LOG_LEVEL.to_string(),
overwrite: false,
debug_model: false,
results_root: current_dir().unwrap().join("muse2_results"),
graph_results_root: current_dir().unwrap().join("muse2_graphs"),
}
}
}
Expand Down Expand Up @@ -141,7 +148,9 @@ mod tests {
Settings {
log_level: "warn".to_string(),
debug_model: false,
overwrite: false
overwrite: false,
results_root: current_dir().unwrap().join("muse2_results"),
graph_results_root: current_dir().unwrap().join("muse2_graphs"),
}
);
}
Expand Down