11//! Code for reading process availabilities CSV file
22use 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 } ;
44use crate :: region:: parse_region_str;
55use crate :: time_slice:: TimeSliceInfo ;
66use 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.
94113fn 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 > >
99119where
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
0 commit comments