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    commission_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    milestone_years: &[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, milestone_years)
94        .with_context(|| input_err_msg(&file_path))
95}
96
97fn read_process_parameters_from_iter<I>(
98    iter: I,
99    processes: &ProcessMap,
100    milestone_years: &[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: Vec<u32> = process.years.clone().collect();
114        let parameter_years = parse_year_str(&param_raw.commission_years, &process_years)
115            .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, milestone_years)?;
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    milestone_years: &[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_regions = &process.regions;
153
154        // Only give an error for missing parameters in milestone years, so that users are not
155        // obliged to supply them for every valid year before the time horizon
156        let mut missing_keys = Vec::new();
157        for year in process
158            .years
159            .clone()
160            .filter(|y| milestone_years.contains(y))
161        {
162            for region in reference_regions {
163                let key = (region.clone(), year);
164                if !parameters.contains_key(&key) {
165                    missing_keys.push(key);
166                }
167            }
168        }
169        ensure!(
170            missing_keys.is_empty(),
171            "Process {process_id} is missing parameters for the following regions and years: {}",
172            format_items_with_cap(&missing_keys)
173        );
174    }
175
176    Ok(())
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::fixture::{assert_error, process_parameter_map, processes, region_id};
183    use crate::process::{ProcessID, ProcessMap, ProcessParameterMap};
184    use crate::region::RegionID;
185    use rstest::rstest;
186    use std::collections::HashMap;
187
188    fn create_param_raw(
189        lifetime: u32,
190        discount_rate: Option<Dimensionless>,
191    ) -> ProcessParameterRaw {
192        ProcessParameterRaw {
193            process_id: "id".to_string(),
194            capital_cost: MoneyPerCapacity(0.0),
195            fixed_operating_cost: MoneyPerCapacityPerYear(0.0),
196            variable_operating_cost: MoneyPerActivity(0.0),
197            lifetime,
198            discount_rate,
199            commission_years: "all".to_string(),
200            regions: "all".to_string(),
201        }
202    }
203
204    fn create_param(discount_rate: Dimensionless) -> ProcessParameter {
205        ProcessParameter {
206            capital_cost: MoneyPerCapacity(0.0),
207            fixed_operating_cost: MoneyPerCapacityPerYear(0.0),
208            variable_operating_cost: MoneyPerActivity(0.0),
209            lifetime: 1,
210            discount_rate,
211        }
212    }
213
214    #[test]
215    fn test_param_raw_into_param_ok() {
216        // No missing values
217        let raw = create_param_raw(1, Some(Dimensionless(1.0)));
218        assert_eq!(
219            raw.into_parameter().unwrap(),
220            create_param(Dimensionless(1.0))
221        );
222
223        // Missing discount_rate
224        let raw = create_param_raw(1, None);
225        assert_eq!(
226            raw.into_parameter().unwrap(),
227            create_param(Dimensionless(0.0))
228        );
229    }
230
231    #[rstest]
232    fn check_process_parameters_ok(
233        processes: ProcessMap,
234        process_parameter_map: ProcessParameterMap,
235    ) {
236        let mut param_map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
237        let process_id = processes.keys().next().unwrap().clone();
238        let milestone_years: Vec<u32> = vec![2010, 2020];
239
240        param_map.insert(process_id, process_parameter_map.clone());
241        let result = check_process_parameters(&processes, &param_map, &milestone_years);
242        assert!(result.is_ok());
243    }
244
245    #[rstest]
246    fn check_process_parameters_ok_missing_before_base_year(
247        processes: ProcessMap,
248        mut process_parameter_map: ProcessParameterMap,
249        region_id: RegionID,
250    ) {
251        let mut param_map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
252        let process_id = processes.keys().next().unwrap().clone();
253        let milestone_years: Vec<u32> = vec![2015, 2020];
254
255        // Remove one entry before base_year
256        process_parameter_map.remove(&(region_id, 2012)).unwrap();
257        param_map.insert(process_id, process_parameter_map);
258
259        let result = check_process_parameters(&processes, &param_map, &milestone_years);
260        assert!(result.is_ok());
261    }
262
263    #[rstest]
264    fn check_process_parameters_missing(
265        processes: ProcessMap,
266        mut process_parameter_map: ProcessParameterMap,
267        region_id: RegionID,
268    ) {
269        let mut param_map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
270        let process_id = processes.keys().next().unwrap().clone();
271        let milestone_years: Vec<u32> = vec![2010, 2020];
272
273        // Remove one region-year key to simulate missing parameter
274        process_parameter_map.remove(&(region_id, 2010)).unwrap();
275        param_map.insert(process_id, process_parameter_map);
276
277        let result = check_process_parameters(&processes, &param_map, &milestone_years);
278        assert_error!(
279            result,
280            "Process process1 is missing parameters for the following regions and years: \
281            [(RegionID(\"GBR\"), 2010)]"
282        );
283    }
284
285    #[test]
286    fn test_param_raw_validate_bad_lifetime() {
287        // lifetime = 0
288        assert!(
289            create_param_raw(0, Some(Dimensionless(1.0)))
290                .validate()
291                .is_err()
292        );
293    }
294
295    #[test]
296    fn test_param_raw_validate_bad_discount_rate() {
297        // discount rate = -1
298        assert!(
299            create_param_raw(1, Some(Dimensionless(-1.0)))
300                .validate()
301                .is_err()
302        );
303    }
304}