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