muse2/input/
time_slice.rs1use super::*;
3use crate::id::IDCollection;
4use crate::time_slice::{Season, TimeOfDay, TimeSliceID, TimeSliceInfo};
5use crate::units::Dimensionless;
6use anyhow::{ensure, Context, Result};
7use indexmap::{IndexMap, IndexSet};
8use serde::Deserialize;
9use std::path::Path;
10
11const TIME_SLICES_FILE_NAME: &str = "time_slices.csv";
12
13#[derive(PartialEq, Debug, Deserialize)]
15struct TimeSliceRaw {
16 season: Season,
17 time_of_day: TimeOfDay,
18 #[serde(deserialize_with = "deserialise_proportion_nonzero")]
19 fraction: Dimensionless,
20}
21
22fn get_or_insert<T: IDLike>(id: T, set: &mut IndexSet<T>) -> T {
26 match set.get_id(&id) {
28 Ok(id) => id.clone(),
29 Err(_) => {
30 set.insert(id.clone());
31 id
32 }
33 }
34}
35
36fn read_time_slice_info_from_iter<I>(iter: I) -> Result<TimeSliceInfo>
38where
39 I: Iterator<Item = TimeSliceRaw>,
40{
41 let mut times_of_day = IndexSet::new();
42 let mut seasons = IndexMap::new();
43 let mut fractions = IndexMap::new();
44 for time_slice in iter {
45 let time_of_day = get_or_insert(time_slice.time_of_day, &mut times_of_day);
46 let season = match seasons.entry(time_slice.season) {
47 indexmap::map::Entry::Occupied(mut entry) => {
48 *entry.get_mut() += time_slice.fraction;
49 entry.key().clone()
50 }
51 indexmap::map::Entry::Vacant(entry) => {
52 let key = entry.key().clone();
53 entry.insert(time_slice.fraction);
54 key
55 }
56 };
57 let id = TimeSliceID {
58 season,
59 time_of_day,
60 };
61
62 ensure!(
63 fractions.insert(id.clone(), time_slice.fraction).is_none(),
64 "Duplicate time slice entry for {id}",
65 );
66 }
67
68 check_fractions_sum_to_one(fractions.values().cloned())
70 .context("Invalid time slice fractions")?;
71
72 Ok(TimeSliceInfo {
73 times_of_day,
74 seasons,
75 time_slices: fractions,
76 })
77}
78
79pub fn read_time_slice_info(model_dir: &Path) -> Result<TimeSliceInfo> {
90 let file_path = model_dir.join(TIME_SLICES_FILE_NAME);
91 if !file_path.exists() {
92 return Ok(TimeSliceInfo::default());
93 }
94
95 let time_slices_csv = read_csv(&file_path)?;
96 read_time_slice_info_from_iter(time_slices_csv).with_context(|| input_err_msg(file_path))
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use std::fs::File;
103 use std::io::Write;
104 use std::path::Path;
105 use tempfile::tempdir;
106
107 fn create_time_slices_file(dir_path: &Path) {
109 let file_path = dir_path.join(TIME_SLICES_FILE_NAME);
110 let mut file = File::create(file_path).unwrap();
111 writeln!(
112 file,
113 "season,time_of_day,fraction
114winter,day,0.25
115peak,night,0.25
116summer,peak,0.25
117autumn,evening,0.25"
118 )
119 .unwrap();
120 }
121
122 #[test]
123 fn test_read_time_slice_info() {
124 let dir = tempdir().unwrap();
125 create_time_slices_file(dir.path());
126
127 let info = read_time_slice_info(dir.path()).unwrap();
128 assert_eq!(
129 info,
130 TimeSliceInfo {
131 seasons: [
132 ("winter".into(), Dimensionless(0.25)),
133 ("peak".into(), Dimensionless(0.25)),
134 ("summer".into(), Dimensionless(0.25)),
135 ("autumn".into(), Dimensionless(0.25))
136 ]
137 .into_iter()
138 .collect(),
139 times_of_day: [
140 "day".into(),
141 "night".into(),
142 "peak".into(),
143 "evening".into()
144 ]
145 .into_iter()
146 .collect(),
147 time_slices: [
148 (
149 TimeSliceID {
150 season: "winter".into(),
151 time_of_day: "day".into(),
152 },
153 Dimensionless(0.25),
154 ),
155 (
156 TimeSliceID {
157 season: "peak".into(),
158 time_of_day: "night".into(),
159 },
160 Dimensionless(0.25),
161 ),
162 (
163 TimeSliceID {
164 season: "summer".into(),
165 time_of_day: "peak".into(),
166 },
167 Dimensionless(0.25),
168 ),
169 (
170 TimeSliceID {
171 season: "autumn".into(),
172 time_of_day: "evening".into(),
173 },
174 Dimensionless(0.25),
175 ),
176 ]
177 .into_iter()
178 .collect()
179 }
180 );
181 }
182
183 #[test]
184 fn test_read_time_slice_info_non_existent() {
185 let actual = read_time_slice_info(tempdir().unwrap().path());
186 assert_eq!(actual.unwrap(), TimeSliceInfo::default());
187 }
188}