muse2/input/process/
parameter.rs

1//! Code for reading process parameters CSV file
2use super::super::*;
3use crate::id::IDCollection;
4use crate::process::{Process, ProcessID, ProcessParameter, ProcessParameterMap};
5use crate::region::parse_region_str;
6use crate::year::parse_year_str;
7use ::log::warn;
8use anyhow::{ensure, Context, Result};
9use indexmap::IndexSet;
10use serde::Deserialize;
11use std::collections::{HashMap, HashSet};
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    year: String,
22    capital_cost: f64,
23    fixed_operating_cost: f64,
24    variable_operating_cost: f64,
25    lifetime: u32,
26    discount_rate: Option<f64>,
27    capacity_to_activity: Option<f64>,
28}
29
30impl ProcessParameterRaw {
31    fn into_parameter(self) -> Result<ProcessParameter> {
32        self.validate()?;
33
34        Ok(ProcessParameter {
35            capital_cost: self.capital_cost,
36            fixed_operating_cost: self.fixed_operating_cost,
37            variable_operating_cost: self.variable_operating_cost,
38            lifetime: self.lifetime,
39            discount_rate: self.discount_rate.unwrap_or(0.0),
40            capacity_to_activity: self.capacity_to_activity.unwrap_or(1.0),
41        })
42    }
43}
44
45impl ProcessParameterRaw {
46    /// Validates the `ProcessParameterRaw` instance.
47    ///
48    /// # Errors
49    ///
50    /// Returns an error if:
51    /// - `lifetime` is 0.
52    /// - `discount_rate` is present and less than 0.0.
53    /// - `capacity_to_activity` is present and less than 0.0.
54    ///
55    /// # Warnings
56    ///
57    /// Logs a warning if:
58    /// - `discount_rate` is present and greater than 1.0.
59    ///
60    /// # Returns
61    ///
62    /// Returns `Ok(())` if all validations pass.
63    fn validate(&self) -> Result<()> {
64        ensure!(
65            self.lifetime > 0,
66            "Error in parameter for process {}: Lifetime must be greater than 0",
67            self.process_id
68        );
69
70        if let Some(dr) = self.discount_rate {
71            ensure!(
72                dr >= 0.0,
73                "Error in parameter for process {}: Discount rate must be positive",
74                self.process_id
75            );
76
77            if dr > 1.0 {
78                warn!(
79                    "Warning in parameter for process {}: Discount rate is greater than 1",
80                    self.process_id
81                );
82            }
83        }
84
85        if let Some(c2a) = self.capacity_to_activity {
86            ensure!(
87                c2a >= 0.0,
88                "Error in parameter for process {}: Cap2act must be positive",
89                self.process_id
90            );
91        }
92
93        Ok(())
94    }
95}
96
97/// Read process parameters from the specified model directory
98pub fn read_process_parameters(
99    model_dir: &Path,
100    process_ids: &IndexSet<ProcessID>,
101    processes: &HashMap<ProcessID, Process>,
102    milestone_years: &[u32],
103) -> Result<HashMap<ProcessID, ProcessParameterMap>> {
104    let file_path = model_dir.join(PROCESS_PARAMETERS_FILE_NAME);
105    let iter = read_csv::<ProcessParameterRaw>(&file_path)?;
106    read_process_parameters_from_iter(iter, process_ids, processes, milestone_years)
107        .with_context(|| input_err_msg(&file_path))
108}
109
110fn read_process_parameters_from_iter<I>(
111    iter: I,
112    process_ids: &IndexSet<ProcessID>,
113    processes: &HashMap<ProcessID, Process>,
114    milestone_years: &[u32],
115) -> Result<HashMap<ProcessID, ProcessParameterMap>>
116where
117    I: Iterator<Item = ProcessParameterRaw>,
118{
119    let mut map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
120    for param_raw in iter {
121        // Get process
122        let id = process_ids.get_id_by_str(&param_raw.process_id)?;
123        let process = processes
124            .get(&id)
125            .with_context(|| format!("Process {id} not found"))?;
126
127        // Get years
128        let process_year_range = &process.years;
129        let process_years: Vec<u32> = milestone_years
130            .iter()
131            .copied()
132            .filter(|year| process_year_range.contains(year))
133            .collect();
134        let parameter_years =
135            parse_year_str(&param_raw.year, &process_years).with_context(|| {
136                format!("Invalid year for process {id}. Valid years are {process_years:?}")
137            })?;
138
139        // Get regions
140        let process_regions = process.regions.clone();
141        let parameter_regions = parse_region_str(&param_raw.regions, &process_regions)
142            .with_context(|| {
143                format!("Invalid region for process {id}. Valid regions are {process_regions:?}")
144            })?;
145
146        // Insert parameter into the map
147        let param = Rc::new(param_raw.into_parameter()?);
148        let entry = map.entry(id.clone()).or_default();
149        for year in parameter_years {
150            for region in parameter_regions.clone() {
151                try_insert(entry, (region, year), param.clone())?;
152            }
153        }
154    }
155
156    // Check parameters cover all years and regions of the process
157    for (id, parameters) in map.iter() {
158        let process = processes.get(id).unwrap();
159        let year_range = &process.years;
160        let reference_years: HashSet<u32> = milestone_years
161            .iter()
162            .copied()
163            .filter(|year| year_range.contains(year))
164            .collect();
165        let reference_regions = process.regions.clone();
166
167        let mut missing_keys = Vec::new();
168        for year in &reference_years {
169            for region in &reference_regions {
170                let key = (region.clone(), *year);
171                if !parameters.contains_key(&key) {
172                    missing_keys.push(key);
173                }
174            }
175        }
176        ensure!(
177            missing_keys.is_empty(),
178            "Process {} is missing parameters for the following regions and years: {:?}",
179            id,
180            missing_keys
181        );
182    }
183    Ok(map)
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    fn create_param_raw(
191        lifetime: u32,
192        discount_rate: Option<f64>,
193        capacity_to_activity: Option<f64>,
194    ) -> ProcessParameterRaw {
195        ProcessParameterRaw {
196            process_id: "id".to_string(),
197            capital_cost: 0.0,
198            fixed_operating_cost: 0.0,
199            variable_operating_cost: 0.0,
200            lifetime,
201            discount_rate,
202            capacity_to_activity,
203            year: "all".to_string(),
204            regions: "all".to_string(),
205        }
206    }
207
208    fn create_param(discount_rate: f64, capacity_to_activity: f64) -> ProcessParameter {
209        ProcessParameter {
210            capital_cost: 0.0,
211            fixed_operating_cost: 0.0,
212            variable_operating_cost: 0.0,
213            lifetime: 1,
214            discount_rate,
215            capacity_to_activity,
216        }
217    }
218
219    #[test]
220    fn test_param_raw_into_param_ok() {
221        // No missing values
222        let raw = create_param_raw(1, Some(1.0), Some(0.0));
223        assert_eq!(raw.into_parameter().unwrap(), create_param(1.0, 0.0));
224
225        // Missing discount_rate
226        let raw = create_param_raw(1, None, Some(0.0));
227        assert_eq!(raw.into_parameter().unwrap(), create_param(0.0, 0.0));
228
229        // Missing capacity_to_activity
230        let raw = create_param_raw(1, Some(1.0), None);
231        assert_eq!(raw.into_parameter().unwrap(), create_param(1.0, 1.0));
232    }
233
234    #[test]
235    fn test_param_raw_validate_bad_lifetime() {
236        // lifetime = 0
237        assert!(create_param_raw(0, Some(1.0), Some(0.0))
238            .validate()
239            .is_err());
240    }
241
242    #[test]
243    fn test_param_raw_validate_bad_discount_rate() {
244        // discount rate = -1
245        assert!(create_param_raw(0, Some(-1.0), Some(0.0))
246            .validate()
247            .is_err());
248    }
249
250    #[test]
251    fn test_param_raw_validate_bad_capt2act() {
252        // capt2act = -1
253        assert!(create_param_raw(0, Some(1.0), Some(-1.0))
254            .validate()
255            .is_err());
256    }
257}