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::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
31/// Read assets CSV file from model directory.
32///
33/// # Arguments
34///
35/// * `model_dir` - Folder containing model configuration files
36/// * `agent_ids` - All possible agent IDs
37/// * `processes` - The model's processes
38/// * `region_ids` - All possible region IDs
39///
40/// # Returns
41///
42/// A `Vec` of [`UserAsset`]s or an error.
43pub 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
55/// Process assets from an iterator.
56///
57/// # Arguments
58///
59/// * `iter` - Iterator of `AssetRaw`s
60/// * `agent_ids` - All possible agent IDs
61/// * `processes` - The model's processes
62/// * `region_ids` - All possible region IDs
63///
64/// # Returns
65///
66/// A [`Vec`] of [`UserAsset`]s or an error.
67fn 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        // Validate commission year. It should be within the process valid range...
83        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        // ... and also have associated process parameters and flows
92        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        // Check that capacity is approximately a multiple of the process unit size
108        // If not, raise a warning
109        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, &region_ids)
179                .unwrap(),
180            iter::once(asset_out),
181        );
182    }
183
184    #[rstest]
185    #[case(AssetRaw { // Bad process ID
186            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 { // Bad agent ID
194            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 { // Bad region ID: not in region_ids
202            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 { // Bad max_decommission_year: before commission_year
210            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 { // Bad max_decommission_year: equal to commission_year
218            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, &region_ids).unwrap_err();
232    }
233}