muse2/input/process/
availability.rs1use super::super::{format_items_with_cap, input_err_msg, read_csv, try_insert};
3use crate::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 value = self.value * ts_length / Year(1.0);
48 match self.limit_type {
49 LimitType::LowerBound => value..=Dimensionless(f64::INFINITY),
50 LimitType::UpperBound => Dimensionless(0.0)..=value,
51 LimitType::Equality => value..=value,
52 }
53 }
54}
55
56#[derive(DeserializeLabeledStringEnum)]
58enum LimitType {
59 #[string = "lo"]
60 LowerBound,
61 #[string = "up"]
62 UpperBound,
63 #[string = "fx"]
64 Equality,
65}
66
67pub fn read_process_availabilities(
83 model_dir: &Path,
84 processes: &ProcessMap,
85 time_slice_info: &TimeSliceInfo,
86) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>> {
87 let file_path = model_dir.join(PROCESS_AVAILABILITIES_FILE_NAME);
88 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}
92
93fn read_process_availabilities_from_iter<I>(
95 iter: I,
96 processes: &ProcessMap,
97 time_slice_info: &TimeSliceInfo,
98) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>>
99where
100 I: Iterator<Item = ProcessAvailabilityRaw>,
101{
102 let mut map = HashMap::new();
103 for record in iter {
104 record.validate()?;
105
106 let (id, process) = processes
108 .get_key_value(record.process_id.as_str())
109 .with_context(|| format!("Process {} not found", record.process_id))?;
110
111 let process_regions = &process.regions;
113 let record_regions =
114 parse_region_str(&record.regions, process_regions).with_context(|| {
115 format!("Invalid region for process {id}. Valid regions are {process_regions:?}")
116 })?;
117
118 let process_years = &process.years;
120 let record_years = parse_year_str(&record.years, process_years).with_context(|| {
121 format!("Invalid year for process {id}. Valid years are {process_years:?}")
122 })?;
123
124 let ts_selection = time_slice_info.get_selection(&record.time_slice)?;
126
127 let limits_map = map
129 .entry(id.clone())
130 .or_insert_with(ProcessActivityLimitsMap::new);
131 for (region_id, year) in iproduct!(&record_regions, &record_years) {
132 let limits_map_inner = limits_map
133 .entry((region_id.clone(), *year))
134 .or_insert_with(|| Rc::new(HashMap::new()));
135 let limits_map_inner = Rc::get_mut(limits_map_inner).unwrap();
136 for (time_slice, ts_length) in ts_selection.iter(time_slice_info) {
137 let bounds = record.to_bounds(ts_length);
138 try_insert(limits_map_inner, time_slice, bounds.clone())?;
139 }
140 }
141 }
142
143 validate_activity_limits_maps(&map, processes, time_slice_info)?;
144
145 Ok(map)
146}
147
148fn validate_activity_limits_maps(
150 all_availabilities: &HashMap<ProcessID, ProcessActivityLimitsMap>,
151 processes: &ProcessMap,
152 time_slice_info: &TimeSliceInfo,
153) -> Result<()> {
154 for (process_id, process) in processes {
155 let map_for_process = all_availabilities
157 .get(process_id)
158 .with_context(|| format!("Missing availabilities for process {process_id}"))?;
159
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 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 missing_keys.extend(time_slice_info.iter_ids().map(|ts| (region_id, *year, ts)));
174 }
175 }
176
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 );
183 }
184
185 Ok(())
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 fn create_process_availability_raw(
193 limit_type: LimitType,
194 value: Dimensionless,
195 ) -> ProcessAvailabilityRaw {
196 ProcessAvailabilityRaw {
197 process_id: "process".into(),
198 regions: "region".into(),
199 years: "2010".into(),
200 time_slice: "day".into(),
201 limit_type,
202 value,
203 }
204 }
205
206 #[test]
207 fn test_validate() {
208 let valid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(0.5));
210 assert!(valid.validate().is_ok());
211 let valid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(0.0));
212 assert!(valid.validate().is_ok());
213 let valid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(1.0));
214 assert!(valid.validate().is_ok());
215
216 let invalid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(-0.5));
218 assert!(invalid.validate().is_err());
219
220 let invalid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(1.5));
222 assert!(invalid.validate().is_err());
223
224 let invalid =
226 create_process_availability_raw(LimitType::LowerBound, Dimensionless(f64::INFINITY));
227 assert!(invalid.validate().is_err());
228
229 let invalid = create_process_availability_raw(
231 LimitType::LowerBound,
232 Dimensionless(f64::NEG_INFINITY),
233 );
234 assert!(invalid.validate().is_err());
235
236 let invalid =
238 create_process_availability_raw(LimitType::LowerBound, Dimensionless(f64::NAN));
239 assert!(invalid.validate().is_err());
240 }
241
242 #[test]
243 fn test_to_bounds() {
244 let ts_length = Year(0.1);
245
246 let raw = create_process_availability_raw(LimitType::LowerBound, Dimensionless(0.5));
248 let bounds = raw.to_bounds(ts_length);
249 assert_eq!(bounds, Dimensionless(0.05)..=Dimensionless(f64::INFINITY));
250
251 let raw = create_process_availability_raw(LimitType::UpperBound, Dimensionless(0.5));
253 let bounds = raw.to_bounds(ts_length);
254 assert_eq!(bounds, Dimensionless(0.0)..=Dimensionless(0.05));
255
256 let raw = create_process_availability_raw(LimitType::Equality, Dimensionless(0.5));
258 let bounds = raw.to_bounds(ts_length);
259 assert_eq!(bounds, Dimensionless(0.05)..=Dimensionless(0.05));
260 }
261}