muse2/input/
asset.rs

1//! Code for reading [Asset]s from a CSV file.
2use super::{input_err_msg, read_csv_optional};
3use crate::agent::AgentID;
4use crate::asset::Asset;
5use crate::id::IDCollection;
6use crate::process::ProcessMap;
7use crate::region::RegionID;
8use crate::units::Capacity;
9use anyhow::{Context, Result, ensure};
10use indexmap::IndexSet;
11use itertools::Itertools;
12use serde::Deserialize;
13use std::path::Path;
14use std::rc::Rc;
15
16const ASSETS_FILE_NAME: &str = "assets.csv";
17
18#[derive(Default, Deserialize, PartialEq)]
19struct AssetRaw {
20    process_id: String,
21    region_id: String,
22    agent_id: String,
23    capacity: Capacity,
24    commission_year: u32,
25    #[serde(default)]
26    max_decommission_year: Option<u32>,
27}
28
29/// Read assets CSV file from model directory.
30///
31/// # Arguments
32///
33/// * `model_dir` - Folder containing model configuration files
34/// * `agent_ids` - All possible process IDs
35/// * `processes` - The model's processes
36/// * `region_ids` - All possible region IDs
37///
38/// # Returns
39///
40/// A `Vec` of [`Asset`]s or an error.
41pub fn read_assets(
42    model_dir: &Path,
43    agent_ids: &IndexSet<AgentID>,
44    processes: &ProcessMap,
45    region_ids: &IndexSet<RegionID>,
46) -> Result<Vec<Asset>> {
47    let file_path = model_dir.join(ASSETS_FILE_NAME);
48    let assets_csv = read_csv_optional(&file_path)?;
49    read_assets_from_iter(assets_csv, agent_ids, processes, region_ids)
50        .with_context(|| input_err_msg(&file_path))
51}
52
53/// Process assets from an iterator.
54///
55/// # Arguments
56///
57/// * `iter` - Iterator of `AssetRaw`s
58/// * `agent_ids` - All possible process IDs
59/// * `processes` - The model's processes
60/// * `region_ids` - All possible region IDs
61///
62/// # Returns
63///
64/// A [`Vec`] of [`Asset`]s or an error.
65fn read_assets_from_iter<I>(
66    iter: I,
67    agent_ids: &IndexSet<AgentID>,
68    processes: &ProcessMap,
69    region_ids: &IndexSet<RegionID>,
70) -> Result<Vec<Asset>>
71where
72    I: Iterator<Item = AssetRaw>,
73{
74    iter.map(|asset| -> Result<_> {
75        let agent_id = agent_ids.get_id(&asset.agent_id)?;
76        let process = processes
77            .get(asset.process_id.as_str())
78            .with_context(|| format!("Invalid process ID: {}", &asset.process_id))?;
79        let region_id = region_ids.get_id(&asset.region_id)?;
80
81        // Validate commission year. It should be within the process valid range...
82        ensure!(
83            process.years.contains(&asset.commission_year),
84            "Agent {} has asset with commission year {}, not within process {} commission years: {:?}",
85            asset.agent_id,
86            asset.commission_year,
87            asset.process_id,
88            process.years
89        );
90        // ... and also have associated process parameters and flows
91        ensure!(
92            process.parameters.contains_key(&(region_id.clone(), asset.commission_year)),
93            "Parameters for process {} do not contain entry for year {}, required for asset in agent {}",
94            asset.process_id,
95            asset.commission_year,
96            asset.agent_id,
97        );
98        ensure!(
99            process.flows.contains_key(&(region_id.clone(), asset.commission_year)),
100            "Flows for process {} do not contain entry for year {}, required for asset in agent {}",
101            asset.process_id,
102            asset.commission_year,
103            asset.agent_id,
104        );
105
106        Asset::new_future_with_max_decommission(
107            agent_id.clone(),
108            Rc::clone(process),
109            region_id.clone(),
110            asset.capacity,
111            asset.commission_year,
112            asset.max_decommission_year,
113        )
114    })
115    .try_collect()
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::fixture::{processes, region_ids};
122
123    use itertools::assert_equal;
124    use rstest::{fixture, rstest};
125    use std::iter;
126
127    #[fixture]
128    fn agent_ids() -> IndexSet<AgentID> {
129        IndexSet::from(["agent1".into()])
130    }
131
132    #[rstest]
133    #[case::max_decommission_year_provided(Some(2015))]
134    #[case::max_decommission_year_not_provided(None)]
135    fn test_read_assets_from_iter_valid(
136        #[case] max_decommission_year: Option<u32>,
137        agent_ids: IndexSet<AgentID>,
138        processes: ProcessMap,
139        region_ids: IndexSet<RegionID>,
140    ) {
141        let asset_in = AssetRaw {
142            agent_id: "agent1".into(),
143            process_id: "process1".into(),
144            region_id: "GBR".into(),
145            capacity: Capacity(1.0),
146            commission_year: 2010,
147            max_decommission_year: max_decommission_year,
148        };
149        let asset_out = Asset::new_future_with_max_decommission(
150            "agent1".into(),
151            Rc::clone(processes.values().next().unwrap()),
152            "GBR".into(),
153            Capacity(1.0),
154            2010,
155            max_decommission_year,
156        )
157        .unwrap();
158        assert_equal(
159            read_assets_from_iter(iter::once(asset_in), &agent_ids, &processes, &region_ids)
160                .unwrap(),
161            iter::once(asset_out),
162        );
163    }
164
165    #[rstest]
166    #[case(AssetRaw { // Bad process ID
167            agent_id: "agent1".into(),
168            process_id: "process2".into(),
169            region_id: "GBR".into(),
170            capacity: Capacity(1.0),
171            commission_year: 2010,
172            max_decommission_year: None,
173        })]
174    #[case(AssetRaw { // Bad agent ID
175            agent_id: "agent2".into(),
176            process_id: "process1".into(),
177            region_id: "GBR".into(),
178            capacity: Capacity(1.0),
179            commission_year: 2010,
180            max_decommission_year: None,
181        })]
182    #[case(AssetRaw { // Bad region ID: not in region_ids
183            agent_id: "agent1".into(),
184            process_id: "process1".into(),
185            region_id: "FRA".into(),
186            capacity: Capacity(1.0),
187            commission_year: 2010,
188            max_decommission_year: None,
189        })]
190    #[case(AssetRaw { // Bad max_decommission_year: before commission_year
191            agent_id: "agent1".into(),
192            process_id: "process1".into(),
193            region_id: "GBR".into(),
194            capacity: Capacity(1.0),
195            commission_year: 2010,
196            max_decommission_year: Some(2005),
197        })]
198    fn test_read_assets_from_iter_invalid(
199        #[case] asset: AssetRaw,
200        agent_ids: IndexSet<AgentID>,
201        processes: ProcessMap,
202        region_ids: IndexSet<RegionID>,
203    ) {
204        assert!(
205            read_assets_from_iter(iter::once(asset), &agent_ids, &processes, &region_ids).is_err()
206        );
207    }
208}