Skip to main content

cobre_core/
initial_conditions.rs

1//! Initial conditions for the optimization study.
2//!
3//! [`InitialConditions`] holds the reservoir storage levels and past inflow
4//! values at the start of the study. Two storage arrays are kept separate
5//! because filling hydros can have an initial volume below dead storage
6//! (`min_storage_hm3`), which is not 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, HydroPastInflows};
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//!     past_inflows: vec![
25//!         HydroPastInflows { hydro_id: EntityId(0), values_m3s: vec![600.0, 500.0] },
26//!     ],
27//! };
28//!
29//! assert_eq!(ic.storage.len(), 2);
30//! assert_eq!(ic.filling_storage.len(), 1);
31//! assert_eq!(ic.past_inflows.len(), 1);
32//! ```
33
34use crate::EntityId;
35
36/// Initial storage volume for a single hydro plant.
37///
38/// For operating hydros, `value_hm3` must be within
39/// `[min_storage_hm3, max_storage_hm3]` (validated by `cobre-io`).
40/// For filling hydros (present in [`InitialConditions::filling_storage`]),
41/// `value_hm3` must be within `[0.0, min_storage_hm3]`.
42#[derive(Debug, Clone, PartialEq)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub struct HydroStorage {
45    /// Hydro plant identifier. Must reference a hydro entity in the system.
46    pub hydro_id: EntityId,
47    /// Reservoir volume at the start of the study, in hm³.
48    pub value_hm3: f64,
49}
50
51/// Past inflow values for PAR(p) lag initialization for a single hydro plant.
52///
53/// Each entry provides the most-recent inflow history for one hydro plant,
54/// ordered from most recent (lag 1) to oldest (lag p). The length of
55/// `values_m3s` must be >= the hydro's PAR order.
56#[derive(Debug, Clone, PartialEq)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct HydroPastInflows {
59    /// Hydro plant identifier. Must reference a hydro entity in the system.
60    pub hydro_id: EntityId,
61    /// Past inflow values in m³/s, ordered from most recent (index 0 = lag 1)
62    /// to oldest (index p-1 = lag p).
63    pub values_m3s: Vec<f64>,
64}
65
66/// Initial system state at the start of the optimization study.
67///
68/// Produced by parsing `initial_conditions.json` (in `cobre-io`) and stored
69/// inside [`crate::System`]. All arrays are sorted by `hydro_id` after
70/// loading to satisfy the declaration-order invariance requirement.
71///
72/// A hydro must appear in exactly one of the two storage arrays, never both.
73/// Hydros with a `filling` configuration belong in [`filling_storage`]; all
74/// other hydros (including late-entry hydros) belong in
75/// [`storage`](InitialConditions::storage).
76///
77/// [`filling_storage`]: InitialConditions::filling_storage
78#[derive(Debug, Clone, PartialEq)]
79#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
80pub struct InitialConditions {
81    /// Initial storage for operating hydros, in hm³ per hydro.
82    pub storage: Vec<HydroStorage>,
83    /// Initial storage for filling hydros (below dead volume), in hm³ per hydro.
84    pub filling_storage: Vec<HydroStorage>,
85    /// Past inflow values for PAR(p) lag initialization, in m³/s per hydro.
86    ///
87    /// For each hydro, `values_m3s[0]` is the most recent past inflow (lag 1)
88    /// and `values_m3s[p-1]` is the oldest (lag p). Absent when lag
89    /// initialization is not required (no PAR models or `inflow_lags: false`).
90    ///
91    /// In JSON: the field is optional on input (`serde(default)` fills an empty
92    /// `Vec` when the key is absent). The field is always emitted on output —
93    /// omitting it would break postcard round-trips used by MPI broadcast.
94    #[cfg_attr(feature = "serde", serde(default))]
95    pub past_inflows: Vec<HydroPastInflows>,
96}
97
98impl Default for InitialConditions {
99    /// Returns an empty `InitialConditions` (no hydros).
100    fn default() -> Self {
101        Self {
102            storage: Vec::new(),
103            filling_storage: Vec::new(),
104            past_inflows: Vec::new(),
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_initial_conditions_construction() {
115        let ic = InitialConditions {
116            storage: vec![
117                HydroStorage {
118                    hydro_id: EntityId(0),
119                    value_hm3: 15_000.0,
120                },
121                HydroStorage {
122                    hydro_id: EntityId(1),
123                    value_hm3: 8_500.0,
124                },
125            ],
126            filling_storage: vec![HydroStorage {
127                hydro_id: EntityId(10),
128                value_hm3: 200.0,
129            }],
130            past_inflows: vec![HydroPastInflows {
131                hydro_id: EntityId(0),
132                values_m3s: vec![600.0, 500.0],
133            }],
134        };
135
136        assert_eq!(ic.storage.len(), 2);
137        assert_eq!(ic.filling_storage.len(), 1);
138        assert_eq!(ic.past_inflows.len(), 1);
139        assert_eq!(ic.storage[0].hydro_id, EntityId(0));
140        assert_eq!(ic.storage[0].value_hm3, 15_000.0);
141        assert_eq!(ic.storage[1].hydro_id, EntityId(1));
142        assert_eq!(ic.filling_storage[0].hydro_id, EntityId(10));
143        assert_eq!(ic.filling_storage[0].value_hm3, 200.0);
144        assert_eq!(ic.past_inflows[0].hydro_id, EntityId(0));
145        assert_eq!(ic.past_inflows[0].values_m3s, vec![600.0, 500.0]);
146    }
147
148    #[test]
149    fn test_initial_conditions_default_is_empty() {
150        let ic = InitialConditions::default();
151        assert!(ic.storage.is_empty());
152        assert!(ic.filling_storage.is_empty());
153        assert!(ic.past_inflows.is_empty());
154    }
155
156    #[test]
157    fn test_hydro_storage_clone() {
158        let hs = HydroStorage {
159            hydro_id: EntityId(5),
160            value_hm3: 1_234.5,
161        };
162        let cloned = hs.clone();
163        assert_eq!(hs, cloned);
164        assert_eq!(cloned.hydro_id, EntityId(5));
165        assert_eq!(cloned.value_hm3, 1_234.5);
166    }
167
168    #[test]
169    fn test_hydro_past_inflows_clone() {
170        let hpi = HydroPastInflows {
171            hydro_id: EntityId(3),
172            values_m3s: vec![300.0, 200.0, 100.0],
173        };
174        let cloned = hpi.clone();
175        assert_eq!(hpi, cloned);
176        assert_eq!(cloned.hydro_id, EntityId(3));
177        assert_eq!(cloned.values_m3s, vec![300.0, 200.0, 100.0]);
178    }
179
180    #[cfg(feature = "serde")]
181    #[test]
182    fn test_initial_conditions_serde_roundtrip() {
183        let ic = InitialConditions {
184            storage: vec![
185                HydroStorage {
186                    hydro_id: EntityId(0),
187                    value_hm3: 15_000.0,
188                },
189                HydroStorage {
190                    hydro_id: EntityId(1),
191                    value_hm3: 8_500.0,
192                },
193            ],
194            filling_storage: vec![HydroStorage {
195                hydro_id: EntityId(10),
196                value_hm3: 200.0,
197            }],
198            past_inflows: vec![HydroPastInflows {
199                hydro_id: EntityId(0),
200                values_m3s: vec![600.0, 500.0],
201            }],
202        };
203
204        let json = serde_json::to_string(&ic).unwrap();
205        let deserialized: InitialConditions = serde_json::from_str(&json).unwrap();
206        assert_eq!(ic, deserialized);
207    }
208
209    #[cfg(feature = "serde")]
210    #[test]
211    fn test_initial_conditions_serde_roundtrip_empty_past_inflows() {
212        // Empty past_inflows is always serialized as [] (never omitted) to keep
213        // postcard round-trips used by MPI broadcast working correctly.
214        let ic = InitialConditions {
215            storage: vec![HydroStorage {
216                hydro_id: EntityId(0),
217                value_hm3: 1_000.0,
218            }],
219            filling_storage: vec![],
220            past_inflows: vec![],
221        };
222
223        let json = serde_json::to_string(&ic).unwrap();
224        let deserialized: InitialConditions = serde_json::from_str(&json).unwrap();
225        assert_eq!(ic, deserialized);
226        // Verify the field round-trips correctly (may or may not be present in JSON).
227        assert_eq!(deserialized.past_inflows.len(), 0);
228    }
229}