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