Skip to main content

powerio_prob/operating/
mod.rs

1//! Operating points: alternate electrical assignments over an immutable
2//! network handle.
3//!
4//! An [`OperatingPoint`] is a small owning handle: one cheap to clone network
5//! handle, one shared column store, and one row index. Building a series
6//! resolves every stable element identity once into a private layout; the
7//! points share the layout and the columns, so retaining one point after
8//! dropping its parent collection copies no network.
9//!
10//! Quantities include voltages, injections, in-service flags, switch
11//! positions, tap positions, phase shifts, and capacitor
12//! or regulator settings. Parameters, the equipment set, connectivity, time varying
13//! bounds, availability, commitment, reserves, and costs are network or
14//! calculation data, never operating point fields.
15//!
16//! Builders accept dense point major columns or sparse per point overrides
17//! over one base assignment. Both forms have the same accessors.
18
19pub(crate) mod balanced;
20pub(crate) mod multiconductor;
21
22pub use balanced::{
23    BalancedOperatingPointBuilder, BalancedOperatingPointFlag, BalancedOperatingPointQuantity,
24};
25pub use multiconductor::{
26    MulticonductorOperatingPointBuilder, MulticonductorOperatingPointFlag,
27    MulticonductorOperatingPointQuantity,
28};
29
30/// One possibly partial alternate electrical assignment: a small owning handle over the
31/// shared network and the series' shared columns. Cloning it or retaining it
32/// after the parent series drops copies no table and no column.
33#[derive(Clone, Debug)]
34pub struct OperatingPoint<N> {
35    pub(crate) network: N,
36    pub(crate) columns: SharedColumns,
37    pub(crate) index: usize,
38}
39
40impl<N> OperatingPoint<N> {
41    /// The network whose equipment identities and defaults this point uses.
42    pub fn network(&self) -> &N {
43        &self.network
44    }
45
46    fn value(&self, quantity: &'static str, identity: &str) -> Option<f64> {
47        self.columns
48            .quantities
49            .get(quantity)?
50            .value(self.index, identity)
51    }
52
53    fn iter_values(&self, quantity: &'static str) -> Option<OperatingPointValues<'_>> {
54        Some(OperatingPointValues {
55            quantity: self.columns.quantities.get(quantity)?,
56            point: self.index,
57            column: 0,
58        })
59    }
60
61    fn iter_flags(&self, quantity: &'static str) -> Option<OperatingPointFlags<'_>> {
62        Some(OperatingPointFlags(self.iter_values(quantity)?))
63    }
64
65    pub(crate) fn identity_order(
66        &self,
67        quantity: &str,
68    ) -> Option<impl ExactSizeIterator<Item = &str>> {
69        Some(self.columns.quantities.get(quantity)?.layout.order())
70    }
71}
72
73/// One operating point quantity in stable component identity order.
74#[derive(Clone, Debug)]
75pub struct OperatingPointValues<'a> {
76    quantity: &'a Quantity,
77    point: usize,
78    column: usize,
79}
80
81impl<'a> Iterator for OperatingPointValues<'a> {
82    type Item = (&'a str, f64);
83
84    fn next(&mut self) -> Option<Self::Item> {
85        let identity = self.quantity.layout.order.get(self.column)?;
86        let value =
87            self.quantity
88                .storage
89                .value(self.point, self.column, self.quantity.layout.len());
90        self.column += 1;
91        Some((identity.as_ref(), value))
92    }
93
94    fn size_hint(&self) -> (usize, Option<usize>) {
95        let remaining = self.quantity.layout.len() - self.column;
96        (remaining, Some(remaining))
97    }
98}
99
100impl ExactSizeIterator for OperatingPointValues<'_> {}
101
102/// One operating point flag in stable component identity order.
103#[derive(Clone, Debug)]
104pub struct OperatingPointFlags<'a>(OperatingPointValues<'a>);
105
106impl<'a> Iterator for OperatingPointFlags<'a> {
107    type Item = (&'a str, bool);
108
109    fn next(&mut self) -> Option<Self::Item> {
110        self.0
111            .next()
112            .map(|(identity, value)| (identity, value != 0.0))
113    }
114
115    fn size_hint(&self) -> (usize, Option<usize>) {
116        self.0.size_hint()
117    }
118}
119
120impl ExactSizeIterator for OperatingPointFlags<'_> {}
121
122use std::collections::HashMap;
123use std::sync::Arc;
124
125use powerio_core::Error;
126
127use crate::diagnostics::codes;
128
129/// One quantity's resolved column block: the identity order the network
130/// tables define, with a hash lookup so keyed access never scans.
131#[derive(Clone, Debug, Default)]
132pub(crate) struct QuantityLayout {
133    /// Column position by resolved identity.
134    index: HashMap<Box<str>, u32>,
135    /// The identities in stable network table order; `index` inverts this.
136    order: Vec<Box<str>>,
137}
138
139impl QuantityLayout {
140    pub(crate) fn from_order(
141        quantity: &'static str,
142        order: impl IntoIterator<Item = String>,
143    ) -> Result<Self, Error> {
144        let mut layout = Self::default();
145        for identity in order {
146            let column = u32::try_from(layout.order.len()).map_err(|_| {
147                Error::new(
148                    &codes::BUILD_OPERATING_POINT_SHAPE_MISMATCH,
149                    format!("{quantity}: more than u32::MAX elements"),
150                )
151            })?;
152            if layout
153                .index
154                .insert(identity.clone().into_boxed_str(), column)
155                .is_some()
156            {
157                return Err(Error::new(
158                    &codes::BUILD_OPERATING_POINT_IDENTITY_UNKNOWN,
159                    format!("{quantity}: duplicate element identity `{identity}`"),
160                ));
161            }
162            layout.order.push(identity.into_boxed_str());
163        }
164        Ok(layout)
165    }
166
167    pub(crate) fn len(&self) -> usize {
168        self.order.len()
169    }
170
171    pub(crate) fn column(&self, identity: &str) -> Option<usize> {
172        self.index.get(identity).map(|column| *column as usize)
173    }
174
175    pub(crate) fn order(&self) -> impl ExactSizeIterator<Item = &str> {
176        self.order.iter().map(AsRef::as_ref)
177    }
178}
179
180/// One point's sparse overrides: `(column, value)` pairs sorted by column.
181pub(crate) type PointChanges = Box<[(u32, f64)]>;
182
183/// One quantity's values across every point: dense rows, or sparse overrides
184/// over one base row. Both address `(point, column)`.
185#[derive(Clone, Debug)]
186pub(crate) enum QuantityStorage {
187    Dense {
188        /// `point_count * width` values, point major.
189        values: Box<[f64]>,
190    },
191    Sparse {
192        /// The base row every point starts from, `width` long.
193        base: Box<[f64]>,
194        /// Per point overrides, each sorted by column.
195        changes: Box<[PointChanges]>,
196    },
197}
198
199impl QuantityStorage {
200    pub(crate) fn value(&self, point: usize, column: usize, width: usize) -> f64 {
201        match self {
202            Self::Dense { values } => values[point * width + column],
203            Self::Sparse { base, changes } => {
204                let column32 = column as u32;
205                match changes[point].binary_search_by_key(&column32, |(c, _)| *c) {
206                    Ok(found) => changes[point][found].1,
207                    Err(_) => base[column],
208                }
209            }
210        }
211    }
212
213    pub(crate) fn replace(
214        &mut self,
215        point: usize,
216        column: usize,
217        width: usize,
218        replacement: f64,
219    ) -> bool {
220        let previous = self.value(point, column, width);
221        if previous.to_bits() == replacement.to_bits() {
222            return false;
223        }
224        match self {
225            Self::Dense { values } => values[point * width + column] = replacement,
226            Self::Sparse { base, changes } => {
227                let column = column as u32;
228                let row = &mut changes[point];
229                let mut updated = row.to_vec();
230                match updated.binary_search_by_key(&column, |(entry, _)| *entry) {
231                    Ok(found) if replacement.to_bits() == base[column as usize].to_bits() => {
232                        updated.remove(found);
233                    }
234                    Ok(found) => updated[found].1 = replacement,
235                    Err(_) if replacement.to_bits() == base[column as usize].to_bits() => {}
236                    Err(insert_at) => updated.insert(insert_at, (column, replacement)),
237                }
238                *row = updated.into_boxed_slice();
239            }
240        }
241        true
242    }
243}
244
245/// One named quantity: its identity layout and its storage.
246#[derive(Clone, Debug)]
247pub(crate) struct Quantity {
248    pub(crate) layout: QuantityLayout,
249    pub(crate) storage: QuantityStorage,
250}
251
252impl Quantity {
253    pub(crate) fn value(&self, point: usize, identity: &str) -> Option<f64> {
254        let column = self.layout.column(identity)?;
255        Some(self.storage.value(point, column, self.layout.len()))
256    }
257}
258
259/// The shared column store behind every point of one series.
260#[derive(Clone, Debug)]
261pub(crate) struct OperatingPointColumns {
262    pub(crate) point_count: usize,
263    pub(crate) quantities: HashMap<&'static str, Quantity>,
264}
265
266pub(crate) type SharedColumns = Arc<OperatingPointColumns>;
267
268impl<N> OperatingPoint<N> {
269    pub(crate) fn replace_value(
270        &mut self,
271        quantity_name: &'static str,
272        layout: QuantityLayout,
273        defaults: &[f64],
274        identity: &str,
275        replacement: f64,
276    ) -> Result<bool, Error> {
277        let columns = Arc::make_mut(&mut self.columns);
278        if !columns.quantities.contains_key(quantity_name) {
279            if defaults.len() != layout.len() {
280                return Err(Error::new(
281                    &codes::BUILD_OPERATING_POINT_SHAPE_MISMATCH,
282                    format!(
283                        "{quantity_name}: {} defaults supplied for {} components",
284                        defaults.len(),
285                        layout.len()
286                    ),
287                ));
288            }
289            let mut values = Vec::with_capacity(defaults.len() * columns.point_count);
290            for _ in 0..columns.point_count {
291                values.extend_from_slice(defaults);
292            }
293            columns.quantities.insert(
294                quantity_name,
295                dense_quantity(quantity_name, layout, columns.point_count, values)?,
296            );
297        }
298        let quantity = columns
299            .quantities
300            .get_mut(quantity_name)
301            .expect("the quantity was inserted above");
302        let Some(column) = quantity.layout.column(identity) else {
303            return Err(Error::new(
304                &codes::BUILD_OPERATING_POINT_IDENTITY_UNKNOWN,
305                format!("{quantity_name}: unknown component identity `{identity}`"),
306            ));
307        };
308        Ok(quantity
309            .storage
310            .replace(self.index, column, quantity.layout.len(), replacement))
311    }
312}
313
314/// Validates one dense column block against its layout.
315pub(crate) fn dense_quantity(
316    quantity: &'static str,
317    layout: QuantityLayout,
318    point_count: usize,
319    values: Vec<f64>,
320) -> Result<Quantity, Error> {
321    let expected = point_count.checked_mul(layout.len()).ok_or_else(|| {
322        Error::new(
323            &codes::BUILD_OPERATING_POINT_SHAPE_MISMATCH,
324            format!(
325                "{quantity}: {point_count} points by {} elements exceeds addressable memory",
326                layout.len()
327            ),
328        )
329    })?;
330    if values.len() != expected {
331        return Err(Error::new(
332            &codes::BUILD_OPERATING_POINT_SHAPE_MISMATCH,
333            format!(
334                "{quantity}: {} values supplied; {point_count} points by {} elements needs {expected}",
335                values.len(),
336                layout.len()
337            ),
338        ));
339    }
340    Ok(Quantity {
341        layout,
342        storage: QuantityStorage::Dense {
343            values: values.into_boxed_slice(),
344        },
345    })
346}
347
348/// Validates one sparse column block: a base row plus per point keyed
349/// overrides, resolved against the layout once.
350pub(crate) fn sparse_quantity(
351    quantity: &'static str,
352    layout: QuantityLayout,
353    point_count: usize,
354    base: Vec<f64>,
355    changes: Vec<Vec<(String, f64)>>,
356) -> Result<Quantity, Error> {
357    if base.len() != layout.len() {
358        return Err(Error::new(
359            &codes::BUILD_OPERATING_POINT_SHAPE_MISMATCH,
360            format!(
361                "{quantity}: base row has {} values; the layout has {} elements",
362                base.len(),
363                layout.len()
364            ),
365        ));
366    }
367    if changes.len() != point_count {
368        return Err(Error::new(
369            &codes::BUILD_OPERATING_POINT_SHAPE_MISMATCH,
370            format!(
371                "{quantity}: {} change sets supplied for {point_count} points",
372                changes.len()
373            ),
374        ));
375    }
376    let mut resolved = Vec::with_capacity(point_count);
377    for point in changes {
378        let mut row: Vec<(u32, f64)> = Vec::with_capacity(point.len());
379        for (identity, value) in point {
380            let Some(column) = layout.column(&identity) else {
381                return Err(Error::new(
382                    &codes::BUILD_OPERATING_POINT_IDENTITY_UNKNOWN,
383                    format!("{quantity}: unknown element identity `{identity}`"),
384                ));
385            };
386            row.push((column as u32, value));
387        }
388        row.sort_unstable_by_key(|(column, _)| *column);
389        row.dedup_by_key(|(column, _)| *column);
390        resolved.push(row.into_boxed_slice());
391    }
392    Ok(Quantity {
393        layout,
394        storage: QuantityStorage::Sparse {
395            base: base.into_boxed_slice(),
396            changes: resolved.into_boxed_slice(),
397        },
398    })
399}
400
401/// The payload identity of one element row: its stated uid, or the
402/// `{table}:{row}` value the stored document mints for a row without one.
403/// Resolution never mutates the network.
404pub(crate) fn row_identity(uid: Option<&str>, table: &str, row: usize) -> String {
405    match uid {
406        Some(uid) => uid.to_owned(),
407        None => format!("{table}:{row}"),
408    }
409}