muse2/input/process/
availability.rs1use super::super::{input_err_msg, parse_range, read_csv_optional, try_insert};
3use crate::id::GetIDValue;
4use crate::input::parse_year_str;
5use crate::process::{ActivityLimits, ProcessActivityLimitsMap, ProcessID, ProcessMap};
6use crate::region::parse_region_str;
7use crate::time_slice::TimeSliceInfo;
8use crate::units::{Dimensionless, Year};
9use anyhow::{Context, Result};
10use itertools::iproduct;
11use serde::Deserialize;
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 commission_years: String,
25 time_slice: String,
26 limits: String,
27}
28
29impl ProcessAvailabilityRaw {
30 fn to_bounds(&self, length: Year) -> Result<RangeInclusive<Dimensionless>> {
36 let availability_range = parse_range(&self.limits, Dimensionless(0.0)..=Dimensionless(1.0))
38 .with_context(|| format!("Could not parse availabilities range: {}", &self.limits))?;
39
40 let ts_frac = length / Year(1.0);
42 let start = *availability_range.start() * ts_frac;
43 let end = *availability_range.end() * ts_frac;
44 Ok(start..=end)
45 }
46}
47
48pub fn read_process_availabilities(
64 model_dir: &Path,
65 processes: &ProcessMap,
66 time_slice_info: &TimeSliceInfo,
67) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>> {
68 let file_path = model_dir.join(PROCESS_AVAILABILITIES_FILE_NAME);
69 let process_availabilities_csv = read_csv_optional(&file_path)?;
70 read_process_availabilities_from_iter(process_availabilities_csv, processes, time_slice_info)
71 .with_context(|| input_err_msg(&file_path))
72}
73
74fn read_process_availabilities_from_iter<I>(
87 iter: I,
88 processes: &ProcessMap,
89 time_slice_info: &TimeSliceInfo,
90) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>>
91where
92 I: Iterator<Item = ProcessAvailabilityRaw>,
93{
94 let mut entries: HashMap<ProcessID, _> = processes
96 .iter()
97 .map(|(id, _)| (id.clone(), HashMap::new()))
98 .collect();
99
100 for record in iter {
101 let (id, process) = processes.get_id_value(&record.process_id)?;
103
104 let process_regions = &process.regions;
106 let record_regions =
107 parse_region_str(&record.regions, process_regions).with_context(|| {
108 format!("Invalid region for process {id}. Valid regions are {process_regions:?}")
109 })?;
110
111 let process_years: Vec<u32> = process.years.clone().collect();
113 let record_years =
114 parse_year_str(&record.commission_years, &process_years).with_context(|| {
115 format!("Invalid year for process {id}. Valid years are {process_years:?}")
116 })?;
117
118 let ts_selection = time_slice_info.get_selection(&record.time_slice)?;
120
121 let entries_for_process = entries.get_mut(id).unwrap();
123 for (region_id, year) in iproduct!(&record_regions, &record_years) {
124 let entries_for_process_region_year = entries_for_process
125 .entry((region_id.clone(), *year))
126 .or_default();
127 let length = time_slice_info.length_for_selection(&ts_selection)?;
128 try_insert(
129 entries_for_process_region_year,
130 &ts_selection,
131 record.to_bounds(length)?,
132 )?;
133 }
134 }
135
136 let mut map = HashMap::new();
141 for (process_id, process) in processes {
142 let mut inner_map = HashMap::new();
143 let entries_for_process = &entries[process_id];
144 for (region_id, year) in iproduct!(&process.regions, process.years.clone()) {
145 let limits = entries_for_process
146 .get(&(region_id.clone(), year))
147 .cloned()
148 .unwrap_or_default();
149 let availabilities = ActivityLimits::new_from_limits(&limits, time_slice_info)
150 .with_context(|| {
151 format!("Error creating activity limits for process {process_id}")
152 })?;
153 inner_map.insert((region_id.clone(), year), Rc::new(availabilities));
154 }
155 map.insert(process_id.clone(), inner_map);
156 }
157
158 Ok(map)
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use float_cmp::assert_approx_eq;
165 use rstest::rstest;
166
167 fn create_process_availability_raw(limits: String) -> ProcessAvailabilityRaw {
168 ProcessAvailabilityRaw {
169 process_id: "process".into(),
170 regions: "region".into(),
171 commission_years: "2010".into(),
172 time_slice: "day".into(),
173 limits,
174 }
175 }
176
177 #[rstest]
178 #[case("0.1..", Year(0.1), Dimensionless(0.01)..=Dimensionless(0.1))] #[case("..0.5", Year(0.1), Dimensionless(0.0)..=Dimensionless(0.05))] #[case("0.5..0.5", Year(0.1), Dimensionless(0.05)..=Dimensionless(0.05))] fn to_bounds(
182 #[case] limits: &str,
183 #[case] ts_length: Year,
184 #[case] expected: RangeInclusive<Dimensionless>,
185 ) {
186 let raw = create_process_availability_raw(limits.into());
187 let bounds = raw.to_bounds(ts_length).unwrap();
188 assert_approx_eq!(Dimensionless, *bounds.start(), *expected.start());
189 assert_approx_eq!(Dimensionless, *bounds.end(), *expected.end());
190 }
191}