1use super::{input_err_msg, read_csv_optional};
3use crate::agent::AgentID;
4use crate::asset::{AssetCapacity, UserAsset};
5use crate::id::{GetIDValue, IDCollection};
6use crate::process::ProcessMap;
7use crate::region::RegionID;
8use crate::units::Capacity;
9use anyhow::{Context, Result, ensure};
10use float_cmp::approx_eq;
11use indexmap::IndexSet;
12use itertools::Itertools;
13use log::warn;
14use serde::Deserialize;
15use std::path::Path;
16use std::sync::Arc;
17
18const ASSETS_FILE_NAME: &str = "assets.csv";
19
20#[derive(Deserialize, PartialEq)]
21struct AssetRaw {
22 process_id: String,
23 region_id: String,
24 agent_id: String,
25 capacity: Capacity,
26 #[serde(default)]
27 num_tranches: Option<u32>,
28 commission_year: u32,
29 #[serde(default)]
30 max_decommission_year: Option<u32>,
31}
32
33pub fn read_user_assets(
46 model_dir: &Path,
47 agent_ids: &IndexSet<AgentID>,
48 processes: &ProcessMap,
49 region_ids: &IndexSet<RegionID>,
50) -> Result<Vec<UserAsset>> {
51 let file_path = model_dir.join(ASSETS_FILE_NAME);
52 let assets_csv = read_csv_optional(&file_path)?;
53 read_assets_from_iter(assets_csv, agent_ids, processes, region_ids)
54 .with_context(|| input_err_msg(&file_path))
55}
56
57fn read_assets_from_iter<I>(
70 iter: I,
71 agent_ids: &IndexSet<AgentID>,
72 processes: &ProcessMap,
73 region_ids: &IndexSet<RegionID>,
74) -> Result<Vec<UserAsset>>
75where
76 I: Iterator<Item = AssetRaw>,
77{
78 iter.map(|asset| -> Result<_> {
79 let agent_id = agent_ids.get_id(&asset.agent_id)?;
80 let (process_id, process) = processes
81 .get_id_value(&asset.process_id)?;
82 let region_id = region_ids.get_id(&asset.region_id)?;
83
84 ensure!(
86 process.years.contains(&asset.commission_year),
87 "Agent {} has asset with commission year {}, not within process {} commission years: {:?}",
88 asset.agent_id,
89 asset.commission_year,
90 process_id,
91 process.years
92 );
93 ensure!(
95 process.parameters.contains_key(&(region_id.clone(), asset.commission_year)),
96 "Parameters for process {} do not contain entry for year {}, required for asset in agent {}",
97 process_id,
98 asset.commission_year,
99 asset.agent_id,
100 );
101 ensure!(
102 process.flows.contains_key(&(region_id.clone(), asset.commission_year)),
103 "Flows for process {} do not contain entry for year {}, required for asset in agent {}",
104 process_id,
105 asset.commission_year,
106 asset.agent_id,
107 );
108
109 ensure!(
111 asset.capacity > Capacity(0.0),
112 "Asset capacity must be positive"
113 );
114 let asset_capacity = if let Some(num_tranches) = asset.num_tranches {
115 ensure!(num_tranches > 0, "num_tranches must be positive");
117
118 let tranche_size = Capacity(asset.capacity.value() / num_tranches as f64);
119 AssetCapacity::new(num_tranches, tranche_size)
120 } else if let Some(tranche_size) = process.tranche_size {
121 let ratio = (asset.capacity / tranche_size).value();
124 let num_tranches = ratio.ceil();
125
126 if !approx_eq!(f64, ratio, ratio.ceil()) {
128 warn!(
129 "Asset capacity {} for process {} is not a multiple of tranche size {}. \
130 Asset will be divided into {} tranches with combined capacity of {}.",
131 asset.capacity,
132 process_id,
133 tranche_size,
134 num_tranches,
135 tranche_size.value() * num_tranches
136 );
137 }
138
139 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
140 AssetCapacity::new(num_tranches as u32, tranche_size)
141 } else {
142 AssetCapacity::single(asset.capacity)
145 };
146
147 UserAsset::new(
148 agent_id.clone(),
149 Arc::clone(process),
150 region_id.clone(),
151 asset_capacity,
152 asset.commission_year,
153 asset.max_decommission_year,
154 )
155 })
156 .try_collect()
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use crate::fixture::{processes, region_ids};
163
164 use itertools::assert_equal;
165 use rstest::{fixture, rstest};
166 use std::iter;
167
168 #[fixture]
169 fn agent_ids() -> IndexSet<AgentID> {
170 IndexSet::from(["agent1".into()])
171 }
172
173 #[rstest]
174 #[case::max_decommission_year_provided(Some(2015))]
175 #[case::max_decommission_year_not_provided(None)]
176 fn read_assets_from_iter_valid(
177 #[case] max_decommission_year: Option<u32>,
178 agent_ids: IndexSet<AgentID>,
179 processes: ProcessMap,
180 region_ids: IndexSet<RegionID>,
181 ) {
182 let asset_in = AssetRaw {
183 agent_id: "agent1".into(),
184 process_id: "process1".into(),
185 region_id: "GBR".into(),
186 capacity: Capacity(1.0),
187 num_tranches: Some(1),
188 commission_year: 2010,
189 max_decommission_year,
190 };
191 let asset_out = UserAsset::new(
192 "agent1".into(),
193 Arc::clone(processes.values().next().unwrap()),
194 "GBR".into(),
195 AssetCapacity::single(Capacity(1.0)),
196 2010,
197 max_decommission_year,
198 )
199 .unwrap();
200 assert_equal(
201 read_assets_from_iter(iter::once(asset_in), &agent_ids, &processes, ®ion_ids)
202 .unwrap(),
203 iter::once(asset_out),
204 );
205 }
206
207 #[rstest]
208 fn explicit_tranche_count_sets_tranche_size(
209 agent_ids: IndexSet<AgentID>,
210 processes: ProcessMap,
211 region_ids: IndexSet<RegionID>,
212 ) {
213 let asset = AssetRaw {
214 process_id: "process1".into(),
215 region_id: "GBR".into(),
216 agent_id: "agent1".into(),
217 capacity: Capacity(6.0),
218 num_tranches: Some(3),
219 commission_year: 2010,
220 max_decommission_year: None,
221 };
222
223 let assets =
224 read_assets_from_iter(iter::once(asset), &agent_ids, &processes, ®ion_ids).unwrap();
225
226 assert_eq!(assets[0].capacity(), AssetCapacity::new(3, Capacity(2.0)));
227 }
228
229 #[rstest]
230 fn missing_tranche_count_uses_process_tranche_size(
231 agent_ids: IndexSet<AgentID>,
232 mut processes: ProcessMap,
233 region_ids: IndexSet<RegionID>,
234 ) {
235 Arc::get_mut(processes.get_mut("process1").unwrap())
236 .unwrap()
237 .tranche_size = Some(Capacity(4.0));
238 let asset = AssetRaw {
239 process_id: "process1".into(),
240 region_id: "GBR".into(),
241 agent_id: "agent1".into(),
242 capacity: Capacity(9.0),
243 num_tranches: None,
244 commission_year: 2010,
245 max_decommission_year: None,
246 };
247
248 let assets =
249 read_assets_from_iter(iter::once(asset), &agent_ids, &processes, ®ion_ids).unwrap();
250
251 assert_eq!(assets[0].capacity(), AssetCapacity::new(3, Capacity(4.0)));
252 }
253
254 #[rstest]
255 #[case(AssetRaw { agent_id: "agent1".into(),
257 process_id: "process2".into(),
258 region_id: "GBR".into(),
259 capacity: Capacity(1.0),
260 num_tranches: None,
261 commission_year: 2010,
262 max_decommission_year: None,
263 })]
264 #[case(AssetRaw { agent_id: "agent2".into(),
266 process_id: "process1".into(),
267 region_id: "GBR".into(),
268 capacity: Capacity(1.0),
269 num_tranches: None,
270 commission_year: 2010,
271 max_decommission_year: None,
272 })]
273 #[case(AssetRaw { agent_id: "agent1".into(),
275 process_id: "process1".into(),
276 region_id: "FRA".into(),
277 capacity: Capacity(1.0),
278 num_tranches: None,
279 commission_year: 2010,
280 max_decommission_year: None,
281 })]
282 #[case(AssetRaw { agent_id: "agent1".into(),
284 process_id: "process1".into(),
285 region_id: "GBR".into(),
286 capacity: Capacity(1.0),
287 num_tranches: None,
288 commission_year: 2010,
289 max_decommission_year: Some(2005),
290 })]
291 #[case(AssetRaw { agent_id: "agent1".into(),
293 process_id: "process1".into(),
294 region_id: "GBR".into(),
295 capacity: Capacity(1.0),
296 num_tranches: None,
297 commission_year: 2010,
298 max_decommission_year: Some(2010),
299 })]
300 fn read_assets_from_iter_invalid(
301 #[case] asset: AssetRaw,
302 agent_ids: IndexSet<AgentID>,
303 processes: ProcessMap,
304 region_ids: IndexSet<RegionID>,
305 ) {
306 read_assets_from_iter(iter::once(asset), &agent_ids, &processes, ®ion_ids).unwrap_err();
307 }
308}