1use chrono::NaiveDate;
37
38use crate::EntityId;
39
40#[derive(Debug, Clone, PartialEq)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48pub struct HydroStorage {
49 pub hydro_id: EntityId,
51 pub value_hm3: f64,
53}
54
55#[derive(Debug, Clone, PartialEq)]
61#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
62pub struct HydroPastInflows {
63 pub hydro_id: EntityId,
65 pub values_m3s: Vec<f64>,
68 #[cfg_attr(feature = "serde", serde(default))]
78 pub season_ids: Option<Vec<u32>>,
79}
80
81#[derive(Debug, Clone, PartialEq)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
92pub struct RecentObservation {
93 pub hydro_id: EntityId,
95 pub start_date: NaiveDate,
97 pub end_date: NaiveDate,
99 pub value_m3s: f64,
102}
103
104#[derive(Debug, Clone, PartialEq)]
117#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
118pub struct InitialConditions {
119 pub storage: Vec<HydroStorage>,
121 pub filling_storage: Vec<HydroStorage>,
123 #[cfg_attr(feature = "serde", serde(default))]
133 pub past_inflows: Vec<HydroPastInflows>,
134 #[cfg_attr(feature = "serde", serde(default))]
143 pub recent_observations: Vec<RecentObservation>,
144}
145
146impl Default for InitialConditions {
147 fn default() -> Self {
149 Self {
150 storage: Vec::new(),
151 filling_storage: Vec::new(),
152 past_inflows: Vec::new(),
153 recent_observations: Vec::new(),
154 }
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn test_initial_conditions_construction() {
164 let ic = InitialConditions {
165 storage: vec![
166 HydroStorage {
167 hydro_id: EntityId(0),
168 value_hm3: 15_000.0,
169 },
170 HydroStorage {
171 hydro_id: EntityId(1),
172 value_hm3: 8_500.0,
173 },
174 ],
175 filling_storage: vec![HydroStorage {
176 hydro_id: EntityId(10),
177 value_hm3: 200.0,
178 }],
179 past_inflows: vec![HydroPastInflows {
180 hydro_id: EntityId(0),
181 values_m3s: vec![600.0, 500.0],
182 season_ids: None,
183 }],
184 recent_observations: vec![],
185 };
186
187 assert_eq!(ic.storage.len(), 2);
188 assert_eq!(ic.filling_storage.len(), 1);
189 assert_eq!(ic.past_inflows.len(), 1);
190 assert_eq!(ic.storage[0].hydro_id, EntityId(0));
191 assert_eq!(ic.storage[0].value_hm3, 15_000.0);
192 assert_eq!(ic.storage[1].hydro_id, EntityId(1));
193 assert_eq!(ic.filling_storage[0].hydro_id, EntityId(10));
194 assert_eq!(ic.filling_storage[0].value_hm3, 200.0);
195 assert_eq!(ic.past_inflows[0].hydro_id, EntityId(0));
196 assert_eq!(ic.past_inflows[0].values_m3s, vec![600.0, 500.0]);
197 assert!(ic.recent_observations.is_empty());
198 }
199
200 #[test]
201 fn test_initial_conditions_default_is_empty() {
202 let ic = InitialConditions::default();
203 assert!(ic.storage.is_empty());
204 assert!(ic.filling_storage.is_empty());
205 assert!(ic.past_inflows.is_empty());
206 assert!(ic.recent_observations.is_empty());
207 }
208
209 #[test]
210 fn test_hydro_storage_clone() {
211 let hs = HydroStorage {
212 hydro_id: EntityId(5),
213 value_hm3: 1_234.5,
214 };
215 let cloned = hs.clone();
216 assert_eq!(hs, cloned);
217 assert_eq!(cloned.hydro_id, EntityId(5));
218 assert_eq!(cloned.value_hm3, 1_234.5);
219 }
220
221 #[test]
222 fn test_hydro_past_inflows_clone() {
223 let hpi = HydroPastInflows {
224 hydro_id: EntityId(3),
225 values_m3s: vec![300.0, 200.0, 100.0],
226 season_ids: None,
227 };
228 let cloned = hpi.clone();
229 assert_eq!(hpi, cloned);
230 assert_eq!(cloned.hydro_id, EntityId(3));
231 assert_eq!(cloned.values_m3s, vec![300.0, 200.0, 100.0]);
232 }
233
234 #[cfg(feature = "serde")]
235 #[test]
236 fn test_initial_conditions_serde_roundtrip() {
237 let ic = InitialConditions {
238 storage: vec![
239 HydroStorage {
240 hydro_id: EntityId(0),
241 value_hm3: 15_000.0,
242 },
243 HydroStorage {
244 hydro_id: EntityId(1),
245 value_hm3: 8_500.0,
246 },
247 ],
248 filling_storage: vec![HydroStorage {
249 hydro_id: EntityId(10),
250 value_hm3: 200.0,
251 }],
252 past_inflows: vec![HydroPastInflows {
253 hydro_id: EntityId(0),
254 values_m3s: vec![600.0, 500.0],
255 season_ids: None,
256 }],
257 recent_observations: vec![],
258 };
259
260 let json = serde_json::to_string(&ic).unwrap();
261 let deserialized: InitialConditions = serde_json::from_str(&json).unwrap();
262 assert_eq!(ic, deserialized);
263 }
264
265 #[cfg(feature = "serde")]
266 #[test]
267 fn test_initial_conditions_serde_roundtrip_empty_past_inflows() {
268 let ic = InitialConditions {
271 storage: vec![HydroStorage {
272 hydro_id: EntityId(0),
273 value_hm3: 1_000.0,
274 }],
275 filling_storage: vec![],
276 past_inflows: vec![],
277 recent_observations: vec![],
278 };
279
280 let json = serde_json::to_string(&ic).unwrap();
281 let deserialized: InitialConditions = serde_json::from_str(&json).unwrap();
282 assert_eq!(ic, deserialized);
283 assert_eq!(deserialized.past_inflows.len(), 0);
285 }
286
287 #[test]
288 fn test_recent_observation_construction_and_clone() {
289 let obs = RecentObservation {
290 hydro_id: EntityId(2),
291 start_date: NaiveDate::from_ymd_opt(2026, 4, 1)
292 .unwrap_or_else(|| unreachable!("hardcoded date is valid")),
293 end_date: NaiveDate::from_ymd_opt(2026, 4, 4)
294 .unwrap_or_else(|| unreachable!("hardcoded date is valid")),
295 value_m3s: 500.0,
296 };
297 let cloned = obs.clone();
298 assert_eq!(obs, cloned);
299 assert_eq!(cloned.hydro_id, EntityId(2));
300 assert_eq!(cloned.value_m3s, 500.0);
301 }
302
303 #[test]
304 fn test_initial_conditions_construction_with_recent_observations() {
305 let ic = InitialConditions {
306 storage: vec![HydroStorage {
307 hydro_id: EntityId(0),
308 value_hm3: 1_000.0,
309 }],
310 filling_storage: vec![],
311 past_inflows: vec![],
312 recent_observations: vec![RecentObservation {
313 hydro_id: EntityId(0),
314 start_date: NaiveDate::from_ymd_opt(2026, 4, 1)
315 .unwrap_or_else(|| unreachable!("hardcoded date is valid")),
316 end_date: NaiveDate::from_ymd_opt(2026, 4, 4)
317 .unwrap_or_else(|| unreachable!("hardcoded date is valid")),
318 value_m3s: 500.0,
319 }],
320 };
321 assert_eq!(ic.recent_observations.len(), 1);
322 assert_eq!(ic.recent_observations[0].hydro_id, EntityId(0));
323 assert_eq!(ic.recent_observations[0].value_m3s, 500.0);
324 }
325
326 #[cfg(feature = "serde")]
327 #[test]
328 fn test_initial_conditions_serde_roundtrip_with_recent_observations() {
329 let ic = InitialConditions {
330 storage: vec![HydroStorage {
331 hydro_id: EntityId(0),
332 value_hm3: 1_000.0,
333 }],
334 filling_storage: vec![],
335 past_inflows: vec![],
336 recent_observations: vec![
337 RecentObservation {
338 hydro_id: EntityId(0),
339 start_date: NaiveDate::from_ymd_opt(2026, 4, 1)
340 .unwrap_or_else(|| unreachable!("hardcoded date is valid")),
341 end_date: NaiveDate::from_ymd_opt(2026, 4, 4)
342 .unwrap_or_else(|| unreachable!("hardcoded date is valid")),
343 value_m3s: 500.0,
344 },
345 RecentObservation {
346 hydro_id: EntityId(0),
347 start_date: NaiveDate::from_ymd_opt(2026, 4, 4)
348 .unwrap_or_else(|| unreachable!("hardcoded date is valid")),
349 end_date: NaiveDate::from_ymd_opt(2026, 4, 11)
350 .unwrap_or_else(|| unreachable!("hardcoded date is valid")),
351 value_m3s: 480.0,
352 },
353 ],
354 };
355 let json = serde_json::to_string(&ic).unwrap();
356 let deserialized: InitialConditions = serde_json::from_str(&json).unwrap();
357 assert_eq!(ic, deserialized);
358 assert_eq!(deserialized.recent_observations.len(), 2);
359 }
360
361 #[cfg(feature = "serde")]
362 #[test]
363 fn test_initial_conditions_serde_default_recent_observations_absent() {
364 let json = r#"{"storage":[],"filling_storage":[]}"#;
366 let ic: InitialConditions = serde_json::from_str(json).unwrap();
367 assert!(ic.recent_observations.is_empty());
368 }
369
370 #[test]
371 fn test_hydro_past_inflows_with_season_ids() {
372 let hpi = HydroPastInflows {
373 hydro_id: EntityId(5),
374 values_m3s: vec![600.0, 500.0],
375 season_ids: Some(vec![3, 2]),
376 };
377 assert_eq!(hpi.hydro_id, EntityId(5));
378 assert_eq!(hpi.values_m3s, vec![600.0, 500.0]);
379 assert_eq!(hpi.season_ids, Some(vec![3, 2]));
380 let cloned = hpi.clone();
382 assert_eq!(cloned, hpi);
383 }
384
385 #[cfg(feature = "serde")]
386 #[test]
387 fn test_hydro_past_inflows_serde_roundtrip_with_season_ids() {
388 let hpi = HydroPastInflows {
389 hydro_id: EntityId(5),
390 values_m3s: vec![600.0, 500.0],
391 season_ids: Some(vec![3, 2]),
392 };
393 let json = serde_json::to_string(&hpi).unwrap();
394 let deserialized: HydroPastInflows = serde_json::from_str(&json).unwrap();
395 assert_eq!(hpi, deserialized);
396 assert_eq!(deserialized.season_ids, Some(vec![3, 2]));
397 }
398
399 #[cfg(feature = "serde")]
400 #[test]
401 fn test_hydro_past_inflows_serde_default_season_ids_absent() {
402 let json = r#"{"hydro_id":0,"values_m3s":[600.0,500.0]}"#;
404 let hpi: HydroPastInflows = serde_json::from_str(json).unwrap();
405 assert_eq!(hpi.hydro_id, EntityId(0));
406 assert_eq!(hpi.values_m3s, vec![600.0, 500.0]);
407 assert_eq!(hpi.season_ids, None);
408 }
409}