Skip to main content

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}