Skip to main content

muse2/input/
asset.rs

1//! Code for reading user assets from a CSV file.
2use 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
33/// Read assets CSV file from model directory.
34///
35/// # Arguments
36///
37/// * `model_dir` - Folder containing model configuration files
38/// * `agent_ids` - All possible agent IDs
39/// * `processes` - The model's processes
40/// * `region_ids` - All possible region IDs
41///
42/// # Returns
43///
44/// A `Vec` of [`UserAsset`]s or an error.
45pub 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
57/// Process assets from an iterator.
58///
59/// # Arguments
60///
61/// * `iter` - Iterator of `AssetRaw`s
62/// * `agent_ids` - All possible agent IDs
63/// * `processes` - The model's processes
64/// * `region_ids` - All possible region IDs
65///
66/// # Returns
67///
68/// A [`Vec`] of [`UserAsset`]s or an error.
69fn 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        // Validate commission year. It should be within the process valid range...
85        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        // ... and also have associated process parameters and flows
94        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        // Split overall capacity into tranches
110        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            // A provided tranche count takes precedence over the process tranche_size.
116            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            // No tranche count was provided, so use the process tranche_size to determine
122            // how many tranches are needed to cover the asset's capacity.
123            let ratio = (asset.capacity / tranche_size).value();
124            let num_tranches = ratio.ceil();
125
126            // Rounding up can increase the combined capacity of the resulting tranches.
127            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            // Without a process tranche_size, lack of num_tranches implies the asset is indivisible
143            // (consists of a single tranche).
144            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, &region_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, &region_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, &region_ids).unwrap();
250
251        assert_eq!(assets[0].capacity(), AssetCapacity::new(3, Capacity(4.0)));
252    }
253
254    #[rstest]
255    #[case(AssetRaw { // Bad process ID
256            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 { // Bad agent ID
265            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 { // Bad region ID: not in region_ids
274            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 { // Bad max_decommission_year: before commission_year
283            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 { // Bad max_decommission_year: equal to commission_year
292            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, &region_ids).unwrap_err();
307    }
308}