1use super::super::{input_err_msg, read_csv_optional, try_insert};
3use crate::commodity::{BalanceType, CommodityID, 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, HashMap<BalanceType, CommodityLevyMap>>> {
55 let file_path = model_dir.join(COMMODITY_LEVIES_FILE_NAME);
56 let commodity_levies_csv = read_csv_optional(&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, HashMap<BalanceType, 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.entry(commodity_id.clone()).or_insert_with(HashMap::new);
104
105 for region in ®ions {
107 commodity_regions
108 .entry(commodity_id.clone())
109 .or_default()
110 .insert(region.clone());
111 for year in &years {
112 for (time_slice, _) in ts_selection.iter(time_slice_info) {
113 match cost.balance_type {
114 BalanceType::Consumption | BalanceType::Production => {
116 let map = map
117 .entry(cost.balance_type.clone())
118 .or_insert_with(CommodityLevyMap::new);
119 try_insert(
120 map,
121 &(region.clone(), *year, time_slice.clone()),
122 cost.value,
123 )?;
124 }
125 BalanceType::Net => {
127 let map_p = map
128 .entry(BalanceType::Production)
129 .or_insert_with(CommodityLevyMap::new);
130 try_insert(
131 map_p,
132 &(region.clone(), *year, time_slice.clone()),
133 cost.value,
134 )?;
135 let map_c = map
136 .entry(BalanceType::Consumption)
137 .or_insert_with(CommodityLevyMap::new);
138 try_insert(
139 map_c,
140 &(region.clone(), *year, time_slice.clone()),
141 -cost.value,
142 )?;
143 }
144 }
145 }
146 }
147 }
148 }
149
150 for (commodity_id, regions) in &commodity_regions {
152 let map = map.get_mut(commodity_id).unwrap();
153
154 for map_inner in map.values_mut() {
155 validate_commodity_levy_map(map_inner, regions, milestone_years, time_slice_info)
156 .with_context(|| format!("Missing costs for commodity {commodity_id}"))?;
157
158 for region_id in region_ids.difference(regions) {
159 add_missing_region_to_commodity_levy_map(
160 map_inner,
161 region_id,
162 milestone_years,
163 time_slice_info,
164 );
165 warn!(
166 "No levy specified for commodity {commodity_id} in region {region_id}. Assuming zero levy."
167 );
168 }
169 }
170 }
171
172 Ok(map)
173}
174
175fn add_missing_region_to_commodity_levy_map(
184 map: &mut CommodityLevyMap,
185 region_id: &RegionID,
186 milestone_years: &[u32],
187 time_slice_info: &TimeSliceInfo,
188) {
189 for year in milestone_years {
190 for time_slice in time_slice_info.iter_ids() {
191 map.insert(
192 (region_id.clone(), *year, time_slice.clone()),
193 MoneyPerFlow(0.0),
194 );
195 }
196 }
197}
198
199fn validate_commodity_levy_map(
212 map: &CommodityLevyMap,
213 regions: &IndexSet<RegionID>,
214 milestone_years: &[u32],
215 time_slice_info: &TimeSliceInfo,
216) -> Result<()> {
217 for region_id in regions {
219 for year in milestone_years {
220 for time_slice in time_slice_info.iter_ids() {
221 ensure!(
222 map.contains_key(&(region_id.clone(), *year, time_slice.clone())),
223 "Missing cost for region {region_id}, year {year}, time slice {time_slice}"
224 );
225 }
226 }
227 }
228 Ok(())
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::fixture::{assert_error, region_id, time_slice, time_slice_info};
235 use crate::time_slice::TimeSliceID;
236 use crate::units::Year;
237 use rstest::{fixture, rstest};
238
239 #[fixture]
240 fn region_ids(region_id: RegionID) -> IndexSet<RegionID> {
241 IndexSet::from([region_id])
242 }
243
244 #[fixture]
245 fn cost_map(time_slice: TimeSliceID) -> CommodityLevyMap {
246 let cost = MoneyPerFlow(1.0);
247
248 let mut map = CommodityLevyMap::new();
249 map.insert(("GBR".into(), 2020, time_slice.clone()), cost.clone());
250 map
251 }
252
253 #[rstest]
254 fn test_validate_commodity_levies_map_valid(
255 cost_map: CommodityLevyMap,
256 time_slice_info: TimeSliceInfo,
257 region_ids: IndexSet<RegionID>,
258 ) {
259 assert!(
261 validate_commodity_levy_map(&cost_map, ®ion_ids, &[2020], &time_slice_info).is_ok()
262 );
263 }
264
265 #[rstest]
266 fn test_validate_commodity_levies_map_invalid_missing_region(
267 cost_map: CommodityLevyMap,
268 time_slice_info: TimeSliceInfo,
269 ) {
270 let region_ids = IndexSet::from(["GBR".into(), "FRA".into()]);
272 assert_error!(
273 validate_commodity_levy_map(&cost_map, ®ion_ids, &[2020], &time_slice_info),
274 "Missing cost for region FRA, year 2020, time slice winter.day"
275 );
276 }
277
278 #[rstest]
279 fn test_validate_commodity_levies_map_invalid_missing_year(
280 cost_map: CommodityLevyMap,
281 time_slice_info: TimeSliceInfo,
282 region_ids: IndexSet<RegionID>,
283 ) {
284 assert_error!(
286 validate_commodity_levy_map(&cost_map, ®ion_ids, &[2020, 2030], &time_slice_info),
287 "Missing cost for region GBR, year 2030, time slice winter.day"
288 );
289 }
290
291 #[rstest]
292 fn test_validate_commodity_levies_map_invalid(
293 cost_map: CommodityLevyMap,
294 region_ids: IndexSet<RegionID>,
295 ) {
296 let time_slice = TimeSliceID {
298 season: "winter".into(),
299 time_of_day: "night".into(),
300 };
301 let time_slice_info = TimeSliceInfo {
302 seasons: [("winter".into(), Year(1.0))].into(),
303 times_of_day: ["day".into(), "night".into()].into(),
304 time_slices: [
305 (time_slice.clone(), Year(0.5)),
306 (time_slice.clone(), Year(0.5)),
307 ]
308 .into(),
309 };
310 assert_error!(
311 validate_commodity_levy_map(&cost_map, ®ion_ids, &[2020], &time_slice_info),
312 "Missing cost for region GBR, year 2020, time slice winter.night"
313 );
314 }
315
316 #[rstest]
317 fn test_add_missing_region_to_commodity_levy_map(
318 cost_map: CommodityLevyMap,
319 time_slice_info: TimeSliceInfo,
320 region_id: RegionID,
321 ) {
322 let mut cost_map = cost_map;
323 add_missing_region_to_commodity_levy_map(
324 &mut cost_map,
325 ®ion_id,
326 &[2020],
327 &time_slice_info,
328 );
329
330 for time_slice in time_slice_info.iter_ids() {
332 assert_eq!(
333 cost_map.get(&(region_id.clone(), 2020, time_slice.clone())),
334 Some(&MoneyPerFlow(0.0))
335 );
336 }
337 }
338}