cobre_core/initial_conditions.rs
1//! Initial conditions for the optimization study.
2//!
3//! [`InitialConditions`] holds the reservoir storage levels at the start of
4//! the study. Two arrays are kept separate because filling hydros can have
5//! an initial volume below dead storage (`min_storage_hm3`), which is not
6//! a valid operating level for regular hydros.
7//!
8//! See `internal-structures.md §16` and `input-constraints.md §1` for the
9//! full specification including validation rules.
10//!
11//! # Examples
12//!
13//! ```
14//! use cobre_core::{EntityId, InitialConditions, HydroStorage};
15//!
16//! let ic = InitialConditions {
17//! storage: vec![
18//! HydroStorage { hydro_id: EntityId(0), value_hm3: 15_000.0 },
19//! HydroStorage { hydro_id: EntityId(1), value_hm3: 8_500.0 },
20//! ],
21//! filling_storage: vec![
22//! HydroStorage { hydro_id: EntityId(10), value_hm3: 200.0 },
23//! ],
24//! };
25//!
26//! assert_eq!(ic.storage.len(), 2);
27//! assert_eq!(ic.filling_storage.len(), 1);
28//! ```
29
30use crate::EntityId;
31
32/// Initial storage volume for a single hydro plant.
33///
34/// For operating hydros, `value_hm3` must be within
35/// `[min_storage_hm3, max_storage_hm3]` (validated by `cobre-io`).
36/// For filling hydros (present in [`InitialConditions::filling_storage`]),
37/// `value_hm3` must be within `[0.0, min_storage_hm3]`.
38#[derive(Debug, Clone, PartialEq)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40pub struct HydroStorage {
41 /// Hydro plant identifier. Must reference a hydro entity in the system.
42 pub hydro_id: EntityId,
43 /// Reservoir volume at the start of the study, in hm³.
44 pub value_hm3: f64,
45}
46
47/// Initial system state at the start of the optimization study.
48///
49/// Produced by parsing `initial_conditions.json` (in `cobre-io`) and stored
50/// inside [`crate::System`]. Both arrays are sorted by `hydro_id` after
51/// loading to satisfy the declaration-order invariance requirement.
52///
53/// A hydro must appear in exactly one of the two arrays, never both. Hydros
54/// with a `filling` configuration belong in [`filling_storage`]; all other
55/// hydros (including late-entry hydros) belong in [`storage`](InitialConditions::storage).
56///
57/// [`filling_storage`]: InitialConditions::filling_storage
58#[derive(Debug, Clone, PartialEq)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60pub struct InitialConditions {
61 /// Initial storage for operating hydros, in hm³ per hydro.
62 pub storage: Vec<HydroStorage>,
63 /// Initial storage for filling hydros (below dead volume), in hm³ per hydro.
64 pub filling_storage: Vec<HydroStorage>,
65}
66
67impl Default for InitialConditions {
68 /// Returns an empty `InitialConditions` (no hydros).
69 fn default() -> Self {
70 Self {
71 storage: Vec::new(),
72 filling_storage: Vec::new(),
73 }
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn test_initial_conditions_construction() {
83 let ic = InitialConditions {
84 storage: vec![
85 HydroStorage {
86 hydro_id: EntityId(0),
87 value_hm3: 15_000.0,
88 },
89 HydroStorage {
90 hydro_id: EntityId(1),
91 value_hm3: 8_500.0,
92 },
93 ],
94 filling_storage: vec![HydroStorage {
95 hydro_id: EntityId(10),
96 value_hm3: 200.0,
97 }],
98 };
99
100 assert_eq!(ic.storage.len(), 2);
101 assert_eq!(ic.filling_storage.len(), 1);
102 assert_eq!(ic.storage[0].hydro_id, EntityId(0));
103 assert_eq!(ic.storage[0].value_hm3, 15_000.0);
104 assert_eq!(ic.storage[1].hydro_id, EntityId(1));
105 assert_eq!(ic.filling_storage[0].hydro_id, EntityId(10));
106 assert_eq!(ic.filling_storage[0].value_hm3, 200.0);
107 }
108
109 #[test]
110 fn test_initial_conditions_default_is_empty() {
111 let ic = InitialConditions::default();
112 assert!(ic.storage.is_empty());
113 assert!(ic.filling_storage.is_empty());
114 }
115
116 #[test]
117 fn test_hydro_storage_clone() {
118 let hs = HydroStorage {
119 hydro_id: EntityId(5),
120 value_hm3: 1_234.5,
121 };
122 let cloned = hs.clone();
123 assert_eq!(hs, cloned);
124 assert_eq!(cloned.hydro_id, EntityId(5));
125 assert_eq!(cloned.value_hm3, 1_234.5);
126 }
127
128 #[cfg(feature = "serde")]
129 #[test]
130 fn test_initial_conditions_serde_roundtrip() {
131 let ic = InitialConditions {
132 storage: vec![
133 HydroStorage {
134 hydro_id: EntityId(0),
135 value_hm3: 15_000.0,
136 },
137 HydroStorage {
138 hydro_id: EntityId(1),
139 value_hm3: 8_500.0,
140 },
141 ],
142 filling_storage: vec![HydroStorage {
143 hydro_id: EntityId(10),
144 value_hm3: 200.0,
145 }],
146 };
147
148 let json = serde_json::to_string(&ic).unwrap();
149 let deserialized: InitialConditions = serde_json::from_str(&json).unwrap();
150 assert_eq!(ic, deserialized);
151 }
152}