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::{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 #[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!(
106 !existing,
107 "Duplicate demand slicing entry (or same time slice covered by more than one entry) \
108 (commodity: {}, region: {}, time slice(s): {})",
109 commodity.id,
110 region_id,
111 ts_selection
112 );
113 }
114 }
115
116 validate_demand_slices(svd_commodities, region_ids, &demand_slices, time_slice_info)?;
117
118 Ok(demand_slices)
119}
120
121fn validate_demand_slices(
129 svd_commodities: &BorrowedCommodityMap,
130 region_ids: &IndexSet<RegionID>,
131 demand_slices: &DemandSliceMap,
132 time_slice_info: &TimeSliceInfo,
133) -> Result<()> {
134 for (commodity, region_id) in iproduct!(svd_commodities.values(), region_ids) {
135 time_slice_info
136 .iter_selections_at_level(commodity.time_slice_level)
137 .map(|ts_selection| {
138 demand_slices
139 .get(&(
140 commodity.id.clone(),
141 region_id.clone(),
142 ts_selection.clone(),
143 ))
144 .with_context(|| {
145 format!(
146 "Demand slice missing for time slice(s) '{}' (commodity: {}, region {})",
147 ts_selection, commodity.id, region_id
148 )
149 })
150 })
151 .process_results(|iter| {
152 check_values_sum_to_one_approx(iter.copied()).context("Invalid demand fractions")
153 })??;
154 }
155
156 Ok(())
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use crate::commodity::Commodity;
163 use crate::fixture::{assert_error, get_svd_map, svd_commodity, time_slice_info};
164 use crate::time_slice::TimeSliceID;
165 use crate::units::Year;
166 use rstest::{fixture, rstest};
167 use std::iter;
168
169 #[fixture]
170 pub fn region_ids() -> IndexSet<RegionID> {
171 IndexSet::from(["GBR".into()])
172 }
173
174 #[rstest]
175 fn test_read_demand_slices_from_iter_valid(
176 svd_commodity: Commodity,
177 region_ids: IndexSet<RegionID>,
178 time_slice_info: TimeSliceInfo,
179 ) {
180 let svd_commodities = get_svd_map(&svd_commodity);
182 let demand_slice = DemandSlice {
183 commodity_id: "commodity1".into(),
184 region_id: "GBR".into(),
185 time_slice: "winter".into(),
186 fraction: Dimensionless(1.0),
187 };
188 let time_slice = time_slice_info
189 .get_time_slice_id_from_str("winter.day")
190 .unwrap();
191 let key = ("commodity1".into(), "GBR".into(), time_slice.into());
192 let expected = DemandSliceMap::from_iter(iter::once((key, Dimensionless(1.0))));
193 assert_eq!(
194 read_demand_slices_from_iter(
195 iter::once(demand_slice.clone()),
196 &svd_commodities,
197 ®ion_ids,
198 &time_slice_info,
199 )
200 .unwrap(),
201 expected
202 );
203 }
204
205 #[rstest]
206 fn test_read_demand_slices_from_iter_valid_multiple_time_slices(
207 svd_commodity: Commodity,
208 region_ids: IndexSet<RegionID>,
209 ) {
210 let svd_commodities = get_svd_map(&svd_commodity);
212 let time_slice_info = TimeSliceInfo {
213 seasons: [("winter".into(), Year(0.5)), ("summer".into(), Year(0.5))]
214 .into_iter()
215 .collect(),
216 times_of_day: ["day".into(), "night".into()].into_iter().collect(),
217 time_slices: [
218 (
219 TimeSliceID {
220 season: "summer".into(),
221 time_of_day: "day".into(),
222 },
223 Year(3.0 / 16.0),
224 ),
225 (
226 TimeSliceID {
227 season: "summer".into(),
228 time_of_day: "night".into(),
229 },
230 Year(5.0 / 16.0),
231 ),
232 (
233 TimeSliceID {
234 season: "winter".into(),
235 time_of_day: "day".into(),
236 },
237 Year(3.0 / 16.0),
238 ),
239 (
240 TimeSliceID {
241 season: "winter".into(),
242 time_of_day: "night".into(),
243 },
244 Year(5.0 / 16.0),
245 ),
246 ]
247 .into_iter()
248 .collect(),
249 };
250 let demand_slices = [
251 DemandSlice {
252 commodity_id: "commodity1".into(),
253 region_id: "GBR".into(),
254 time_slice: "winter".into(),
255 fraction: Dimensionless(0.5),
256 },
257 DemandSlice {
258 commodity_id: "commodity1".into(),
259 region_id: "GBR".into(),
260 time_slice: "summer".into(),
261 fraction: Dimensionless(0.5),
262 },
263 ];
264
265 fn demand_slice_entry(
266 season: &str,
267 time_of_day: &str,
268 fraction: Dimensionless,
269 ) -> ((CommodityID, RegionID, TimeSliceSelection), Dimensionless) {
270 (
271 (
272 "commodity1".into(),
273 "GBR".into(),
274 TimeSliceID {
275 season: season.into(),
276 time_of_day: time_of_day.into(),
277 }
278 .into(),
279 ),
280 fraction,
281 )
282 }
283 let expected = DemandSliceMap::from_iter([
284 demand_slice_entry("summer", "day", Dimensionless(3.0 / 16.0)),
285 demand_slice_entry("summer", "night", Dimensionless(5.0 / 16.0)),
286 demand_slice_entry("winter", "day", Dimensionless(3.0 / 16.0)),
287 demand_slice_entry("winter", "night", Dimensionless(5.0 / 16.0)),
288 ]);
289
290 assert_eq!(
291 read_demand_slices_from_iter(
292 demand_slices.into_iter(),
293 &svd_commodities,
294 ®ion_ids,
295 &time_slice_info,
296 )
297 .unwrap(),
298 expected
299 );
300 }
301
302 #[rstest]
303 fn test_read_demand_slices_from_iter_invalid_empty_file(
304 svd_commodity: Commodity,
305 region_ids: IndexSet<RegionID>,
306 time_slice_info: TimeSliceInfo,
307 ) {
308 let svd_commodities = get_svd_map(&svd_commodity);
310 assert_error!(
311 read_demand_slices_from_iter(
312 iter::empty(),
313 &svd_commodities,
314 ®ion_ids,
315 &time_slice_info,
316 ),
317 "Demand slice missing for time slice(s) 'winter.day' (commodity: commodity1, region GBR)"
318 );
319 }
320
321 #[rstest]
322 fn test_read_demand_slices_from_iter_invalid_bad_commodity(
323 svd_commodity: Commodity,
324 region_ids: IndexSet<RegionID>,
325 time_slice_info: TimeSliceInfo,
326 ) {
327 let svd_commodities = get_svd_map(&svd_commodity);
329 let demand_slice = DemandSlice {
330 commodity_id: "commodity2".into(),
331 region_id: "GBR".into(),
332 time_slice: "winter.day".into(),
333 fraction: Dimensionless(1.0),
334 };
335 assert_error!(
336 read_demand_slices_from_iter(
337 iter::once(demand_slice.clone()),
338 &svd_commodities,
339 ®ion_ids,
340 &time_slice_info,
341 ),
342 "Can only provide demand slice data for SVD commodities. Found entry for 'commodity2'"
343 );
344 }
345
346 #[rstest]
347 fn test_read_demand_slices_from_iter_invalid_bad_region(
348 svd_commodity: Commodity,
349 region_ids: IndexSet<RegionID>,
350 time_slice_info: TimeSliceInfo,
351 ) {
352 let svd_commodities = get_svd_map(&svd_commodity);
354 let demand_slice = DemandSlice {
355 commodity_id: "commodity1".into(),
356 region_id: "FRA".into(),
357 time_slice: "winter.day".into(),
358 fraction: Dimensionless(1.0),
359 };
360 assert_error!(
361 read_demand_slices_from_iter(
362 iter::once(demand_slice.clone()),
363 &svd_commodities,
364 ®ion_ids,
365 &time_slice_info,
366 ),
367 "Unknown ID FRA found"
368 );
369 }
370
371 #[rstest]
372 fn test_read_demand_slices_from_iter_invalid_bad_time_slice(
373 svd_commodity: Commodity,
374 region_ids: IndexSet<RegionID>,
375 time_slice_info: TimeSliceInfo,
376 ) {
377 let svd_commodities = get_svd_map(&svd_commodity);
379 let demand_slice = DemandSlice {
380 commodity_id: "commodity1".into(),
381 region_id: "GBR".into(),
382 time_slice: "summer".into(),
383 fraction: Dimensionless(1.0),
384 };
385 assert_error!(
386 read_demand_slices_from_iter(
387 iter::once(demand_slice.clone()),
388 &svd_commodities,
389 ®ion_ids,
390 &time_slice_info,
391 ),
392 "'summer' is not a valid season"
393 );
394 }
395
396 #[rstest]
397 fn test_read_demand_slices_from_iter_invalid_missing_time_slices(
398 svd_commodity: Commodity,
399 region_ids: IndexSet<RegionID>,
400 ) {
401 let svd_commodities = get_svd_map(&svd_commodity);
403 let time_slice_info = TimeSliceInfo {
404 seasons: [("winter".into(), Year(0.5)), ("summer".into(), Year(0.5))]
405 .into_iter()
406 .collect(),
407 times_of_day: iter::once("day".into()).collect(),
408 time_slices: [
409 (
410 TimeSliceID {
411 season: "winter".into(),
412 time_of_day: "day".into(),
413 },
414 Year(0.5),
415 ),
416 (
417 TimeSliceID {
418 season: "summer".into(),
419 time_of_day: "day".into(),
420 },
421 Year(0.5),
422 ),
423 ]
424 .into_iter()
425 .collect(),
426 };
427 let demand_slice = DemandSlice {
428 commodity_id: "commodity1".into(),
429 region_id: "GBR".into(),
430 time_slice: "winter".into(),
431 fraction: Dimensionless(1.0),
432 };
433 assert_error!(
434 read_demand_slices_from_iter(
435 iter::once(demand_slice.clone()),
436 &svd_commodities,
437 ®ion_ids,
438 &time_slice_info,
439 ),
440 "Demand slice missing for time slice(s) 'summer.day' (commodity: commodity1, region GBR)"
441 );
442 }
443
444 #[rstest]
445 fn test_read_demand_slices_from_iter_invalid_duplicate_time_slice(
446 svd_commodity: Commodity,
447 region_ids: IndexSet<RegionID>,
448 time_slice_info: TimeSliceInfo,
449 ) {
450 let svd_commodities = get_svd_map(&svd_commodity);
452 let demand_slice = DemandSlice {
453 commodity_id: "commodity1".into(),
454 region_id: "GBR".into(),
455 time_slice: "winter.day".into(),
456 fraction: Dimensionless(0.5),
457 };
458 assert_error!(
459 read_demand_slices_from_iter(
460 iter::repeat_n(demand_slice.clone(), 2),
461 &svd_commodities,
462 ®ion_ids,
463 &time_slice_info,
464 ),
465 "Duplicate demand slicing entry (or same time slice covered by more than one entry) \
466 (commodity: commodity1, region: GBR, time slice(s): winter.day)"
467 );
468 }
469
470 #[rstest]
471 fn test_read_demand_slices_from_iter_invalid_season_time_slice_conflict(
472 svd_commodity: Commodity,
473 region_ids: IndexSet<RegionID>,
474 time_slice_info: TimeSliceInfo,
475 ) {
476 let svd_commodities = get_svd_map(&svd_commodity);
478 let demand_slice = DemandSlice {
479 commodity_id: "commodity1".into(),
480 region_id: "GBR".into(),
481 time_slice: "winter.day".into(),
482 fraction: Dimensionless(0.5),
483 };
484 let demand_slice_season = DemandSlice {
485 commodity_id: "commodity1".into(),
486 region_id: "GBR".into(),
487 time_slice: "winter".into(),
488 fraction: Dimensionless(0.5),
489 };
490 assert_error!(
491 read_demand_slices_from_iter(
492 [demand_slice, demand_slice_season].into_iter(),
493 &svd_commodities,
494 ®ion_ids,
495 &time_slice_info,
496 ),
497 "Duplicate demand slicing entry (or same time slice covered by more than one entry) \
498 (commodity: commodity1, region: GBR, time slice(s): winter.day)"
499 );
500 }
501
502 #[rstest]
503 fn test_read_demand_slices_from_iter_invalid_bad_fractions(
504 svd_commodity: Commodity,
505 region_ids: IndexSet<RegionID>,
506 time_slice_info: TimeSliceInfo,
507 ) {
508 let svd_commodities = get_svd_map(&svd_commodity);
510 let demand_slice = DemandSlice {
511 commodity_id: "commodity1".into(),
512 region_id: "GBR".into(),
513 time_slice: "winter".into(),
514 fraction: Dimensionless(0.5),
515 };
516 assert_error!(
517 read_demand_slices_from_iter(
518 iter::once(demand_slice),
519 &svd_commodities,
520 ®ion_ids,
521 &time_slice_info,
522 ),
523 "Invalid demand fractions"
524 );
525 }
526}