muse2/input/process/
parameter.rs

1//! Code for reading process parameters CSV file
2use super::super::{format_items_with_cap, input_err_msg, read_csv, try_insert};
3use crate::process::{ProcessID, ProcessMap, ProcessParameter, ProcessParameterMap};
4use crate::region::parse_region_str;
5use crate::units::{Dimensionless, MoneyPerActivity, MoneyPerCapacity, MoneyPerCapacityPerYear};
6use crate::year::parse_year_str;
7use ::log::warn;
8use anyhow::{Context, Result, ensure};
9use serde::Deserialize;
10use std::collections::HashMap;
11use std::path::Path;
12use std::rc::Rc;
13
14const PROCESS_PARAMETERS_FILE_NAME: &str = "process_parameters.csv";
15
16#[derive(PartialEq, Debug, Deserialize)]
17struct ProcessParameterRaw {
18    process_id: String,
19    regions: String,
20    years: String,
21    capital_cost: MoneyPerCapacity,
22    fixed_operating_cost: MoneyPerCapacityPerYear,
23    variable_operating_cost: MoneyPerActivity,
24    lifetime: u32,
25    discount_rate: Option<Dimensionless>,
26}
27
28impl ProcessParameterRaw {
29    fn into_parameter(self) -> Result<ProcessParameter> {
30        self.validate()?;
31
32        Ok(ProcessParameter {
33            capital_cost: self.capital_cost,
34            fixed_operating_cost: self.fixed_operating_cost,
35            variable_operating_cost: self.variable_operating_cost,
36            lifetime: self.lifetime,
37            discount_rate: self.discount_rate.unwrap_or(Dimensionless(0.0)),
38        })
39    }
40}
41
42impl ProcessParameterRaw {
43    /// Validates the `ProcessParameterRaw` instance.
44    ///
45    /// # Errors
46    ///
47    /// Returns an error if:
48    /// - `lifetime` is 0.
49    /// - `discount_rate` is present and less than 0.0.
50    ///
51    /// # Warnings
52    ///
53    /// Logs a warning if:
54    /// - `discount_rate` is present and greater than 1.0.
55    ///
56    /// # Returns
57    ///
58    /// Returns `Ok(())` if all validations pass.
59    fn validate(&self) -> Result<()> {
60        ensure!(
61            self.lifetime > 0,
62            "Error in parameter for process {}: Lifetime must be greater than 0",
63            self.process_id
64        );
65
66        if let Some(dr) = self.discount_rate {
67            ensure!(
68                dr >= Dimensionless(0.0),
69                "Error in parameter for process {}: Discount rate must be positive",
70                self.process_id
71            );
72
73            if dr > Dimensionless(1.0) {
74                warn!(
75                    "Warning in parameter for process {}: Discount rate is greater than 1",
76                    self.process_id
77                );
78            }
79        }
80
81        Ok(())
82    }
83}
84
85/// Read process parameters from the specified model directory
86pub fn read_process_parameters(
87    model_dir: &Path,
88    processes: &ProcessMap,
89    base_year: u32,
90) -> Result<HashMap<ProcessID, ProcessParameterMap>> {
91    let file_path = model_dir.join(PROCESS_PARAMETERS_FILE_NAME);
92    let iter = read_csv::<ProcessParameterRaw>(&file_path)?;
93    read_process_parameters_from_iter(iter, processes, base_year)
94        .with_context(|| input_err_msg(&file_path))
95}
96
97fn read_process_parameters_from_iter<I>(
98    iter: I,
99    processes: &ProcessMap,
100    base_year: u32,
101) -> Result<HashMap<ProcessID, ProcessParameterMap>>
102where
103    I: Iterator<Item = ProcessParameterRaw>,
104{
105    let mut map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
106    for param_raw in iter {
107        // Get process
108        let (id, process) = processes
109            .get_key_value(param_raw.process_id.as_str())
110            .with_context(|| format!("Process {} not found", param_raw.process_id))?;
111
112        // Get years
113        let process_years = &process.years;
114        let parameter_years =
115            parse_year_str(&param_raw.years, process_years).with_context(|| {
116                format!("Invalid year for process {id}. Valid years are {process_years:?}")
117            })?;
118
119        // Get regions
120        let process_regions = &process.regions;
121        let parameter_regions = parse_region_str(&param_raw.regions, process_regions)
122            .with_context(|| {
123                format!("Invalid region for process {id}. Valid regions are {process_regions:?}")
124            })?;
125
126        // Insert parameter into the map
127        let param = Rc::new(param_raw.into_parameter()?);
128        let entry = map.entry(id.clone()).or_default();
129        for year in parameter_years {
130            for region in parameter_regions.clone() {
131                try_insert(entry, &(region, year), param.clone())?;
132            }
133        }
134    }
135
136    check_process_parameters(processes, &map, base_year)?;
137
138    Ok(map)
139}
140
141/// Check parameters cover all years and regions of the process
142fn check_process_parameters(
143    processes: &ProcessMap,
144    map: &HashMap<ProcessID, ProcessParameterMap>,
145    base_year: u32,
146) -> Result<()> {
147    for (process_id, process) in processes {
148        let parameters = map
149            .get(process_id)
150            .with_context(|| format!("Missing parameters for process {process_id}"))?;
151
152        let reference_years = &process.years;
153        let reference_regions = &process.regions;
154
155        // Only give an error for missing parameters >=base_year, so that users are not obliged to
156        // supply them for every valid year before the time horizon
157        let mut missing_keys = Vec::new();
158        for year in reference_years.iter().filter(|year| **year >= base_year) {
159            for region in reference_regions {
160                let key = (region.clone(), *year);
161                if !parameters.contains_key(&key) {
162                    missing_keys.push(key);
163                }
164            }
165        }
166        ensure!(
167            missing_keys.is_empty(),
168            "Process {process_id} is missing parameters for the following regions and years: {}",
169            format_items_with_cap(&missing_keys)
170        );
171    }
172
173    Ok(())
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::fixture::{assert_error, process_parameter_map, processes, region_id};
180    use crate::process::{ProcessID, ProcessMap, ProcessParameterMap};
181    use crate::region::RegionID;
182    use rstest::rstest;
183    use std::collections::HashMap;
184
185    fn create_param_raw(
186        lifetime: u32,
187        discount_rate: Option<Dimensionless>,
188    ) -> ProcessParameterRaw {
189        ProcessParameterRaw {
190            process_id: "id".to_string(),
191            capital_cost: MoneyPerCapacity(0.0),
192            fixed_operating_cost: MoneyPerCapacityPerYear(0.0),
193            variable_operating_cost: MoneyPerActivity(0.0),
194            lifetime,
195            discount_rate,
196            years: "all".to_string(),
197            regions: "all".to_string(),
198        }
199    }
200
201    fn create_param(discount_rate: Dimensionless) -> ProcessParameter {
202        ProcessParameter {
203            capital_cost: MoneyPerCapacity(0.0),
204            fixed_operating_cost: MoneyPerCapacityPerYear(0.0),
205            variable_operating_cost: MoneyPerActivity(0.0),
206            lifetime: 1,
207            discount_rate,
208        }
209    }
210
211    #[test]
212    fn test_param_raw_into_param_ok() {
213        // No missing values
214        let raw = create_param_raw(1, Some(Dimensionless(1.0)));
215        assert_eq!(
216            raw.into_parameter().unwrap(),
217            create_param(Dimensionless(1.0))
218        );
219
220        // Missing discount_rate
221        let raw = create_param_raw(1, None);
222        assert_eq!(
223            raw.into_parameter().unwrap(),
224            create_param(Dimensionless(0.0))
225        );
226    }
227
228    #[rstest]
229    fn check_process_parameters_ok(
230        processes: ProcessMap,
231        process_parameter_map: ProcessParameterMap,
232    ) {
233        let mut param_map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
234        let process_id = processes.keys().next().unwrap().clone();
235        let base_year = 2010;
236
237        param_map.insert(process_id, process_parameter_map.clone());
238        let result = check_process_parameters(&processes, &param_map, base_year);
239        assert!(result.is_ok());
240    }
241
242    #[rstest]
243    fn check_process_parameters_ok_missing_before_base_year(
244        processes: ProcessMap,
245        mut process_parameter_map: ProcessParameterMap,
246        region_id: RegionID,
247    ) {
248        let mut param_map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
249        let process_id = processes.keys().next().unwrap().clone();
250        let base_year = 2015;
251
252        // Remove one entry before base_year
253        process_parameter_map.remove(&(region_id, 2012)).unwrap();
254        param_map.insert(process_id, process_parameter_map);
255
256        let result = check_process_parameters(&processes, &param_map, base_year);
257        assert!(result.is_ok());
258    }
259
260    #[rstest]
261    fn check_process_parameters_missing(
262        processes: ProcessMap,
263        mut process_parameter_map: ProcessParameterMap,
264        region_id: RegionID,
265    ) {
266        let mut param_map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
267        let process_id = processes.keys().next().unwrap().clone();
268        let base_year = 2010;
269
270        // Remove one region-year key to simulate missing parameter
271        process_parameter_map.remove(&(region_id, 2010)).unwrap();
272        param_map.insert(process_id, process_parameter_map);
273
274        let result = check_process_parameters(&processes, &param_map, base_year);
275        assert_error!(
276            result,
277            "Process process1 is missing parameters for the following regions and years: \
278            [(RegionID(\"GBR\"), 2010)]"
279        );
280    }
281
282    #[test]
283    fn test_param_raw_validate_bad_lifetime() {
284        // lifetime = 0
285        assert!(
286            create_param_raw(0, Some(Dimensionless(1.0)))
287                .validate()
288                .is_err()
289        );
290    }
291
292    #[test]
293    fn test_param_raw_validate_bad_discount_rate() {
294        // discount rate = -1
295        assert!(
296            create_param_raw(1, Some(Dimensionless(-1.0)))
297                .validate()
298                .is_err()
299        );
300    }
301}