Skip to main content

muse2/input/process/
parameter.rs

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