1use super::super::{check_values_sum_to_one_approx, input_err_msg, read_csv};
3use crate::commodity::CommodityID;
4use crate::id::IDCollection;
5use crate::input::commodity::demand::BorrowedCommodityMap;
6use crate::region::RegionID;
7use crate::time_slice::{TimeSliceInfo, TimeSliceSelection};
8use crate::units::Dimensionless;
9use anyhow::{Context, Result, ensure};
10use indexmap::IndexSet;
11use itertools::{Itertools, iproduct};
12use serde::Deserialize;
13use std::collections::HashMap;
14use std::path::Path;
15
16const DEMAND_SLICING_FILE_NAME: &str = "demand_slicing.csv";
17
18#[derive(Clone, Deserialize)]
19struct DemandSlice {
20 commodity_id: String,
21 region_id: String,
22 time_slice: String,
23 fraction: Dimensionless,
24}
25
26pub type DemandSliceMap = HashMap<(CommodityID, RegionID, TimeSliceSelection), Dimensionless>;
28
29pub fn read_demand_slices(
39 model_dir: &Path,
40 svd_commodities: &BorrowedCommodityMap,
41 region_ids: &IndexSet<RegionID>,
42 time_slice_info: &TimeSliceInfo,
43) -> Result<DemandSliceMap> {
44 let file_path = model_dir.join(DEMAND_SLICING_FILE_NAME);
45 let demand_slices_csv = read_csv(&file_path)?;
46 read_demand_slices_from_iter(
47 demand_slices_csv,
48 svd_commodities,
49 region_ids,
50 time_slice_info,
51 )
52 .with_context(|| input_err_msg(file_path))
53}
54
55fn read_demand_slices_from_iter<I>(
57 iter: I,
58 svd_commodities: &BorrowedCommodityMap,
59 region_ids: &IndexSet<RegionID>,
60 time_slice_info: &TimeSliceInfo,
61) -> Result<DemandSliceMap>
62where
63 I: Iterator<Item = DemandSlice>,
64{
65 let mut demand_slices = DemandSliceMap::new();
66
67 for slice in iter {
68 let commodity = svd_commodities
69 .get(slice.commodity_id.as_str())
70 .with_context(|| {
71 format!(
72 "Can only provide demand slice data for SVD commodities. Found entry for '{}'",
73 slice.commodity_id
74 )
75 })?;
76 let region_id = region_ids.get_id(&slice.region_id)?;
77
78 let ts_selection = time_slice_info.get_selection(&slice.time_slice)?;
82
83 let iter = time_slice_info
85 .calculate_share(&ts_selection, commodity.time_slice_level, slice.fraction)
86 .with_context(|| {
87 format!(
88 "Cannot provide demand at {:?} level when commodity time slice level is {:?}",
89 ts_selection.level(),
90 commodity.time_slice_level
91 )
92 })?;
93 for (ts_selection, demand_fraction) in iter {
94 let existing = demand_slices
95 .insert(
96 (
97 commodity.id.clone(),
98 region_id.clone(),
99 ts_selection.clone(),
100 ),
101 demand_fraction,
102 )
103 .is_some();
104 ensure!(
105 !existing,
106 "Duplicate demand slicing entry (or same time slice covered by more than one entry) \
107 (commodity: {}, region: {}, time slice(s): {})",
108 commodity.id,
109 region_id,
110 ts_selection
111 );
112 }
113 }
114
115 validate_demand_slices(svd_commodities, region_ids, &demand_slices, time_slice_info)?;
116
117 Ok(demand_slices)
118}
119
120fn validate_demand_slices(
128 svd_commodities: &BorrowedCommodityMap,
129 region_ids: &IndexSet<RegionID>,
130 demand_slices: &DemandSliceMap,
131 time_slice_info: &TimeSliceInfo,
132) -> Result<()> {
133 for (commodity, region_id) in iproduct!(svd_commodities.values(), region_ids) {
134 time_slice_info
135 .iter_selections_at_level(commodity.time_slice_level)
136 .map(|ts_selection| {
137 demand_slices
138 .get(&(
139 commodity.id.clone(),
140 region_id.clone(),
141 ts_selection.clone(),
142 ))
143 .with_context(|| {
144 format!(
145 "Demand slice missing for time slice(s) '{}' (commodity: {}, region {})",
146 ts_selection, commodity.id, region_id
147 )
148 })
149 })
150 .process_results(|iter| {
151 check_values_sum_to_one_approx(iter.copied()).context("Invalid demand fractions")
152 })??;
153 }
154
155 Ok(())
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::commodity::Commodity;
162 use crate::fixture::{assert_error, get_svd_map, svd_commodity, time_slice_info};
163 use crate::time_slice::TimeSliceID;
164 use crate::units::Year;
165 use rstest::{fixture, rstest};
166 use std::iter;
167
168 #[fixture]
169 pub fn region_ids() -> IndexSet<RegionID> {
170 IndexSet::from(["GBR".into()])
171 }
172
173 #[rstest]
174 fn test_read_demand_slices_from_iter_valid(
175 svd_commodity: Commodity,
176 region_ids: IndexSet<RegionID>,
177 time_slice_info: TimeSliceInfo,
178 ) {
179 let svd_commodities = get_svd_map(&svd_commodity);
181 let demand_slice = DemandSlice {
182 commodity_id: "commodity1".into(),
183 region_id: "GBR".into(),
184 time_slice: "winter".into(),
185 fraction: Dimensionless(1.0),
186 };
187 let time_slice = time_slice_info
188 .get_time_slice_id_from_str("winter.day")
189 .unwrap();
190 let key = ("commodity1".into(), "GBR".into(), time_slice.into());
191 let expected = DemandSliceMap::from_iter(iter::once((key, Dimensionless(1.0))));
192 assert_eq!(
193 read_demand_slices_from_iter(
194 iter::once(demand_slice.clone()),
195 &svd_commodities,
196 ®ion_ids,
197 &time_slice_info,
198 )
199 .unwrap(),
200 expected
201 );
202 }
203
204 #[rstest]
205 fn test_read_demand_slices_from_iter_valid_multiple_time_slices(
206 svd_commodity: Commodity,
207 region_ids: IndexSet<RegionID>,
208 ) {
209 let svd_commodities = get_svd_map(&svd_commodity);
211 let time_slice_info = TimeSliceInfo {
212 seasons: [("winter".into(), Year(0.5)), ("summer".into(), Year(0.5))]
213 .into_iter()
214 .collect(),
215 times_of_day: ["day".into(), "night".into()].into_iter().collect(),
216 time_slices: [
217 (
218 TimeSliceID {
219 season: "summer".into(),
220 time_of_day: "day".into(),
221 },
222 Year(3.0 / 16.0),
223 ),
224 (
225 TimeSliceID {
226 season: "summer".into(),
227 time_of_day: "night".into(),
228 },
229 Year(5.0 / 16.0),
230 ),
231 (
232 TimeSliceID {
233 season: "winter".into(),
234 time_of_day: "day".into(),
235 },
236 Year(3.0 / 16.0),
237 ),
238 (
239 TimeSliceID {
240 season: "winter".into(),
241 time_of_day: "night".into(),
242 },
243 Year(5.0 / 16.0),
244 ),
245 ]
246 .into_iter()
247 .collect(),
248 };
249 let demand_slices = [
250 DemandSlice {
251 commodity_id: "commodity1".into(),
252 region_id: "GBR".into(),
253 time_slice: "winter".into(),
254 fraction: Dimensionless(0.5),
255 },
256 DemandSlice {
257 commodity_id: "commodity1".into(),
258 region_id: "GBR".into(),
259 time_slice: "summer".into(),
260 fraction: Dimensionless(0.5),
261 },
262 ];
263
264 fn demand_slice_entry(
265 season: &str,
266 time_of_day: &str,
267 fraction: Dimensionless,
268 ) -> ((CommodityID, RegionID, TimeSliceSelection), Dimensionless) {
269 (
270 (
271 "commodity1".into(),
272 "GBR".into(),
273 TimeSliceID {
274 season: season.into(),
275 time_of_day: time_of_day.into(),
276 }
277 .into(),
278 ),
279 fraction,
280 )
281 }
282 let expected = DemandSliceMap::from_iter([
283 demand_slice_entry("summer", "day", Dimensionless(3.0 / 16.0)),
284 demand_slice_entry("summer", "night", Dimensionless(5.0 / 16.0)),
285 demand_slice_entry("winter", "day", Dimensionless(3.0 / 16.0)),
286 demand_slice_entry("winter", "night", Dimensionless(5.0 / 16.0)),
287 ]);
288
289 assert_eq!(
290 read_demand_slices_from_iter(
291 demand_slices.into_iter(),
292 &svd_commodities,
293 ®ion_ids,
294 &time_slice_info,
295 )
296 .unwrap(),
297 expected
298 );
299 }
300
301 #[rstest]
302 fn test_read_demand_slices_from_iter_invalid_empty_file(
303 svd_commodity: Commodity,
304 region_ids: IndexSet<RegionID>,
305 time_slice_info: TimeSliceInfo,
306 ) {
307 let svd_commodities = get_svd_map(&svd_commodity);
309 assert_error!(
310 read_demand_slices_from_iter(
311 iter::empty(),
312 &svd_commodities,
313 ®ion_ids,
314 &time_slice_info,
315 ),
316 "Demand slice missing for time slice(s) 'winter.day' (commodity: commodity1, region GBR)"
317 );
318 }
319
320 #[rstest]
321 fn test_read_demand_slices_from_iter_invalid_bad_commodity(
322 svd_commodity: Commodity,
323 region_ids: IndexSet<RegionID>,
324 time_slice_info: TimeSliceInfo,
325 ) {
326 let svd_commodities = get_svd_map(&svd_commodity);
328 let demand_slice = DemandSlice {
329 commodity_id: "commodity2".into(),
330 region_id: "GBR".into(),
331 time_slice: "winter.day".into(),
332 fraction: Dimensionless(1.0),
333 };
334 assert_error!(
335 read_demand_slices_from_iter(
336 iter::once(demand_slice.clone()),
337 &svd_commodities,
338 ®ion_ids,
339 &time_slice_info,
340 ),
341 "Can only provide demand slice data for SVD commodities. Found entry for 'commodity2'"
342 );
343 }
344
345 #[rstest]
346 fn test_read_demand_slices_from_iter_invalid_bad_region(
347 svd_commodity: Commodity,
348 region_ids: IndexSet<RegionID>,
349 time_slice_info: TimeSliceInfo,
350 ) {
351 let svd_commodities = get_svd_map(&svd_commodity);
353 let demand_slice = DemandSlice {
354 commodity_id: "commodity1".into(),
355 region_id: "FRA".into(),
356 time_slice: "winter.day".into(),
357 fraction: Dimensionless(1.0),
358 };
359 assert_error!(
360 read_demand_slices_from_iter(
361 iter::once(demand_slice.clone()),
362 &svd_commodities,
363 ®ion_ids,
364 &time_slice_info,
365 ),
366 "Unknown ID FRA found"
367 );
368 }
369
370 #[rstest]
371 fn test_read_demand_slices_from_iter_invalid_bad_time_slice(
372 svd_commodity: Commodity,
373 region_ids: IndexSet<RegionID>,
374 time_slice_info: TimeSliceInfo,
375 ) {
376 let svd_commodities = get_svd_map(&svd_commodity);
378 let demand_slice = DemandSlice {
379 commodity_id: "commodity1".into(),
380 region_id: "GBR".into(),
381 time_slice: "summer".into(),
382 fraction: Dimensionless(1.0),
383 };
384 assert_error!(
385 read_demand_slices_from_iter(
386 iter::once(demand_slice.clone()),
387 &svd_commodities,
388 ®ion_ids,
389 &time_slice_info,
390 ),
391 "'summer' is not a valid season"
392 );
393 }
394
395 #[rstest]
396 fn test_read_demand_slices_from_iter_invalid_missing_time_slices(
397 svd_commodity: Commodity,
398 region_ids: IndexSet<RegionID>,
399 ) {
400 let svd_commodities = get_svd_map(&svd_commodity);
402 let time_slice_info = TimeSliceInfo {
403 seasons: [("winter".into(), Year(0.5)), ("summer".into(), Year(0.5))]
404 .into_iter()
405 .collect(),
406 times_of_day: iter::once("day".into()).collect(),
407 time_slices: [
408 (
409 TimeSliceID {
410 season: "winter".into(),
411 time_of_day: "day".into(),
412 },
413 Year(0.5),
414 ),
415 (
416 TimeSliceID {
417 season: "summer".into(),
418 time_of_day: "day".into(),
419 },
420 Year(0.5),
421 ),
422 ]
423 .into_iter()
424 .collect(),
425 };
426 let demand_slice = DemandSlice {
427 commodity_id: "commodity1".into(),
428 region_id: "GBR".into(),
429 time_slice: "winter".into(),
430 fraction: Dimensionless(1.0),
431 };
432 assert_error!(
433 read_demand_slices_from_iter(
434 iter::once(demand_slice.clone()),
435 &svd_commodities,
436 ®ion_ids,
437 &time_slice_info,
438 ),
439 "Demand slice missing for time slice(s) 'summer.day' (commodity: commodity1, region GBR)"
440 );
441 }
442
443 #[rstest]
444 fn test_read_demand_slices_from_iter_invalid_duplicate_time_slice(
445 svd_commodity: Commodity,
446 region_ids: IndexSet<RegionID>,
447 time_slice_info: TimeSliceInfo,
448 ) {
449 let svd_commodities = get_svd_map(&svd_commodity);
451 let demand_slice = DemandSlice {
452 commodity_id: "commodity1".into(),
453 region_id: "GBR".into(),
454 time_slice: "winter.day".into(),
455 fraction: Dimensionless(0.5),
456 };
457 assert_error!(
458 read_demand_slices_from_iter(
459 iter::repeat_n(demand_slice.clone(), 2),
460 &svd_commodities,
461 ®ion_ids,
462 &time_slice_info,
463 ),
464 "Duplicate demand slicing entry (or same time slice covered by more than one entry) \
465 (commodity: commodity1, region: GBR, time slice(s): winter.day)"
466 );
467 }
468
469 #[rstest]
470 fn test_read_demand_slices_from_iter_invalid_season_time_slice_conflict(
471 svd_commodity: Commodity,
472 region_ids: IndexSet<RegionID>,
473 time_slice_info: TimeSliceInfo,
474 ) {
475 let svd_commodities = get_svd_map(&svd_commodity);
477 let demand_slice = DemandSlice {
478 commodity_id: "commodity1".into(),
479 region_id: "GBR".into(),
480 time_slice: "winter.day".into(),
481 fraction: Dimensionless(0.5),
482 };
483 let demand_slice_season = DemandSlice {
484 commodity_id: "commodity1".into(),
485 region_id: "GBR".into(),
486 time_slice: "winter".into(),
487 fraction: Dimensionless(0.5),
488 };
489 assert_error!(
490 read_demand_slices_from_iter(
491 [demand_slice, demand_slice_season].into_iter(),
492 &svd_commodities,
493 ®ion_ids,
494 &time_slice_info,
495 ),
496 "Duplicate demand slicing entry (or same time slice covered by more than one entry) \
497 (commodity: commodity1, region: GBR, time slice(s): winter.day)"
498 );
499 }
500
501 #[rstest]
502 fn test_read_demand_slices_from_iter_invalid_bad_fractions(
503 svd_commodity: Commodity,
504 region_ids: IndexSet<RegionID>,
505 time_slice_info: TimeSliceInfo,
506 ) {
507 let svd_commodities = get_svd_map(&svd_commodity);
509 let demand_slice = DemandSlice {
510 commodity_id: "commodity1".into(),
511 region_id: "GBR".into(),
512 time_slice: "winter".into(),
513 fraction: Dimensionless(0.5),
514 };
515 assert_error!(
516 read_demand_slices_from_iter(
517 iter::once(demand_slice),
518 &svd_commodities,
519 ®ion_ids,
520 &time_slice_info,
521 ),
522 "Invalid demand fractions"
523 );
524 }
525}