muse2/input/process/
availability.rs

1//! Code for reading process availabilities CSV file
2use super::super::{format_items_with_cap, input_err_msg, read_csv, try_insert};
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, ensure};
9use itertools::iproduct;
10use serde::Deserialize;
11use serde_string_enum::DeserializeLabeledStringEnum;
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/// Represents a row of the process availabilities CSV file
20#[derive(Deserialize)]
21struct ProcessAvailabilityRaw {
22    process_id: String,
23    regions: String,
24    years: String,
25    time_slice: String,
26    limit_type: LimitType,
27    value: Dimensionless,
28}
29
30impl ProcessAvailabilityRaw {
31    fn validate(&self) -> Result<()> {
32        // Check availability value
33        ensure!(
34            self.value >= Dimensionless(0.0) && self.value <= Dimensionless(1.0),
35            "Value for availability must be between 0 and 1 inclusive"
36        );
37
38        Ok(())
39    }
40
41    /// Calculate fraction of annual energy as availability multiplied by time slice length.
42    ///
43    /// The resulting limits are max/min energy produced/consumed in each time slice per
44    /// `capacity_to_activity` units of capacity.
45    fn to_bounds(&self, ts_length: Year) -> RangeInclusive<Dimensionless> {
46        // We know ts_length also represents a fraction of a year, so this is ok.
47        let value = self.value * ts_length / Year(1.0);
48        match self.limit_type {
49            LimitType::LowerBound => value..=Dimensionless(f64::INFINITY),
50            LimitType::UpperBound => Dimensionless(0.0)..=value,
51            LimitType::Equality => value..=value,
52        }
53    }
54}
55
56/// The type of limit given for availability
57#[derive(DeserializeLabeledStringEnum)]
58enum LimitType {
59    #[string = "lo"]
60    LowerBound,
61    #[string = "up"]
62    UpperBound,
63    #[string = "fx"]
64    Equality,
65}
66
67/// Read the process availabilities CSV file.
68///
69/// This file contains information about the availability of processes over the course of a year as
70/// a proportion of their maximum capacity.
71///
72/// # Arguments
73///
74/// * `model_dir` - Folder containing model configuration files
75/// * `processes` - Map of processes
76/// * `time_slice_info` - Information about seasons and times of day
77///
78/// # Returns
79///
80/// A [`HashMap`] with process IDs as the keys and [`ProcessActivityLimitsMap`]s as the values or an
81/// error.
82pub fn read_process_availabilities(
83    model_dir: &Path,
84    processes: &ProcessMap,
85    time_slice_info: &TimeSliceInfo,
86) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>> {
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(process_availabilities_csv, processes, time_slice_info)
90        .with_context(|| input_err_msg(&file_path))
91}
92
93/// Process raw process availabilities input data into [`ProcessActivityLimitsMap`]s
94fn read_process_availabilities_from_iter<I>(
95    iter: I,
96    processes: &ProcessMap,
97    time_slice_info: &TimeSliceInfo,
98) -> Result<HashMap<ProcessID, ProcessActivityLimitsMap>>
99where
100    I: Iterator<Item = ProcessAvailabilityRaw>,
101{
102    let mut map = HashMap::new();
103    for record in iter {
104        record.validate()?;
105
106        // Get process
107        let (id, process) = processes
108            .get_key_value(record.process_id.as_str())
109            .with_context(|| format!("Process {} not found", record.process_id))?;
110
111        // Get regions
112        let process_regions = &process.regions;
113        let record_regions =
114            parse_region_str(&record.regions, process_regions).with_context(|| {
115                format!("Invalid region for process {id}. Valid regions are {process_regions:?}")
116            })?;
117
118        // Get years
119        let process_years = &process.years;
120        let record_years = parse_year_str(&record.years, process_years).with_context(|| {
121            format!("Invalid year for process {id}. Valid years are {process_years:?}")
122        })?;
123
124        // Get time slices
125        let ts_selection = time_slice_info.get_selection(&record.time_slice)?;
126
127        // Insert the activity limit into the map
128        let limits_map = map
129            .entry(id.clone())
130            .or_insert_with(ProcessActivityLimitsMap::new);
131        for (region_id, year) in iproduct!(&record_regions, &record_years) {
132            let limits_map_inner = limits_map
133                .entry((region_id.clone(), *year))
134                .or_insert_with(|| Rc::new(HashMap::new()));
135            let limits_map_inner = Rc::get_mut(limits_map_inner).unwrap();
136            for (time_slice, ts_length) in ts_selection.iter(time_slice_info) {
137                let bounds = record.to_bounds(ts_length);
138                try_insert(limits_map_inner, time_slice, bounds.clone())?;
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    all_availabilities: &HashMap<ProcessID, ProcessActivityLimitsMap>,
151    processes: &ProcessMap,
152    time_slice_info: &TimeSliceInfo,
153) -> Result<()> {
154    for (process_id, process) in processes {
155        // A map of maps: the outer map is keyed by region and year; the inner one by time slice
156        let map_for_process = all_availabilities
157            .get(process_id)
158            .with_context(|| format!("Missing availabilities for process {process_id}"))?;
159
160        let mut missing_keys = Vec::new();
161        for (region_id, year) in iproduct!(&process.regions, &process.years) {
162            if let Some(map_for_region_year) = map_for_process.get(&(region_id.clone(), *year)) {
163                // There are at least some entries for this region/year combo; check if there are
164                // any time slices not covered
165                missing_keys.extend(
166                    time_slice_info
167                        .iter_ids()
168                        .filter(|ts| !map_for_region_year.contains_key(ts))
169                        .map(|ts| (region_id, *year, ts)),
170                );
171            } else {
172                // No entries for this region/year combo: by definition no time slices are covered
173                missing_keys.extend(time_slice_info.iter_ids().map(|ts| (region_id, *year, ts)));
174            }
175        }
176
177        ensure!(
178            missing_keys.is_empty(),
179            "Process {process_id} is missing availabilities for the following regions, years and \
180            time slices: {}",
181            format_items_with_cap(&missing_keys)
182        );
183    }
184
185    Ok(())
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    fn create_process_availability_raw(
193        limit_type: LimitType,
194        value: Dimensionless,
195    ) -> ProcessAvailabilityRaw {
196        ProcessAvailabilityRaw {
197            process_id: "process".into(),
198            regions: "region".into(),
199            years: "2010".into(),
200            time_slice: "day".into(),
201            limit_type,
202            value,
203        }
204    }
205
206    #[test]
207    fn test_validate() {
208        // Valid
209        let valid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(0.5));
210        assert!(valid.validate().is_ok());
211        let valid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(0.0));
212        assert!(valid.validate().is_ok());
213        let valid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(1.0));
214        assert!(valid.validate().is_ok());
215
216        // Invalid: negative value
217        let invalid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(-0.5));
218        assert!(invalid.validate().is_err());
219
220        // Invalid: value greater than 1
221        let invalid = create_process_availability_raw(LimitType::LowerBound, Dimensionless(1.5));
222        assert!(invalid.validate().is_err());
223
224        // Invalid: infinity value
225        let invalid =
226            create_process_availability_raw(LimitType::LowerBound, Dimensionless(f64::INFINITY));
227        assert!(invalid.validate().is_err());
228
229        // Invalid: negative infinity value
230        let invalid = create_process_availability_raw(
231            LimitType::LowerBound,
232            Dimensionless(f64::NEG_INFINITY),
233        );
234        assert!(invalid.validate().is_err());
235
236        // Invalid: NaN value
237        let invalid =
238            create_process_availability_raw(LimitType::LowerBound, Dimensionless(f64::NAN));
239        assert!(invalid.validate().is_err());
240    }
241
242    #[test]
243    fn test_to_bounds() {
244        let ts_length = Year(0.1);
245
246        // Lower bound
247        let raw = create_process_availability_raw(LimitType::LowerBound, Dimensionless(0.5));
248        let bounds = raw.to_bounds(ts_length);
249        assert_eq!(bounds, Dimensionless(0.05)..=Dimensionless(f64::INFINITY));
250
251        // Upper bound
252        let raw = create_process_availability_raw(LimitType::UpperBound, Dimensionless(0.5));
253        let bounds = raw.to_bounds(ts_length);
254        assert_eq!(bounds, Dimensionless(0.0)..=Dimensionless(0.05));
255
256        // Equality
257        let raw = create_process_availability_raw(LimitType::Equality, Dimensionless(0.5));
258        let bounds = raw.to_bounds(ts_length);
259        assert_eq!(bounds, Dimensionless(0.05)..=Dimensionless(0.05));
260    }
261}