Skip to main content

muse2/input/process/
parameter.rs

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