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, Year};
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: Year) -> RangeInclusive<Dimensionless> {
44 let value = self.value * ts_length / Year(1.0);
46 match self.limit_type {
47 LimitType::LowerBound => value..=Dimensionless(f64::INFINITY),
48 LimitType::UpperBound => Dimensionless(0.0)..=value,
49 LimitType::Equality => value..=value,
50 }
51 }
52}
53
54#[derive(DeserializeLabeledStringEnum)]
56enum LimitType {
57 #[string = "lo"]
58 LowerBound,
59 #[string = "up"]
60 UpperBound,
61 #[string = "fx"]
62 Equality,
63}
64
65pub fn read_process_availabilities(
81 model_dir: &Path,
82 processes: &ProcessMap,
83 time_slice_info: &TimeSliceInfo,
84) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>> {
85 let file_path = model_dir.join(PROCESS_AVAILABILITIES_FILE_NAME);
86 let process_availabilities_csv = read_csv(&file_path)?;
87 read_process_availabilities_from_iter(process_availabilities_csv, processes, time_slice_info)
88 .with_context(|| input_err_msg(&file_path))
89}
90
91fn read_process_availabilities_from_iter<I>(
93 iter: I,
94 processes: &ProcessMap,
95 time_slice_info: &TimeSliceInfo,
96) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>>
97where
98 I: Iterator<Item = ProcessAvailabilityRaw>,
99{
100 let mut map = HashMap::new();
101 for record in iter {
102 record.validate()?;
103
104 let (id, process) = processes
106 .get_key_value(record.process_id.as_str())
107 .with_context(|| format!("Process {} not found", record.process_id))?;
108
109 let process_regions = &process.regions;
111 let record_regions =
112 parse_region_str(&record.regions, process_regions).with_context(|| {
113 format!("Invalid region for process {id}. Valid regions are {process_regions:?}")
114 })?;
115
116 let process_years = &process.years;
118 let record_years = parse_year_str(&record.years, process_years).with_context(|| {
119 format!("Invalid year for process {id}. Valid years are {process_years:?}")
120 })?;
121
122 let ts_selection = time_slice_info.get_selection(&record.time_slice)?;
124
125 let entry = map
127 .entry(id.clone())
128 .or_insert_with(ProcessActivityLimitsMap::new);
129 for (time_slice, ts_length) in ts_selection.iter(time_slice_info) {
130 let bounds = record.to_bounds(ts_length);
131
132 for region in &record_regions {
133 for year in record_years.iter().copied() {
134 try_insert(
135 entry,
136 (region.clone(), year, time_slice.clone()),
137 bounds.clone(),
138 )?;
139 }
140 }
141 }
142 }
143
144 validate_activity_limits_maps(&map, processes, time_slice_info)?;
145
146 Ok(map)
147}
148
149fn validate_activity_limits_maps(
151 map: &HashMap<ProcessID, ProcessActivityLimitsMap>,
152 processes: &ProcessMap,
153 time_slice_info: &TimeSliceInfo,
154) -> Result<()> {
155 for (process_id, process) in processes.iter() {
156 let map = map
157 .get(process_id)
158 .with_context(|| format!("Missing availabilities for process {process_id}"))?;
159
160 let reference_years = &process.years.clone();
161 let reference_regions = &process.regions;
162 let mut missing_keys = Vec::new();
163 for year in reference_years {
164 for region in reference_regions {
165 for time_slice in time_slice_info.iter_ids() {
166 let key = (region.clone(), *year, time_slice.clone());
167 if !map.contains_key(&key) {
168 missing_keys.push(key);
169 }
170 }
171 }
172 }
173 ensure!(
174 missing_keys.is_empty(),
175 "Process {} is missing availabilities for the following regions, years and timeslice: {:?}",
176 process_id,
177 missing_keys
178 );
179 }
180
181 Ok(())
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 fn create_process_availability_raw(
189 limit_type: LimitType,
190 value: Dimensionless,
191 ) -> ProcessAvailabilityRaw {
192 ProcessAvailabilityRaw {
193 process_id: "process".into(),
194 regions: "region".into(),
195 years: "2010".into(),
196 time_slice: "day".into(),
197 limit_type,
198 value,
199 }
200 }
201
202 #[test]
203 fn test_validate() {
204 let valid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(0.5));
206 assert!(valid.validate().is_ok());
207 let valid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(0.0));
208 assert!(valid.validate().is_ok());
209 let valid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(1.0));
210 assert!(valid.validate().is_ok());
211
212 let invalid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(-0.5));
214 assert!(invalid.validate().is_err());
215
216 let invalid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(1.5));
218 assert!(invalid.validate().is_err());
219
220 let invalid =
222 create_process_availability_raw(LimitType::LowerBound, Dimensionless(f64::INFINITY));
223 assert!(invalid.validate().is_err());
224
225 let invalid = create_process_availability_raw(
227 LimitType::LowerBound,
228 Dimensionless(f64::NEG_INFINITY),
229 );
230 assert!(invalid.validate().is_err());
231
232 let invalid =
234 create_process_availability_raw(LimitType::LowerBound, Dimensionless(f64::NAN));
235 assert!(invalid.validate().is_err());
236 }
237
238 #[test]
239 fn test_to_bounds() {
240 let ts_length = Year(0.1);
241
242 let raw = create_process_availability_raw(LimitType::LowerBound, Dimensionless(0.5));
244 let bounds = raw.to_bounds(ts_length);
245 assert_eq!(bounds, Dimensionless(0.05)..=Dimensionless(f64::INFINITY));
246
247 let raw = create_process_availability_raw(LimitType::UpperBound, Dimensionless(0.5));
249 let bounds = raw.to_bounds(ts_length);
250 assert_eq!(bounds, Dimensionless(0.0)..=Dimensionless(0.05));
251
252 let raw = create_process_availability_raw(LimitType::Equality, Dimensionless(0.5));
254 let bounds = raw.to_bounds(ts_length);
255 assert_eq!(bounds, Dimensionless(0.05)..=Dimensionless(0.05));
256 }
257}