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
2 changes: 1 addition & 1 deletion src/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
6 changes: 4 additions & 2 deletions src/input/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@ pub fn read_processes(
time_slice_info: &TimeSliceInfo,
milestone_years: &[u32],
) -> Result<ProcessMap> {
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 {
Expand Down
116 changes: 90 additions & 26 deletions src/input/process/availability.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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
///
Expand All @@ -83,18 +84,37 @@ pub fn read_process_availabilities(
model_dir: &Path,
processes: &ProcessMap,
time_slice_info: &TimeSliceInfo,
base_year: u32,
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.

Add to the docstring.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Oops! It's a pity there doesn't seem to be any Rust tooling to check these doc comments...

) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>> {
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
/// 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<I>(
iter: I,
processes: &ProcessMap,
time_slice_info: &TimeSliceInfo,
base_year: u32,
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.

Same here. This function is missing details in the docstring, anyway.

) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>>
where
I: Iterator<Item = ProcessAvailabilityRaw>,
Expand Down Expand Up @@ -140,7 +160,7 @@ where
}
}

validate_activity_limits_maps(&map, processes, time_slice_info)?;
validate_activity_limits_maps(&map, processes, time_slice_info, base_year)?;

Ok(map)
}
Expand All @@ -150,38 +170,82 @@ fn validate_activity_limits_maps(
all_availabilities: &HashMap<ProcessID, ProcessActivityLimitsMap>,
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
let map_for_process = all_availabilities
.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);
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

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

The filtering logic for milestone years should be documented to clarify why only years >= base_year are considered milestone years, especially since this is a key part of the bug fix.

Copilot uses AI. Check for mistakes.
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)),
Copy link

Copilot AI Oct 16, 2025

Choose a reason for hiding this comment

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

The tuple construction uses year directly, but year is of type &Year from the iterator. This should be *year to dereference the borrowed value for consistency with the tuple type expected.

Suggested change
.map(|ts| (region_id, year, ts)),
.map(|ts| (region_id, *year, ts)),

Copilot uses AI. Check for mistakes.
);
}
}

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(())
}

Expand Down
2 changes: 1 addition & 1 deletion src/input/process/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down