1use super::super::*;
3use crate::id::IDCollection;
4use crate::process::{Process, ProcessID, ProcessParameter, ProcessParameterMap};
5use crate::region::parse_region_str;
6use crate::year::parse_year_str;
7use ::log::warn;
8use anyhow::{ensure, Context, Result};
9use indexmap::IndexSet;
10use serde::Deserialize;
11use std::collections::{HashMap, HashSet};
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 year: String,
22 capital_cost: f64,
23 fixed_operating_cost: f64,
24 variable_operating_cost: f64,
25 lifetime: u32,
26 discount_rate: Option<f64>,
27 capacity_to_activity: Option<f64>,
28}
29
30impl ProcessParameterRaw {
31 fn into_parameter(self) -> Result<ProcessParameter> {
32 self.validate()?;
33
34 Ok(ProcessParameter {
35 capital_cost: self.capital_cost,
36 fixed_operating_cost: self.fixed_operating_cost,
37 variable_operating_cost: self.variable_operating_cost,
38 lifetime: self.lifetime,
39 discount_rate: self.discount_rate.unwrap_or(0.0),
40 capacity_to_activity: self.capacity_to_activity.unwrap_or(1.0),
41 })
42 }
43}
44
45impl ProcessParameterRaw {
46 fn validate(&self) -> Result<()> {
64 ensure!(
65 self.lifetime > 0,
66 "Error in parameter for process {}: Lifetime must be greater than 0",
67 self.process_id
68 );
69
70 if let Some(dr) = self.discount_rate {
71 ensure!(
72 dr >= 0.0,
73 "Error in parameter for process {}: Discount rate must be positive",
74 self.process_id
75 );
76
77 if dr > 1.0 {
78 warn!(
79 "Warning in parameter for process {}: Discount rate is greater than 1",
80 self.process_id
81 );
82 }
83 }
84
85 if let Some(c2a) = self.capacity_to_activity {
86 ensure!(
87 c2a >= 0.0,
88 "Error in parameter for process {}: Cap2act must be positive",
89 self.process_id
90 );
91 }
92
93 Ok(())
94 }
95}
96
97pub fn read_process_parameters(
99 model_dir: &Path,
100 process_ids: &IndexSet<ProcessID>,
101 processes: &HashMap<ProcessID, Process>,
102 milestone_years: &[u32],
103) -> Result<HashMap<ProcessID, ProcessParameterMap>> {
104 let file_path = model_dir.join(PROCESS_PARAMETERS_FILE_NAME);
105 let iter = read_csv::<ProcessParameterRaw>(&file_path)?;
106 read_process_parameters_from_iter(iter, process_ids, processes, milestone_years)
107 .with_context(|| input_err_msg(&file_path))
108}
109
110fn read_process_parameters_from_iter<I>(
111 iter: I,
112 process_ids: &IndexSet<ProcessID>,
113 processes: &HashMap<ProcessID, Process>,
114 milestone_years: &[u32],
115) -> Result<HashMap<ProcessID, ProcessParameterMap>>
116where
117 I: Iterator<Item = ProcessParameterRaw>,
118{
119 let mut map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
120 for param_raw in iter {
121 let id = process_ids.get_id_by_str(¶m_raw.process_id)?;
123 let process = processes
124 .get(&id)
125 .with_context(|| format!("Process {id} not found"))?;
126
127 let process_year_range = &process.years;
129 let process_years: Vec<u32> = milestone_years
130 .iter()
131 .copied()
132 .filter(|year| process_year_range.contains(year))
133 .collect();
134 let parameter_years =
135 parse_year_str(¶m_raw.year, &process_years).with_context(|| {
136 format!("Invalid year for process {id}. Valid years are {process_years:?}")
137 })?;
138
139 let process_regions = process.regions.clone();
141 let parameter_regions = parse_region_str(¶m_raw.regions, &process_regions)
142 .with_context(|| {
143 format!("Invalid region for process {id}. Valid regions are {process_regions:?}")
144 })?;
145
146 let param = Rc::new(param_raw.into_parameter()?);
148 let entry = map.entry(id.clone()).or_default();
149 for year in parameter_years {
150 for region in parameter_regions.clone() {
151 try_insert(entry, (region, year), param.clone())?;
152 }
153 }
154 }
155
156 for (id, parameters) in map.iter() {
158 let process = processes.get(id).unwrap();
159 let year_range = &process.years;
160 let reference_years: HashSet<u32> = milestone_years
161 .iter()
162 .copied()
163 .filter(|year| year_range.contains(year))
164 .collect();
165 let reference_regions = process.regions.clone();
166
167 let mut missing_keys = Vec::new();
168 for year in &reference_years {
169 for region in &reference_regions {
170 let key = (region.clone(), *year);
171 if !parameters.contains_key(&key) {
172 missing_keys.push(key);
173 }
174 }
175 }
176 ensure!(
177 missing_keys.is_empty(),
178 "Process {} is missing parameters for the following regions and years: {:?}",
179 id,
180 missing_keys
181 );
182 }
183 Ok(map)
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 fn create_param_raw(
191 lifetime: u32,
192 discount_rate: Option<f64>,
193 capacity_to_activity: Option<f64>,
194 ) -> ProcessParameterRaw {
195 ProcessParameterRaw {
196 process_id: "id".to_string(),
197 capital_cost: 0.0,
198 fixed_operating_cost: 0.0,
199 variable_operating_cost: 0.0,
200 lifetime,
201 discount_rate,
202 capacity_to_activity,
203 year: "all".to_string(),
204 regions: "all".to_string(),
205 }
206 }
207
208 fn create_param(discount_rate: f64, capacity_to_activity: f64) -> ProcessParameter {
209 ProcessParameter {
210 capital_cost: 0.0,
211 fixed_operating_cost: 0.0,
212 variable_operating_cost: 0.0,
213 lifetime: 1,
214 discount_rate,
215 capacity_to_activity,
216 }
217 }
218
219 #[test]
220 fn test_param_raw_into_param_ok() {
221 let raw = create_param_raw(1, Some(1.0), Some(0.0));
223 assert_eq!(raw.into_parameter().unwrap(), create_param(1.0, 0.0));
224
225 let raw = create_param_raw(1, None, Some(0.0));
227 assert_eq!(raw.into_parameter().unwrap(), create_param(0.0, 0.0));
228
229 let raw = create_param_raw(1, Some(1.0), None);
231 assert_eq!(raw.into_parameter().unwrap(), create_param(1.0, 1.0));
232 }
233
234 #[test]
235 fn test_param_raw_validate_bad_lifetime() {
236 assert!(create_param_raw(0, Some(1.0), Some(0.0))
238 .validate()
239 .is_err());
240 }
241
242 #[test]
243 fn test_param_raw_validate_bad_discount_rate() {
244 assert!(create_param_raw(0, Some(-1.0), Some(0.0))
246 .validate()
247 .is_err());
248 }
249
250 #[test]
251 fn test_param_raw_validate_bad_capt2act() {
252 assert!(create_param_raw(0, Some(1.0), Some(-1.0))
254 .validate()
255 .is_err());
256 }
257}