1use super::super::*;
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::{ensure, Context, Result};
10use indexmap::IndexSet;
11use itertools::{iproduct, Itertools};
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 #[serde(deserialize_with = "deserialise_proportion_nonzero")]
24 fraction: Dimensionless,
25}
26
27pub type DemandSliceMap = HashMap<(CommodityID, RegionID, TimeSliceSelection), Dimensionless>;
29
30pub fn read_demand_slices(
40 model_dir: &Path,
41 svd_commodities: &BorrowedCommodityMap,
42 region_ids: &IndexSet<RegionID>,
43 time_slice_info: &TimeSliceInfo,
44) -> Result<DemandSliceMap> {
45 let file_path = model_dir.join(DEMAND_SLICING_FILE_NAME);
46 let demand_slices_csv = read_csv(&file_path)?;
47 read_demand_slices_from_iter(
48 demand_slices_csv,
49 svd_commodities,
50 region_ids,
51 time_slice_info,
52 )
53 .with_context(|| input_err_msg(file_path))
54}
55
56fn read_demand_slices_from_iter<I>(
58 iter: I,
59 svd_commodities: &BorrowedCommodityMap,
60 region_ids: &IndexSet<RegionID>,
61 time_slice_info: &TimeSliceInfo,
62) -> Result<DemandSliceMap>
63where
64 I: Iterator<Item = DemandSlice>,
65{
66 let mut demand_slices = DemandSliceMap::new();
67
68 for slice in iter {
69 let commodity = svd_commodities
70 .get(slice.commodity_id.as_str())
71 .with_context(|| {
72 format!(
73 "Can only provide demand slice data for SVD commodities. Found entry for '{}'",
74 slice.commodity_id
75 )
76 })?;
77 let region_id = region_ids.get_id(&slice.region_id)?;
78
79 let ts_selection = time_slice_info.get_selection(&slice.time_slice)?;
83
84 let iter = time_slice_info
86 .calculate_share(&ts_selection, commodity.time_slice_level, slice.fraction)
87 .with_context(|| {
88 format!(
89 "Cannot provide demand at {:?} level when commodity time slice level is {:?}",
90 ts_selection.level(),
91 commodity.time_slice_level
92 )
93 })?;
94 for (ts_selection, demand_fraction) in iter {
95 let existing = demand_slices
96 .insert(
97 (
98 commodity.id.clone(),
99 region_id.clone(),
100 ts_selection.clone(),
101 ),
102 demand_fraction,
103 )
104 .is_some();
105 ensure!(!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,region_id,ts_selection
109 );
110 }
111 }
112
113 validate_demand_slices(svd_commodities, region_ids, &demand_slices, time_slice_info)?;
114
115 Ok(demand_slices)
116}
117
118fn validate_demand_slices(
126 svd_commodities: &BorrowedCommodityMap,
127 region_ids: &IndexSet<RegionID>,
128 demand_slices: &DemandSliceMap,
129 time_slice_info: &TimeSliceInfo,
130) -> Result<()> {
131 for (commodity, region_id) in iproduct!(svd_commodities.values(), region_ids) {
132 time_slice_info
133 .iter_selections_at_level(commodity.time_slice_level)
134 .map(|ts_selection| {
135 demand_slices
136 .get(&(
137 commodity.id.clone(),
138 region_id.clone(),
139 ts_selection.clone(),
140 ))
141 .with_context(|| {
142 format!(
143 "Demand slice missing for time slice(s) '{}' (commodity: {}, region {})",
144 ts_selection, commodity.id, region_id
145 )
146 })
147 })
148 .process_results(|iter| {
149 check_fractions_sum_to_one(iter.copied()).context("Invalid demand fractions")
150 })??;
151 }
152
153 Ok(())
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::commodity::Commodity;
160 use crate::fixture::{assert_error, get_svd_map, svd_commodity, time_slice_info};
161 use crate::time_slice::TimeSliceID;
162 use rstest::{fixture, rstest};
163 use std::iter;
164
165 #[fixture]
166 pub fn region_ids() -> IndexSet<RegionID> {
167 IndexSet::from(["GBR".into()])
168 }
169
170 #[rstest]
171 fn test_read_demand_slices_from_iter_valid(
172 svd_commodity: Commodity,
173 region_ids: IndexSet<RegionID>,
174 time_slice_info: TimeSliceInfo,
175 ) {
176 let svd_commodities = get_svd_map(&svd_commodity);
178 let demand_slice = DemandSlice {
179 commodity_id: "commodity1".into(),
180 region_id: "GBR".into(),
181 time_slice: "winter".into(),
182 fraction: Dimensionless(1.0),
183 };
184 let time_slice = time_slice_info
185 .get_time_slice_id_from_str("winter.day")
186 .unwrap();
187 let key = ("commodity1".into(), "GBR".into(), time_slice.into());
188 let expected = DemandSliceMap::from_iter(iter::once((key, Dimensionless(1.0))));
189 assert_eq!(
190 read_demand_slices_from_iter(
191 iter::once(demand_slice.clone()),
192 &svd_commodities,
193 ®ion_ids,
194 &time_slice_info,
195 )
196 .unwrap(),
197 expected
198 );
199 }
200
201 #[rstest]
202 fn test_read_demand_slices_from_iter_valid_multiple_time_slices(
203 svd_commodity: Commodity,
204 region_ids: IndexSet<RegionID>,
205 ) {
206 let svd_commodities = get_svd_map(&svd_commodity);
208 let time_slice_info = TimeSliceInfo {
209 seasons: [
210 ("winter".into(), Dimensionless(0.5)),
211 ("summer".into(), Dimensionless(0.5)),
212 ]
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 Dimensionless(3.0 / 16.0),
223 ),
224 (
225 TimeSliceID {
226 season: "summer".into(),
227 time_of_day: "night".into(),
228 },
229 Dimensionless(5.0 / 16.0),
230 ),
231 (
232 TimeSliceID {
233 season: "winter".into(),
234 time_of_day: "day".into(),
235 },
236 Dimensionless(3.0 / 16.0),
237 ),
238 (
239 TimeSliceID {
240 season: "winter".into(),
241 time_of_day: "night".into(),
242 },
243 Dimensionless(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: [
404 ("winter".into(), Dimensionless(0.5)),
405 ("summer".into(), Dimensionless(0.5)),
406 ]
407 .into_iter()
408 .collect(),
409 times_of_day: iter::once("day".into()).collect(),
410 time_slices: [
411 (
412 TimeSliceID {
413 season: "winter".into(),
414 time_of_day: "day".into(),
415 },
416 Dimensionless(0.5),
417 ),
418 (
419 TimeSliceID {
420 season: "summer".into(),
421 time_of_day: "day".into(),
422 },
423 Dimensionless(0.5),
424 ),
425 ]
426 .into_iter()
427 .collect(),
428 };
429 let demand_slice = DemandSlice {
430 commodity_id: "commodity1".into(),
431 region_id: "GBR".into(),
432 time_slice: "winter".into(),
433 fraction: Dimensionless(1.0),
434 };
435 assert_error!(
436 read_demand_slices_from_iter(
437 iter::once(demand_slice.clone()),
438 &svd_commodities,
439 ®ion_ids,
440 &time_slice_info,
441 ),
442 "Demand slice missing for time slice(s) 'summer.day' (commodity: commodity1, region GBR)"
443 );
444 }
445
446 #[rstest]
447 fn test_read_demand_slices_from_iter_invalid_duplicate_time_slice(
448 svd_commodity: Commodity,
449 region_ids: IndexSet<RegionID>,
450 time_slice_info: TimeSliceInfo,
451 ) {
452 let svd_commodities = get_svd_map(&svd_commodity);
454 let demand_slice = DemandSlice {
455 commodity_id: "commodity1".into(),
456 region_id: "GBR".into(),
457 time_slice: "winter.day".into(),
458 fraction: Dimensionless(0.5),
459 };
460 assert_error!(
461 read_demand_slices_from_iter(
462 iter::repeat_n(demand_slice.clone(), 2),
463 &svd_commodities,
464 ®ion_ids,
465 &time_slice_info,
466 ),
467 "Duplicate demand slicing entry (or same time slice covered by more than one entry) \
468 (commodity: commodity1, region: GBR, time slice(s): winter.day)"
469 );
470 }
471
472 #[rstest]
473 fn test_read_demand_slices_from_iter_invalid_season_time_slice_conflict(
474 svd_commodity: Commodity,
475 region_ids: IndexSet<RegionID>,
476 time_slice_info: TimeSliceInfo,
477 ) {
478 let svd_commodities = get_svd_map(&svd_commodity);
480 let demand_slice = DemandSlice {
481 commodity_id: "commodity1".into(),
482 region_id: "GBR".into(),
483 time_slice: "winter.day".into(),
484 fraction: Dimensionless(0.5),
485 };
486 let demand_slice_season = DemandSlice {
487 commodity_id: "commodity1".into(),
488 region_id: "GBR".into(),
489 time_slice: "winter".into(),
490 fraction: Dimensionless(0.5),
491 };
492 assert_error!(
493 read_demand_slices_from_iter(
494 [demand_slice, demand_slice_season].into_iter(),
495 &svd_commodities,
496 ®ion_ids,
497 &time_slice_info,
498 ),
499 "Duplicate demand slicing entry (or same time slice covered by more than one entry) \
500 (commodity: commodity1, region: GBR, time slice(s): winter.day)"
501 );
502 }
503
504 #[rstest]
505 fn test_read_demand_slices_from_iter_invalid_bad_fractions(
506 svd_commodity: Commodity,
507 region_ids: IndexSet<RegionID>,
508 time_slice_info: TimeSliceInfo,
509 ) {
510 let svd_commodities = get_svd_map(&svd_commodity);
512 let demand_slice = DemandSlice {
513 commodity_id: "commodity1".into(),
514 region_id: "GBR".into(),
515 time_slice: "winter".into(),
516 fraction: Dimensionless(0.5),
517 };
518 assert_error!(
519 read_demand_slices_from_iter(
520 iter::once(demand_slice),
521 &svd_commodities,
522 ®ion_ids,
523 &time_slice_info,
524 ),
525 "Invalid demand fractions"
526 );
527 }
528}