muse2/input/process/
parameter.rs

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