1use 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#[derive(PartialEq, Debug, Deserialize, Clone)]
19struct CommodityLevyRaw {
20 commodity_id: String,
22 regions: String,
24 balance_type: BalanceType,
26 years: String,
28 time_slice: String,
30 value: MoneyPerFlow,
32}
33
34pub 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 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 let map = map
90 .entry(commodity_id.clone())
91 .or_insert_with(CommodityLevyMap::new);
92
93 let cost = CommodityLevy {
95 balance_type: cost.balance_type,
96 value: cost.value,
97 };
98
99 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 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 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 assert!(
181 validate_commodity_levy_map(&cost_map, ®ion_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 let region_ids = IndexSet::from(["GBR".into(), "FRA".into()]);
192 assert_error!(
193 validate_commodity_levy_map(&cost_map, ®ion_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 assert_error!(
206 validate_commodity_levy_map(&cost_map, ®ion_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 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, ®ion_ids, &[2020], &time_slice_info),
232 "Missing cost for region GBR, year 2020, time slice winter.night"
233 );
234 }
235}