1use super::{input_err_msg, read_csv_optional};
3use crate::agent::AgentID;
4use crate::asset::{Asset, AssetRef};
5use crate::id::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::rc::Rc;
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 commission_year: u32,
27 #[serde(default)]
28 max_decommission_year: Option<u32>,
29}
30
31pub fn read_user_assets(
44 model_dir: &Path,
45 agent_ids: &IndexSet<AgentID>,
46 processes: &ProcessMap,
47 region_ids: &IndexSet<RegionID>,
48) -> Result<Vec<AssetRef>> {
49 let file_path = model_dir.join(ASSETS_FILE_NAME);
50 let assets_csv = read_csv_optional(&file_path)?;
51 read_assets_from_iter(assets_csv, agent_ids, processes, region_ids)
52 .with_context(|| input_err_msg(&file_path))
53}
54
55fn read_assets_from_iter<I>(
68 iter: I,
69 agent_ids: &IndexSet<AgentID>,
70 processes: &ProcessMap,
71 region_ids: &IndexSet<RegionID>,
72) -> Result<Vec<AssetRef>>
73where
74 I: Iterator<Item = AssetRaw>,
75{
76 iter.map(|asset| -> Result<_> {
77 let agent_id = agent_ids.get_id(&asset.agent_id)?;
78 let process = processes
79 .get(asset.process_id.as_str())
80 .with_context(|| format!("Invalid process ID: {}", &asset.process_id))?;
81 let region_id = region_ids.get_id(&asset.region_id)?;
82
83 ensure!(
85 process.years.contains(&asset.commission_year),
86 "Agent {} has asset with commission year {}, not within process {} commission years: {:?}",
87 asset.agent_id,
88 asset.commission_year,
89 asset.process_id,
90 process.years
91 );
92 ensure!(
94 process.parameters.contains_key(&(region_id.clone(), asset.commission_year)),
95 "Parameters for process {} do not contain entry for year {}, required for asset in agent {}",
96 asset.process_id,
97 asset.commission_year,
98 asset.agent_id,
99 );
100 ensure!(
101 process.flows.contains_key(&(region_id.clone(), asset.commission_year)),
102 "Flows for process {} do not contain entry for year {}, required for asset in agent {}",
103 asset.process_id,
104 asset.commission_year,
105 asset.agent_id,
106 );
107
108 if let Some(unit_size) = process.unit_size {
111 let ratio = (asset.capacity / unit_size).value();
112 if !approx_eq!(f64, ratio, ratio.ceil()) {
113 let n_units = ratio.ceil();
114 warn!(
115 "Asset capacity {} for process {} is not a multiple of unit size {}. \
116 Asset will be divided into {} units with combined capacity of {}.",
117 asset.capacity,
118 asset.process_id,
119 unit_size,
120 n_units,
121 unit_size.value() * n_units
122 );
123 }
124 }
125
126 let asset = Asset::new_future_with_max_decommission(
127 agent_id.clone(),
128 Rc::clone(process),
129 region_id.clone(),
130 asset.capacity,
131 asset.commission_year,
132 asset.max_decommission_year,
133 )?;
134 Ok(asset.into())
135 })
136 .try_collect()
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::fixture::{processes, region_ids};
143
144 use itertools::assert_equal;
145 use rstest::{fixture, rstest};
146 use std::iter;
147
148 #[fixture]
149 fn agent_ids() -> IndexSet<AgentID> {
150 IndexSet::from(["agent1".into()])
151 }
152
153 #[rstest]
154 #[case::max_decommission_year_provided(Some(2015))]
155 #[case::max_decommission_year_not_provided(None)]
156 fn read_assets_from_iter_valid(
157 #[case] max_decommission_year: Option<u32>,
158 agent_ids: IndexSet<AgentID>,
159 processes: ProcessMap,
160 region_ids: IndexSet<RegionID>,
161 ) {
162 let asset_in = AssetRaw {
163 agent_id: "agent1".into(),
164 process_id: "process1".into(),
165 region_id: "GBR".into(),
166 capacity: Capacity(1.0),
167 commission_year: 2010,
168 max_decommission_year,
169 };
170 let asset_out = Asset::new_future_with_max_decommission(
171 "agent1".into(),
172 Rc::clone(processes.values().next().unwrap()),
173 "GBR".into(),
174 Capacity(1.0),
175 2010,
176 max_decommission_year,
177 )
178 .unwrap()
179 .into();
180 assert_equal(
181 read_assets_from_iter(iter::once(asset_in), &agent_ids, &processes, ®ion_ids)
182 .unwrap(),
183 iter::once(asset_out),
184 );
185 }
186
187 #[rstest]
188 #[case(AssetRaw { agent_id: "agent1".into(),
190 process_id: "process2".into(),
191 region_id: "GBR".into(),
192 capacity: Capacity(1.0),
193 commission_year: 2010,
194 max_decommission_year: None,
195 })]
196 #[case(AssetRaw { agent_id: "agent2".into(),
198 process_id: "process1".into(),
199 region_id: "GBR".into(),
200 capacity: Capacity(1.0),
201 commission_year: 2010,
202 max_decommission_year: None,
203 })]
204 #[case(AssetRaw { agent_id: "agent1".into(),
206 process_id: "process1".into(),
207 region_id: "FRA".into(),
208 capacity: Capacity(1.0),
209 commission_year: 2010,
210 max_decommission_year: None,
211 })]
212 #[case(AssetRaw { agent_id: "agent1".into(),
214 process_id: "process1".into(),
215 region_id: "GBR".into(),
216 capacity: Capacity(1.0),
217 commission_year: 2010,
218 max_decommission_year: Some(2005),
219 })]
220 fn read_assets_from_iter_invalid(
221 #[case] asset: AssetRaw,
222 agent_ids: IndexSet<AgentID>,
223 processes: ProcessMap,
224 region_ids: IndexSet<RegionID>,
225 ) {
226 read_assets_from_iter(iter::once(asset), &agent_ids, &processes, ®ion_ids).unwrap_err();
227 }
228}