muse2/input/process/
availability.rs1use super::super::*;
3use crate::process::{ProcessActivityLimitsMap, ProcessID, ProcessMap};
4use crate::region::parse_region_str;
5use crate::time_slice::TimeSliceInfo;
6use crate::units::Dimensionless;
7use crate::year::parse_year_str;
8use anyhow::{Context, Result};
9use serde::Deserialize;
10use serde_string_enum::DeserializeLabeledStringEnum;
11use std::collections::HashMap;
12use std::ops::RangeInclusive;
13use std::path::Path;
14
15const PROCESS_AVAILABILITIES_FILE_NAME: &str = "process_availabilities.csv";
16
17#[derive(Deserialize)]
19struct ProcessAvailabilityRaw {
20 process_id: String,
21 regions: String,
22 years: String,
23 time_slice: String,
24 limit_type: LimitType,
25 value: Dimensionless,
26}
27
28impl ProcessAvailabilityRaw {
29 fn validate(&self) -> Result<()> {
30 ensure!(
32 self.value >= Dimensionless(0.0) && self.value <= Dimensionless(1.0),
33 "Value for availability must be between 0 and 1 inclusive"
34 );
35
36 Ok(())
37 }
38
39 fn to_bounds(&self, ts_length: Dimensionless) -> RangeInclusive<Dimensionless> {
44 let value = self.value * ts_length;
45 match self.limit_type {
46 LimitType::LowerBound => value..=Dimensionless(f64::INFINITY),
47 LimitType::UpperBound => Dimensionless(0.0)..=value,
48 LimitType::Equality => value..=value,
49 }
50 }
51}
52
53#[derive(DeserializeLabeledStringEnum)]
55enum LimitType {
56 #[string = "lo"]
57 LowerBound,
58 #[string = "up"]
59 UpperBound,
60 #[string = "fx"]
61 Equality,
62}
63
64pub fn read_process_availabilities(
80 model_dir: &Path,
81 processes: &ProcessMap,
82 time_slice_info: &TimeSliceInfo,
83) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>> {
84 let file_path = model_dir.join(PROCESS_AVAILABILITIES_FILE_NAME);
85 let process_availabilities_csv = read_csv(&file_path)?;
86 read_process_availabilities_from_iter(process_availabilities_csv, processes, time_slice_info)
87 .with_context(|| input_err_msg(&file_path))
88}
89
90fn read_process_availabilities_from_iter<I>(
92 iter: I,
93 processes: &ProcessMap,
94 time_slice_info: &TimeSliceInfo,
95) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>>
96where
97 I: Iterator<Item = ProcessAvailabilityRaw>,
98{
99 let mut map = HashMap::new();
100 for record in iter {
101 record.validate()?;
102
103 let (id, process) = processes
105 .get_key_value(record.process_id.as_str())
106 .with_context(|| format!("Process {} not found", record.process_id))?;
107
108 let process_regions = &process.regions;
110 let record_regions =
111 parse_region_str(&record.regions, process_regions).with_context(|| {
112 format!("Invalid region for process {id}. Valid regions are {process_regions:?}")
113 })?;
114
115 let process_years = &process.years;
117 let record_years = parse_year_str(&record.years, process_years).with_context(|| {
118 format!("Invalid year for process {id}. Valid years are {process_years:?}")
119 })?;
120
121 let ts_selection = time_slice_info.get_selection(&record.time_slice)?;
123
124 let entry = map
126 .entry(id.clone())
127 .or_insert_with(ProcessActivityLimitsMap::new);
128 for (time_slice, ts_length) in ts_selection.iter(time_slice_info) {
129 let bounds = record.to_bounds(ts_length);
130
131 for region in &record_regions {
132 for year in record_years.iter().copied() {
133 try_insert(
134 entry,
135 (region.clone(), year, time_slice.clone()),
136 bounds.clone(),
137 )?;
138 }
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 map: &HashMap<ProcessID, ProcessActivityLimitsMap>,
151 processes: &ProcessMap,
152 time_slice_info: &TimeSliceInfo,
153) -> Result<()> {
154 for (process_id, map) in map.iter() {
155 let process = processes.get(process_id).unwrap();
156 let reference_years = &process.years.clone();
157 let reference_regions = &process.regions;
158 let mut missing_keys = Vec::new();
159 for year in reference_years {
160 for region in reference_regions {
161 for time_slice in time_slice_info.iter_ids() {
162 let key = (region.clone(), *year, time_slice.clone());
163 if !map.contains_key(&key) {
164 missing_keys.push(key);
165 }
166 }
167 }
168 }
169 ensure!(
170 missing_keys.is_empty(),
171 "Process {} is missing availabilities for the following regions, years and timeslice: {:?}",
172 process_id,
173 missing_keys
174 );
175 }
176
177 Ok(())
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 fn create_process_availability_raw(
185 limit_type: LimitType,
186 value: Dimensionless,
187 ) -> ProcessAvailabilityRaw {
188 ProcessAvailabilityRaw {
189 process_id: "process".into(),
190 regions: "region".into(),
191 years: "2010".into(),
192 time_slice: "day".into(),
193 limit_type,
194 value,
195 }
196 }
197
198 #[test]
199 fn test_validate() {
200 let valid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(0.5));
202 assert!(valid.validate().is_ok());
203 let valid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(0.0));
204 assert!(valid.validate().is_ok());
205 let valid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(1.0));
206 assert!(valid.validate().is_ok());
207
208 let invalid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(-0.5));
210 assert!(invalid.validate().is_err());
211
212 let invalid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(1.5));
214 assert!(invalid.validate().is_err());
215
216 let invalid =
218 create_process_availability_raw(LimitType::LowerBound, Dimensionless(f64::INFINITY));
219 assert!(invalid.validate().is_err());
220
221 let invalid = create_process_availability_raw(
223 LimitType::LowerBound,
224 Dimensionless(f64::NEG_INFINITY),
225 );
226 assert!(invalid.validate().is_err());
227
228 let invalid =
230 create_process_availability_raw(LimitType::LowerBound, Dimensionless(f64::NAN));
231 assert!(invalid.validate().is_err());
232 }
233
234 #[test]
235 fn test_to_bounds() {
236 let ts_length = Dimensionless(0.1);
237
238 let raw = create_process_availability_raw(LimitType::LowerBound, Dimensionless(0.5));
240 let bounds = raw.to_bounds(ts_length);
241 assert_eq!(bounds, Dimensionless(0.05)..=Dimensionless(f64::INFINITY));
242
243 let raw = create_process_availability_raw(LimitType::UpperBound, Dimensionless(0.5));
245 let bounds = raw.to_bounds(ts_length);
246 assert_eq!(bounds, Dimensionless(0.0)..=Dimensionless(0.05));
247
248 let raw = create_process_availability_raw(LimitType::Equality, Dimensionless(0.5));
250 let bounds = raw.to_bounds(ts_length);
251 assert_eq!(bounds, Dimensionless(0.05)..=Dimensionless(0.05));
252 }
253}