muse2/input/process/
availability.rs

1//! Code for reading process availabilities CSV file
2use 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/// Represents a row of the process availabilities CSV file
19#[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        // Check availability value
32        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    /// Calculate fraction of annual energy as availability multiplied by time slice length.
41    ///
42    /// The resulting limits are max/min PAC energy produced/consumed in each timeslice per
43    /// cap2act units of capacity
44    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/// The type of limit given for availability
55#[derive(DeserializeLabeledStringEnum)]
56enum LimitType {
57    #[string = "lo"]
58    LowerBound,
59    #[string = "up"]
60    UpperBound,
61    #[string = "fx"]
62    Equality,
63}
64
65/// Read the process availabilities CSV file.
66///
67/// This file contains information about the availability of processes over the course of a year as
68/// a proportion of their maximum capacity.
69///
70/// # Arguments
71///
72/// * `model_dir` - Folder containing model configuration files
73/// * `process_ids` - The possible valid process IDs
74/// * `time_slice_info` - Information about seasons and times of day
75///
76/// # Returns
77///
78/// A [`HashMap`] with process IDs as the keys and [`ProcessEnergyLimitsMap`]s as the values or an
79/// error.
80pub 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
99/// Process raw process availabilities input data into [`ProcessEnergyLimitsMap`]s
100fn 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        // Get process
115        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        // Get regions
121        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        // Get years
128        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        // Get timeslices
139        let ts_selection = time_slice_info.get_selection(&record.time_slice)?;
140
141        // Insert the energy limit into the map
142        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
163/// Check that every energy limits covers every time slice, and all regions/years of the process
164fn 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        // Valid
222        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        // Invalid: negative value
230        let invalid = create_process_availability_raw(LimitType::LowerBound, -0.5);
231        assert!(invalid.validate().is_err());
232
233        // Invalid: value greater than 1
234        let invalid = create_process_availability_raw(LimitType::LowerBound, 1.5);
235        assert!(invalid.validate().is_err());
236
237        // Invalid: infinity value
238        let invalid = create_process_availability_raw(LimitType::LowerBound, f64::INFINITY);
239        assert!(invalid.validate().is_err());
240
241        // Invalid: negative infinity value
242        let invalid = create_process_availability_raw(LimitType::LowerBound, f64::NEG_INFINITY);
243        assert!(invalid.validate().is_err());
244
245        // Invalid: NaN value
246        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        // Lower bound
255        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        // Upper bound
260        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        // Equality
265        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}