muse2/input/process/
availability.rs1use super::super::*;
3use crate::id::IDCollection;
4use crate::process::{Process, ProcessEnergyLimitsMap, ProcessID};
5use crate::region::parse_region_str;
6use crate::time_slice::TimeSliceInfo;
7use crate::year::parse_year_str;
8use anyhow::{Context, Result};
9use indexmap::IndexSet;
10use serde::Deserialize;
11use serde_string_enum::DeserializeLabeledStringEnum;
12use std::collections::{HashMap, HashSet};
13use std::ops::RangeInclusive;
14use std::path::Path;
15
16const PROCESS_AVAILABILITIES_FILE_NAME: &str = "process_availabilities.csv";
17
18#[derive(Deserialize)]
20struct ProcessAvailabilityRaw {
21 process_id: String,
22 regions: String,
23 year: String,
24 time_slice: String,
25 limit_type: LimitType,
26 value: f64,
27}
28
29impl ProcessAvailabilityRaw {
30 fn validate(&self) -> Result<()> {
31 ensure!(
33 self.value >= 0.0 && self.value <= 1.0,
34 "Value for availability must be between 0 and 1 inclusive"
35 );
36
37 Ok(())
38 }
39
40 fn to_bounds(&self, ts_length: f64) -> RangeInclusive<f64> {
45 let value = self.value * ts_length;
46 match self.limit_type {
47 LimitType::LowerBound => value..=f64::INFINITY,
48 LimitType::UpperBound => 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 process_ids: &IndexSet<ProcessID>,
83 processes: &HashMap<ProcessID, Process>,
84 time_slice_info: &TimeSliceInfo,
85 milestone_years: &[u32],
86) -> Result<HashMap<ProcessID, ProcessEnergyLimitsMap>> {
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(
90 process_availabilities_csv,
91 process_ids,
92 processes,
93 time_slice_info,
94 milestone_years,
95 )
96 .with_context(|| input_err_msg(&file_path))
97}
98
99fn read_process_availabilities_from_iter<I>(
101 iter: I,
102 process_ids: &IndexSet<ProcessID>,
103 processes: &HashMap<ProcessID, Process>,
104 time_slice_info: &TimeSliceInfo,
105 milestone_years: &[u32],
106) -> Result<HashMap<ProcessID, ProcessEnergyLimitsMap>>
107where
108 I: Iterator<Item = ProcessAvailabilityRaw>,
109{
110 let mut map = HashMap::new();
111 for record in iter {
112 record.validate()?;
113
114 let id = process_ids.get_id_by_str(&record.process_id)?;
116 let process = processes
117 .get(&id)
118 .with_context(|| format!("Process {id} not found"))?;
119
120 let process_regions = process.regions.clone();
122 let record_regions =
123 parse_region_str(&record.regions, &process_regions).with_context(|| {
124 format!("Invalid region for process {id}. Valid regions are {process_regions:?}")
125 })?;
126
127 let process_year_range = &process.years;
129 let process_years: Vec<u32> = milestone_years
130 .iter()
131 .copied()
132 .filter(|year| process_year_range.contains(year))
133 .collect();
134 let record_years = parse_year_str(&record.year, &process_years).with_context(|| {
135 format!("Invalid year for process {id}. Valid years are {process_years:?}")
136 })?;
137
138 let ts_selection = time_slice_info.get_selection(&record.time_slice)?;
140
141 let entry = map.entry(id).or_insert_with(ProcessEnergyLimitsMap::new);
143 for (time_slice, ts_length) in time_slice_info.iter_selection(&ts_selection) {
144 let bounds = record.to_bounds(ts_length);
145
146 for region in &record_regions {
147 for year in record_years.iter().copied() {
148 try_insert(
149 entry,
150 (region.clone(), year, time_slice.clone()),
151 bounds.clone(),
152 )?;
153 }
154 }
155 }
156 }
157
158 validate_energy_limits_maps(&map, processes, time_slice_info, milestone_years)?;
159
160 Ok(map)
161}
162
163fn validate_energy_limits_maps(
165 map: &HashMap<ProcessID, ProcessEnergyLimitsMap>,
166 processes: &HashMap<ProcessID, Process>,
167 time_slice_info: &TimeSliceInfo,
168 milestone_years: &[u32],
169) -> Result<()> {
170 for (process_id, map) in map.iter() {
171 let process = processes.get(process_id).unwrap();
172 let year_range = &process.years;
173 let reference_years: HashSet<u32> = milestone_years
174 .iter()
175 .copied()
176 .filter(|year| year_range.contains(year))
177 .collect();
178 let reference_regions = &process.regions;
179 let mut missing_keys = Vec::new();
180 for year in &reference_years {
181 for region in reference_regions {
182 for time_slice in time_slice_info.iter_ids() {
183 let key = (region.clone(), *year, time_slice.clone());
184 if !map.contains_key(&key) {
185 missing_keys.push(key);
186 }
187 }
188 }
189 }
190 ensure!(
191 missing_keys.is_empty(),
192 "Process {} is missing availabilities for the following regions, years and timeslice: {:?}",
193 process_id,
194 missing_keys
195 );
196 }
197
198 Ok(())
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 fn create_process_availability_raw(
206 limit_type: LimitType,
207 value: f64,
208 ) -> ProcessAvailabilityRaw {
209 ProcessAvailabilityRaw {
210 process_id: "process".into(),
211 regions: "region".into(),
212 year: "2010".into(),
213 time_slice: "day".into(),
214 limit_type,
215 value,
216 }
217 }
218
219 #[test]
220 fn test_validate() {
221 let valid = create_process_availability_raw(LimitType::LowerBound, 0.5);
223 assert!(valid.validate().is_ok());
224 let valid = create_process_availability_raw(LimitType::LowerBound, 0.0);
225 assert!(valid.validate().is_ok());
226 let valid = create_process_availability_raw(LimitType::LowerBound, 1.0);
227 assert!(valid.validate().is_ok());
228
229 let invalid = create_process_availability_raw(LimitType::LowerBound, -0.5);
231 assert!(invalid.validate().is_err());
232
233 let invalid = create_process_availability_raw(LimitType::LowerBound, 1.5);
235 assert!(invalid.validate().is_err());
236
237 let invalid = create_process_availability_raw(LimitType::LowerBound, f64::INFINITY);
239 assert!(invalid.validate().is_err());
240
241 let invalid = create_process_availability_raw(LimitType::LowerBound, f64::NEG_INFINITY);
243 assert!(invalid.validate().is_err());
244
245 let invalid = create_process_availability_raw(LimitType::LowerBound, f64::NAN);
247 assert!(invalid.validate().is_err());
248 }
249
250 #[test]
251 fn test_to_bounds() {
252 let ts_length = 0.1;
253
254 let raw = create_process_availability_raw(LimitType::LowerBound, 0.5);
256 let bounds = raw.to_bounds(ts_length);
257 assert_eq!(bounds, 0.05..=f64::INFINITY);
258
259 let raw = create_process_availability_raw(LimitType::UpperBound, 0.5);
261 let bounds = raw.to_bounds(ts_length);
262 assert_eq!(bounds, 0.0..=0.05);
263
264 let raw = create_process_availability_raw(LimitType::Equality, 0.5);
266 let bounds = raw.to_bounds(ts_length);
267 assert_eq!(bounds, 0.05..=0.05);
268 }
269}