1use super::super::*;
3use crate::commodity::CommodityID;
4use crate::id::IDCollection;
5use crate::region::RegionID;
6use crate::time_slice::{TimeSliceID, TimeSliceInfo};
7use anyhow::{ensure, Context, Result};
8use itertools::Itertools;
9use serde::Deserialize;
10use std::collections::{HashMap, HashSet};
11use std::path::Path;
12
13const DEMAND_SLICING_FILE_NAME: &str = "demand_slicing.csv";
14
15#[derive(Clone, Deserialize)]
16struct DemandSlice {
17 commodity_id: String,
18 region_id: String,
19 time_slice: String,
20 #[serde(deserialize_with = "deserialise_proportion_nonzero")]
21 fraction: f64,
22}
23
24pub type DemandSliceMap = HashMap<(CommodityID, RegionID, TimeSliceID), f64>;
26
27pub fn read_demand_slices(
37 model_dir: &Path,
38 svd_commodity_ids: &HashSet<CommodityID>,
39 region_ids: &HashSet<RegionID>,
40 time_slice_info: &TimeSliceInfo,
41) -> Result<DemandSliceMap> {
42 let file_path = model_dir.join(DEMAND_SLICING_FILE_NAME);
43 let demand_slices_csv = read_csv(&file_path)?;
44 read_demand_slices_from_iter(
45 demand_slices_csv,
46 svd_commodity_ids,
47 region_ids,
48 time_slice_info,
49 )
50 .with_context(|| input_err_msg(file_path))
51}
52
53fn read_demand_slices_from_iter<I>(
55 iter: I,
56 svd_commodity_ids: &HashSet<CommodityID>,
57 region_ids: &HashSet<RegionID>,
58 time_slice_info: &TimeSliceInfo,
59) -> Result<DemandSliceMap>
60where
61 I: Iterator<Item = DemandSlice>,
62{
63 let mut demand_slices = DemandSliceMap::new();
64
65 for slice in iter {
66 let commodity_id = svd_commodity_ids
67 .get_id_by_str(&slice.commodity_id)
68 .with_context(|| {
69 format!(
70 "Can only provide demand slice data for SVD commodities. Found entry for '{}'",
71 slice.commodity_id
72 )
73 })?;
74 let region_id = region_ids.get_id_by_str(&slice.region_id)?;
75
76 let ts_selection = time_slice_info.get_selection(&slice.time_slice)?;
80 for (ts, demand_fraction) in time_slice_info.calculate_share(&ts_selection, slice.fraction)
81 {
82 ensure!(demand_slices.insert((commodity_id.clone(), region_id.clone(), ts.clone()), demand_fraction).is_none(),
84 "Duplicate demand slicing entry (or same time slice covered by more than one entry) \
85 (commodity: {commodity_id}, region: {region_id}, time slice: {ts})"
86 );
87 }
88 }
89
90 validate_demand_slices(
91 svd_commodity_ids,
92 region_ids,
93 &demand_slices,
94 time_slice_info,
95 )?;
96
97 Ok(demand_slices)
98}
99
100fn validate_demand_slices(
108 svd_commodity_ids: &HashSet<CommodityID>,
109 region_ids: &HashSet<RegionID>,
110 demand_slices: &DemandSliceMap,
111 time_slice_info: &TimeSliceInfo,
112) -> Result<()> {
113 let commodity_regions = svd_commodity_ids
114 .iter()
115 .cartesian_product(region_ids.iter())
116 .collect::<HashSet<_>>();
117 for (commodity_id, region_id) in commodity_regions {
118 time_slice_info
119 .iter_ids()
120 .map(|time_slice| {
121 demand_slices
122 .get(&(commodity_id.clone(), region_id.clone(), time_slice.clone()))
123 .with_context(|| {
124 format!(
125 "Demand slice missing for time slice {} (commodity: {}, region {})",
126 time_slice, commodity_id, region_id
127 )
128 })
129 })
130 .process_results(|iter| {
131 check_fractions_sum_to_one(iter.copied()).context("Invalid demand fractions")
132 })??;
133 }
134
135 Ok(())
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::time_slice::TimeSliceID;
142 use std::iter;
143
144 #[test]
145 fn test_read_demand_slices_from_iter() {
146 let time_slice_info = TimeSliceInfo {
147 seasons: iter::once("winter".into()).collect(),
148 times_of_day: iter::once("day".into()).collect(),
149 fractions: [(
150 TimeSliceID {
151 season: "winter".into(),
152 time_of_day: "day".into(),
153 },
154 1.0,
155 )]
156 .into_iter()
157 .collect(),
158 };
159 let commodity_ids = HashSet::from_iter(iter::once("COM1".into()));
160 let region_ids = HashSet::from_iter(iter::once("GBR".into()));
161
162 let demand_slice = DemandSlice {
164 commodity_id: "COM1".into(),
165 region_id: "GBR".into(),
166 time_slice: "winter".into(),
167 fraction: 1.0,
168 };
169 let time_slice = time_slice_info
170 .get_time_slice_id_from_str("winter.day")
171 .unwrap();
172 let key = ("COM1".into(), "GBR".into(), time_slice);
173 let expected = DemandSliceMap::from_iter(iter::once((key, 1.0)));
174 assert_eq!(
175 read_demand_slices_from_iter(
176 iter::once(demand_slice.clone()),
177 &commodity_ids,
178 ®ion_ids,
179 &time_slice_info,
180 )
181 .unwrap(),
182 expected
183 );
184
185 {
187 let time_slice_info = TimeSliceInfo {
188 seasons: ["winter".into(), "summer".into()].into_iter().collect(),
189 times_of_day: ["day".into(), "night".into()].into_iter().collect(),
190 fractions: [
191 (
192 TimeSliceID {
193 season: "summer".into(),
194 time_of_day: "day".into(),
195 },
196 3.0 / 16.0,
197 ),
198 (
199 TimeSliceID {
200 season: "summer".into(),
201 time_of_day: "night".into(),
202 },
203 5.0 / 16.0,
204 ),
205 (
206 TimeSliceID {
207 season: "winter".into(),
208 time_of_day: "day".into(),
209 },
210 3.0 / 16.0,
211 ),
212 (
213 TimeSliceID {
214 season: "winter".into(),
215 time_of_day: "night".into(),
216 },
217 5.0 / 16.0,
218 ),
219 ]
220 .into_iter()
221 .collect(),
222 };
223 let demand_slices = [
224 DemandSlice {
225 commodity_id: "COM1".into(),
226 region_id: "GBR".into(),
227 time_slice: "winter".into(),
228 fraction: 0.5,
229 },
230 DemandSlice {
231 commodity_id: "COM1".into(),
232 region_id: "GBR".into(),
233 time_slice: "summer".into(),
234 fraction: 0.5,
235 },
236 ];
237 let expected = DemandSliceMap::from_iter([
238 (
239 (
240 "COM1".into(),
241 "GBR".into(),
242 TimeSliceID {
243 season: "summer".into(),
244 time_of_day: "day".into(),
245 },
246 ),
247 3.0 / 16.0,
248 ),
249 (
250 (
251 "COM1".into(),
252 "GBR".into(),
253 TimeSliceID {
254 season: "summer".into(),
255 time_of_day: "night".into(),
256 },
257 ),
258 5.0 / 16.0,
259 ),
260 (
261 (
262 "COM1".into(),
263 "GBR".into(),
264 TimeSliceID {
265 season: "winter".into(),
266 time_of_day: "day".into(),
267 },
268 ),
269 3.0 / 16.0,
270 ),
271 (
272 (
273 "COM1".into(),
274 "GBR".into(),
275 TimeSliceID {
276 season: "winter".into(),
277 time_of_day: "night".into(),
278 },
279 ),
280 5.0 / 16.0,
281 ),
282 ]);
283 assert_eq!(
284 read_demand_slices_from_iter(
285 demand_slices.into_iter(),
286 &commodity_ids,
287 ®ion_ids,
288 &time_slice_info,
289 )
290 .unwrap(),
291 expected
292 );
293 }
294
295 assert!(read_demand_slices_from_iter(
297 iter::empty(),
298 &commodity_ids,
299 ®ion_ids,
300 &time_slice_info,
301 )
302 .is_err());
303
304 let demand_slice = DemandSlice {
306 commodity_id: "COM2".into(),
307 region_id: "GBR".into(),
308 time_slice: "winter.day".into(),
309 fraction: 1.0,
310 };
311 assert!(read_demand_slices_from_iter(
312 iter::once(demand_slice.clone()),
313 &commodity_ids,
314 ®ion_ids,
315 &time_slice_info,
316 )
317 .is_err());
318
319 let demand_slice = DemandSlice {
321 commodity_id: "COM1".into(),
322 region_id: "FRA".into(),
323 time_slice: "winter.day".into(),
324 fraction: 1.0,
325 };
326 assert!(read_demand_slices_from_iter(
327 iter::once(demand_slice.clone()),
328 &commodity_ids,
329 ®ion_ids,
330 &time_slice_info,
331 )
332 .is_err());
333
334 let demand_slice = DemandSlice {
336 commodity_id: "COM1".into(),
337 region_id: "GBR".into(),
338 time_slice: "summer".into(),
339 fraction: 1.0,
340 };
341 assert!(read_demand_slices_from_iter(
342 iter::once(demand_slice.clone()),
343 &commodity_ids,
344 ®ion_ids,
345 &time_slice_info,
346 )
347 .is_err());
348
349 {
350 let time_slice_info = TimeSliceInfo {
352 seasons: ["winter".into(), "summer".into()].into_iter().collect(),
353 times_of_day: iter::once("day".into()).collect(),
354 fractions: [
355 (
356 TimeSliceID {
357 season: "winter".into(),
358 time_of_day: "day".into(),
359 },
360 0.5,
361 ),
362 (
363 TimeSliceID {
364 season: "summer".into(),
365 time_of_day: "day".into(),
366 },
367 0.5,
368 ),
369 ]
370 .into_iter()
371 .collect(),
372 };
373 let demand_slice = DemandSlice {
374 commodity_id: "COM1".into(),
375 region_id: "GBR".into(),
376 time_slice: "winter".into(),
377 fraction: 1.0,
378 };
379 assert!(read_demand_slices_from_iter(
380 iter::once(demand_slice.clone()),
381 &commodity_ids,
382 ®ion_ids,
383 &time_slice_info,
384 )
385 .is_err());
386 }
387
388 let demand_slice = DemandSlice {
390 commodity_id: "COM1".into(),
391 region_id: "GBR".into(),
392 time_slice: "winter.day".into(),
393 fraction: 0.5,
394 };
395 assert!(read_demand_slices_from_iter(
396 iter::repeat_n(demand_slice.clone(), 2),
397 &commodity_ids,
398 ®ion_ids,
399 &time_slice_info,
400 )
401 .is_err());
402
403 let demand_slice_season = DemandSlice {
405 commodity_id: "COM1".into(),
406 region_id: "GBR".into(),
407 time_slice: "winter".into(),
408 fraction: 0.5,
409 };
410 assert!(read_demand_slices_from_iter(
411 [demand_slice, demand_slice_season].into_iter(),
412 &commodity_ids,
413 ®ion_ids,
414 &time_slice_info,
415 )
416 .is_err());
417
418 let demand_slice = DemandSlice {
420 commodity_id: "COM1".into(),
421 region_id: "GBR".into(),
422 time_slice: "winter".into(),
423 fraction: 0.5,
424 };
425 assert!(read_demand_slices_from_iter(
426 iter::once(demand_slice),
427 &commodity_ids,
428 ®ion_ids,
429 &time_slice_info,
430 )
431 .is_err());
432 }
433}