1use super::super::{format_items_with_cap, input_err_msg, read_csv, try_insert};
3use crate::process::{Process, ProcessActivityLimitsMap, ProcessID, ProcessMap};
4use crate::region::parse_region_str;
5use crate::time_slice::TimeSliceInfo;
6use crate::units::{Dimensionless, Year};
7use crate::year::parse_year_str;
8use anyhow::{Context, Result, ensure};
9use itertools::iproduct;
10use serde::Deserialize;
11use serde_string_enum::DeserializeLabeledStringEnum;
12use std::collections::HashMap;
13use std::ops::RangeInclusive;
14use std::path::Path;
15use std::rc::Rc;
16
17const PROCESS_AVAILABILITIES_FILE_NAME: &str = "process_availabilities.csv";
18
19#[derive(Deserialize)]
21struct ProcessAvailabilityRaw {
22 process_id: String,
23 regions: String,
24 years: String,
25 time_slice: String,
26 limit_type: LimitType,
27 value: Dimensionless,
28}
29
30impl ProcessAvailabilityRaw {
31 fn validate(&self) -> Result<()> {
32 ensure!(
34 self.value >= Dimensionless(0.0) && self.value <= Dimensionless(1.0),
35 "Value for availability must be between 0 and 1 inclusive"
36 );
37
38 Ok(())
39 }
40
41 fn to_bounds(&self, ts_length: Year) -> RangeInclusive<Dimensionless> {
46 let ts_frac = ts_length / Year(1.0);
48 let value = self.value * ts_frac;
49 match self.limit_type {
50 LimitType::LowerBound => value..=ts_frac,
51 LimitType::UpperBound => Dimensionless(0.0)..=value,
52 LimitType::Equality => value..=value,
53 }
54 }
55}
56
57#[derive(DeserializeLabeledStringEnum)]
59enum LimitType {
60 #[string = "lo"]
61 LowerBound,
62 #[string = "up"]
63 UpperBound,
64 #[string = "fx"]
65 Equality,
66}
67
68pub fn read_process_availabilities(
85 model_dir: &Path,
86 processes: &ProcessMap,
87 time_slice_info: &TimeSliceInfo,
88 base_year: u32,
89) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>> {
90 let file_path = model_dir.join(PROCESS_AVAILABILITIES_FILE_NAME);
91 let process_availabilities_csv = read_csv(&file_path)?;
92 read_process_availabilities_from_iter(
93 process_availabilities_csv,
94 processes,
95 time_slice_info,
96 base_year,
97 )
98 .with_context(|| input_err_msg(&file_path))
99}
100
101fn read_process_availabilities_from_iter<I>(
115 iter: I,
116 processes: &ProcessMap,
117 time_slice_info: &TimeSliceInfo,
118 base_year: u32,
119) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>>
120where
121 I: Iterator<Item = ProcessAvailabilityRaw>,
122{
123 let mut map = HashMap::new();
124 for record in iter {
125 record.validate()?;
126
127 let (id, process) = processes
129 .get_key_value(record.process_id.as_str())
130 .with_context(|| format!("Process {} not found", record.process_id))?;
131
132 let process_regions = &process.regions;
134 let record_regions =
135 parse_region_str(&record.regions, process_regions).with_context(|| {
136 format!("Invalid region for process {id}. Valid regions are {process_regions:?}")
137 })?;
138
139 let process_years = &process.years;
141 let record_years = parse_year_str(&record.years, process_years).with_context(|| {
142 format!("Invalid year for process {id}. Valid years are {process_years:?}")
143 })?;
144
145 let ts_selection = time_slice_info.get_selection(&record.time_slice)?;
147
148 let limits_map = map
150 .entry(id.clone())
151 .or_insert_with(ProcessActivityLimitsMap::new);
152 for (region_id, year) in iproduct!(&record_regions, &record_years) {
153 let limits_map_inner = limits_map
154 .entry((region_id.clone(), *year))
155 .or_insert_with(|| Rc::new(HashMap::new()));
156 let limits_map_inner = Rc::get_mut(limits_map_inner).unwrap();
157 for (time_slice, ts_length) in ts_selection.iter(time_slice_info) {
158 let bounds = record.to_bounds(ts_length);
159 try_insert(limits_map_inner, time_slice, bounds.clone())?;
160 }
161 }
162 }
163
164 validate_activity_limits_maps(&map, processes, time_slice_info, base_year)?;
165
166 Ok(map)
167}
168
169fn validate_activity_limits_maps(
171 all_availabilities: &HashMap<ProcessID, ProcessActivityLimitsMap>,
172 processes: &ProcessMap,
173 time_slice_info: &TimeSliceInfo,
174 base_year: u32,
175) -> Result<()> {
176 for (process_id, process) in processes {
177 let map_for_process = all_availabilities
179 .get(process_id)
180 .with_context(|| format!("Missing availabilities for process {process_id}"))?;
181
182 check_missing_milestone_years(process, map_for_process, base_year)?;
183 check_missing_time_slices(process, map_for_process, time_slice_info)?;
184 }
185
186 Ok(())
187}
188
189fn check_missing_milestone_years(
195 process: &Process,
196 map_for_process: &ProcessActivityLimitsMap,
197 base_year: u32,
198) -> Result<()> {
199 let process_milestone_years = process
200 .years
201 .iter()
202 .copied()
203 .filter(|&year| year >= base_year);
204 let mut missing = Vec::new();
205 for (region_id, year) in iproduct!(&process.regions, process_milestone_years) {
206 if !map_for_process.contains_key(&(region_id.clone(), year)) {
207 missing.push((region_id, year));
208 }
209 }
210
211 ensure!(
212 missing.is_empty(),
213 "Process {} is missing availabilities for the following regions and milestone years: {}",
214 &process.id,
215 format_items_with_cap(&missing)
216 );
217
218 Ok(())
219}
220
221fn check_missing_time_slices(
224 process: &Process,
225 map_for_process: &ProcessActivityLimitsMap,
226 time_slice_info: &TimeSliceInfo,
227) -> Result<()> {
228 let mut missing = Vec::new();
229 for (region_id, &year) in iproduct!(&process.regions, &process.years) {
230 if let Some(map_for_region_year) = map_for_process.get(&(region_id.clone(), year)) {
231 missing.extend(
234 time_slice_info
235 .iter_ids()
236 .filter(|ts| !map_for_region_year.contains_key(ts))
237 .map(|ts| (region_id, year, ts)),
238 );
239 }
240 }
241
242 ensure!(
243 missing.is_empty(),
244 "Availabilities supplied for some, but not all time slices, for process {}. The following \
245 regions, years and time slices are missing: {}",
246 &process.id,
247 format_items_with_cap(&missing)
248 );
249
250 Ok(())
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 fn create_process_availability_raw(
258 limit_type: LimitType,
259 value: Dimensionless,
260 ) -> ProcessAvailabilityRaw {
261 ProcessAvailabilityRaw {
262 process_id: "process".into(),
263 regions: "region".into(),
264 years: "2010".into(),
265 time_slice: "day".into(),
266 limit_type,
267 value,
268 }
269 }
270
271 #[test]
272 fn test_validate() {
273 let valid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(0.5));
275 assert!(valid.validate().is_ok());
276 let valid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(0.0));
277 assert!(valid.validate().is_ok());
278 let valid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(1.0));
279 assert!(valid.validate().is_ok());
280
281 let invalid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(-0.5));
283 assert!(invalid.validate().is_err());
284
285 let invalid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(1.5));
287 assert!(invalid.validate().is_err());
288
289 let invalid =
291 create_process_availability_raw(LimitType::LowerBound, Dimensionless(f64::INFINITY));
292 assert!(invalid.validate().is_err());
293
294 let invalid = create_process_availability_raw(
296 LimitType::LowerBound,
297 Dimensionless(f64::NEG_INFINITY),
298 );
299 assert!(invalid.validate().is_err());
300
301 let invalid =
303 create_process_availability_raw(LimitType::LowerBound, Dimensionless(f64::NAN));
304 assert!(invalid.validate().is_err());
305 }
306
307 #[test]
308 fn test_to_bounds() {
309 let ts_length = Year(0.1);
310
311 let raw = create_process_availability_raw(LimitType::LowerBound, Dimensionless(0.5));
313 let bounds = raw.to_bounds(ts_length);
314 assert_eq!(bounds, Dimensionless(0.05)..=Dimensionless(0.1));
315
316 let raw = create_process_availability_raw(LimitType::UpperBound, Dimensionless(0.5));
318 let bounds = raw.to_bounds(ts_length);
319 assert_eq!(bounds, Dimensionless(0.0)..=Dimensionless(0.05));
320
321 let raw = create_process_availability_raw(LimitType::Equality, Dimensionless(0.5));
323 let bounds = raw.to_bounds(ts_length);
324 assert_eq!(bounds, Dimensionless(0.05)..=Dimensionless(0.05));
325 }
326}