1use crate::id::{define_id_type, IDCollection};
6use crate::units::Dimensionless;
7use anyhow::{Context, Result};
8use indexmap::{IndexMap, IndexSet};
9use itertools::Itertools;
10use serde::de::Error;
11use serde::{Deserialize, Serialize};
12use serde_string_enum::DeserializeLabeledStringEnum;
13use std::fmt::Display;
14use std::iter;
15
16define_id_type! {Season}
17define_id_type! {TimeOfDay}
18
19#[derive(Hash, Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
21pub struct TimeSliceID {
22 pub season: Season,
24 pub time_of_day: TimeOfDay,
26}
27
28#[cfg(test)]
30impl From<&str> for TimeSliceID {
31 fn from(value: &str) -> Self {
32 let (season, time_of_day) = value
33 .split(".")
34 .collect_tuple()
35 .expect("Time slice not in form season.time_of_day");
36 TimeSliceID {
37 season: season.into(),
38 time_of_day: time_of_day.into(),
39 }
40 }
41}
42
43impl Display for TimeSliceID {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 write!(f, "{}.{}", self.season, self.time_of_day)
46 }
47}
48
49impl<'de> Deserialize<'de> for TimeSliceID {
50 fn deserialize<D>(deserialiser: D) -> std::result::Result<Self, D::Error>
51 where
52 D: serde::Deserializer<'de>,
53 {
54 let s: &str = Deserialize::deserialize(deserialiser)?;
55 let (season, time_of_day) = s.split(".").collect_tuple().ok_or_else(|| {
56 D::Error::custom(format!(
57 "Invalid input '{}': Should be in form season.time_of_day",
58 s
59 ))
60 })?;
61 Ok(Self {
62 season: season.into(),
63 time_of_day: time_of_day.into(),
64 })
65 }
66}
67
68impl Serialize for TimeSliceID {
69 fn serialize<S>(&self, serialiser: S) -> std::result::Result<S::Ok, S::Error>
70 where
71 S: serde::Serializer,
72 {
73 serialiser.collect_str(self)
74 }
75}
76
77#[derive(PartialEq, Eq, Hash, Clone, Debug)]
79pub enum TimeSliceSelection {
80 Annual,
82 Season(Season),
84 Single(TimeSliceID),
86}
87
88impl TimeSliceSelection {
89 pub fn level(&self) -> TimeSliceLevel {
91 match self {
92 Self::Annual => TimeSliceLevel::Annual,
93 Self::Season(_) => TimeSliceLevel::Season,
94 Self::Single(_) => TimeSliceLevel::DayNight,
95 }
96 }
97
98 pub fn iter<'a>(
100 &'a self,
101 time_slice_info: &'a TimeSliceInfo,
102 ) -> Box<dyn Iterator<Item = (&'a TimeSliceID, Dimensionless)> + 'a> {
103 let ts_info = time_slice_info;
104 match self {
105 Self::Annual => Box::new(ts_info.iter()),
106 Self::Season(season) => {
107 Box::new(ts_info.iter().filter(move |(ts, _)| ts.season == *season))
108 }
109 Self::Single(ts) => Box::new(iter::once((ts, *ts_info.time_slices.get(ts).unwrap()))),
110 }
111 }
112
113 pub fn iter_at_level<'a>(
124 &'a self,
125 time_slice_info: &'a TimeSliceInfo,
126 level: TimeSliceLevel,
127 ) -> Option<Box<dyn Iterator<Item = (Self, Dimensionless)> + 'a>> {
128 if level > self.level() {
129 return None;
130 }
131
132 let ts_info = time_slice_info;
133 let iter: Box<dyn Iterator<Item = _>> = match self {
134 Self::Annual => match level {
135 TimeSliceLevel::Annual => Box::new(iter::once((Self::Annual, Dimensionless(1.0)))),
136 TimeSliceLevel::Season => Box::new(
137 ts_info
138 .seasons
139 .iter()
140 .map(|(season, fraction)| (season.clone().into(), *fraction)),
141 ),
142 TimeSliceLevel::DayNight => Box::new(
143 ts_info
144 .time_slices
145 .iter()
146 .map(|(ts, fraction)| (ts.clone().into(), *fraction)),
147 ),
148 },
149 Self::Season(season) => match level {
150 TimeSliceLevel::Season => Box::new(iter::once((
151 self.clone(),
152 *ts_info.seasons.get(season).unwrap(),
153 ))),
154 TimeSliceLevel::DayNight => Box::new(
155 ts_info
156 .time_slices
157 .iter()
158 .filter(move |(ts, _)| &ts.season == season)
159 .map(|(ts, fraction)| (ts.clone().into(), *fraction)),
160 ),
161 _ => unreachable!(),
162 },
163 Self::Single(time_slice) => Box::new(iter::once((
164 time_slice.clone().into(),
165 *ts_info.time_slices.get(time_slice).unwrap(),
166 ))),
167 };
168
169 Some(iter)
170 }
171}
172
173impl From<TimeSliceID> for TimeSliceSelection {
174 fn from(value: TimeSliceID) -> Self {
175 Self::Single(value)
176 }
177}
178
179impl From<Season> for TimeSliceSelection {
180 fn from(value: Season) -> Self {
181 Self::Season(value)
182 }
183}
184
185impl Display for TimeSliceSelection {
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 match self {
188 Self::Annual => write!(f, "annual"),
189 Self::Season(season) => write!(f, "{season}"),
190 Self::Single(ts) => write!(f, "{ts}"),
191 }
192 }
193}
194
195#[derive(PartialEq, PartialOrd, Copy, Clone, Debug, DeserializeLabeledStringEnum)]
197pub enum TimeSliceLevel {
198 #[string = "daynight"]
200 DayNight,
201 #[string = "season"]
203 Season,
204 #[string = "annual"]
206 Annual,
207}
208
209#[derive(PartialEq, Debug)]
211pub struct TimeSliceInfo {
212 pub times_of_day: IndexSet<TimeOfDay>,
214 pub seasons: IndexMap<Season, Dimensionless>,
216 pub time_slices: IndexMap<TimeSliceID, Dimensionless>,
218}
219
220impl Default for TimeSliceInfo {
221 fn default() -> Self {
223 let id = TimeSliceID {
224 season: "all-year".into(),
225 time_of_day: "all-day".into(),
226 };
227 let fractions = [(id.clone(), Dimensionless(1.0))].into_iter().collect();
228
229 Self {
230 seasons: iter::once((id.season, Dimensionless(1.0))).collect(),
231 times_of_day: iter::once(id.time_of_day).collect(),
232 time_slices: fractions,
233 }
234 }
235}
236
237impl TimeSliceInfo {
238 pub fn get_time_slice_id_from_str(&self, time_slice: &str) -> Result<TimeSliceID> {
242 let (season, time_of_day) = time_slice
243 .split('.')
244 .collect_tuple()
245 .context("Time slice must be in the form season.time_of_day")?;
246 let season = self
247 .seasons
248 .get_id(season)
249 .with_context(|| format!("{} is not a known season", season))?;
250 let time_of_day = self
251 .times_of_day
252 .get_id(time_of_day)
253 .with_context(|| format!("{} is not a known time of day", time_of_day))?;
254
255 Ok(TimeSliceID {
256 season: season.clone(),
257 time_of_day: time_of_day.clone(),
258 })
259 }
260
261 pub fn get_selection(&self, time_slice: &str) -> Result<TimeSliceSelection> {
265 if time_slice.eq_ignore_ascii_case("annual") {
266 Ok(TimeSliceSelection::Annual)
267 } else if time_slice.contains('.') {
268 let time_slice = self.get_time_slice_id_from_str(time_slice)?;
269 Ok(TimeSliceSelection::Single(time_slice))
270 } else {
271 let season = self
272 .seasons
273 .get_id(time_slice)
274 .with_context(|| format!("'{time_slice}' is not a valid season"))?
275 .clone();
276 Ok(TimeSliceSelection::Season(season))
277 }
278 }
279
280 pub fn iter_ids(&self) -> indexmap::map::Keys<TimeSliceID, Dimensionless> {
282 self.time_slices.keys()
283 }
284
285 pub fn iter(&self) -> impl Iterator<Item = (&TimeSliceID, Dimensionless)> {
287 self.time_slices
288 .iter()
289 .map(|(ts, fraction)| (ts, *fraction))
290 }
291
292 pub fn iter_selections_at_level(
297 &self,
298 level: TimeSliceLevel,
299 ) -> Box<dyn Iterator<Item = TimeSliceSelection> + '_> {
300 match level {
301 TimeSliceLevel::Annual => Box::new(iter::once(TimeSliceSelection::Annual)),
302 TimeSliceLevel::Season => {
303 Box::new(self.seasons.keys().cloned().map(TimeSliceSelection::Season))
304 }
305 TimeSliceLevel::DayNight => {
306 Box::new(self.iter_ids().cloned().map(TimeSliceSelection::Single))
307 }
308 }
309 }
310
311 pub fn iter_selection_share<'a>(
324 &'a self,
325 selection: &'a TimeSliceSelection,
326 level: TimeSliceLevel,
327 ) -> Option<impl Iterator<Item = (TimeSliceSelection, Dimensionless)>> {
328 let selections = selection.iter_at_level(self, level)?.collect_vec();
330
331 let time_total: Dimensionless = selections.iter().map(|(_, fraction)| *fraction).sum();
333
334 let iter = selections
336 .into_iter()
337 .map(move |(selection, time_fraction)| (selection, time_fraction / time_total));
338 Some(iter)
339 }
340
341 pub fn calculate_share<'a>(
355 &'a self,
356 selection: &'a TimeSliceSelection,
357 level: TimeSliceLevel,
358 value: Dimensionless,
359 ) -> Option<impl Iterator<Item = (TimeSliceSelection, Dimensionless)>> {
360 let iter = self
361 .iter_selection_share(selection, level)?
362 .map(move |(selection, share)| (selection, value * share));
363 Some(iter)
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use itertools::assert_equal;
371 use rstest::{fixture, rstest};
372
373 #[fixture]
374 fn time_slices1() -> [TimeSliceID; 2] {
375 [
376 TimeSliceID {
377 season: "winter".into(),
378 time_of_day: "day".into(),
379 },
380 TimeSliceID {
381 season: "summer".into(),
382 time_of_day: "night".into(),
383 },
384 ]
385 }
386
387 #[fixture]
388 fn time_slice_info1(time_slices1: [TimeSliceID; 2]) -> TimeSliceInfo {
389 TimeSliceInfo {
390 seasons: [
391 ("winter".into(), Dimensionless(0.5)),
392 ("summer".into(), Dimensionless(0.5)),
393 ]
394 .into_iter()
395 .collect(),
396 times_of_day: ["day".into(), "night".into()].into_iter().collect(),
397 time_slices: time_slices1
398 .map(|ts| (ts, Dimensionless(0.5)))
399 .into_iter()
400 .collect(),
401 }
402 }
403
404 #[fixture]
405 fn time_slice_info2() -> TimeSliceInfo {
406 let time_slices = [
407 TimeSliceID {
408 season: "winter".into(),
409 time_of_day: "day".into(),
410 },
411 TimeSliceID {
412 season: "winter".into(),
413 time_of_day: "night".into(),
414 },
415 TimeSliceID {
416 season: "summer".into(),
417 time_of_day: "day".into(),
418 },
419 TimeSliceID {
420 season: "summer".into(),
421 time_of_day: "night".into(),
422 },
423 ];
424 TimeSliceInfo {
425 times_of_day: ["day".into(), "night".into()].into_iter().collect(),
426 seasons: [
427 ("winter".into(), Dimensionless(0.5)),
428 ("summer".into(), Dimensionless(0.5)),
429 ]
430 .into_iter()
431 .collect(),
432 time_slices: time_slices
433 .iter()
434 .map(|ts| (ts.clone(), Dimensionless(0.25)))
435 .collect(),
436 }
437 }
438
439 #[rstest]
440 fn test_ts_selection_iter_annual(
441 time_slice_info1: TimeSliceInfo,
442 time_slices1: [TimeSliceID; 2],
443 ) {
444 assert_equal(
445 TimeSliceSelection::Annual.iter(&time_slice_info1),
446 time_slices1.iter().map(|ts| (ts, Dimensionless(0.5))),
447 );
448 }
449
450 #[rstest]
451 fn test_ts_selection_iter_season(
452 time_slice_info1: TimeSliceInfo,
453 time_slices1: [TimeSliceID; 2],
454 ) {
455 assert_equal(
456 TimeSliceSelection::Season("winter".into()).iter(&time_slice_info1),
457 iter::once((&time_slices1[0], Dimensionless(0.5))),
458 );
459 }
460
461 #[rstest]
462 fn test_ts_selection_iter_single(
463 time_slice_info1: TimeSliceInfo,
464 time_slices1: [TimeSliceID; 2],
465 ) {
466 let ts = time_slice_info1
467 .get_time_slice_id_from_str("summer.night")
468 .unwrap();
469 assert_equal(
470 TimeSliceSelection::Single(ts).iter(&time_slice_info1),
471 iter::once((&time_slices1[1], Dimensionless(0.5))),
472 );
473 }
474
475 fn assert_selection_equal<I>(actual: Option<I>, expected: Option<Vec<(&str, Dimensionless)>>)
476 where
477 I: Iterator<Item = (TimeSliceSelection, Dimensionless)>,
478 {
479 let Some(actual) = actual else {
480 assert!(expected.is_none());
481 return;
482 };
483
484 let ts_info = time_slice_info2();
485 let expected = expected
486 .unwrap()
487 .into_iter()
488 .map(move |(sel, frac)| (ts_info.get_selection(sel).unwrap(), frac));
489 assert_equal(actual, expected);
490 }
491
492 #[rstest]
493 #[case(TimeSliceSelection::Annual, TimeSliceLevel::Annual, Some(vec![("annual", Dimensionless(1.0))]))]
494 #[case(TimeSliceSelection::Annual, TimeSliceLevel::Season, Some(vec![("winter", Dimensionless(0.5)), ("summer", Dimensionless(0.5))]))]
495 #[case(TimeSliceSelection::Annual, TimeSliceLevel::DayNight,
496 Some(vec![("winter.day", Dimensionless(0.25)), ("winter.night", Dimensionless(0.25)), ("summer.day", Dimensionless(0.25)), ("summer.night", Dimensionless(0.25))]))]
497 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Annual, None)]
498 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Season, Some(vec![("winter", Dimensionless(0.5))]))]
499 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::DayNight,
500 Some(vec![("winter.day", Dimensionless(0.25)), ("winter.night", Dimensionless(0.25))]))]
501 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Annual, None)]
502 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Season, None)]
503 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::DayNight, Some(vec![("winter.day", Dimensionless(0.25))]))]
504 fn test_ts_selection_iter_at_level(
505 time_slice_info2: TimeSliceInfo,
506 #[case] selection: TimeSliceSelection,
507 #[case] level: TimeSliceLevel,
508 #[case] expected: Option<Vec<(&str, Dimensionless)>>,
509 ) {
510 let actual = selection.iter_at_level(&time_slice_info2, level);
511 assert_selection_equal(actual, expected);
512 }
513
514 #[rstest]
515 #[case(TimeSliceSelection::Annual, TimeSliceLevel::Annual, Some(vec![("annual", Dimensionless(8.0))]))]
516 #[case(TimeSliceSelection::Annual, TimeSliceLevel::Season, Some(vec![("winter", Dimensionless(4.0)), ("summer", Dimensionless(4.0))]))]
517 #[case(TimeSliceSelection::Annual, TimeSliceLevel::DayNight,
518 Some(vec![("winter.day", Dimensionless(2.0)), ("winter.night", Dimensionless(2.0)), ("summer.day", Dimensionless(2.0)), ("summer.night", Dimensionless(2.0))]))]
519 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Annual, None)]
520 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Season, Some(vec![("winter", Dimensionless(8.0))]))]
521 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::DayNight,
522 Some(vec![("winter.day", Dimensionless(4.0)), ("winter.night", Dimensionless(4.0))]))]
523 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Annual, None)]
524 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Season, None)]
525 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::DayNight, Some(vec![("winter.day", Dimensionless(8.0))]))]
526 fn test_calculate_share(
527 time_slice_info2: TimeSliceInfo,
528 #[case] selection: TimeSliceSelection,
529 #[case] level: TimeSliceLevel,
530 #[case] expected: Option<Vec<(&str, Dimensionless)>>,
531 ) {
532 let actual = time_slice_info2.calculate_share(&selection, level, Dimensionless(8.0));
533 assert_selection_equal(actual, expected);
534 }
535}