Skip to main content

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}