Skip to content

Commit 5c427d1

Browse files
committed
Fix validation for availabilities
Fixes #866.
1 parent c2acd36 commit 5c427d1

2 files changed

Lines changed: 80 additions & 27 deletions

File tree

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: 76 additions & 25 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};
@@ -83,18 +83,25 @@ pub fn read_process_availabilities(
8383
model_dir: &Path,
8484
processes: &ProcessMap,
8585
time_slice_info: &TimeSliceInfo,
86+
base_year: u32,
8687
) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>> {
8788
let file_path = model_dir.join(PROCESS_AVAILABILITIES_FILE_NAME);
8889
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))
90+
read_process_availabilities_from_iter(
91+
process_availabilities_csv,
92+
processes,
93+
time_slice_info,
94+
base_year,
95+
)
96+
.with_context(|| input_err_msg(&file_path))
9197
}
9298

9399
/// Process raw process availabilities input data into [`ProcessActivityLimitsMap`]s
94100
fn read_process_availabilities_from_iter<I>(
95101
iter: I,
96102
processes: &ProcessMap,
97103
time_slice_info: &TimeSliceInfo,
104+
base_year: u32,
98105
) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>>
99106
where
100107
I: Iterator<Item = ProcessAvailabilityRaw>,
@@ -140,7 +147,7 @@ where
140147
}
141148
}
142149

143-
validate_activity_limits_maps(&map, processes, time_slice_info)?;
150+
validate_activity_limits_maps(&map, processes, time_slice_info, base_year)?;
144151

145152
Ok(map)
146153
}
@@ -150,38 +157,82 @@ fn validate_activity_limits_maps(
150157
all_availabilities: &HashMap<ProcessID, ProcessActivityLimitsMap>,
151158
processes: &ProcessMap,
152159
time_slice_info: &TimeSliceInfo,
160+
base_year: u32,
153161
) -> Result<()> {
154162
for (process_id, process) in processes {
155163
// A map of maps: the outer map is keyed by region and year; the inner one by time slice
156164
let map_for_process = all_availabilities
157165
.get(process_id)
158166
.with_context(|| format!("Missing availabilities for process {process_id}"))?;
159167

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-
}
168+
check_missing_milestone_years(process, map_for_process, base_year)?;
169+
check_missing_time_slices(process, map_for_process, time_slice_info)?;
170+
}
171+
172+
Ok(())
173+
}
174+
175+
/// Check every milestone year in which the process can be commissioned has availabilities.
176+
///
177+
/// Entries for non-milestone years in which the process can be commissioned (which are only
178+
/// required for pre-defined assets, if at all) are not required and will be checked lazily when
179+
/// assets requiring them are constructed.
180+
fn check_missing_milestone_years(
181+
process: &Process,
182+
map_for_process: &ProcessActivityLimitsMap,
183+
base_year: u32,
184+
) -> Result<()> {
185+
let process_milestone_years = process
186+
.years
187+
.iter()
188+
.copied()
189+
.filter(|&year| year >= base_year);
190+
let mut missing = Vec::new();
191+
for (region_id, year) in iproduct!(&process.regions, process_milestone_years) {
192+
if !map_for_process.contains_key(&(region_id.clone(), year)) {
193+
missing.push((region_id, year));
175194
}
195+
}
176196

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-
);
197+
ensure!(
198+
missing.is_empty(),
199+
"Process {} is missing availabilities for the following regions and milestone years: {}",
200+
&process.id,
201+
format_items_with_cap(&missing)
202+
);
203+
204+
Ok(())
205+
}
206+
207+
/// Check that entries for all time slices are provided for any process/region/year combo for which
208+
/// we have any entries at all
209+
fn check_missing_time_slices(
210+
process: &Process,
211+
map_for_process: &ProcessActivityLimitsMap,
212+
time_slice_info: &TimeSliceInfo,
213+
) -> Result<()> {
214+
let mut missing = Vec::new();
215+
for (region_id, &year) in iproduct!(&process.regions, &process.years) {
216+
if let Some(map_for_region_year) = map_for_process.get(&(region_id.clone(), year)) {
217+
// There are at least some entries for this region/year combo; check if there are
218+
// any time slices not covered
219+
missing.extend(
220+
time_slice_info
221+
.iter_ids()
222+
.filter(|ts| !map_for_region_year.contains_key(ts))
223+
.map(|ts| (region_id, year, ts)),
224+
);
225+
}
183226
}
184227

228+
ensure!(
229+
missing.is_empty(),
230+
"Availabilities supplied for some, but not all time slices, for process {}. The following \
231+
regions, years and time slices are missing: {}",
232+
&process.id,
233+
format_items_with_cap(&missing)
234+
);
235+
185236
Ok(())
186237
}
187238

0 commit comments

Comments
 (0)