muse2/
time_slice.rs

1//! Code for working with time slices.
2//!
3//! Time slices provide a mechanism for users to indicate production etc. varies with the time of
4//! day and time of year.
5use 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/// An ID describing season and time of day
20#[derive(Hash, Eq, PartialEq, Ord, PartialOrd, Clone, Debug)]
21pub struct TimeSliceID {
22    /// The name of each season.
23    pub season: Season,
24    /// The name of each time slice within a day.
25    pub time_of_day: TimeOfDay,
26}
27
28/// Only implement for tests as this is a bit of a footgun
29#[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/// Represents a time slice read from an input file, which can be all
78#[derive(PartialEq, Eq, Hash, Clone, Debug)]
79pub enum TimeSliceSelection {
80    /// All year and all day
81    Annual,
82    /// Only applies to one season
83    Season(Season),
84    /// Only applies to a single time slice
85    Single(TimeSliceID),
86}
87
88impl TimeSliceSelection {
89    /// The [`TimeSliceLevel`] to which this [`TimeSliceSelection`] corresponds
90    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    /// Iterate over the subset of time slices in this selection
99    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    /// Iterate over this [`TimeSliceSelection`] at the specified level.
114    ///
115    /// For example, this allows you to iterate over a [`TimeSliceSelection::Season`] at the level
116    /// of either seasons (in which case, the iterator will just contain the season) or time slices
117    /// (in which case it will contain all time slices for that season).
118    ///
119    /// Note that you cannot iterate over a [`TimeSliceSelection`] with coarser temporal granularity
120    /// than the [`TimeSliceSelection`] itself (for example, you cannot iterate over a
121    /// [`TimeSliceSelection::Season`] at the [`TimeSliceLevel::Annual`] level). In this case, the
122    /// function will return `None`.
123    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/// The time granularity for a particular operation
196#[derive(PartialEq, PartialOrd, Copy, Clone, Debug, DeserializeLabeledStringEnum)]
197pub enum TimeSliceLevel {
198    /// Treat individual time slices separately
199    #[string = "daynight"]
200    DayNight,
201    /// Whole seasons
202    #[string = "season"]
203    Season,
204    /// The whole year
205    #[string = "annual"]
206    Annual,
207}
208
209/// Information about the time slices in the simulation, including names and fractions
210#[derive(PartialEq, Debug)]
211pub struct TimeSliceInfo {
212    /// Names of times of day (e.g. "evening")
213    pub times_of_day: IndexSet<TimeOfDay>,
214    /// Names and fraction of year occupied by each season
215    pub seasons: IndexMap<Season, Dimensionless>,
216    /// The fraction of the year that this combination of season and time of day occupies
217    pub time_slices: IndexMap<TimeSliceID, Dimensionless>,
218}
219
220impl Default for TimeSliceInfo {
221    /// The default `TimeSliceInfo` is a single time slice covering the whole year
222    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    /// Get the `TimeSliceID` corresponding to the `time_slice`.
239    ///
240    /// `time_slice` must be in the form "season.time_of_day".
241    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    /// Get a `TimeSliceSelection` from the specified string.
262    ///
263    /// If the string is empty, the default value is `TimeSliceSelection::Annual`.
264    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    /// Iterate over all [`TimeSliceID`]s
281    pub fn iter_ids(&self) -> indexmap::map::Keys<TimeSliceID, Dimensionless> {
282        self.time_slices.keys()
283    }
284
285    /// Iterate over all time slices
286    pub fn iter(&self) -> impl Iterator<Item = (&TimeSliceID, Dimensionless)> {
287        self.time_slices
288            .iter()
289            .map(|(ts, fraction)| (ts, *fraction))
290    }
291
292    /// Iterate over the different time slice selections for a given time slice level.
293    ///
294    /// For example, if [`TimeSliceLevel::Season`] is specified, this function will return an
295    /// iterator of [`TimeSliceSelection`]s covering each season.
296    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    /// Iterate over a subset of time slices calculating the relative duration of each.
312    ///
313    /// The relative duration is specified as a fraction of the total time (proportion of year)
314    /// covered by `selection`.
315    ///
316    /// # Arguments
317    ///
318    /// * `selection` - A subset of time slices
319    ///
320    /// # Returns
321    ///
322    /// An iterator of time slices along with the fraction of the total selection.
323    pub fn iter_selection_share<'a>(
324        &'a self,
325        selection: &'a TimeSliceSelection,
326        level: TimeSliceLevel,
327    ) -> Option<impl Iterator<Item = (TimeSliceSelection, Dimensionless)>> {
328        // Store selections as we have to iterate twice
329        let selections = selection.iter_at_level(self, level)?.collect_vec();
330
331        // Total fraction of year covered by selection
332        let time_total: Dimensionless = selections.iter().map(|(_, fraction)| *fraction).sum();
333
334        // Calculate share
335        let iter = selections
336            .into_iter()
337            .map(move |(selection, time_fraction)| (selection, time_fraction / time_total));
338        Some(iter)
339    }
340
341    /// Share a value between a subset of time slices in proportion to their lengths.
342    ///
343    /// For instance, you could use this function to compute how demand is distributed between the
344    /// different time slices of winter.
345    ///
346    /// # Arguments
347    ///
348    /// * `selection` - A subset of time slices
349    /// * `value` - The value to be shared between the time slices
350    ///
351    /// # Returns
352    ///
353    /// An iterator of time slices along with a fraction of `value`.
354    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}