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 anyhow::{Context, Result};
6use indexmap::{IndexMap, IndexSet};
7use itertools::Itertools;
8use serde_string_enum::DeserializeLabeledStringEnum;
9use std::fmt::Display;
10use std::iter;
11use std::rc::Rc;
12
13/// An ID describing season and time of day
14#[derive(Hash, Eq, PartialEq, Clone, Debug)]
15pub struct TimeSliceID {
16    /// The name of each season.
17    pub season: Rc<str>,
18    /// The name of each time slice within a day.
19    pub time_of_day: Rc<str>,
20}
21
22impl Display for TimeSliceID {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        write!(f, "{}.{}", self.season, self.time_of_day)
25    }
26}
27
28/// Represents a time slice read from an input file, which can be all
29#[derive(PartialEq, Clone, Debug)]
30pub enum TimeSliceSelection {
31    /// All year and all day
32    Annual,
33    /// Only applies to one season
34    Season(Rc<str>),
35    /// Only applies to a single time slice
36    Single(TimeSliceID),
37}
38
39/// The time granularity for a particular operation
40#[derive(PartialEq, Copy, Clone, Debug, DeserializeLabeledStringEnum)]
41pub enum TimeSliceLevel {
42    /// The whole year
43    #[string = "annual"]
44    Annual,
45    /// Whole seasons
46    #[string = "season"]
47    Season,
48    /// Treat individual time slices separately
49    #[string = "daynight"]
50    DayNight,
51}
52
53/// Information about the time slices in the simulation, including names and fractions
54#[derive(PartialEq, Debug)]
55pub struct TimeSliceInfo {
56    /// Names of seasons
57    pub seasons: IndexSet<Rc<str>>,
58    /// Names of times of day (e.g. "evening")
59    pub times_of_day: IndexSet<Rc<str>>,
60    /// The fraction of the year that this combination of season and time of day occupies
61    pub fractions: IndexMap<TimeSliceID, f64>,
62}
63
64impl Default for TimeSliceInfo {
65    /// The default `TimeSliceInfo` is a single time slice covering the whole year
66    fn default() -> Self {
67        let id = TimeSliceID {
68            season: "all-year".into(),
69            time_of_day: "all-day".into(),
70        };
71        let fractions = [(id.clone(), 1.0)].into_iter().collect();
72
73        Self {
74            seasons: [id.season].into_iter().collect(),
75            times_of_day: [id.time_of_day].into_iter().collect(),
76            fractions,
77        }
78    }
79}
80
81impl TimeSliceInfo {
82    /// Get the `TimeSliceID` corresponding to the `time_slice`.
83    ///
84    /// `time_slice` must be in the form "season.time_of_day".
85    pub fn get_time_slice_id_from_str(&self, time_slice: &str) -> Result<TimeSliceID> {
86        let (season, time_of_day) = time_slice
87            .split('.')
88            .collect_tuple()
89            .context("Time slice must be in the form season.time_of_day")?;
90        let season = self
91            .seasons
92            .iter()
93            .find(|item| item.eq_ignore_ascii_case(season))
94            .with_context(|| format!("{} is not a known season", season))?;
95        let time_of_day = self
96            .times_of_day
97            .iter()
98            .find(|item| item.eq_ignore_ascii_case(time_of_day))
99            .with_context(|| format!("{} is not a known time of day", time_of_day))?;
100
101        Ok(TimeSliceID {
102            season: Rc::clone(season),
103            time_of_day: Rc::clone(time_of_day),
104        })
105    }
106
107    /// Get a `TimeSliceSelection` from the specified string.
108    ///
109    /// If the string is empty, the default value is `TimeSliceSelection::Annual`.
110    pub fn get_selection(&self, time_slice: &str) -> Result<TimeSliceSelection> {
111        if time_slice.is_empty() || time_slice.eq_ignore_ascii_case("annual") {
112            Ok(TimeSliceSelection::Annual)
113        } else if time_slice.contains('.') {
114            let time_slice = self.get_time_slice_id_from_str(time_slice)?;
115            Ok(TimeSliceSelection::Single(time_slice))
116        } else {
117            let season = self
118                .seasons
119                .get(time_slice)
120                .with_context(|| format!("'{time_slice}' is not a valid season"))?
121                .clone();
122            Ok(TimeSliceSelection::Season(season))
123        }
124    }
125
126    /// Iterate over all [`TimeSliceID`]s.
127    ///
128    /// The order will be consistent each time this is called, but not every time the program is
129    /// run.
130    pub fn iter_ids(&self) -> impl Iterator<Item = &TimeSliceID> {
131        self.fractions.keys()
132    }
133
134    /// Iterate over all time slices.
135    ///
136    /// The order will be consistent each time this is called, but not every time the program is
137    /// run.
138    pub fn iter(&self) -> impl Iterator<Item = (&TimeSliceID, f64)> {
139        self.fractions.iter().map(|(ts, fraction)| (ts, *fraction))
140    }
141
142    /// Iterate over the subset of time slices indicated by `selection`.
143    ///
144    /// The order will be consistent each time this is called, but not every time the program is
145    /// run.
146    pub fn iter_selection<'a>(
147        &'a self,
148        selection: &'a TimeSliceSelection,
149    ) -> Box<dyn Iterator<Item = (&'a TimeSliceID, f64)> + 'a> {
150        match selection {
151            TimeSliceSelection::Annual => Box::new(self.iter()),
152            TimeSliceSelection::Season(season) => {
153                Box::new(self.iter().filter(move |(ts, _)| ts.season == *season))
154            }
155            TimeSliceSelection::Single(ts) => {
156                Box::new(iter::once((ts, *self.fractions.get(ts).unwrap())))
157            }
158        }
159    }
160
161    /// Iterate over the different time slice selections for a given time slice level.
162    ///
163    /// For example, if [`TimeSliceLevel::Season`] is specified, this function will return an
164    /// iterator of [`TimeSliceSelection`]s covering each season.
165    pub fn iter_selections_for_level(
166        &self,
167        level: TimeSliceLevel,
168    ) -> Box<dyn Iterator<Item = TimeSliceSelection> + '_> {
169        match level {
170            TimeSliceLevel::Annual => Box::new(iter::once(TimeSliceSelection::Annual)),
171            TimeSliceLevel::Season => {
172                Box::new(self.seasons.iter().cloned().map(TimeSliceSelection::Season))
173            }
174            TimeSliceLevel::DayNight => {
175                Box::new(self.iter_ids().cloned().map(TimeSliceSelection::Single))
176            }
177        }
178    }
179
180    /// Iterate over a subset of time slices calculating the relative duration of each.
181    ///
182    /// The relative duration is specified as a fraction of the total time (proportion of year)
183    /// covered by `selection`.
184    ///
185    /// # Arguments
186    ///
187    /// * `selection` - A subset of time slices
188    ///
189    /// # Returns
190    ///
191    /// An iterator of time slices along with the fraction of the total selection.
192    pub fn iterate_selection_share<'a>(
193        &'a self,
194        selection: &'a TimeSliceSelection,
195    ) -> impl Iterator<Item = (&'a TimeSliceID, f64)> {
196        // Store time slices as we have to iterate over selection twice
197        let time_slices = self.iter_selection(selection).collect_vec();
198
199        // Total fraction of year covered by selection
200        let time_total: f64 = time_slices.iter().map(|(_, fraction)| *fraction).sum();
201
202        // Calculate share
203        time_slices
204            .into_iter()
205            .map(move |(ts, time_fraction)| (ts, time_fraction / time_total))
206    }
207
208    /// Share a value between a subset of time slices in proportion to their lengths.
209    ///
210    /// For instance, you could use this function to compute how demand is distributed between the
211    /// different time slices of winter.
212    ///
213    /// # Arguments
214    ///
215    /// * `selection` - A subset of time slices
216    /// * `value` - The value to be shared between the time slices
217    ///
218    /// # Returns
219    ///
220    /// An iterator of time slices along with a fraction of `value`.
221    pub fn calculate_share<'a>(
222        &'a self,
223        selection: &'a TimeSliceSelection,
224        value: f64,
225    ) -> impl Iterator<Item = (&'a TimeSliceID, f64)> {
226        self.iterate_selection_share(selection)
227            .map(move |(ts, share)| (ts, value * share))
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234    use float_cmp::assert_approx_eq;
235    use itertools::assert_equal;
236
237    #[test]
238    fn test_iter_selection() {
239        let slices = [
240            TimeSliceID {
241                season: "winter".into(),
242                time_of_day: "day".into(),
243            },
244            TimeSliceID {
245                season: "summer".into(),
246                time_of_day: "night".into(),
247            },
248        ];
249        let ts_info = TimeSliceInfo {
250            seasons: ["winter".into(), "summer".into()].into_iter().collect(),
251            times_of_day: ["day".into(), "night".into()].into_iter().collect(),
252            fractions: [(slices[0].clone(), 0.5), (slices[1].clone(), 0.5)]
253                .into_iter()
254                .collect(),
255        };
256
257        assert_equal(
258            ts_info
259                .iter_selection(&TimeSliceSelection::Annual)
260                .map(|(ts, _)| ts),
261            slices.iter(),
262        );
263        assert_equal(
264            ts_info
265                .iter_selection(&TimeSliceSelection::Season("winter".into()))
266                .map(|(ts, _)| ts),
267            iter::once(&slices[0]),
268        );
269        let ts = ts_info.get_time_slice_id_from_str("summer.night").unwrap();
270        assert_equal(
271            ts_info
272                .iter_selection(&TimeSliceSelection::Single(ts))
273                .map(|(ts, _)| ts),
274            iter::once(&slices[1]),
275        );
276    }
277
278    #[test]
279    fn test_calculate_share() {
280        let slices = [
281            TimeSliceID {
282                season: "winter".into(),
283                time_of_day: "day".into(),
284            },
285            TimeSliceID {
286                season: "winter".into(),
287                time_of_day: "night".into(),
288            },
289            TimeSliceID {
290                season: "summer".into(),
291                time_of_day: "day".into(),
292            },
293            TimeSliceID {
294                season: "summer".into(),
295                time_of_day: "night".into(),
296            },
297        ];
298        let ts_info = TimeSliceInfo {
299            seasons: ["winter".into(), "summer".into()].into_iter().collect(),
300            times_of_day: ["day".into(), "night".into()].into_iter().collect(),
301            fractions: slices.iter().map(|ts| (ts.clone(), 0.25)).collect(),
302        };
303
304        macro_rules! check_share {
305            ($selection:expr, $expected:expr) => {
306                let expected = $expected;
307                let actual: IndexMap<_, _> = IndexMap::from_iter(
308                    ts_info
309                        .calculate_share(&$selection, 8.0)
310                        .map(|(ts, share)| (ts.clone(), share)),
311                );
312                assert!(actual.len() == expected.len());
313                for (k, v) in actual {
314                    assert_approx_eq!(f64, v, *expected.get(&k).unwrap());
315                }
316            };
317        }
318
319        // Whole year
320        let expected: IndexMap<_, _> =
321            IndexMap::from_iter(slices.iter().map(|ts| (ts.clone(), 2.0)));
322        check_share!(TimeSliceSelection::Annual, expected);
323
324        // One season
325        let selection = TimeSliceSelection::Season("winter".into());
326        let expected: IndexMap<_, _> = IndexMap::from_iter(
327            ts_info
328                .iter_selection(&selection)
329                .map(|(ts, _)| (ts.clone(), 4.0)),
330        );
331        check_share!(selection, expected);
332
333        // Single time slice
334        let time_slice = ts_info.get_time_slice_id_from_str("winter.day").unwrap();
335        let selection = TimeSliceSelection::Single(time_slice.clone());
336        let expected: IndexMap<_, _> = IndexMap::from_iter(iter::once((time_slice, 8.0)));
337        check_share!(selection, expected);
338    }
339}