1use super::super::{
3 check_values_sum_to_one_approx, deserialise_proportion_nonzero, input_err_msg, read_csv,
4};
5use crate::commodity::CommodityID;
6use crate::id::IDCollection;
7use crate::input::commodity::demand::BorrowedCommodityMap;
8use crate::region::RegionID;
9use crate::time_slice::{TimeSliceInfo, TimeSliceSelection};
10use crate::units::Dimensionless;
11use anyhow::{Context, Result, ensure};
12use indexmap::IndexSet;
13use itertools::{Itertools, iproduct};
14use serde::Deserialize;
15use std::collections::HashMap;
16use std::path::Path;
17
18const DEMAND_SLICING_FILE_NAME: &str = "demand_slicing.csv";
19
20#[derive(Clone, Deserialize)]
21struct DemandSlice {
22 commodity_id: String,
23 region_id: String,
24 time_slice: String,
25 #[serde(deserialize_with = "deserialise_proportion_nonzero")]
26 fraction: Dimensionless,
27}
28
29pub type DemandSliceMap = HashMap<(CommodityID, RegionID, TimeSliceSelection), Dimensionless>;
31
32pub fn read_demand_slices(
42 model_dir: &Path,
43 svd_commodities: &BorrowedCommodityMap,
44 region_ids: &IndexSet<RegionID>,
45 time_slice_info: &TimeSliceInfo,
46) -> Result<DemandSliceMap> {
47 let file_path = model_dir.join(DEMAND_SLICING_FILE_NAME);
48 let demand_slices_csv = read_csv(&file_path)?;
49 read_demand_slices_from_iter(
50 demand_slices_csv,
51 svd_commodities,
52 region_ids,
53 time_slice_info,
54 )
55 .with_context(|| input_err_msg(file_path))
56}
57
58fn read_demand_slices_from_iter<I>(
60 iter: I,
61 svd_commodities: &BorrowedCommodityMap,
62 region_ids: &IndexSet<RegionID>,
63 time_slice_info: &TimeSliceInfo,
64) -> Result<DemandSliceMap>
65where
66 I: Iterator<Item = DemandSlice>,
67{
68 let mut demand_slices = DemandSliceMap::new();
69
70 for slice in iter {
71 let commodity = svd_commodities
72 .get(slice.commodity_id.as_str())
73 .with_context(|| {
74 format!(
75 "Can only provide demand slice data for SVD commodities. Found entry for '{}'",
76 slice.commodity_id
77 )
78 })?;
79 let region_id = region_ids.get_id(&slice.region_id)?;
80
81 let ts_selection = time_slice_info.get_selection(&slice.time_slice)?;
85
86 let iter = time_slice_info
88 .calculate_share(&ts_selection, commodity.time_slice_level, slice.fraction)
89 .with_context(|| {
90 format!(
91 "Cannot provide demand at {:?} level when commodity time slice level is {:?}",
92 ts_selection.level(),
93 commodity.time_slice_level
94 )
95 })?;
96 for (ts_selection, demand_fraction) in iter {
97 let existing = demand_slices
98 .insert(
99 (
100 commodity.id.clone(),
101 region_id.clone(),
102 ts_selection.clone(),
103 ),
104 demand_fraction,
105 )
106 .is_some();
107 ensure!(
108 !existing,
109 "Duplicate demand slicing entry (or same time slice covered by more than one entry) \
110 (commodity: {}, region: {}, time slice(s): {})",
111 commodity.id,
112 region_id,
113 ts_selection
114 );
115 }
116 }
117
118 validate_demand_slices(svd_commodities, region_ids, &demand_slices, time_slice_info)?;
119
120 Ok(demand_slices)
121}
122
123fn validate_demand_slices(
131 svd_commodities: &BorrowedCommodityMap,
132 region_ids: &IndexSet<RegionID>,
133 demand_slices: &DemandSliceMap,
134 time_slice_info: &TimeSliceInfo,
135) -> Result<()> {
136 for (commodity, region_id) in iproduct!(svd_commodities.values(), region_ids) {
137 time_slice_info
138 .iter_selections_at_level(commodity.time_slice_level)
139 .map(|ts_selection| {
140 demand_slices
141 .get(&(
142 commodity.id.clone(),
143 region_id.clone(),
144 ts_selection.clone(),
145 ))
146 .with_context(|| {
147 format!(
148 "Demand slice missing for time slice(s) '{}' (commodity: {}, region {})",
149 ts_selection, commodity.id, region_id
150 )
151 })
152 })
153 .process_results(|iter| {
154 check_values_sum_to_one_approx(iter.copied()).context("Invalid demand fractions")
155 })??;
156 }
157
158 Ok(())
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::commodity::Commodity;
165 use crate::fixture::{assert_error, get_svd_map, svd_commodity, time_slice_info};
166 use crate::time_slice::TimeSliceID;
167 use crate::units::Year;
168 use rstest::{fixture, rstest};
169 use std::iter;
170
171 #[fixture]
172 pub fn region_ids() -> IndexSet<RegionID> {
173 IndexSet::from(["GBR".into()])
174 }
175
176 #[rstest]
177 fn test_read_demand_slices_from_iter_valid(
178 svd_commodity: Commodity,
179 region_ids: IndexSet<RegionID>,
180 time_slice_info: TimeSliceInfo,
181 ) {
182 let svd_commodities = get_svd_map(&svd_commodity);
184 let demand_slice = DemandSlice {
185 commodity_id: "commodity1".into(),
186 region_id: "GBR".into(),
187 time_slice: "winter".into(),
188 fraction: Dimensionless(1.0),
189 };
190 let time_slice = time_slice_info
191 .get_time_slice_id_from_str("winter.day")
192 .unwrap();
193 let key = ("commodity1".into(), "GBR".into(), time_slice.into());
194 let expected = DemandSliceMap::from_iter(iter::once((key, Dimensionless(1.0))));
195 assert_eq!(
196 read_demand_slices_from_iter(
197 iter::once(demand_slice.clone()),
198 &svd_commodities,
199 ®ion_ids,
200 &time_slice_info,
201 )
202 .unwrap(),
203 expected
204 );
205 }
206
207 #[rstest]
208 fn test_read_demand_slices_from_iter_valid_multiple_time_slices(
209 svd_commodity: Commodity,
210 region_ids: IndexSet<RegionID>,
211 ) {
212 let svd_commodities = get_svd_map(&svd_commodity);
214 let time_slice_info = TimeSliceInfo {
215 seasons: [("winter".into(), Year(0.5)), ("summer".into(), Year(0.5))]
216 .into_iter()
217 .collect(),
218 times_of_day: ["day".into(), "night".into()].into_iter().collect(),
219 time_slices: [
220 (
221 TimeSliceID {
222 season: "summer".into(),
223 time_of_day: "day".into(),
224 },
225 Year(3.0 / 16.0),
226 ),
227 (
228 TimeSliceID {
229 season: "summer".into(),
230 time_of_day: "night".into(),
231 },
232 Year(5.0 / 16.0),
233 ),
234 (
235 TimeSliceID {
236 season: "winter".into(),
237 time_of_day: "day".into(),
238 },
239 Year(3.0 / 16.0),
240 ),
241 (
242 TimeSliceID {
243 season: "winter".into(),
244 time_of_day: "night".into(),
245 },
246 Year(5.0 / 16.0),
247 ),
248 ]
249 .into_iter()
250 .collect(),
251 };
252 let demand_slices = [
253 DemandSlice {
254 commodity_id: "commodity1".into(),
255 region_id: "GBR".into(),
256 time_slice: "winter".into(),
257 fraction: Dimensionless(0.5),
258 },
259 DemandSlice {
260 commodity_id: "commodity1".into(),
261 region_id: "GBR".into(),
262 time_slice: "summer".into(),
263 fraction: Dimensionless(0.5),
264 },
265 ];
266
267 fn demand_slice_entry(
268 season: &str,
269 time_of_day: &str,
270 fraction: Dimensionless,
271 ) -> ((CommodityID, RegionID, TimeSliceSelection), Dimensionless) {
272 (
273 (
274 "commodity1".into(),
275 "GBR".into(),
276 TimeSliceID {
277 season: season.into(),
278 time_of_day: time_of_day.into(),
279 }
280 .into(),
281 ),
282 fraction,
283 )
284 }
285 let expected = DemandSliceMap::from_iter([
286 demand_slice_entry("summer", "day", Dimensionless(3.0 / 16.0)),
287 demand_slice_entry("summer", "night", Dimensionless(5.0 / 16.0)),
288 demand_slice_entry("winter", "day", Dimensionless(3.0 / 16.0)),
289 demand_slice_entry("winter", "night", Dimensionless(5.0 / 16.0)),
290 ]);
291
292 assert_eq!(
293 read_demand_slices_from_iter(
294 demand_slices.into_iter(),
295 &svd_commodities,
296 ®ion_ids,
297 &time_slice_info,
298 )
299 .unwrap(),
300 expected
301 );
302 }
303
304 #[rstest]
305 fn test_read_demand_slices_from_iter_invalid_empty_file(
306 svd_commodity: Commodity,
307 region_ids: IndexSet<RegionID>,
308 time_slice_info: TimeSliceInfo,
309 ) {
310 let svd_commodities = get_svd_map(&svd_commodity);
312 assert_error!(
313 read_demand_slices_from_iter(
314 iter::empty(),
315 &svd_commodities,
316 ®ion_ids,
317 &time_slice_info,
318 ),
319 "Demand slice missing for time slice(s) 'winter.day' (commodity: commodity1, region GBR)"
320 );
321 }
322
323 #[rstest]
324 fn test_read_demand_slices_from_iter_invalid_bad_commodity(
325 svd_commodity: Commodity,
326 region_ids: IndexSet<RegionID>,
327 time_slice_info: TimeSliceInfo,
328 ) {
329 let svd_commodities = get_svd_map(&svd_commodity);
331 let demand_slice = DemandSlice {
332 commodity_id: "commodity2".into(),
333 region_id: "GBR".into(),
334 time_slice: "winter.day".into(),
335 fraction: Dimensionless(1.0),
336 };
337 assert_error!(
338 read_demand_slices_from_iter(
339 iter::once(demand_slice.clone()),
340 &svd_commodities,
341 ®ion_ids,
342 &time_slice_info,
343 ),
344 "Can only provide demand slice data for SVD commodities. Found entry for 'commodity2'"
345 );
346 }
347
348 #[rstest]
349 fn test_read_demand_slices_from_iter_invalid_bad_region(
350 svd_commodity: Commodity,
351 region_ids: IndexSet<RegionID>,
352 time_slice_info: TimeSliceInfo,
353 ) {
354 let svd_commodities = get_svd_map(&svd_commodity);
356 let demand_slice = DemandSlice {
357 commodity_id: "commodity1".into(),
358 region_id: "FRA".into(),
359 time_slice: "winter.day".into(),
360 fraction: Dimensionless(1.0),
361 };
362 assert_error!(
363 read_demand_slices_from_iter(
364 iter::once(demand_slice.clone()),
365 &svd_commodities,
366 ®ion_ids,
367 &time_slice_info,
368 ),
369 "Unknown ID FRA found"
370 );
371 }
372
373 #[rstest]
374 fn test_read_demand_slices_from_iter_invalid_bad_time_slice(
375 svd_commodity: Commodity,
376 region_ids: IndexSet<RegionID>,
377 time_slice_info: TimeSliceInfo,
378 ) {
379 let svd_commodities = get_svd_map(&svd_commodity);
381 let demand_slice = DemandSlice {
382 commodity_id: "commodity1".into(),
383 region_id: "GBR".into(),
384 time_slice: "summer".into(),
385 fraction: Dimensionless(1.0),
386 };
387 assert_error!(
388 read_demand_slices_from_iter(
389 iter::once(demand_slice.clone()),
390 &svd_commodities,
391 ®ion_ids,
392 &time_slice_info,
393 ),
394 "'summer' is not a valid season"
395 );
396 }
397
398 #[rstest]
399 fn test_read_demand_slices_from_iter_invalid_missing_time_slices(
400 svd_commodity: Commodity,
401 region_ids: IndexSet<RegionID>,
402 ) {
403 let svd_commodities = get_svd_map(&svd_commodity);
405 let time_slice_info = TimeSliceInfo {
406 seasons: [("winter".into(), Year(0.5)), ("summer".into(), Year(0.5))]
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 Year(0.5),
417 ),
418 (
419 TimeSliceID {
420 season: "summer".into(),
421 time_of_day: "day".into(),
422 },
423 Year(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}