muse2/input/commodity/
cost.rs

1//! Code for reading in the commodity cost CSV file.
2use super::super::*;
3use crate::commodity::{BalanceType, CommodityCost, CommodityCostMap, CommodityID};
4use crate::id::IDCollection;
5use crate::region::{parse_region_str, RegionID};
6use crate::time_slice::TimeSliceInfo;
7use crate::year::parse_year_str;
8use anyhow::{ensure, Context, Result};
9use serde::Deserialize;
10use std::collections::{HashMap, HashSet};
11use std::path::Path;
12
13const COMMODITY_COSTS_FILE_NAME: &str = "commodity_costs.csv";
14
15/// Cost parameters for each commodity
16#[derive(PartialEq, Debug, Deserialize, Clone)]
17struct CommodityCostRaw {
18    /// Unique identifier for the commodity (e.g. "ELC")
19    commodity_id: String,
20    /// The region(s) to which the commodity cost applies.
21    regions: String,
22    /// Type of balance for application of cost.
23    balance_type: BalanceType,
24    /// The year(s) to which the cost applies.
25    year: String,
26    /// The time slice to which the cost applies.
27    time_slice: String,
28    /// Cost per unit commodity. For example, if a CO2 price is specified in input data, it can be applied to net CO2 via this value.
29    value: f64,
30}
31
32/// Read costs associated with each commodity from commodity costs CSV file.
33///
34/// # Arguments
35///
36/// * `model_dir` - Folder containing model configuration files
37/// * `commodity_ids` - All possible commodity IDs
38/// * `region_ids` - All possible region IDs
39/// * `time_slice_info` - Information about time slices
40/// * `milestone_years` - All milestone years
41///
42/// # Returns
43///
44/// A map containing commodity costs, grouped by commodity ID.
45pub fn read_commodity_costs(
46    model_dir: &Path,
47    commodity_ids: &HashSet<CommodityID>,
48    region_ids: &HashSet<RegionID>,
49    time_slice_info: &TimeSliceInfo,
50    milestone_years: &[u32],
51) -> Result<HashMap<CommodityID, CommodityCostMap>> {
52    let file_path = model_dir.join(COMMODITY_COSTS_FILE_NAME);
53    let commodity_costs_csv = read_csv::<CommodityCostRaw>(&file_path)?;
54    read_commodity_costs_iter(
55        commodity_costs_csv,
56        commodity_ids,
57        region_ids,
58        time_slice_info,
59        milestone_years,
60    )
61    .with_context(|| input_err_msg(&file_path))
62}
63
64fn read_commodity_costs_iter<I>(
65    iter: I,
66    commodity_ids: &HashSet<CommodityID>,
67    region_ids: &HashSet<RegionID>,
68    time_slice_info: &TimeSliceInfo,
69    milestone_years: &[u32],
70) -> Result<HashMap<CommodityID, CommodityCostMap>>
71where
72    I: Iterator<Item = CommodityCostRaw>,
73{
74    let mut map = HashMap::new();
75
76    // Keep track of commodity/region combinations specified. We will check that all years and
77    // time slices are covered for each commodity/region combination.
78    let mut commodity_regions: HashMap<CommodityID, HashSet<RegionID>> = HashMap::new();
79
80    for cost in iter {
81        let commodity_id = commodity_ids.get_id_by_str(&cost.commodity_id)?;
82        let regions = parse_region_str(&cost.regions, region_ids)?;
83        let years = parse_year_str(&cost.year, milestone_years)?;
84        let ts_selection = time_slice_info.get_selection(&cost.time_slice)?;
85
86        // Get or create CommodityCostMap for this commodity
87        let map = map
88            .entry(commodity_id.clone())
89            .or_insert_with(CommodityCostMap::new);
90
91        // Create CommodityCost
92        let cost = CommodityCost {
93            balance_type: cost.balance_type,
94            value: cost.value,
95        };
96
97        // Insert cost into map for each region/year/time slice
98        for region in regions.iter() {
99            commodity_regions
100                .entry(commodity_id.clone())
101                .or_default()
102                .insert(region.clone());
103            for year in years.iter() {
104                for (time_slice, _) in time_slice_info.iter_selection(&ts_selection) {
105                    try_insert(
106                        map,
107                        (region.clone(), *year, time_slice.clone()),
108                        cost.clone(),
109                    )?;
110                }
111            }
112        }
113    }
114
115    // Validate map
116    for (commodity_id, regions) in commodity_regions.iter() {
117        let map = map.get(commodity_id).unwrap();
118        validate_commodity_cost_map(map, regions, milestone_years, time_slice_info)
119            .with_context(|| format!("Missing costs for commodity {}", commodity_id))?;
120    }
121    Ok(map)
122}
123
124fn validate_commodity_cost_map(
125    map: &CommodityCostMap,
126    regions: &HashSet<RegionID>,
127    milestone_years: &[u32],
128    time_slice_info: &TimeSliceInfo,
129) -> Result<()> {
130    // Check that all regions, years and time slices are covered
131    for region_id in regions.iter() {
132        for year in milestone_years.iter() {
133            for time_slice in time_slice_info.iter_ids() {
134                ensure!(
135                    map.contains_key(&(region_id.clone(), *year, time_slice.clone())),
136                    "Missing cost for region {}, year {}, time slice {}",
137                    region_id,
138                    year,
139                    time_slice
140                );
141            }
142        }
143    }
144    Ok(())
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::time_slice::TimeSliceID;
151
152    #[test]
153    fn test_validate_commodity_costs_map() {
154        // Set up time slices
155        let slice = TimeSliceID {
156            season: "winter".into(),
157            time_of_day: "day".into(),
158        };
159        let ts_info = TimeSliceInfo {
160            seasons: ["winter".into()].into(),
161            times_of_day: ["day".into()].into(),
162            fractions: [(slice.clone(), 1.0)].into(),
163        };
164
165        let regions = HashSet::from(["UK".into()]);
166        let milestone_years = [2020];
167        let cost = CommodityCost {
168            balance_type: BalanceType::Net,
169            value: 1.0,
170        };
171
172        // Valid map
173        let mut map = CommodityCostMap::new();
174        map.insert(("UK".into(), 2020, slice.clone()), cost.clone());
175        assert!(validate_commodity_cost_map(&map, &regions, &milestone_years, &ts_info).is_ok());
176
177        // Missing region
178        let regions2 = HashSet::from(["UK".into(), "FR".into()]);
179        assert!(validate_commodity_cost_map(&map, &regions2, &milestone_years, &ts_info).is_err());
180
181        // Missing year
182        assert!(validate_commodity_cost_map(&map, &regions, &[2020, 2030], &ts_info).is_err());
183
184        // Missing time slice
185        let slice2 = TimeSliceID {
186            season: "winter".into(),
187            time_of_day: "night".into(),
188        };
189        let ts_info2 = TimeSliceInfo {
190            seasons: ["winter".into()].into(),
191            times_of_day: ["day".into(), "night".into()].into(),
192            fractions: [(slice.clone(), 0.5), (slice2.clone(), 0.5)].into(),
193        };
194        assert!(validate_commodity_cost_map(&map, &regions, &milestone_years, &ts_info2).is_err());
195    }
196}