1use 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 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
100pub 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 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 let process_years = &process.years;
129 let parameter_years =
130 parse_year_str(¶m_raw.years, process_years).with_context(|| {
131 format!("Invalid year for process {id}. Valid years are {process_years:?}")
132 })?;
133
134 let process_regions = &process.regions;
136 let parameter_regions = parse_region_str(¶m_raw.regions, process_regions)
137 .with_context(|| {
138 format!("Invalid region for process {id}. Valid regions are {process_regions:?}")
139 })?;
140
141 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
156fn 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 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 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 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 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, ¶m_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 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, ¶m_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 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, ¶m_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 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 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 assert!(
335 create_param_raw(0, Some(Dimensionless(1.0)), Some(ActivityPerCapacity(-1.0)))
336 .validate()
337 .is_err()
338 );
339 }
340}