muse2/input/commodity/
levy.rs

1//! Code for reading in the commodity levies CSV file.
2use super::super::*;
3use crate::commodity::{BalanceType, CommodityID, CommodityLevy, CommodityLevyMap};
4use crate::id::IDCollection;
5use crate::region::{parse_region_str, RegionID};
6use crate::time_slice::TimeSliceInfo;
7use crate::units::MoneyPerFlow;
8use crate::year::parse_year_str;
9use anyhow::{ensure, Context, Result};
10use indexmap::IndexSet;
11use serde::Deserialize;
12use std::collections::HashMap;
13use std::path::Path;
14
15const COMMODITY_LEVIES_FILE_NAME: &str = "commodity_levies.csv";
16
17/// Cost parameters for each commodity
18#[derive(PartialEq, Debug, Deserialize, Clone)]
19struct CommodityLevyRaw {
20    /// Unique identifier for the commodity (e.g. "ELC")
21    commodity_id: String,
22    /// The region(s) to which the levy applies.
23    regions: String,
24    /// Type of balance for application of cost.
25    balance_type: BalanceType,
26    /// The year(s) to which the cost applies.
27    years: String,
28    /// The time slice to which the cost applies.
29    time_slice: String,
30    /// Cost per unit commodity
31    value: MoneyPerFlow,
32}
33
34/// Read costs associated with each commodity from levies CSV file.
35///
36/// # Arguments
37///
38/// * `model_dir` - Folder containing model configuration files
39/// * `commodity_ids` - All possible commodity IDs
40/// * `region_ids` - All possible region IDs
41/// * `time_slice_info` - Information about time slices
42/// * `milestone_years` - All milestone years
43///
44/// # Returns
45///
46/// A map containing levies, grouped by commodity ID.
47pub fn read_commodity_levies(
48    model_dir: &Path,
49    commodity_ids: &IndexSet<CommodityID>,
50    region_ids: &IndexSet<RegionID>,
51    time_slice_info: &TimeSliceInfo,
52    milestone_years: &[u32],
53) -> Result<HashMap<CommodityID, CommodityLevyMap>> {
54    let file_path = model_dir.join(COMMODITY_LEVIES_FILE_NAME);
55    let commodity_levies_csv = read_csv::<CommodityLevyRaw>(&file_path)?;
56    read_commodity_levies_iter(
57        commodity_levies_csv,
58        commodity_ids,
59        region_ids,
60        time_slice_info,
61        milestone_years,
62    )
63    .with_context(|| input_err_msg(&file_path))
64}
65
66fn read_commodity_levies_iter<I>(
67    iter: I,
68    commodity_ids: &IndexSet<CommodityID>,
69    region_ids: &IndexSet<RegionID>,
70    time_slice_info: &TimeSliceInfo,
71    milestone_years: &[u32],
72) -> Result<HashMap<CommodityID, CommodityLevyMap>>
73where
74    I: Iterator<Item = CommodityLevyRaw>,
75{
76    let mut map = HashMap::new();
77
78    // Keep track of commodity/region combinations specified. We will check that all years and
79    // time slices are covered for each commodity/region combination.
80    let mut commodity_regions: HashMap<CommodityID, IndexSet<RegionID>> = HashMap::new();
81
82    for cost in iter {
83        let commodity_id = commodity_ids.get_id(&cost.commodity_id)?;
84        let regions = parse_region_str(&cost.regions, region_ids)?;
85        let years = parse_year_str(&cost.years, milestone_years)?;
86        let ts_selection = time_slice_info.get_selection(&cost.time_slice)?;
87
88        // Get or create CommodityLevyMap for this commodity
89        let map = map
90            .entry(commodity_id.clone())
91            .or_insert_with(CommodityLevyMap::new);
92
93        // Create CommodityLevy
94        let cost = CommodityLevy {
95            balance_type: cost.balance_type,
96            value: cost.value,
97        };
98
99        // Insert cost into map for each region/year/time slice
100        for region in regions.iter() {
101            commodity_regions
102                .entry(commodity_id.clone())
103                .or_default()
104                .insert(region.clone());
105            for year in years.iter() {
106                for (time_slice, _) in ts_selection.iter(time_slice_info) {
107                    try_insert(
108                        map,
109                        (region.clone(), *year, time_slice.clone()),
110                        cost.clone(),
111                    )?;
112                }
113            }
114        }
115    }
116
117    // Validate map
118    for (commodity_id, regions) in commodity_regions.iter() {
119        let map = map.get(commodity_id).unwrap();
120        validate_commodity_levy_map(map, regions, milestone_years, time_slice_info)
121            .with_context(|| format!("Missing costs for commodity {}", commodity_id))?;
122    }
123    Ok(map)
124}
125
126fn validate_commodity_levy_map(
127    map: &CommodityLevyMap,
128    regions: &IndexSet<RegionID>,
129    milestone_years: &[u32],
130    time_slice_info: &TimeSliceInfo,
131) -> Result<()> {
132    // Check that all regions, years and time slices are covered
133    for region_id in regions.iter() {
134        for year in milestone_years.iter() {
135            for time_slice in time_slice_info.iter_ids() {
136                ensure!(
137                    map.contains_key(&(region_id.clone(), *year, time_slice.clone())),
138                    "Missing cost for region {}, year {}, time slice {}",
139                    region_id,
140                    year,
141                    time_slice
142                );
143            }
144        }
145    }
146    Ok(())
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use crate::fixture::{assert_error, region_id, time_slice, time_slice_info};
153    use crate::time_slice::TimeSliceID;
154    use rstest::{fixture, rstest};
155
156    #[fixture]
157    fn region_ids(region_id: RegionID) -> IndexSet<RegionID> {
158        IndexSet::from([region_id])
159    }
160
161    #[fixture]
162    fn cost_map(time_slice: TimeSliceID) -> CommodityLevyMap {
163        let cost = CommodityLevy {
164            balance_type: BalanceType::Net,
165            value: MoneyPerFlow(1.0),
166        };
167
168        let mut map = CommodityLevyMap::new();
169        map.insert(("GBR".into(), 2020, time_slice.clone()), cost.clone());
170        map
171    }
172
173    #[rstest]
174    fn test_validate_commodity_levies_map_valid(
175        cost_map: CommodityLevyMap,
176        time_slice_info: TimeSliceInfo,
177        region_ids: IndexSet<RegionID>,
178    ) {
179        // Valid map
180        assert!(
181            validate_commodity_levy_map(&cost_map, &region_ids, &[2020], &time_slice_info).is_ok()
182        );
183    }
184
185    #[rstest]
186    fn test_validate_commodity_levies_map_invalid_missing_region(
187        cost_map: CommodityLevyMap,
188        time_slice_info: TimeSliceInfo,
189    ) {
190        // Missing region
191        let region_ids = IndexSet::from(["GBR".into(), "FRA".into()]);
192        assert_error!(
193            validate_commodity_levy_map(&cost_map, &region_ids, &[2020], &time_slice_info),
194            "Missing cost for region FRA, year 2020, time slice winter.day"
195        );
196    }
197
198    #[rstest]
199    fn test_validate_commodity_levies_map_invalid_missing_year(
200        cost_map: CommodityLevyMap,
201        time_slice_info: TimeSliceInfo,
202        region_ids: IndexSet<RegionID>,
203    ) {
204        // Missing year
205        assert_error!(
206            validate_commodity_levy_map(&cost_map, &region_ids, &[2020, 2030], &time_slice_info),
207            "Missing cost for region GBR, year 2030, time slice winter.day"
208        );
209    }
210
211    #[rstest]
212    fn test_validate_commodity_levies_map_invalid(
213        cost_map: CommodityLevyMap,
214        region_ids: IndexSet<RegionID>,
215    ) {
216        // Missing time slice
217        let time_slice = TimeSliceID {
218            season: "winter".into(),
219            time_of_day: "night".into(),
220        };
221        let time_slice_info = TimeSliceInfo {
222            seasons: [("winter".into(), Dimensionless(1.0))].into(),
223            times_of_day: ["day".into(), "night".into()].into(),
224            time_slices: [
225                (time_slice.clone(), Dimensionless(0.5)),
226                (time_slice.clone(), Dimensionless(0.5)),
227            ]
228            .into(),
229        };
230        assert_error!(
231            validate_commodity_levy_map(&cost_map, &region_ids, &[2020], &time_slice_info),
232            "Missing cost for region GBR, year 2020, time slice winter.night"
233        );
234    }
235}