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::{ensure, Context, Result};
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) -> Result<HashMap<ProcessID, ProcessParameterMap>> {
105    let file_path = model_dir.join(PROCESS_PARAMETERS_FILE_NAME);
106    let iter = read_csv::<ProcessParameterRaw>(&file_path)?;
107    read_process_parameters_from_iter(iter, processes).with_context(|| input_err_msg(&file_path))
108}
109
110fn read_process_parameters_from_iter<I>(
111    iter: I,
112    processes: &ProcessMap,
113) -> Result<HashMap<ProcessID, ProcessParameterMap>>
114where
115    I: Iterator<Item = ProcessParameterRaw>,
116{
117    let mut map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
118    for param_raw in iter {
119        // Get process
120        let (id, process) = processes
121            .get_key_value(param_raw.process_id.as_str())
122            .with_context(|| format!("Process {} not found", param_raw.process_id))?;
123
124        // Get years
125        let process_years = &process.years;
126        let parameter_years =
127            parse_year_str(&param_raw.years, process_years).with_context(|| {
128                format!("Invalid year for process {id}. Valid years are {process_years:?}")
129            })?;
130
131        // Get regions
132        let process_regions = &process.regions;
133        let parameter_regions = parse_region_str(&param_raw.regions, process_regions)
134            .with_context(|| {
135                format!("Invalid region for process {id}. Valid regions are {process_regions:?}")
136            })?;
137
138        // Insert parameter into the map
139        let param = Rc::new(param_raw.into_parameter()?);
140        let entry = map.entry(id.clone()).or_default();
141        for year in parameter_years {
142            for region in parameter_regions.clone() {
143                try_insert(entry, (region, year), param.clone())?;
144            }
145        }
146    }
147
148    // Check parameters cover all years and regions of the process
149    for (id, parameters) in map.iter() {
150        let process = processes.get(id).unwrap();
151        let reference_years = &process.years;
152        let reference_regions = &process.regions;
153
154        let mut missing_keys = Vec::new();
155        for year in reference_years {
156            for region in reference_regions {
157                let key = (region.clone(), *year);
158                if !parameters.contains_key(&key) {
159                    missing_keys.push(key);
160                }
161            }
162        }
163        ensure!(
164            missing_keys.is_empty(),
165            "Process {} is missing parameters for the following regions and years: {:?}",
166            id,
167            missing_keys
168        );
169    }
170    Ok(map)
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    fn create_param_raw(
178        lifetime: u32,
179        discount_rate: Option<Dimensionless>,
180        capacity_to_activity: Option<ActivityPerCapacity>,
181    ) -> ProcessParameterRaw {
182        ProcessParameterRaw {
183            process_id: "id".to_string(),
184            capital_cost: MoneyPerCapacity(0.0),
185            fixed_operating_cost: MoneyPerCapacityPerYear(0.0),
186            variable_operating_cost: MoneyPerActivity(0.0),
187            lifetime,
188            discount_rate,
189            capacity_to_activity,
190            years: "all".to_string(),
191            regions: "all".to_string(),
192        }
193    }
194
195    fn create_param(
196        discount_rate: Dimensionless,
197        capacity_to_activity: ActivityPerCapacity,
198    ) -> ProcessParameter {
199        ProcessParameter {
200            capital_cost: MoneyPerCapacity(0.0),
201            fixed_operating_cost: MoneyPerCapacityPerYear(0.0),
202            variable_operating_cost: MoneyPerActivity(0.0),
203            lifetime: 1,
204            discount_rate,
205            capacity_to_activity,
206        }
207    }
208
209    #[test]
210    fn test_param_raw_into_param_ok() {
211        // No missing values
212        let raw = create_param_raw(1, Some(Dimensionless(1.0)), Some(ActivityPerCapacity(0.0)));
213        assert_eq!(
214            raw.into_parameter().unwrap(),
215            create_param(Dimensionless(1.0), ActivityPerCapacity(0.0))
216        );
217
218        // Missing discount_rate
219        let raw = create_param_raw(1, None, Some(ActivityPerCapacity(0.0)));
220        assert_eq!(
221            raw.into_parameter().unwrap(),
222            create_param(Dimensionless(0.0), ActivityPerCapacity(0.0))
223        );
224
225        // Missing capacity_to_activity
226        let raw = create_param_raw(1, Some(Dimensionless(1.0)), None);
227        assert_eq!(
228            raw.into_parameter().unwrap(),
229            create_param(Dimensionless(1.0), ActivityPerCapacity(1.0))
230        );
231    }
232
233    #[test]
234    fn test_param_raw_validate_bad_lifetime() {
235        // lifetime = 0
236        assert!(
237            create_param_raw(0, Some(Dimensionless(1.0)), Some(ActivityPerCapacity(0.0)))
238                .validate()
239                .is_err()
240        );
241    }
242
243    #[test]
244    fn test_param_raw_validate_bad_discount_rate() {
245        // discount rate = -1
246        assert!(
247            create_param_raw(0, Some(Dimensionless(-1.0)), Some(ActivityPerCapacity(0.0)))
248                .validate()
249                .is_err()
250        );
251    }
252
253    #[test]
254    fn test_param_raw_validate_bad_capt2act() {
255        // capt2act = -1
256        assert!(
257            create_param_raw(0, Some(Dimensionless(1.0)), Some(ActivityPerCapacity(-1.0)))
258                .validate()
259                .is_err()
260        );
261    }
262}