1use super::super::*;
3use crate::commodity::{BalanceType, CommodityID, CommodityLevy, CommodityLevyMap};
4use crate::id::IDCollection;
5use crate::region::{RegionID, parse_region_str};
6use crate::time_slice::TimeSliceInfo;
7use crate::units::MoneyPerFlow;
8use crate::year::parse_year_str;
9use anyhow::{Context, Result, ensure};
10use indexmap::IndexSet;
11use log::warn;
12use serde::Deserialize;
13use std::collections::HashMap;
14use std::path::Path;
15
16const COMMODITY_LEVIES_FILE_NAME: &str = "commodity_levies.csv";
17
18#[derive(PartialEq, Debug, Deserialize, Clone)]
20struct CommodityLevyRaw {
21 commodity_id: String,
23 regions: String,
25 balance_type: BalanceType,
27 years: String,
29 time_slice: String,
31 value: MoneyPerFlow,
33}
34
35pub fn read_commodity_levies(
49 model_dir: &Path,
50 commodity_ids: &IndexSet<CommodityID>,
51 region_ids: &IndexSet<RegionID>,
52 time_slice_info: &TimeSliceInfo,
53 milestone_years: &[u32],
54) -> Result<HashMap<CommodityID, CommodityLevyMap>> {
55 let file_path = model_dir.join(COMMODITY_LEVIES_FILE_NAME);
56 let commodity_levies_csv = read_csv::<CommodityLevyRaw>(&file_path)?;
57 read_commodity_levies_iter(
58 commodity_levies_csv,
59 commodity_ids,
60 region_ids,
61 time_slice_info,
62 milestone_years,
63 )
64 .with_context(|| input_err_msg(&file_path))
65}
66
67fn read_commodity_levies_iter<I>(
81 iter: I,
82 commodity_ids: &IndexSet<CommodityID>,
83 region_ids: &IndexSet<RegionID>,
84 time_slice_info: &TimeSliceInfo,
85 milestone_years: &[u32],
86) -> Result<HashMap<CommodityID, CommodityLevyMap>>
87where
88 I: Iterator<Item = CommodityLevyRaw>,
89{
90 let mut map = HashMap::new();
91
92 let mut commodity_regions: HashMap<CommodityID, IndexSet<RegionID>> = HashMap::new();
95
96 for cost in iter {
97 let commodity_id = commodity_ids.get_id(&cost.commodity_id)?;
98 let regions = parse_region_str(&cost.regions, region_ids)?;
99 let years = parse_year_str(&cost.years, milestone_years)?;
100 let ts_selection = time_slice_info.get_selection(&cost.time_slice)?;
101
102 let map = map
104 .entry(commodity_id.clone())
105 .or_insert_with(CommodityLevyMap::new);
106
107 let cost = CommodityLevy {
109 balance_type: cost.balance_type,
110 value: cost.value,
111 };
112
113 for region in regions.iter() {
115 commodity_regions
116 .entry(commodity_id.clone())
117 .or_default()
118 .insert(region.clone());
119 for year in years.iter() {
120 for (time_slice, _) in ts_selection.iter(time_slice_info) {
121 try_insert(
122 map,
123 (region.clone(), *year, time_slice.clone()),
124 cost.clone(),
125 )?;
126 }
127 }
128 }
129 }
130
131 for (commodity_id, regions) in commodity_regions.iter() {
133 let map = map.get_mut(commodity_id).unwrap();
134 validate_commodity_levy_map(map, regions, milestone_years, time_slice_info)
135 .with_context(|| format!("Missing costs for commodity {commodity_id}"))?;
136
137 for region_id in region_ids.difference(regions) {
138 add_missing_region_to_commodity_levy_map(
139 map,
140 region_id,
141 milestone_years,
142 time_slice_info,
143 );
144 warn!(
145 "No levy specified for commodity {commodity_id} in region {region_id}. Assuming zero levy."
146 );
147 }
148 }
149
150 Ok(map)
151}
152
153fn add_missing_region_to_commodity_levy_map(
162 map: &mut CommodityLevyMap,
163 region_id: &RegionID,
164 milestone_years: &[u32],
165 time_slice_info: &TimeSliceInfo,
166) {
167 for year in milestone_years.iter() {
168 for time_slice in time_slice_info.iter_ids() {
169 map.insert(
170 (region_id.clone(), *year, time_slice.clone()),
171 CommodityLevy {
172 balance_type: BalanceType::Net,
173 value: MoneyPerFlow(0.0),
174 },
175 );
176 }
177 }
178}
179
180fn validate_commodity_levy_map(
193 map: &CommodityLevyMap,
194 regions: &IndexSet<RegionID>,
195 milestone_years: &[u32],
196 time_slice_info: &TimeSliceInfo,
197) -> Result<()> {
198 for region_id in regions.iter() {
200 for year in milestone_years.iter() {
201 for time_slice in time_slice_info.iter_ids() {
202 ensure!(
203 map.contains_key(&(region_id.clone(), *year, time_slice.clone())),
204 "Missing cost for region {}, year {}, time slice {}",
205 region_id,
206 year,
207 time_slice
208 );
209 }
210 }
211 }
212 Ok(())
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use crate::fixture::{assert_error, region_id, time_slice, time_slice_info};
219 use crate::time_slice::TimeSliceID;
220 use crate::units::Year;
221 use rstest::{fixture, rstest};
222
223 #[fixture]
224 fn region_ids(region_id: RegionID) -> IndexSet<RegionID> {
225 IndexSet::from([region_id])
226 }
227
228 #[fixture]
229 fn cost_map(time_slice: TimeSliceID) -> CommodityLevyMap {
230 let cost = CommodityLevy {
231 balance_type: BalanceType::Net,
232 value: MoneyPerFlow(1.0),
233 };
234
235 let mut map = CommodityLevyMap::new();
236 map.insert(("GBR".into(), 2020, time_slice.clone()), cost.clone());
237 map
238 }
239
240 #[rstest]
241 fn test_validate_commodity_levies_map_valid(
242 cost_map: CommodityLevyMap,
243 time_slice_info: TimeSliceInfo,
244 region_ids: IndexSet<RegionID>,
245 ) {
246 assert!(
248 validate_commodity_levy_map(&cost_map, ®ion_ids, &[2020], &time_slice_info).is_ok()
249 );
250 }
251
252 #[rstest]
253 fn test_validate_commodity_levies_map_invalid_missing_region(
254 cost_map: CommodityLevyMap,
255 time_slice_info: TimeSliceInfo,
256 ) {
257 let region_ids = IndexSet::from(["GBR".into(), "FRA".into()]);
259 assert_error!(
260 validate_commodity_levy_map(&cost_map, ®ion_ids, &[2020], &time_slice_info),
261 "Missing cost for region FRA, year 2020, time slice winter.day"
262 );
263 }
264
265 #[rstest]
266 fn test_validate_commodity_levies_map_invalid_missing_year(
267 cost_map: CommodityLevyMap,
268 time_slice_info: TimeSliceInfo,
269 region_ids: IndexSet<RegionID>,
270 ) {
271 assert_error!(
273 validate_commodity_levy_map(&cost_map, ®ion_ids, &[2020, 2030], &time_slice_info),
274 "Missing cost for region GBR, year 2030, time slice winter.day"
275 );
276 }
277
278 #[rstest]
279 fn test_validate_commodity_levies_map_invalid(
280 cost_map: CommodityLevyMap,
281 region_ids: IndexSet<RegionID>,
282 ) {
283 let time_slice = TimeSliceID {
285 season: "winter".into(),
286 time_of_day: "night".into(),
287 };
288 let time_slice_info = TimeSliceInfo {
289 seasons: [("winter".into(), Year(1.0))].into(),
290 times_of_day: ["day".into(), "night".into()].into(),
291 time_slices: [
292 (time_slice.clone(), Year(0.5)),
293 (time_slice.clone(), Year(0.5)),
294 ]
295 .into(),
296 };
297 assert_error!(
298 validate_commodity_levy_map(&cost_map, ®ion_ids, &[2020], &time_slice_info),
299 "Missing cost for region GBR, year 2020, time slice winter.night"
300 );
301 }
302
303 #[rstest]
304 fn test_add_missing_region_to_commodity_levy_map(
305 cost_map: CommodityLevyMap,
306 time_slice_info: TimeSliceInfo,
307 region_id: RegionID,
308 ) {
309 let mut cost_map = cost_map;
310 add_missing_region_to_commodity_levy_map(
311 &mut cost_map,
312 ®ion_id,
313 &[2020],
314 &time_slice_info,
315 );
316
317 for time_slice in time_slice_info.iter_ids() {
319 assert_eq!(
320 cost_map.get(&(region_id.clone(), 2020, time_slice.clone())),
321 Some(&CommodityLevy {
322 balance_type: BalanceType::Net,
323 value: MoneyPerFlow(0.0)
324 })
325 );
326 }
327 }
328}