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::{ensure, Context, Result};
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) -> Result<HashMap<ProcessID, ProcessParameterMap>> {
105 let file_path = model_dir.join(PROCESS_PARAMETERS_FILE_NAME);
106 let iter = read_csv::<ProcessParameterRaw>(&file_path)?;
107 read_process_parameters_from_iter(iter, processes).with_context(|| input_err_msg(&file_path))
108}
109
110fn read_process_parameters_from_iter<I>(
111 iter: I,
112 processes: &ProcessMap,
113) -> Result<HashMap<ProcessID, ProcessParameterMap>>
114where
115 I: Iterator<Item = ProcessParameterRaw>,
116{
117 let mut map: HashMap<ProcessID, ProcessParameterMap> = HashMap::new();
118 for param_raw in iter {
119 let (id, process) = processes
121 .get_key_value(param_raw.process_id.as_str())
122 .with_context(|| format!("Process {} not found", param_raw.process_id))?;
123
124 let process_years = &process.years;
126 let parameter_years =
127 parse_year_str(¶m_raw.years, process_years).with_context(|| {
128 format!("Invalid year for process {id}. Valid years are {process_years:?}")
129 })?;
130
131 let process_regions = &process.regions;
133 let parameter_regions = parse_region_str(¶m_raw.regions, process_regions)
134 .with_context(|| {
135 format!("Invalid region for process {id}. Valid regions are {process_regions:?}")
136 })?;
137
138 let param = Rc::new(param_raw.into_parameter()?);
140 let entry = map.entry(id.clone()).or_default();
141 for year in parameter_years {
142 for region in parameter_regions.clone() {
143 try_insert(entry, (region, year), param.clone())?;
144 }
145 }
146 }
147
148 for (id, parameters) in map.iter() {
150 let process = processes.get(id).unwrap();
151 let reference_years = &process.years;
152 let reference_regions = &process.regions;
153
154 let mut missing_keys = Vec::new();
155 for year in reference_years {
156 for region in reference_regions {
157 let key = (region.clone(), *year);
158 if !parameters.contains_key(&key) {
159 missing_keys.push(key);
160 }
161 }
162 }
163 ensure!(
164 missing_keys.is_empty(),
165 "Process {} is missing parameters for the following regions and years: {:?}",
166 id,
167 missing_keys
168 );
169 }
170 Ok(map)
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 fn create_param_raw(
178 lifetime: u32,
179 discount_rate: Option<Dimensionless>,
180 capacity_to_activity: Option<ActivityPerCapacity>,
181 ) -> ProcessParameterRaw {
182 ProcessParameterRaw {
183 process_id: "id".to_string(),
184 capital_cost: MoneyPerCapacity(0.0),
185 fixed_operating_cost: MoneyPerCapacityPerYear(0.0),
186 variable_operating_cost: MoneyPerActivity(0.0),
187 lifetime,
188 discount_rate,
189 capacity_to_activity,
190 years: "all".to_string(),
191 regions: "all".to_string(),
192 }
193 }
194
195 fn create_param(
196 discount_rate: Dimensionless,
197 capacity_to_activity: ActivityPerCapacity,
198 ) -> ProcessParameter {
199 ProcessParameter {
200 capital_cost: MoneyPerCapacity(0.0),
201 fixed_operating_cost: MoneyPerCapacityPerYear(0.0),
202 variable_operating_cost: MoneyPerActivity(0.0),
203 lifetime: 1,
204 discount_rate,
205 capacity_to_activity,
206 }
207 }
208
209 #[test]
210 fn test_param_raw_into_param_ok() {
211 let raw = create_param_raw(1, Some(Dimensionless(1.0)), Some(ActivityPerCapacity(0.0)));
213 assert_eq!(
214 raw.into_parameter().unwrap(),
215 create_param(Dimensionless(1.0), ActivityPerCapacity(0.0))
216 );
217
218 let raw = create_param_raw(1, None, Some(ActivityPerCapacity(0.0)));
220 assert_eq!(
221 raw.into_parameter().unwrap(),
222 create_param(Dimensionless(0.0), ActivityPerCapacity(0.0))
223 );
224
225 let raw = create_param_raw(1, Some(Dimensionless(1.0)), None);
227 assert_eq!(
228 raw.into_parameter().unwrap(),
229 create_param(Dimensionless(1.0), ActivityPerCapacity(1.0))
230 );
231 }
232
233 #[test]
234 fn test_param_raw_validate_bad_lifetime() {
235 assert!(
237 create_param_raw(0, Some(Dimensionless(1.0)), Some(ActivityPerCapacity(0.0)))
238 .validate()
239 .is_err()
240 );
241 }
242
243 #[test]
244 fn test_param_raw_validate_bad_discount_rate() {
245 assert!(
247 create_param_raw(0, Some(Dimensionless(-1.0)), Some(ActivityPerCapacity(0.0)))
248 .validate()
249 .is_err()
250 );
251 }
252
253 #[test]
254 fn test_param_raw_validate_bad_capt2act() {
255 assert!(
257 create_param_raw(0, Some(Dimensionless(1.0)), Some(ActivityPerCapacity(-1.0)))
258 .validate()
259 .is_err()
260 );
261 }
262}