muse2/input/
asset.rs

1//! Code for reading [Asset]s from a CSV file.
2use super::*;
3use crate::agent::AgentID;
4use crate::asset::Asset;
5use crate::id::IDCollection;
6use crate::process::ProcessMap;
7use crate::region::RegionID;
8use anyhow::{Context, Result};
9use itertools::Itertools;
10use serde::Deserialize;
11use std::collections::HashSet;
12use std::path::Path;
13use std::rc::Rc;
14
15const ASSETS_FILE_NAME: &str = "assets.csv";
16
17#[derive(Deserialize, PartialEq)]
18struct AssetRaw {
19    process_id: String,
20    region_id: String,
21    agent_id: String,
22    capacity: f64,
23    commission_year: u32,
24}
25
26/// Read assets CSV file from model directory.
27///
28/// # Arguments
29///
30/// * `model_dir` - Folder containing model configuration files
31/// * `agent_ids` - All possible process IDs
32/// * `processes` - The model's processes
33/// * `region_ids` - All possible region IDs
34///
35/// # Returns
36///
37/// A `HashMap` containing assets grouped by agent ID.
38pub fn read_assets(
39    model_dir: &Path,
40    agent_ids: &HashSet<AgentID>,
41    processes: &ProcessMap,
42    region_ids: &HashSet<RegionID>,
43) -> Result<Vec<Asset>> {
44    let file_path = model_dir.join(ASSETS_FILE_NAME);
45    let assets_csv = read_csv(&file_path)?;
46    read_assets_from_iter(assets_csv, agent_ids, processes, region_ids)
47        .with_context(|| input_err_msg(&file_path))
48}
49
50/// Process assets from an iterator.
51///
52/// # Arguments
53///
54/// * `iter` - Iterator of `AssetRaw`s
55/// * `agent_ids` - All possible process IDs
56/// * `processes` - The model's processes
57/// * `region_ids` - All possible region IDs
58///
59/// # Returns
60///
61/// A [`Vec`] of [`Asset`]s or an error.
62fn read_assets_from_iter<I>(
63    iter: I,
64    agent_ids: &HashSet<AgentID>,
65    processes: &ProcessMap,
66    region_ids: &HashSet<RegionID>,
67) -> Result<Vec<Asset>>
68where
69    I: Iterator<Item = AssetRaw>,
70{
71    iter.map(|asset| -> Result<_> {
72        let agent_id = agent_ids.get_id_by_str(&asset.agent_id)?;
73        let process = processes
74            .get(asset.process_id.as_str())
75            .with_context(|| format!("Invalid process ID: {}", &asset.process_id))?;
76        let region_id = region_ids.get_id_by_str(&asset.region_id)?;
77
78        Asset::new(
79            agent_id,
80            Rc::clone(process),
81            region_id,
82            asset.capacity,
83            asset.commission_year,
84        )
85    })
86    .try_collect()
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::fixture::{processes, region_ids};
93
94    use itertools::assert_equal;
95    use rstest::{fixture, rstest};
96    use std::iter;
97
98    #[fixture]
99    fn agent_ids() -> HashSet<AgentID> {
100        iter::once("agent1".into()).collect()
101    }
102
103    #[rstest]
104    fn test_read_assets_from_iter_valid(
105        agent_ids: HashSet<AgentID>,
106        processes: ProcessMap,
107        region_ids: HashSet<RegionID>,
108    ) {
109        let asset_in = AssetRaw {
110            agent_id: "agent1".into(),
111            process_id: "process1".into(),
112            region_id: "GBR".into(),
113            capacity: 1.0,
114            commission_year: 2010,
115        };
116        let asset_out = Asset::new(
117            "agent1".into(),
118            Rc::clone(processes.values().next().unwrap()),
119            "GBR".into(),
120            1.0,
121            2010,
122        )
123        .unwrap();
124        assert_equal(
125            read_assets_from_iter(iter::once(asset_in), &agent_ids, &processes, &region_ids)
126                .unwrap(),
127            iter::once(asset_out),
128        );
129    }
130
131    #[rstest]
132    #[case(AssetRaw { // Bad process ID
133            agent_id: "agent1".into(),
134            process_id: "process2".into(),
135            region_id: "GBR".into(),
136            capacity: 1.0,
137            commission_year: 2010,
138        })]
139    #[case(AssetRaw { // Bad agent ID
140            agent_id: "agent2".into(),
141            process_id: "process1".into(),
142            region_id: "GBR".into(),
143            capacity: 1.0,
144            commission_year: 2010,
145        })]
146    #[case(AssetRaw { // Bad region ID: not in region_ids
147            agent_id: "agent1".into(),
148            process_id: "process1".into(),
149            region_id: "FRA".into(),
150            capacity: 1.0,
151            commission_year: 2010,
152        })]
153    fn test_read_assets_from_iter_invalid(
154        #[case] asset: AssetRaw,
155        agent_ids: HashSet<AgentID>,
156        processes: ProcessMap,
157        region_ids: HashSet<RegionID>,
158    ) {
159        assert!(
160            read_assets_from_iter(iter::once(asset), &agent_ids, &processes, &region_ids).is_err()
161        );
162    }
163}