From c2acd3635e0ed7f375c466dc159b8fa102f71675 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Tue, 30 Sep 2025 11:47:03 +0100 Subject: [PATCH 1/3] Make error message for missing availabilities clearer --- src/asset.rs | 2 +- src/input/process/flow.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index 2d0b7638e..9a3b4c6c8 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -185,7 +185,7 @@ impl Asset { .get(&key) .with_context(|| { format!( - "No activity limits supplied for process {} in region {} in year {}. \ + "No process availabilities supplied for process {} in region {} in year {}. \ You should update process_availabilities.csv.", &process.id, region_id, commission_year ) diff --git a/src/input/process/flow.rs b/src/input/process/flow.rs index cdad8ef68..25650a20b 100644 --- a/src/input/process/flow.rs +++ b/src/input/process/flow.rs @@ -211,7 +211,7 @@ fn check_flows_primary_output( ) -> Result<()> { if let Some(primary_output) = primary_output { let flow = flows_map.get(primary_output).with_context(|| { - format!("Primary output commodity '{primary_output}' isn't a process flow",) + format!("Primary output commodity '{primary_output}' isn't a process flow") })?; ensure!( From 5c427d17d28df0ac87040524643993312aada8fc Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Tue, 30 Sep 2025 12:28:23 +0100 Subject: [PATCH 2/3] Fix validation for availabilities Fixes #866. --- src/input/process.rs | 6 +- src/input/process/availability.rs | 101 ++++++++++++++++++++++-------- 2 files changed, 80 insertions(+), 27 deletions(-) diff --git a/src/input/process.rs b/src/input/process.rs index 35c5931ad..3352e7d8d 100644 --- a/src/input/process.rs +++ b/src/input/process.rs @@ -57,10 +57,12 @@ pub fn read_processes( time_slice_info: &TimeSliceInfo, milestone_years: &[u32], ) -> Result { + let base_year = milestone_years[0]; let mut processes = read_processes_file(model_dir, milestone_years, region_ids, commodities)?; - let mut activity_limits = read_process_availabilities(model_dir, &processes, time_slice_info)?; + let mut activity_limits = + read_process_availabilities(model_dir, &processes, time_slice_info, base_year)?; let mut flows = read_process_flows(model_dir, &mut processes, commodities)?; - let mut parameters = read_process_parameters(model_dir, &processes, milestone_years[0])?; + let mut parameters = read_process_parameters(model_dir, &processes, base_year)?; // Add data to Process objects for (id, process) in &mut processes { diff --git a/src/input/process/availability.rs b/src/input/process/availability.rs index 7ab1deb01..cc88dcd48 100644 --- a/src/input/process/availability.rs +++ b/src/input/process/availability.rs @@ -1,6 +1,6 @@ //! Code for reading process availabilities CSV file use super::super::{format_items_with_cap, input_err_msg, read_csv, try_insert}; -use crate::process::{ProcessActivityLimitsMap, ProcessID, ProcessMap}; +use crate::process::{Process, ProcessActivityLimitsMap, ProcessID, ProcessMap}; use crate::region::parse_region_str; use crate::time_slice::TimeSliceInfo; use crate::units::{Dimensionless, Year}; @@ -83,11 +83,17 @@ pub fn read_process_availabilities( model_dir: &Path, processes: &ProcessMap, time_slice_info: &TimeSliceInfo, + base_year: u32, ) -> Result> { let file_path = model_dir.join(PROCESS_AVAILABILITIES_FILE_NAME); let process_availabilities_csv = read_csv(&file_path)?; - read_process_availabilities_from_iter(process_availabilities_csv, processes, time_slice_info) - .with_context(|| input_err_msg(&file_path)) + read_process_availabilities_from_iter( + process_availabilities_csv, + processes, + time_slice_info, + base_year, + ) + .with_context(|| input_err_msg(&file_path)) } /// Process raw process availabilities input data into [`ProcessActivityLimitsMap`]s @@ -95,6 +101,7 @@ fn read_process_availabilities_from_iter( iter: I, processes: &ProcessMap, time_slice_info: &TimeSliceInfo, + base_year: u32, ) -> Result> where I: Iterator, @@ -140,7 +147,7 @@ where } } - validate_activity_limits_maps(&map, processes, time_slice_info)?; + validate_activity_limits_maps(&map, processes, time_slice_info, base_year)?; Ok(map) } @@ -150,6 +157,7 @@ fn validate_activity_limits_maps( all_availabilities: &HashMap, processes: &ProcessMap, time_slice_info: &TimeSliceInfo, + base_year: u32, ) -> Result<()> { for (process_id, process) in processes { // A map of maps: the outer map is keyed by region and year; the inner one by time slice @@ -157,31 +165,74 @@ fn validate_activity_limits_maps( .get(process_id) .with_context(|| format!("Missing availabilities for process {process_id}"))?; - let mut missing_keys = Vec::new(); - for (region_id, year) in iproduct!(&process.regions, &process.years) { - if let Some(map_for_region_year) = map_for_process.get(&(region_id.clone(), *year)) { - // There are at least some entries for this region/year combo; check if there are - // any time slices not covered - missing_keys.extend( - time_slice_info - .iter_ids() - .filter(|ts| !map_for_region_year.contains_key(ts)) - .map(|ts| (region_id, *year, ts)), - ); - } else { - // No entries for this region/year combo: by definition no time slices are covered - missing_keys.extend(time_slice_info.iter_ids().map(|ts| (region_id, *year, ts))); - } + check_missing_milestone_years(process, map_for_process, base_year)?; + check_missing_time_slices(process, map_for_process, time_slice_info)?; + } + + Ok(()) +} + +/// Check every milestone year in which the process can be commissioned has availabilities. +/// +/// Entries for non-milestone years in which the process can be commissioned (which are only +/// required for pre-defined assets, if at all) are not required and will be checked lazily when +/// assets requiring them are constructed. +fn check_missing_milestone_years( + process: &Process, + map_for_process: &ProcessActivityLimitsMap, + base_year: u32, +) -> Result<()> { + let process_milestone_years = process + .years + .iter() + .copied() + .filter(|&year| year >= base_year); + let mut missing = Vec::new(); + for (region_id, year) in iproduct!(&process.regions, process_milestone_years) { + if !map_for_process.contains_key(&(region_id.clone(), year)) { + missing.push((region_id, year)); } + } - ensure!( - missing_keys.is_empty(), - "Process {process_id} is missing availabilities for the following regions, years and \ - time slices: {}", - format_items_with_cap(&missing_keys) - ); + ensure!( + missing.is_empty(), + "Process {} is missing availabilities for the following regions and milestone years: {}", + &process.id, + format_items_with_cap(&missing) + ); + + Ok(()) +} + +/// Check that entries for all time slices are provided for any process/region/year combo for which +/// we have any entries at all +fn check_missing_time_slices( + process: &Process, + map_for_process: &ProcessActivityLimitsMap, + time_slice_info: &TimeSliceInfo, +) -> Result<()> { + let mut missing = Vec::new(); + for (region_id, &year) in iproduct!(&process.regions, &process.years) { + if let Some(map_for_region_year) = map_for_process.get(&(region_id.clone(), year)) { + // There are at least some entries for this region/year combo; check if there are + // any time slices not covered + missing.extend( + time_slice_info + .iter_ids() + .filter(|ts| !map_for_region_year.contains_key(ts)) + .map(|ts| (region_id, year, ts)), + ); + } } + ensure!( + missing.is_empty(), + "Availabilities supplied for some, but not all time slices, for process {}. The following \ + regions, years and time slices are missing: {}", + &process.id, + format_items_with_cap(&missing) + ); + Ok(()) } From 4704d9c4349549eed63ec0e8eb6d0e419647b1b1 Mon Sep 17 00:00:00 2001 From: Alex Dewar Date: Fri, 17 Oct 2025 11:31:32 +0100 Subject: [PATCH 3/3] Add missing detail to doc comments --- src/input/process/availability.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/input/process/availability.rs b/src/input/process/availability.rs index cc88dcd48..47a4587c8 100644 --- a/src/input/process/availability.rs +++ b/src/input/process/availability.rs @@ -74,6 +74,7 @@ enum LimitType { /// * `model_dir` - Folder containing model configuration files /// * `processes` - Map of processes /// * `time_slice_info` - Information about seasons and times of day +/// * `base_year` - First milestone year of simulation /// /// # Returns /// @@ -96,7 +97,19 @@ pub fn read_process_availabilities( .with_context(|| input_err_msg(&file_path)) } -/// Process raw process availabilities input data into [`ProcessActivityLimitsMap`]s +/// Process raw process availabilities input data into [`ProcessActivityLimitsMap`]s. +/// +/// # Arguments +/// +/// * `iter` - Iterator of raw process availability records +/// * `processes` - Map of processes +/// * `time_slice_info` - Information about seasons and times of day +/// * `base_year` - First milestone year of simulation +/// +/// # Returns +/// +/// A [`HashMap`] with process IDs as the keys and [`ProcessActivityLimitsMap`]s as the values or an +/// error. fn read_process_availabilities_from_iter( iter: I, processes: &ProcessMap,