Skip to main content

muse2/
time_slice.rs

1//! Code for working with time slices.
2//!
3//! Time slices provide a mechanism for users to indicate that production (or other quantities)
4//! varies with time of day and season.
5use 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/// An ID describing season and time of day
18#[derive(Hash, Eq, PartialEq, Ord, PartialOrd, Clone, Debug, derive_more::Display)]
19#[display("{season}.{time_of_day}")]
20pub struct TimeSliceID {
21    /// The name of each season.
22    pub season: Season,
23    /// The name of each time slice within a day.
24    pub time_of_day: TimeOfDay,
25}
26
27/// Only implement for tests as this is a bit of a footgun
28#[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/// Represents a time slice read from an input file, which can be all
70#[derive(PartialEq, Eq, Hash, Clone, Debug, derive_more::From, derive_more::Display)]
71pub enum TimeSliceSelection {
72    /// All year and all day
73    #[display("annual")]
74    Annual,
75    /// Only applies to one season
76    #[from]
77    #[display("{_0}")]
78    Season(Season),
79    /// Only applies to a single time slice
80    #[from]
81    #[display("{_0}")]
82    Single(TimeSliceID),
83}
84
85impl TimeSliceSelection {
86    /// The [`TimeSliceLevel`] to which this [`TimeSliceSelection`] corresponds
87    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    /// Get the [`TimeSliceSelection`] containing this selection at the specified level.
96    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    /// Iterate over the subset of time slices in this selection
119    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    /// Iterate over this [`TimeSliceSelection`] at the specified level.
134    ///
135    /// For example, this allows you to iterate over a [`TimeSliceSelection::Season`] at the level
136    /// of either seasons (in which case, the iterator will just contain the season) or time slices
137    /// (in which case it will contain all time slices for that season).
138    ///
139    /// Note that you cannot iterate over a [`TimeSliceSelection`] with coarser temporal granularity
140    /// than the [`TimeSliceSelection`] itself (for example, you cannot iterate over a
141    /// [`TimeSliceSelection::Season`] at the [`TimeSliceLevel::Annual`] level). In this case, the
142    /// function will return `None`.
143    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/// The time granularity for a particular operation
193#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug, Deserialize, strum::EnumIter)]
194pub enum TimeSliceLevel {
195    /// Treat individual time slices separately
196    #[serde(rename = "daynight")]
197    DayNight,
198    /// Whole seasons
199    #[serde(rename = "season")]
200    Season,
201    /// The whole year
202    #[serde(rename = "annual")]
203    Annual,
204}
205
206impl TimeSliceLevel {
207    /// Get the [`TimeSliceSelection`] containing the given [`TimeSliceID`] at this level.
208    pub fn containing_selection(&self, ts: &TimeSliceID) -> TimeSliceSelection {
209        match self {
210            Self::Annual => TimeSliceSelection::Annual,
211            Self::Season => TimeSliceSelection::Season(ts.season.clone()),
212            Self::DayNight => TimeSliceSelection::Single(ts.clone()),
213        }
214    }
215}
216
217/// Information about the time slices in the simulation, including names and durations
218#[derive(PartialEq, Debug)]
219pub struct TimeSliceInfo {
220    /// Names of times of day (e.g. "evening")
221    pub times_of_day: IndexSet<TimeOfDay>,
222    /// Names and fraction of year occupied by each season
223    pub seasons: IndexMap<Season, Year>,
224    /// The fraction of the year that this combination of season and time of day occupies
225    pub time_slices: IndexMap<TimeSliceID, Year>,
226}
227
228impl Default for TimeSliceInfo {
229    /// The default `TimeSliceInfo` is a single time slice covering the whole year
230    fn default() -> Self {
231        let id = TimeSliceID {
232            season: "all-year".into(),
233            time_of_day: "all-day".into(),
234        };
235        let time_slices = [(id.clone(), Year(1.0))].into_iter().collect();
236
237        Self {
238            seasons: iter::once((id.season, Year(1.0))).collect(),
239            times_of_day: iter::once(id.time_of_day).collect(),
240            time_slices,
241        }
242    }
243}
244
245impl TimeSliceInfo {
246    #[allow(clippy::doc_markdown)]
247    /// Get the `TimeSliceID` corresponding to the `time_slice`.
248    ///
249    /// `time_slice` must be in the form "season.time_of_day".
250    pub fn get_time_slice_id_from_str(&self, time_slice: &str) -> Result<TimeSliceID> {
251        let (season, time_of_day) = time_slice
252            .split('.')
253            .collect_tuple()
254            .context("Time slice must be in the form season.time_of_day")?;
255        let season = self.seasons.get_id(season)?;
256        let time_of_day = self.times_of_day.get_id(time_of_day)?;
257
258        Ok(TimeSliceID {
259            season: season.clone(),
260            time_of_day: time_of_day.clone(),
261        })
262    }
263
264    /// Get a `TimeSliceSelection` from the specified string.
265    ///
266    /// If the string is empty, the default value is `TimeSliceSelection::Annual`.
267    pub fn get_selection(&self, time_slice: &str) -> Result<TimeSliceSelection> {
268        if time_slice.eq_ignore_ascii_case("annual") {
269            Ok(TimeSliceSelection::Annual)
270        } else if time_slice.contains('.') {
271            let time_slice = self.get_time_slice_id_from_str(time_slice)?;
272            Ok(TimeSliceSelection::Single(time_slice))
273        } else {
274            let season = self.seasons.get_id(time_slice)?.clone();
275            Ok(TimeSliceSelection::Season(season))
276        }
277    }
278
279    /// Iterate over all [`TimeSliceID`]s
280    pub fn iter_ids(&self) -> indexmap::map::Keys<'_, TimeSliceID, Year> {
281        self.time_slices.keys()
282    }
283
284    /// Iterate over all seasons
285    pub fn iter_seasons(&self) -> indexmap::map::Keys<'_, Season, Year> {
286        self.seasons.keys()
287    }
288
289    /// Iterate over all time slices
290    pub fn iter(&self) -> impl Iterator<Item = (&TimeSliceID, Year)> {
291        self.time_slices
292            .iter()
293            .map(|(ts, duration)| (ts, *duration))
294    }
295
296    /// Iterate over the different time slice selections for a given time slice level.
297    ///
298    /// For example, if [`TimeSliceLevel::Season`] is specified, this function will return an
299    /// iterator of [`TimeSliceSelection`]s covering each season.
300    pub fn iter_selections_at_level(
301        &self,
302        level: TimeSliceLevel,
303    ) -> Box<dyn Iterator<Item = TimeSliceSelection> + '_> {
304        match level {
305            TimeSliceLevel::Annual => Box::new(iter::once(TimeSliceSelection::Annual)),
306            TimeSliceLevel::Season => {
307                Box::new(self.seasons.keys().cloned().map(TimeSliceSelection::Season))
308            }
309            TimeSliceLevel::DayNight => {
310                Box::new(self.iter_ids().cloned().map(TimeSliceSelection::Single))
311            }
312        }
313    }
314
315    /// Iterate over a subset of time slices calculating the relative duration of each.
316    ///
317    /// The relative duration is specified as a fraction of the total time covered by `selection`.
318    ///
319    /// # Arguments
320    ///
321    /// * `selection` - A subset of time slices
322    ///
323    /// # Returns
324    ///
325    /// An iterator of time slices along with the fraction of the total selection.
326    pub fn iter_selection_share<'a>(
327        &'a self,
328        selection: &'a TimeSliceSelection,
329        level: TimeSliceLevel,
330    ) -> Option<impl Iterator<Item = (TimeSliceSelection, Dimensionless)> + use<>> {
331        // Store selections as we have to iterate twice
332        let selections = selection.iter_at_level(self, level)?.collect_vec();
333
334        // Total duration covered by selection
335        let total_duration: Year = selections.iter().map(|(_, duration)| *duration).sum();
336
337        // Calculate share
338        let iter = selections
339            .into_iter()
340            .map(move |(selection, duration)| (selection, duration / total_duration));
341        Some(iter)
342    }
343
344    /// Calculate the total length of a selection of time slices.
345    pub fn length_for_selection(&self, selection: &TimeSliceSelection) -> Result<Year> {
346        let length: Year = selection.iter(self).map(|(_, duration)| duration).sum();
347        Ok(length)
348    }
349
350    /// Share a value between a subset of time slices in proportion to their lengths.
351    ///
352    /// For instance, you could use this function to compute how demand is distributed between the
353    /// different time slices of winter.
354    ///
355    /// # Arguments
356    ///
357    /// * `selection` - A subset of time slices
358    /// * `value` - The value to be shared between the time slices
359    ///
360    /// # Returns
361    ///
362    /// An iterator of time slices along with a fraction of `value`.
363    pub fn calculate_share<'a>(
364        &'a self,
365        selection: &'a TimeSliceSelection,
366        level: TimeSliceLevel,
367        value: Dimensionless,
368    ) -> Option<impl Iterator<Item = (TimeSliceSelection, Dimensionless)> + use<>> {
369        let iter = self
370            .iter_selection_share(selection, level)?
371            .map(move |(selection, share)| (selection, value * share));
372        Some(iter)
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use crate::units::UnitType;
380    use itertools::assert_equal;
381    use rstest::{fixture, rstest};
382
383    #[fixture]
384    fn time_slices1() -> [TimeSliceID; 2] {
385        [
386            TimeSliceID {
387                season: "winter".into(),
388                time_of_day: "day".into(),
389            },
390            TimeSliceID {
391                season: "summer".into(),
392                time_of_day: "night".into(),
393            },
394        ]
395    }
396
397    #[fixture]
398    fn time_slice_info1(time_slices1: [TimeSliceID; 2]) -> TimeSliceInfo {
399        TimeSliceInfo {
400            seasons: [("winter".into(), Year(0.5)), ("summer".into(), Year(0.5))]
401                .into_iter()
402                .collect(),
403            times_of_day: ["day".into(), "night".into()].into_iter().collect(),
404            time_slices: time_slices1.map(|ts| (ts, Year(0.5))).into_iter().collect(),
405        }
406    }
407
408    #[fixture]
409    fn time_slice_info2() -> TimeSliceInfo {
410        let time_slices = [
411            TimeSliceID {
412                season: "winter".into(),
413                time_of_day: "day".into(),
414            },
415            TimeSliceID {
416                season: "winter".into(),
417                time_of_day: "night".into(),
418            },
419            TimeSliceID {
420                season: "summer".into(),
421                time_of_day: "day".into(),
422            },
423            TimeSliceID {
424                season: "summer".into(),
425                time_of_day: "night".into(),
426            },
427        ];
428        TimeSliceInfo {
429            times_of_day: ["day".into(), "night".into()].into_iter().collect(),
430            seasons: [("winter".into(), Year(0.5)), ("summer".into(), Year(0.5))]
431                .into_iter()
432                .collect(),
433            time_slices: time_slices
434                .iter()
435                .map(|ts| (ts.clone(), Year(0.25)))
436                .collect(),
437        }
438    }
439
440    #[rstest]
441    fn ts_selection_iter_annual(time_slice_info1: TimeSliceInfo, time_slices1: [TimeSliceID; 2]) {
442        assert_equal(
443            TimeSliceSelection::Annual.iter(&time_slice_info1),
444            time_slices1.iter().map(|ts| (ts, Year(0.5))),
445        );
446    }
447
448    #[rstest]
449    fn ts_selection_iter_season(time_slice_info1: TimeSliceInfo, time_slices1: [TimeSliceID; 2]) {
450        assert_equal(
451            TimeSliceSelection::Season("winter".into()).iter(&time_slice_info1),
452            iter::once((&time_slices1[0], Year(0.5))),
453        );
454    }
455
456    #[rstest]
457    fn ts_selection_iter_single(time_slice_info1: TimeSliceInfo, time_slices1: [TimeSliceID; 2]) {
458        let ts = time_slice_info1
459            .get_time_slice_id_from_str("summer.night")
460            .unwrap();
461        assert_equal(
462            TimeSliceSelection::Single(ts).iter(&time_slice_info1),
463            iter::once((&time_slices1[1], Year(0.5))),
464        );
465    }
466
467    fn assert_selection_equal<I, T>(actual: Option<I>, expected: Option<Vec<(&str, T)>>)
468    where
469        T: UnitType,
470        I: Iterator<Item = (TimeSliceSelection, T)>,
471    {
472        let Some(actual) = actual else {
473            assert!(expected.is_none());
474            return;
475        };
476
477        let ts_info = time_slice_info2();
478        let expected = expected
479            .unwrap()
480            .into_iter()
481            .map(move |(sel, frac)| (ts_info.get_selection(sel).unwrap(), frac));
482        assert_equal(actual, expected);
483    }
484
485    #[rstest]
486    #[case(TimeSliceSelection::Annual, TimeSliceLevel::Annual, Some(vec![("annual", Year(1.0))]))]
487    #[case(TimeSliceSelection::Annual, TimeSliceLevel::Season, Some(vec![("winter", Year(0.5)), ("summer", Year(0.5))]))]
488    #[case(TimeSliceSelection::Annual, TimeSliceLevel::DayNight,
489           Some(vec![("winter.day", Year(0.25)), ("winter.night", Year(0.25)), ("summer.day", Year(0.25)), ("summer.night", Year(0.25))]))]
490    #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Annual, None)]
491    #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Season, Some(vec![("winter", Year(0.5))]))]
492    #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::DayNight,
493           Some(vec![("winter.day", Year(0.25)), ("winter.night", Year(0.25))]))]
494    #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Annual, None)]
495    #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Season, None)]
496    #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::DayNight, Some(vec![("winter.day", Year(0.25))]))]
497    fn ts_selection_iter_at_level(
498        time_slice_info2: TimeSliceInfo,
499        #[case] selection: TimeSliceSelection,
500        #[case] level: TimeSliceLevel,
501        #[case] expected: Option<Vec<(&str, Year)>>,
502    ) {
503        let actual = selection.iter_at_level(&time_slice_info2, level);
504        assert_selection_equal(actual, expected);
505    }
506
507    #[rstest]
508    #[case(TimeSliceSelection::Annual, TimeSliceLevel::Annual, Some(vec![("annual", Dimensionless(8.0))]))]
509    #[case(TimeSliceSelection::Annual, TimeSliceLevel::Season, Some(vec![("winter", Dimensionless(4.0)), ("summer", Dimensionless(4.0))]))]
510    #[case(TimeSliceSelection::Annual, TimeSliceLevel::DayNight,
511           Some(vec![("winter.day", Dimensionless(2.0)), ("winter.night", Dimensionless(2.0)), ("summer.day", Dimensionless(2.0)), ("summer.night", Dimensionless(2.0))]))]
512    #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Annual, None)]
513    #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::Season, Some(vec![("winter", Dimensionless(8.0))]))]
514    #[case(TimeSliceSelection::Season("winter".into()), TimeSliceLevel::DayNight,
515           Some(vec![("winter.day", Dimensionless(4.0)), ("winter.night", Dimensionless(4.0))]))]
516    #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Annual, None)]
517    #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::Season, None)]
518    #[case(TimeSliceSelection::Single("winter.day".into()), TimeSliceLevel::DayNight, Some(vec![("winter.day", Dimensionless(8.0))]))]
519    fn calculate_share(
520        time_slice_info2: TimeSliceInfo,
521        #[case] selection: TimeSliceSelection,
522        #[case] level: TimeSliceLevel,
523        #[case] expected: Option<Vec<(&str, Dimensionless)>>,
524    ) {
525        let actual = time_slice_info2.calculate_share(&selection, level, Dimensionless(8.0));
526        assert_selection_equal(actual, expected);
527    }
528}