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