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