Skip to content

Commit 8482274

Browse files
authored
Merge pull request #935 from EnergySystemsModellingLab/fix-availabilities-required-all-years
Fix availabilities validation for non-milestone years
2 parents b80ce22 + 4704d9c commit 8482274

4 files changed

Lines changed: 96 additions & 30 deletions

File tree

src/asset.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ impl Asset {
185185
.get(&key)
186186
.with_context(|| {
187187
format!(
188-
"No activity limits supplied for process {} in region {} in year {}. \
188+
"No process availabilities supplied for process {} in region {} in year {}. \
189189
You should update process_availabilities.csv.",
190190
&process.id, region_id, commission_year
191191
)

src/input/process.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ pub fn read_processes(
5757
time_slice_info: &TimeSliceInfo,
5858
milestone_years: &[u32],
5959
) -> Result<ProcessMap> {
60+
let base_year = milestone_years[0];
6061
let mut processes = read_processes_file(model_dir, milestone_years, region_ids, commodities)?;
61-
let mut activity_limits = read_process_availabilities(model_dir, &processes, time_slice_info)?;
62+
let mut activity_limits =
63+
read_process_availabilities(model_dir, &processes, time_slice_info, base_year)?;
6264
let mut flows = read_process_flows(model_dir, &mut processes, commodities)?;
63-
let mut parameters = read_process_parameters(model_dir, &processes, milestone_years[0])?;
65+
let mut parameters = read_process_parameters(model_dir, &processes, base_year)?;
6466

6567
// Add data to Process objects
6668
for (id, process) in &mut processes {

src/input/process/availability.rs

Lines changed: 90 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Code for reading process availabilities CSV file
22
use super::super::{format_items_with_cap, input_err_msg, read_csv, try_insert};
3-
use crate::process::{ProcessActivityLimitsMap, ProcessID, ProcessMap};
3+
use crate::process::{Process, ProcessActivityLimitsMap, ProcessID, ProcessMap};
44
use crate::region::parse_region_str;
55
use crate::time_slice::TimeSliceInfo;
66
use crate::units::{Dimensionless, Year};
@@ -74,6 +74,7 @@ enum LimitType {
7474
/// * `model_dir` - Folder containing model configuration files
7575
/// * `processes` - Map of processes
7676
/// * `time_slice_info` - Information about seasons and times of day
77+
/// * `base_year` - First milestone year of simulation
7778
///
7879
/// # Returns
7980
///
@@ -83,18 +84,37 @@ pub fn read_process_availabilities(
8384
model_dir: &Path,
8485
processes: &ProcessMap,
8586
time_slice_info: &TimeSliceInfo,
87+
base_year: u32,
8688
) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>> {
8789
let file_path = model_dir.join(PROCESS_AVAILABILITIES_FILE_NAME);
8890
let process_availabilities_csv = read_csv(&file_path)?;
89-
read_process_availabilities_from_iter(process_availabilities_csv, processes, time_slice_info)
90-
.with_context(|| input_err_msg(&file_path))
91+
read_process_availabilities_from_iter(
92+
process_availabilities_csv,
93+
processes,
94+
time_slice_info,
95+
base_year,
96+
)
97+
.with_context(|| input_err_msg(&file_path))
9198
}
9299

93-
/// Process raw process availabilities input data into [`ProcessActivityLimitsMap`]s
100+
/// Process raw process availabilities input data into [`ProcessActivityLimitsMap`]s.
101+
///
102+
/// # Arguments
103+
///
104+
/// * `iter` - Iterator of raw process availability records
105+
/// * `processes` - Map of processes
106+
/// * `time_slice_info` - Information about seasons and times of day
107+
/// * `base_year` - First milestone year of simulation
108+
///
109+
/// # Returns
110+
///
111+
/// A [`HashMap`] with process IDs as the keys and [`ProcessActivityLimitsMap`]s as the values or an
112+
/// error.
94113
fn read_process_availabilities_from_iter<I>(
95114
iter: I,
96115
processes: &ProcessMap,
97116
time_slice_info: &TimeSliceInfo,
117+
base_year: u32,
98118
) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>>
99119
where
100120
I: Iterator<Item = ProcessAvailabilityRaw>,
@@ -140,7 +160,7 @@ where
140160
}
141161
}
142162

143-
validate_activity_limits_maps(&map, processes, time_slice_info)?;
163+
validate_activity_limits_maps(&map, processes, time_slice_info, base_year)?;
144164

145165
Ok(map)
146166
}
@@ -150,38 +170,82 @@ fn validate_activity_limits_maps(
150170
all_availabilities: &HashMap<ProcessID, ProcessActivityLimitsMap>,
151171
processes: &ProcessMap,
152172
time_slice_info: &TimeSliceInfo,
173+
base_year: u32,
153174
) -> Result<()> {
154175
for (process_id, process) in processes {
155176
// A map of maps: the outer map is keyed by region and year; the inner one by time slice
156177
let map_for_process = all_availabilities
157178
.get(process_id)
158179
.with_context(|| format!("Missing availabilities for process {process_id}"))?;
159180

160-
let mut missing_keys = Vec::new();
161-
for (region_id, year) in iproduct!(&process.regions, &process.years) {
162-
if let Some(map_for_region_year) = map_for_process.get(&(region_id.clone(), *year)) {
163-
// There are at least some entries for this region/year combo; check if there are
164-
// any time slices not covered
165-
missing_keys.extend(
166-
time_slice_info
167-
.iter_ids()
168-
.filter(|ts| !map_for_region_year.contains_key(ts))
169-
.map(|ts| (region_id, *year, ts)),
170-
);
171-
} else {
172-
// No entries for this region/year combo: by definition no time slices are covered
173-
missing_keys.extend(time_slice_info.iter_ids().map(|ts| (region_id, *year, ts)));
174-
}
181+
check_missing_milestone_years(process, map_for_process, base_year)?;
182+
check_missing_time_slices(process, map_for_process, time_slice_info)?;
183+
}
184+
185+
Ok(())
186+
}
187+
188+
/// Check every milestone year in which the process can be commissioned has availabilities.
189+
///
190+
/// Entries for non-milestone years in which the process can be commissioned (which are only
191+
/// required for pre-defined assets, if at all) are not required and will be checked lazily when
192+
/// assets requiring them are constructed.
193+
fn check_missing_milestone_years(
194+
process: &Process,
195+
map_for_process: &ProcessActivityLimitsMap,
196+
base_year: u32,
197+
) -> Result<()> {
198+
let process_milestone_years = process
199+
.years
200+
.iter()
201+
.copied()
202+
.filter(|&year| year >= base_year);
203+
let mut missing = Vec::new();
204+
for (region_id, year) in iproduct!(&process.regions, process_milestone_years) {
205+
if !map_for_process.contains_key(&(region_id.clone(), year)) {
206+
missing.push((region_id, year));
175207
}
208+
}
176209

177-
ensure!(
178-
missing_keys.is_empty(),
179-
"Process {process_id} is missing availabilities for the following regions, years and \
180-
time slices: {}",
181-
format_items_with_cap(&missing_keys)
182-
);
210+
ensure!(
211+
missing.is_empty(),
212+
"Process {} is missing availabilities for the following regions and milestone years: {}",
213+
&process.id,
214+
format_items_with_cap(&missing)
215+
);
216+
217+
Ok(())
218+
}
219+
220+
/// Check that entries for all time slices are provided for any process/region/year combo for which
221+
/// we have any entries at all
222+
fn check_missing_time_slices(
223+
process: &Process,
224+
map_for_process: &ProcessActivityLimitsMap,
225+
time_slice_info: &TimeSliceInfo,
226+
) -> Result<()> {
227+
let mut missing = Vec::new();
228+
for (region_id, &year) in iproduct!(&process.regions, &process.years) {
229+
if let Some(map_for_region_year) = map_for_process.get(&(region_id.clone(), year)) {
230+
// There are at least some entries for this region/year combo; check if there are
231+
// any time slices not covered
232+
missing.extend(
233+
time_slice_info
234+
.iter_ids()
235+
.filter(|ts| !map_for_region_year.contains_key(ts))
236+
.map(|ts| (region_id, year, ts)),
237+
);
238+
}
183239
}
184240

241+
ensure!(
242+
missing.is_empty(),
243+
"Availabilities supplied for some, but not all time slices, for process {}. The following \
244+
regions, years and time slices are missing: {}",
245+
&process.id,
246+
format_items_with_cap(&missing)
247+
);
248+
185249
Ok(())
186250
}
187251

src/input/process/flow.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ fn check_flows_primary_output(
211211
) -> Result<()> {
212212
if let Some(primary_output) = primary_output {
213213
let flow = flows_map.get(primary_output).with_context(|| {
214-
format!("Primary output commodity '{primary_output}' isn't a process flow",)
214+
format!("Primary output commodity '{primary_output}' isn't a process flow")
215215
})?;
216216

217217
ensure!(

0 commit comments

Comments
 (0)