1use crate::id::{IDCollection, define_id_type};
6use crate::units::{Dimensionless, Year};
7use anyhow::{Context, Result};
8use indexmap::{IndexMap, IndexSet};
9use itertools::Itertools;
10use serde::de::Error;
11use serde::{Deserialize, Serialize};
12use std::iter;
13
14define_id_type! {Season, "season"}
15define_id_type! {TimeOfDay, "time of day"}
16
17#[derive(Hash, Eq, PartialEq, Ord, PartialOrd, Clone, Debug, derive_more::Display)]
19#[display("{season}.{time_of_day}")]
20pub struct TimeSliceID {
21 pub season: Season,
23 pub time_of_day: TimeOfDay,
25}
26
27#[cfg(test)]
29impl From<&str> for TimeSliceID {
30 fn from(value: &str) -> Self {
31 let (season, time_of_day) = value
32 .split('.')
33 .collect_tuple()
34 .expect("Time slice not in form season.time_of_day");
35 TimeSliceID {
36 season: season.into(),
37 time_of_day: time_of_day.into(),
38 }
39 }
40}
41
42impl<'de> Deserialize<'de> for TimeSliceID {
43 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
44 where
45 D: serde::Deserializer<'de>,
46 {
47 let s: &str = Deserialize::deserialize(deserializer)?;
48 let (season, time_of_day) = s.split('.').collect_tuple().ok_or_else(|| {
49 D::Error::custom(format!(
50 "Invalid input '{s}': Should be in form season.time_of_day"
51 ))
52 })?;
53 Ok(Self {
54 season: season.into(),
55 time_of_day: time_of_day.into(),
56 })
57 }
58}
59
60impl Serialize for TimeSliceID {
61 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
62 where
63 S: serde::Serializer,
64 {
65 serializer.collect_str(self)
66 }
67}
68
69#[derive(PartialEq, Eq, Hash, Clone, Debug, derive_more::From, derive_more::Display)]
71pub enum TimeSliceSelection {
72 #[display("annual")]
74 Annual,
75 #[from]
77 #[display("{_0}")]
78 Season(Season),
79 #[from]
81 #[display("{_0}")]
82 Single(TimeSliceID),
83}
84
85impl TimeSliceSelection {
86 pub fn level(&self) -> TimeSliceLevel {
88 match self {
89 Self::Annual => TimeSliceLevel::Annual,
90 Self::Season(_) => TimeSliceLevel::Season,
91 Self::Single(_) => TimeSliceLevel::DayNight,
92 }
93 }
94
95 pub fn containing_selection_at_level(
97 &self,
98 level: TimeSliceLevel,
99 ) -> Option<TimeSliceSelection> {
100 if level < self.level() {
101 return None;
102 }
103
104 let mut selection = self.clone();
105 while selection.level() < level {
106 selection = match selection {
107 TimeSliceSelection::Single(time_slice_id) => {
108 TimeSliceSelection::Season(time_slice_id.season.clone())
109 }
110 TimeSliceSelection::Season(_) => TimeSliceSelection::Annual,
111 TimeSliceSelection::Annual => unreachable!(),
112 };
113 }
114
115 Some(selection)
116 }
117
118 pub fn iter<'a>(
120 &'a self,
121 time_slice_info: &'a TimeSliceInfo,
122 ) -> Box<dyn Iterator<Item = (&'a TimeSliceID, Year)> + 'a> {
123 let ts_info = time_slice_info;
124 match self {
125 Self::Annual => Box::new(ts_info.iter()),
126 Self::Season(season) => {
127 Box::new(ts_info.iter().filter(move |(ts, _)| ts.season == *season))
128 }
129 Self::Single(ts) => Box::new(iter::once((ts, ts_info.time_slices[ts]))),
130 }
131 }
132
133 pub fn iter_at_level<'a>(
144 &'a self,
145 time_slice_info: &'a TimeSliceInfo,
146 level: TimeSliceLevel,
147 ) -> Option<Box<dyn Iterator<Item = (Self, Year)> + 'a>> {
148 if level > self.level() {
149 return None;
150 }
151
152 let ts_info = time_slice_info;
153 let iter: Box<dyn Iterator<Item = _>> = match self {
154 Self::Annual => match level {
155 TimeSliceLevel::Annual => Box::new(iter::once((Self::Annual, Year(1.0)))),
156 TimeSliceLevel::Season => Box::new(
157 ts_info
158 .seasons
159 .iter()
160 .map(|(season, duration)| (season.clone().into(), *duration)),
161 ),
162 TimeSliceLevel::DayNight => Box::new(
163 ts_info
164 .time_slices
165 .iter()
166 .map(|(ts, duration)| (ts.clone().into(), *duration)),
167 ),
168 },
169 Self::Season(season) => match level {
170 TimeSliceLevel::Season => {
171 Box::new(iter::once((self.clone(), ts_info.seasons[season])))
172 }
173 TimeSliceLevel::DayNight => Box::new(
174 ts_info
175 .time_slices
176 .iter()
177 .filter(move |(ts, _)| &ts.season == season)
178 .map(|(ts, duration)| (ts.clone().into(), *duration)),
179 ),
180 TimeSliceLevel::Annual => unreachable!(),
181 },
182 Self::Single(time_slice) => Box::new(iter::once((
183 time_slice.clone().into(),
184 ts_info.time_slices[time_slice],
185 ))),
186 };
187
188 Some(iter)
189 }
190}
191
192#[derive(
194 PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug, Serialize, Deserialize, strum::EnumIter,
195)]
196pub enum TimeSliceLevel {
197 #[serde(rename = "daynight")]
199 DayNight,
200 #[serde(rename = "season")]
202 Season,
203 #[serde(rename = "annual")]
205 Annual,
206}
207
208impl TimeSliceLevel {
209 pub fn containing_selection(&self, ts: &TimeSliceID) -> TimeSliceSelection {
211 match self {
212 Self::Annual => TimeSliceSelection::Annual,
213 Self::Season => TimeSliceSelection::Season(ts.season.clone()),
214 Self::DayNight => TimeSliceSelection::Single(ts.clone()),
215 }
216 }
217}
218
219#[derive(PartialEq, Debug)]
221pub struct TimeSliceInfo {
222 pub times_of_day: IndexSet<TimeOfDay>,
224 pub seasons: IndexMap<Season, Year>,
226 pub time_slices: IndexMap<TimeSliceID, Year>,
228}
229
230impl Default for TimeSliceInfo {
231 fn default() -> Self {
233 let id = TimeSliceID {
234 season: "all-year".into(),
235 time_of_day: "all-day".into(),
236 };
237 let time_slices = [(id.clone(), Year(1.0))].into_iter().collect();
238
239 Self {
240 seasons: iter::once((id.season, Year(1.0))).collect(),
241 times_of_day: iter::once(id.time_of_day).collect(),
242 time_slices,
243 }
244 }
245}
246
247impl TimeSliceInfo {
248 #[allow(clippy::doc_markdown)]
249 pub fn get_time_slice_id_from_str(&self, time_slice: &str) -> Result<TimeSliceID> {
253 let (season, time_of_day) = time_slice
254 .split('.')
255 .collect_tuple()
256 .context("Time slice must be in the form season.time_of_day")?;
257 let season = self.seasons.get_id(season)?;
258 let time_of_day = self.times_of_day.get_id(time_of_day)?;
259
260 Ok(TimeSliceID {
261 season: season.clone(),
262 time_of_day: time_of_day.clone(),
263 })
264 }
265
266 pub fn get_selection(&self, time_slice: &str) -> Result<TimeSliceSelection> {
270 if time_slice.eq_ignore_ascii_case("annual") {
271 Ok(TimeSliceSelection::Annual)
272 } else if time_slice.contains('.') {
273 let time_slice = self.get_time_slice_id_from_str(time_slice)?;
274 Ok(TimeSliceSelection::Single(time_slice))
275 } else {
276 let season = self.seasons.get_id(time_slice)?.clone();
277 Ok(TimeSliceSelection::Season(season))
278 }
279 }
280
281 pub fn iter_ids(&self) -> indexmap::map::Keys<'_, TimeSliceID, Year> {
283 self.time_slices.keys()
284 }
285
286 pub fn iter_seasons(&self) -> indexmap::map::Keys<'_, Season, Year> {
288 self.seasons.keys()
289 }
290
291 pub fn iter(&self) -> impl Iterator<Item = (&TimeSliceID, Year)> {
293 self.time_slices
294 .iter()
295 .map(|(ts, duration)| (ts, *duration))
296 }
297
298 pub fn iter_selections_at_level(
303 &self,
304 level: TimeSliceLevel,
305 ) -> Box<dyn Iterator<Item = TimeSliceSelection> + '_> {
306 match level {
307 TimeSliceLevel::Annual => Box::new(iter::once(TimeSliceSelection::Annual)),
308 TimeSliceLevel::Season => {
309 Box::new(self.seasons.keys().cloned().map(TimeSliceSelection::Season))
310 }
311 TimeSliceLevel::DayNight => {
312 Box::new(self.iter_ids().cloned().map(TimeSliceSelection::Single))
313 }
314 }
315 }
316
317 pub fn iter_selection_share<'a>(
329 &'a self,
330 selection: &'a TimeSliceSelection,
331 level: TimeSliceLevel,
332 ) -> Option<impl Iterator<Item = (TimeSliceSelection, Dimensionless)> + use<>> {
333 let selections = selection.iter_at_level(self, level)?.collect_vec();
335
336 let total_duration: Year = selections.iter().map(|(_, duration)| *duration).sum();
338
339 let iter = selections
341 .into_iter()
342 .map(move |(selection, duration)| (selection, duration / total_duration));
343 Some(iter)
344 }
345
346 pub fn length_for_selection(&self, selection: &TimeSliceSelection) -> Result<Year> {
348 let length: Year = selection.iter(self).map(|(_, duration)| duration).sum();
349 Ok(length)
350 }
351
352 pub fn calculate_share<'a>(
366 &'a self,
367 selection: &'a TimeSliceSelection,
368 level: TimeSliceLevel,
369 value: Dimensionless,
370 ) -> Option<impl Iterator<Item = (TimeSliceSelection, Dimensionless)> + use<>> {
371 let iter = self
372 .iter_selection_share(selection, level)?
373 .map(move |(selection, share)| (selection, value * share));
374 Some(iter)
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use crate::units::UnitType;
382 use itertools::assert_equal;
383 use rstest::{fixture, rstest};
384
385 #[fixture]
386 fn time_slices1() -> [TimeSliceID; 2] {
387 [
388 TimeSliceID {
389 season: "winter".into(),
390 time_of_day: "day".into(),
391 },
392 TimeSliceID {
393 season: "summer".into(),
394 time_of_day: "night".into(),
395 },
396 ]
397 }
398
399 #[fixture]
400 fn time_slice_info1(time_slices1: [TimeSliceID; 2]) -> TimeSliceInfo {
401 TimeSliceInfo {
402 seasons: [("winter".into(), Year(0.5)), ("summer".into(), Year(0.5))]
403 .into_iter()
404 .collect(),
405 times_of_day: ["day".into(), "night".into()].into_iter().collect(),
406 time_slices: time_slices1.map(|ts| (ts, Year(0.5))).into_iter().collect(),
407 }
408 }
409
410 #[fixture]
411 fn time_slice_info2() -> TimeSliceInfo {
412 let time_slices = [
413 TimeSliceID {
414 season: "winter".into(),
415 time_of_day: "day".into(),
416 },
417 TimeSliceID {
418 season: "winter".into(),
419 time_of_day: "night".into(),
420 },
421 TimeSliceID {
422 season: "summer".into(),
423 time_of_day: "day".into(),
424 },
425 TimeSliceID {
426 season: "summer".into(),
427 time_of_day: "night".into(),
428 },
429 ];
430 TimeSliceInfo {
431 times_of_day: ["day".into(), "night".into()].into_iter().collect(),
432 seasons: [("winter".into(), Year(0.5)), ("summer".into(), Year(0.5))]
433 .into_iter()
434 .collect(),
435 time_slices: time_slices
436 .iter()
437 .map(|ts| (ts.clone(), Year(0.25)))
438 .collect(),
439 }
440 }
441
442 #[rstest]
443 fn ts_selection_iter_annual(time_slice_info1: TimeSliceInfo, time_slices1: [TimeSliceID; 2]) {
444 assert_equal(
445 TimeSliceSelection::Annual.iter(&time_slice_info1),
446 time_slices1.iter().map(|ts| (ts, Year(0.5))),
447 );
448 }
449
450 #[rstest]
451 fn ts_selection_iter_season(time_slice_info1: TimeSliceInfo, time_slices1: [TimeSliceID; 2]) {
452 assert_equal(
453 TimeSliceSelection::Season("winter".into()).iter(&time_slice_info1),
454 iter::once((&time_slices1[0], Year(0.5))),
455 );
456 }
457
458 #[rstest]
459 fn ts_selection_iter_single(time_slice_info1: TimeSliceInfo, time_slices1: [TimeSliceID; 2]) {
460 let ts = time_slice_info1
461 .get_time_slice_id_from_str("summer.night")
462 .unwrap();
463 assert_equal(
464 TimeSliceSelection::Single(ts).iter(&time_slice_info1),
465 iter::once((&time_slices1[1], Year(0.5))),
466 );
467 }
468
469 fn assert_selection_equal<I, T>(actual: Option<I>, expected: Option<Vec<(&str, T)>>)
470 where
471 T: UnitType,
472 I: Iterator<Item = (TimeSliceSelection, T)>,
473 {
474 let Some(actual) = actual else {
475 assert!(expected.is_none());
476 return;
477 };
478
479 let ts_info = time_slice_info2();
480 let expected = expected
481 .unwrap()
482 .into_iter()
483 .map(move |(sel, frac)| (ts_info.get_selection(sel).unwrap(), frac));
484 assert_equal(actual, expected);
485 }
486
487 #[rstest]
488 #[case(TimeSliceSelection::Annual, TimeSliceLevel::Annual, Some(vec![("annual", Year(1.0))]))]
489 #[case(TimeSliceSelection::Annual, TimeSliceLevel::Season, Some(vec![("winter", Year(0.5)), ("summer", Year(0.5))]))]
490 #[case(TimeSliceSelection::Annual, TimeSliceLevel::DayNight,
491 Some(vec![("winter.day", Year(0.25)), ("winter.night", Year(0.25)), ("summer.day", Year(0.25)), ("summer.night", Year(0.25))]))]
492 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Annual, None)]
493 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Season, Some(vec![("winter", Year(0.5))]))]
494 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::DayNight,
495 Some(vec![("winter.day", Year(0.25)), ("winter.night", Year(0.25))]))]
496 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Annual, None)]
497 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Season, None)]
498 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::DayNight, Some(vec![("winter.day", Year(0.25))]))]
499 fn ts_selection_iter_at_level(
500 time_slice_info2: TimeSliceInfo,
501 #[case] selection: TimeSliceSelection,
502 #[case] level: TimeSliceLevel,
503 #[case] expected: Option<Vec<(&str, Year)>>,
504 ) {
505 let actual = selection.iter_at_level(&time_slice_info2, level);
506 assert_selection_equal(actual, expected);
507 }
508
509 #[rstest]
510 #[case(TimeSliceSelection::Annual, TimeSliceLevel::Annual, Some(vec![("annual", Dimensionless(8.0))]))]
511 #[case(TimeSliceSelection::Annual, TimeSliceLevel::Season, Some(vec![("winter", Dimensionless(4.0)), ("summer", Dimensionless(4.0))]))]
512 #[case(TimeSliceSelection::Annual, TimeSliceLevel::DayNight,
513 Some(vec![("winter.day", Dimensionless(2.0)), ("winter.night", Dimensionless(2.0)), ("summer.day", Dimensionless(2.0)), ("summer.night", Dimensionless(2.0))]))]
514 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Annual, None)]
515 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Season, Some(vec![("winter", Dimensionless(8.0))]))]
516 #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::DayNight,
517 Some(vec![("winter.day", Dimensionless(4.0)), ("winter.night", Dimensionless(4.0))]))]
518 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Annual, None)]
519 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Season, None)]
520 #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::DayNight, Some(vec![("winter.day", Dimensionless(8.0))]))]
521 fn calculate_share(
522 time_slice_info2: TimeSliceInfo,
523 #[case] selection: TimeSliceSelection,
524 #[case] level: TimeSliceLevel,
525 #[case] expected: Option<Vec<(&str, Dimensionless)>>,
526 ) {
527 let actual = time_slice_info2.calculate_share(&selection, level, Dimensionless(8.0));
528 assert_selection_equal(actual, expected);
529 }
530}