cobre_core/model/resolved/group_bounds.rs
1//! Pre-resolved per-`(hydro unit group, stage, block)` override overlay: layers
2//! 1 and 2 of the bound-precedence law on the group axis. A row's optional
3//! `block_id` selects one block of a stage rather than the whole stage; layer
4//! 3 (the group's own declared value) stays on [`HydroUnitGroup`](crate::HydroUnitGroup)
5//! and is applied by the consumer, never copied into this table. A `None`
6//! field in [`HydroUnitGroupOverride`] means no override for that column.
7//! Populated by `cobre-io`; never modified after construction.
8//!
9//! The group axis is ragged (group counts vary per plant), so it is addressed
10//! as `(hydro_idx, group_pos)` — `group_pos` being the position within the
11//! owning plant's `unit_groups` slice — and stored via a CSR offset vector
12//! rather than a dense `max_groups_per_plant` stride.
13
14/// Per-(stage or block) override for a hydro unit group's four declared bounds.
15#[derive(Debug, Clone, Copy, PartialEq, Default)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17pub struct HydroUnitGroupOverride {
18 /// Minimum turbined flow override \[m³/s\].
19 pub min_turbined_m3s: Option<f64>,
20 /// Maximum turbined flow override \[m³/s\].
21 pub max_turbined_m3s: Option<f64>,
22 /// Minimum generation override \[MW\].
23 pub min_generation_mw: Option<f64>,
24 /// Maximum generation override \[MW\].
25 pub max_generation_mw: Option<f64>,
26}
27
28// ─── Pre-resolved container ───────────────────────────────────────────────────
29
30/// Group counts for constructing a [`ResolvedHydroUnitGroupBounds`] table.
31#[derive(Debug, Clone)]
32pub struct HydroUnitGroupBoundsCountsSpec<'a> {
33 /// Number of unit groups declared by each plant, plant-index-ordered.
34 pub groups_per_plant: &'a [usize],
35 /// Number of time stages.
36 pub n_stages: usize,
37 /// Maximum `stage.blocks.len()` across study stages.
38 pub max_blocks: usize,
39}
40
41/// Pre-resolved per-`(hydro unit group, stage, block)` override table.
42///
43/// `stage` and `block` are independently lazy: each stays empty until a row
44/// actually resolves to a cell in that layer, so a study with no group-bound
45/// rows — or only unresolvable ones — leaves both empty regardless of how many
46/// groups `hydros` declares. [`is_empty`](Self::is_empty) reports exactly this.
47///
48/// # Examples
49///
50/// ```
51/// use cobre_core::resolved::ResolvedHydroUnitGroupBounds;
52///
53/// let empty = ResolvedHydroUnitGroupBounds::empty();
54/// assert!(empty.is_empty());
55/// assert_eq!(
56/// empty.override_at_block(0, 0, 0, 0),
57/// Default::default()
58/// );
59/// ```
60#[derive(Debug, Clone, PartialEq)]
61#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
62pub struct ResolvedHydroUnitGroupBounds {
63 n_stages: usize,
64 max_blocks: usize,
65 /// CSR offsets into the group axis, length `groups_per_plant.len() + 1`;
66 /// plant `h`'s groups occupy slots `plant_group_start[h]..plant_group_start[h + 1]`.
67 plant_group_start: Vec<usize>,
68 stage: Vec<HydroUnitGroupOverride>,
69 block: Vec<HydroUnitGroupOverride>,
70}
71
72impl Default for ResolvedHydroUnitGroupBounds {
73 fn default() -> Self {
74 Self::empty()
75 }
76}
77
78impl ResolvedHydroUnitGroupBounds {
79 /// Create an empty override table; every reader returns the column default
80 /// and every writer returns `None`.
81 #[must_use]
82 pub fn empty() -> Self {
83 Self {
84 n_stages: 0,
85 max_blocks: 0,
86 plant_group_start: Vec::new(),
87 stage: Vec::new(),
88 block: Vec::new(),
89 }
90 }
91
92 /// Allocate the group axis for a new override table. `stage` and `block`
93 /// start empty — [`stage_override_mut`](Self::stage_override_mut) and
94 /// [`block_override_mut`](Self::block_override_mut) grow each to full size
95 /// on its own first successful write, so a table with zero applied rows
96 /// stays [`is_empty`](Self::is_empty).
97 #[must_use]
98 pub fn new(spec: &HydroUnitGroupBoundsCountsSpec<'_>) -> Self {
99 let mut plant_group_start = Vec::with_capacity(spec.groups_per_plant.len() + 1);
100 plant_group_start.push(0);
101 let mut total = 0usize;
102 for &n in spec.groups_per_plant {
103 total += n;
104 plant_group_start.push(total);
105 }
106 Self {
107 n_stages: spec.n_stages,
108 max_blocks: spec.max_blocks,
109 plant_group_start,
110 stage: Vec::new(),
111 block: Vec::new(),
112 }
113 }
114
115 fn total_groups(&self) -> usize {
116 self.plant_group_start.last().copied().unwrap_or(0)
117 }
118
119 /// Resolve `(hydro_idx, group_pos)` to its CSR slot, or `None` when
120 /// `hydro_idx` is out of range or `group_pos` is not one of that plant's
121 /// declared groups — never aliasing a sibling plant's slot.
122 fn group_slot(&self, hydro_idx: usize, group_pos: usize) -> Option<usize> {
123 let &start = self.plant_group_start.get(hydro_idx)?;
124 let &end = self.plant_group_start.get(hydro_idx + 1)?;
125 let slot = start + group_pos;
126 (slot < end).then_some(slot)
127 }
128
129 fn stage_flat_index(
130 &self,
131 hydro_idx: usize,
132 group_pos: usize,
133 stage_idx: usize,
134 ) -> Option<usize> {
135 let slot = self.group_slot(hydro_idx, group_pos)?;
136 (stage_idx < self.n_stages).then_some(slot * self.n_stages + stage_idx)
137 }
138
139 fn block_flat_index(
140 &self,
141 hydro_idx: usize,
142 group_pos: usize,
143 stage_idx: usize,
144 block_idx: usize,
145 ) -> Option<usize> {
146 let stage_flat = self.stage_flat_index(hydro_idx, group_pos, stage_idx)?;
147 (block_idx < self.max_blocks).then_some(stage_flat * self.max_blocks + block_idx)
148 }
149
150 /// Look up the stage-wide override at `(hydro_idx, group_pos, stage_idx)`.
151 /// Returns [`HydroUnitGroupOverride::default`] (all `None`) when the layer
152 /// is empty or any index is out of range.
153 #[inline]
154 #[must_use]
155 pub fn stage_override(
156 &self,
157 hydro_idx: usize,
158 group_pos: usize,
159 stage_idx: usize,
160 ) -> HydroUnitGroupOverride {
161 self.stage_flat_index(hydro_idx, group_pos, stage_idx)
162 .and_then(|idx| self.stage.get(idx))
163 .copied()
164 .unwrap_or_default()
165 }
166
167 /// Return a mutable handle to the stage-wide override cell, growing the
168 /// layer to full size on its first call, or `None` when any index is out
169 /// of range.
170 #[inline]
171 pub fn stage_override_mut(
172 &mut self,
173 hydro_idx: usize,
174 group_pos: usize,
175 stage_idx: usize,
176 ) -> Option<&mut HydroUnitGroupOverride> {
177 let idx = self.stage_flat_index(hydro_idx, group_pos, stage_idx)?;
178 if self.stage.is_empty() {
179 self.stage =
180 vec![HydroUnitGroupOverride::default(); self.total_groups() * self.n_stages];
181 }
182 self.stage.get_mut(idx)
183 }
184
185 /// Look up the per-block override at `(hydro_idx, group_pos, stage_idx, block_idx)`.
186 /// Returns [`HydroUnitGroupOverride::default`] (all `None`) when the layer
187 /// is empty or any index is out of range.
188 #[inline]
189 #[must_use]
190 pub fn block_override(
191 &self,
192 hydro_idx: usize,
193 group_pos: usize,
194 stage_idx: usize,
195 block_idx: usize,
196 ) -> HydroUnitGroupOverride {
197 self.block_flat_index(hydro_idx, group_pos, stage_idx, block_idx)
198 .and_then(|idx| self.block.get(idx))
199 .copied()
200 .unwrap_or_default()
201 }
202
203 /// Return a mutable handle to the per-block override cell, growing the
204 /// layer to full size on its first call, or `None` when any index is out
205 /// of range.
206 #[inline]
207 pub fn block_override_mut(
208 &mut self,
209 hydro_idx: usize,
210 group_pos: usize,
211 stage_idx: usize,
212 block_idx: usize,
213 ) -> Option<&mut HydroUnitGroupOverride> {
214 let idx = self.block_flat_index(hydro_idx, group_pos, stage_idx, block_idx)?;
215 if self.block.is_empty() {
216 self.block = vec![
217 HydroUnitGroupOverride::default();
218 self.total_groups() * self.n_stages * self.max_blocks
219 ];
220 }
221 self.block.get_mut(idx)
222 }
223
224 /// Return the resolved override at `(hydro_idx, group_pos, stage_idx, block_idx)`,
225 /// applying the bound-precedence law column-independently:
226 /// `block.<col>.or(stage.<col>)`. A block row touching only one column
227 /// never erases a stage-wide value on another column.
228 #[inline]
229 #[must_use]
230 pub fn override_at_block(
231 &self,
232 hydro_idx: usize,
233 group_pos: usize,
234 stage_idx: usize,
235 block_idx: usize,
236 ) -> HydroUnitGroupOverride {
237 let stage = self.stage_override(hydro_idx, group_pos, stage_idx);
238 let block = self.block_override(hydro_idx, group_pos, stage_idx, block_idx);
239 HydroUnitGroupOverride {
240 min_turbined_m3s: block.min_turbined_m3s.or(stage.min_turbined_m3s),
241 max_turbined_m3s: block.max_turbined_m3s.or(stage.max_turbined_m3s),
242 min_generation_mw: block.min_generation_mw.or(stage.min_generation_mw),
243 max_generation_mw: block.max_generation_mw.or(stage.max_generation_mw),
244 }
245 }
246
247 /// Returns `true` when both layers are empty — no row has ever resolved
248 /// to a written cell.
249 #[inline]
250 #[must_use]
251 pub fn is_empty(&self) -> bool {
252 self.stage.is_empty() && self.block.is_empty()
253 }
254}