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 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 base_year: 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, base_year)
94 .with_context(|| input_err_msg(&file_path))
95}
96
97fn read_process_parameters_from_iter<I>(
98 iter: I,
99 processes: &ProcessMap,
100 base_year: 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 = &process.years;
114 let parameter_years =
115 parse_year_str(¶m_raw.years, process_years).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, base_year)?;
137
138 Ok(map)
139}
140
141fn check_process_parameters(
143 processes: &ProcessMap,
144 map: &HashMap<ProcessID, ProcessParameterMap>,
145 base_year: 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_years = &process.years;
153 let reference_regions = &process.regions;
154
155 let mut missing_keys = Vec::new();
158 for year in reference_years.iter().filter(|year| **year >= base_year) {
159 for region in reference_regions {
160 let key = (region.clone(), *year);
161 if !parameters.contains_key(&key) {
162 missing_keys.push(key);
163 }
164 }
165 }
166 ensure!(
167 missing_keys.is_empty(),
168 "Process {process_id} is missing parameters for the following regions and years: {}",
169 format_items_with_cap(&missing_keys)
170 );
171 }
172
173 Ok(())
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::fixture::{assert_error, process_parameter_map, processes, region_id};
180 use crate::process::{ProcessID, ProcessMap, ProcessParameterMap};
181 use crate::region::RegionID;
182 use rstest::rstest;
183 use std::collections::HashMap;
184
185 fn create_param_raw(
186 lifetime: u32,
187 discount_rate: Option<Dimensionless>,
188 ) -> ProcessParameterRaw {
189 ProcessParameterRaw {
190 process_id: "id".to_string(),
191 capital_cost: MoneyPerCapacity(0.0),
192 fixed_operating_cost: MoneyPerCapacityPerYear(0.0),
193 variable_operating_cost: MoneyPerActivity(0.0),
194 lifetime,
195 discount_rate,
196 years: "all".to_string(),
197 regions: "all".to_string(),
198 }
199 }
200
201 fn create_param(discount_rate: Dimensionless) -> ProcessParameter {
202 ProcessParameter {
203 capital_cost: MoneyPerCapacity(0.0),
204 fixed_operating_cost: MoneyPerCapacityPerYear(0.0),
205 variable_operating_cost: MoneyPerActivity(0.0),
206 lifetime: 1,
207 discount_rate,
208 }
209 }
210
211 #[test]
212 fn test_param_raw_into_param_ok() {
213 let raw = create_param_raw(1, Some(Dimensionless(1.0)));
215 assert_eq!(
216 raw.into_parameter().unwrap(),
217 create_param(Dimensionless(1.0))
218 );
219
220 let raw = create_param_raw(1, None);
222 assert_eq!(
223 raw.into_parameter().unwrap(),
224 create_param(Dimensionless(0.0))
225 );
226 }
227
228 #[rstest]
229 fn check_process_parameters_ok(
230 processes: ProcessMap,
231 process_parameter_map: ProcessParameterMap,
232 ) {
233 let mut param_map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
234 let process_id = processes.keys().next().unwrap().clone();
235 let base_year = 2010;
236
237 param_map.insert(process_id, process_parameter_map.clone());
238 let result = check_process_parameters(&processes, ¶m_map, base_year);
239 assert!(result.is_ok());
240 }
241
242 #[rstest]
243 fn check_process_parameters_ok_missing_before_base_year(
244 processes: ProcessMap,
245 mut process_parameter_map: ProcessParameterMap,
246 region_id: RegionID,
247 ) {
248 let mut param_map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
249 let process_id = processes.keys().next().unwrap().clone();
250 let base_year = 2015;
251
252 process_parameter_map.remove(&(region_id, 2012)).unwrap();
254 param_map.insert(process_id, process_parameter_map);
255
256 let result = check_process_parameters(&processes, ¶m_map, base_year);
257 assert!(result.is_ok());
258 }
259
260 #[rstest]
261 fn check_process_parameters_missing(
262 processes: ProcessMap,
263 mut process_parameter_map: ProcessParameterMap,
264 region_id: RegionID,
265 ) {
266 let mut param_map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
267 let process_id = processes.keys().next().unwrap().clone();
268 let base_year = 2010;
269
270 process_parameter_map.remove(&(region_id, 2010)).unwrap();
272 param_map.insert(process_id, process_parameter_map);
273
274 let result = check_process_parameters(&processes, ¶m_map, base_year);
275 assert_error!(
276 result,
277 "Process process1 is missing parameters for the following regions and years: \
278 [(RegionID(\"GBR\"), 2010)]"
279 );
280 }
281
282 #[test]
283 fn test_param_raw_validate_bad_lifetime() {
284 assert!(
286 create_param_raw(0, Some(Dimensionless(1.0)))
287 .validate()
288 .is_err()
289 );
290 }
291
292 #[test]
293 fn test_param_raw_validate_bad_discount_rate() {
294 assert!(
296 create_param_raw(1, Some(Dimensionless(-1.0)))
297 .validate()
298 .is_err()
299 );
300 }
301}