muse2/input/process/
availability.rs

1//! Code for reading process availabilities CSV file
2use 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/// Represents a row of the process availabilities CSV file
18#[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        // Check availability value
31        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    /// Calculate fraction of annual energy as availability multiplied by time slice length.
40    ///
41    /// The resulting limits are max/min energy produced/consumed in each timeslice per
42    /// `capacity_to_activity` units of capacity.
43    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/// The type of limit given for availability
54#[derive(DeserializeLabeledStringEnum)]
55enum LimitType {
56    #[string = "lo"]
57    LowerBound,
58    #[string = "up"]
59    UpperBound,
60    #[string = "fx"]
61    Equality,
62}
63
64/// Read the process availabilities CSV file.
65///
66/// This file contains information about the availability of processes over the course of a year as
67/// a proportion of their maximum capacity.
68///
69/// # Arguments
70///
71/// * `model_dir` - Folder containing model configuration files
72/// * `processes` - Map of processes
73/// * `time_slice_info` - Information about seasons and times of day
74///
75/// # Returns
76///
77/// A [`HashMap`] with process IDs as the keys and [`ProcessActivityLimitsMap`]s as the values or an
78/// error.
79pub 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
90/// Process raw process availabilities input data into [`ProcessActivityLimitsMap`]s
91fn 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        // Get process
104        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        // Get regions
109        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        // Get years
116        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        // Get timeslices
122        let ts_selection = time_slice_info.get_selection(&record.time_slice)?;
123
124        // Insert the activity limit into the map
125        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
148/// Check that the activity limits cover every time slice and all regions/years of the process
149fn 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        // Valid
201        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        // Invalid: negative value
209        let invalid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(-0.5));
210        assert!(invalid.validate().is_err());
211
212        // Invalid: value greater than 1
213        let invalid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(1.5));
214        assert!(invalid.validate().is_err());
215
216        // Invalid: infinity value
217        let invalid =
218            create_process_availability_raw(LimitType::LowerBound, Dimensionless(f64::INFINITY));
219        assert!(invalid.validate().is_err());
220
221        // Invalid: negative infinity value
222        let invalid = create_process_availability_raw(
223            LimitType::LowerBound,
224            Dimensionless(f64::NEG_INFINITY),
225        );
226        assert!(invalid.validate().is_err());
227
228        // Invalid: NaN value
229        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        // Lower bound
239        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        // Upper bound
244        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        // Equality
249        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}