1use super::{input_err_msg, read_csv_optional};
3use crate::agent::AgentID;
4use crate::asset::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::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<UserAsset>> {
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<UserAsset>>
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_id, process) = processes
79 .get_id_value(&asset.process_id)?;
80 let region_id = region_ids.get_id(&asset.region_id)?;
81
82 ensure!(
84 process.years.contains(&asset.commission_year),
85 "Agent {} has asset with commission year {}, not within process {} commission years: {:?}",
86 asset.agent_id,
87 asset.commission_year,
88 process_id,
89 process.years
90 );
91 ensure!(
93 process.parameters.contains_key(&(region_id.clone(), asset.commission_year)),
94 "Parameters for process {} do not contain entry for year {}, required for asset in agent {}",
95 process_id,
96 asset.commission_year,
97 asset.agent_id,
98 );
99 ensure!(
100 process.flows.contains_key(&(region_id.clone(), asset.commission_year)),
101 "Flows for process {} do not contain entry for year {}, required for asset in agent {}",
102 process_id,
103 asset.commission_year,
104 asset.agent_id,
105 );
106
107 if let Some(unit_size) = process.unit_size {
110 let ratio = (asset.capacity / unit_size).value();
111 if !approx_eq!(f64, ratio, ratio.ceil()) {
112 let n_units = ratio.ceil();
113 warn!(
114 "Asset capacity {} for process {} is not a multiple of unit size {}. \
115 Asset will be divided into {} units with combined capacity of {}.",
116 asset.capacity,
117 process_id,
118 unit_size,
119 n_units,
120 unit_size.value() * n_units
121 );
122 }
123 }
124
125 UserAsset::new(
126 agent_id.clone(),
127 Rc::clone(process),
128 region_id.clone(),
129 asset.capacity,
130 asset.commission_year,
131 asset.max_decommission_year,
132 )
133 })
134 .try_collect()
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use crate::fixture::{processes, region_ids};
141
142 use itertools::assert_equal;
143 use rstest::{fixture, rstest};
144 use std::iter;
145
146 #[fixture]
147 fn agent_ids() -> IndexSet<AgentID> {
148 IndexSet::from(["agent1".into()])
149 }
150
151 #[rstest]
152 #[case::max_decommission_year_provided(Some(2015))]
153 #[case::max_decommission_year_not_provided(None)]
154 fn read_assets_from_iter_valid(
155 #[case] max_decommission_year: Option<u32>,
156 agent_ids: IndexSet<AgentID>,
157 processes: ProcessMap,
158 region_ids: IndexSet<RegionID>,
159 ) {
160 let asset_in = AssetRaw {
161 agent_id: "agent1".into(),
162 process_id: "process1".into(),
163 region_id: "GBR".into(),
164 capacity: Capacity(1.0),
165 commission_year: 2010,
166 max_decommission_year,
167 };
168 let asset_out = UserAsset::new(
169 "agent1".into(),
170 Rc::clone(processes.values().next().unwrap()),
171 "GBR".into(),
172 Capacity(1.0),
173 2010,
174 max_decommission_year,
175 )
176 .unwrap();
177 assert_equal(
178 read_assets_from_iter(iter::once(asset_in), &agent_ids, &processes, ®ion_ids)
179 .unwrap(),
180 iter::once(asset_out),
181 );
182 }
183
184 #[rstest]
185 #[case(AssetRaw { agent_id: "agent1".into(),
187 process_id: "process2".into(),
188 region_id: "GBR".into(),
189 capacity: Capacity(1.0),
190 commission_year: 2010,
191 max_decommission_year: None,
192 })]
193 #[case(AssetRaw { agent_id: "agent2".into(),
195 process_id: "process1".into(),
196 region_id: "GBR".into(),
197 capacity: Capacity(1.0),
198 commission_year: 2010,
199 max_decommission_year: None,
200 })]
201 #[case(AssetRaw { agent_id: "agent1".into(),
203 process_id: "process1".into(),
204 region_id: "FRA".into(),
205 capacity: Capacity(1.0),
206 commission_year: 2010,
207 max_decommission_year: None,
208 })]
209 #[case(AssetRaw { agent_id: "agent1".into(),
211 process_id: "process1".into(),
212 region_id: "GBR".into(),
213 capacity: Capacity(1.0),
214 commission_year: 2010,
215 max_decommission_year: Some(2005),
216 })]
217 #[case(AssetRaw { agent_id: "agent1".into(),
219 process_id: "process1".into(),
220 region_id: "GBR".into(),
221 capacity: Capacity(1.0),
222 commission_year: 2010,
223 max_decommission_year: Some(2010),
224 })]
225 fn read_assets_from_iter_invalid(
226 #[case] asset: AssetRaw,
227 agent_ids: IndexSet<AgentID>,
228 processes: ProcessMap,
229 region_ids: IndexSet<RegionID>,
230 ) {
231 read_assets_from_iter(iter::once(asset), &agent_ids, &processes, ®ion_ids).unwrap_err();
232 }
233}