Skip to main content

powerio/
value.rs

1//! The dynamic Rust boundary for values produced by universal parsing.
2
3use powerio_core::{Scenario, ScenarioSet, TimePoint, TimeSeries};
4use powerio_dist::MulticonductorNetwork;
5use powerio_prob::OperatingPoint;
6use powerio_tx::{BalancedNetwork, ContingencySet, GeoLayer, MonitoredSet, SubsystemSet};
7
8/// A dynamically typed time series at the universal parser boundary.
9///
10/// Typed Rust code uses `TimeSeries<T>` directly. This wrapper preserves the
11/// element type for empty collections and lets C and PowerIO IR report the
12/// same structural type name without a flattened collection registry.
13#[derive(Clone, Debug)]
14pub struct PioTimeSeries {
15    element_type: Box<str>,
16    type_name: Box<str>,
17    values: TimeSeries<PioValue>,
18}
19
20impl PioTimeSeries {
21    fn from_typed<T>(element_type: &'static str, values: TimeSeries<T>) -> Self
22    where
23        T: Clone + Into<PioValue>,
24    {
25        let values = values.map_values(Into::into);
26        debug_assert!(
27            values
28                .values()
29                .iter()
30                .all(|value| value.type_name() == element_type)
31        );
32        Self {
33            element_type: element_type.into(),
34            type_name: format!("powerio.TimeSeries<{element_type}>").into_boxed_str(),
35            values,
36        }
37    }
38
39    /// Construct the dynamic form from values that already crossed the
40    /// universal parser boundary.
41    ///
42    /// Typed Rust code should use [`TimeSeries<T>`] and its ordinary
43    /// [`From`] conversion. This constructor exists for dynamic language
44    /// bindings. An empty input has no value from which to infer `T` and is
45    /// rejected.
46    pub fn from_values(
47        time_points: Vec<TimePoint>,
48        values: Vec<PioValue>,
49    ) -> Result<Self, powerio_core::Error> {
50        let Some(first) = values.first() else {
51            return Err(powerio_core::Error::new(
52                &crate::codes::VALIDATE_COLLECTION_EMPTY,
53                "a time series needs at least one value to infer its element type",
54            ));
55        };
56        let element_type = first.type_name().to_owned();
57        if !is_time_series_element_type(&element_type) {
58            return Err(powerio_core::Error::new(
59                &crate::codes::VALIDATE_COLLECTION_ELEMENT_TYPE,
60                format!("PowerIO IR does not define a time series of `{element_type}`"),
61            ));
62        }
63        require_element_type(values.iter(), &element_type)?;
64        let values = TimeSeries::new(time_points, values)?;
65        Ok(Self {
66            type_name: format!("powerio.TimeSeries<{element_type}>").into_boxed_str(),
67            element_type: element_type.into_boxed_str(),
68            values,
69        })
70    }
71
72    #[must_use]
73    pub fn element_type(&self) -> &str {
74        &self.element_type
75    }
76
77    #[must_use]
78    pub fn type_name(&self) -> &str {
79        &self.type_name
80    }
81
82    #[must_use]
83    pub fn time_points(&self) -> &[TimePoint] {
84        self.values.time_points()
85    }
86
87    #[must_use]
88    pub fn get(&self, index: usize) -> Option<&PioValue> {
89        self.values.get(index)
90    }
91
92    /// Mutably borrow one entry through the collection's copy on write
93    /// storage.
94    pub fn get_mut(&mut self, index: usize) -> Option<&mut PioValue> {
95        self.values.get_mut(index)
96    }
97
98    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&TimePoint, &PioValue)> {
99        self.values.iter()
100    }
101
102    /// Iterate over mutable entries through one copy on write split.
103    pub fn iter_mut(&mut self) -> impl ExactSizeIterator<Item = (&TimePoint, &mut PioValue)> {
104        self.values.iter_mut()
105    }
106
107    #[must_use]
108    pub fn len(&self) -> usize {
109        self.values.len()
110    }
111
112    #[must_use]
113    pub fn is_empty(&self) -> bool {
114        self.values.is_empty()
115    }
116
117    pub(crate) fn values(&self) -> &TimeSeries<PioValue> {
118        &self.values
119    }
120}
121
122/// A dynamically typed scenario set at the universal parser boundary.
123/// Typed Rust code uses `ScenarioSet<T>` directly.
124#[derive(Clone, Debug)]
125pub struct PioScenarioSet {
126    element_type: Box<str>,
127    type_name: Box<str>,
128    values: ScenarioSet<PioValue>,
129}
130
131impl PioScenarioSet {
132    fn from_typed<T>(element_type: &'static str, values: ScenarioSet<T>) -> Self
133    where
134        T: Clone + Into<PioValue>,
135    {
136        let values = values.map_values(Into::into);
137        debug_assert!(
138            values
139                .iter()
140                .all(|scenario| scenario.value().type_name() == element_type)
141        );
142        Self {
143            element_type: element_type.into(),
144            type_name: format!("powerio.ScenarioSet<{element_type}>").into_boxed_str(),
145            values,
146        }
147    }
148
149    /// Construct the dynamic form from scenarios that already crossed the
150    /// universal parser boundary.
151    ///
152    /// Typed Rust code should use [`ScenarioSet<T>`] and its ordinary
153    /// [`From`] conversion. An empty input has no value from which to infer
154    /// `T` and is rejected.
155    pub fn from_scenarios(scenarios: Vec<Scenario<PioValue>>) -> Result<Self, powerio_core::Error> {
156        let Some(first) = scenarios.first() else {
157            return Err(powerio_core::Error::new(
158                &crate::codes::VALIDATE_COLLECTION_EMPTY,
159                "a scenario set needs at least one value to infer its element type",
160            ));
161        };
162        let element_type = first.value().type_name().to_owned();
163        if !is_scenario_element_type(&element_type) {
164            return Err(powerio_core::Error::new(
165                &crate::codes::VALIDATE_COLLECTION_ELEMENT_TYPE,
166                format!("PowerIO IR does not define a scenario set of `{element_type}`"),
167            ));
168        }
169        require_element_type(scenarios.iter().map(Scenario::value), &element_type)?;
170        let values = ScenarioSet::new(scenarios)?;
171        Ok(Self {
172            type_name: format!("powerio.ScenarioSet<{element_type}>").into_boxed_str(),
173            element_type: element_type.into_boxed_str(),
174            values,
175        })
176    }
177
178    #[must_use]
179    pub fn element_type(&self) -> &str {
180        &self.element_type
181    }
182
183    #[must_use]
184    pub fn type_name(&self) -> &str {
185        &self.type_name
186    }
187
188    #[must_use]
189    pub fn get(&self, id: &str) -> Option<&PioValue> {
190        self.values.get(id)
191    }
192
193    /// Mutably borrow one entry by scenario ID through copy on write.
194    pub fn get_mut(&mut self, id: &str) -> Option<&mut PioValue> {
195        self.values.get_mut(id)
196    }
197
198    #[must_use]
199    pub fn get_at(&self, position: usize) -> Option<&PioValue> {
200        self.values.get_at(position)
201    }
202
203    /// Mutably borrow one entry by insertion position through copy on write.
204    pub fn get_at_mut(&mut self, position: usize) -> Option<&mut PioValue> {
205        self.values.get_at_mut(position)
206    }
207
208    pub fn iter(&self) -> impl ExactSizeIterator<Item = &Scenario<PioValue>> {
209        self.values.iter()
210    }
211
212    /// Iterate over mutable scenario entries through one copy on write split.
213    pub fn iter_mut(&mut self) -> impl ExactSizeIterator<Item = &mut Scenario<PioValue>> {
214        self.values.iter_mut()
215    }
216
217    #[must_use]
218    pub fn len(&self) -> usize {
219        self.values.len()
220    }
221
222    #[must_use]
223    pub fn is_empty(&self) -> bool {
224        self.values.is_empty()
225    }
226
227    pub(crate) fn values(&self) -> &ScenarioSet<PioValue> {
228        &self.values
229    }
230}
231
232const BALANCED_NETWORK_TYPE: &str = "powerio.BalancedNetwork";
233const MULTICONDUCTOR_NETWORK_TYPE: &str = "powerio.MulticonductorNetwork";
234const BALANCED_OPERATING_POINT_TYPE: &str = "powerio.OperatingPoint<powerio.BalancedNetwork>";
235const MULTICONDUCTOR_OPERATING_POINT_TYPE: &str =
236    "powerio.OperatingPoint<powerio.MulticonductorNetwork>";
237
238fn is_time_series_element_type(type_name: &str) -> bool {
239    matches!(
240        type_name,
241        BALANCED_NETWORK_TYPE
242            | MULTICONDUCTOR_NETWORK_TYPE
243            | BALANCED_OPERATING_POINT_TYPE
244            | MULTICONDUCTOR_OPERATING_POINT_TYPE
245    )
246}
247
248fn is_scenario_element_type(type_name: &str) -> bool {
249    is_time_series_element_type(type_name)
250        || matches!(
251            type_name,
252            "powerio.TimeSeries<powerio.BalancedNetwork>"
253                | "powerio.TimeSeries<powerio.MulticonductorNetwork>"
254                | "powerio.TimeSeries<powerio.OperatingPoint<powerio.BalancedNetwork>>"
255                | "powerio.TimeSeries<powerio.OperatingPoint<powerio.MulticonductorNetwork>>"
256        )
257}
258
259fn require_element_type<'a>(
260    values: impl IntoIterator<Item = &'a PioValue>,
261    element_type: &str,
262) -> Result<(), powerio_core::Error> {
263    if let Some(value) = values
264        .into_iter()
265        .find(|value| value.type_name() != element_type)
266    {
267        return Err(powerio_core::Error::new(
268            &crate::codes::VALIDATE_COLLECTION_ELEMENT_TYPE,
269            format!(
270                "collection starts with `{element_type}` but also contains `{}`",
271                value.type_name()
272            ),
273        ));
274    }
275    Ok(())
276}
277
278/// A value produced by PowerIO's universal parser or decoded from PowerIO IR.
279/// Application values remain ordinary `PioModule<T>` values.
280#[derive(Clone, Debug)]
281#[non_exhaustive]
282#[allow(clippy::large_enum_variant)]
283pub enum PioValue {
284    BalancedNetwork(BalancedNetwork),
285    MulticonductorNetwork(MulticonductorNetwork),
286    /// A standalone geographic document: element points and routes keyed by
287    /// element identity, placed onto a network by
288    /// [`crate::apply_geo_layer`].
289    GeoLayer(GeoLayer),
290    /// A PSS/E contingency description file (`.con`): the outages a
291    /// contingency analysis runs, bound to a network by
292    /// [`ContingencySet::resolve`].
293    ContingencySet(ContingencySet),
294    /// A PSS/E subsystem description file (`.sub`): the named bus groups a
295    /// contingency description file and a monitored element file draw on.
296    SubsystemSet(SubsystemSet),
297    /// A PSS/E monitored element file (`.mon`): the branch flows, interface
298    /// flows, and bus voltages a contingency analysis reports on.
299    MonitoredSet(MonitoredSet),
300    BalancedOperatingPoint(OperatingPoint<BalancedNetwork>),
301    MulticonductorOperatingPoint(OperatingPoint<MulticonductorNetwork>),
302    TimeSeries(PioTimeSeries),
303    ScenarioSet(PioScenarioSet),
304    DcPfInstance(powerio_prob::DcPfInstance),
305    AcPfInstance(powerio_prob::AcPfInstance),
306    DcOpfInstance(powerio_prob::DcOpfInstance),
307    AcOpfInstance(powerio_prob::AcOpfInstance),
308    AcOtsInstance(powerio_prob::AcOtsInstance),
309    McAcPfInstance(powerio_prob::McAcPfInstance),
310    McAcOpfInstance(powerio_prob::McAcOpfInstance),
311    LinDist3FlowPfInstance(powerio_prob::LinDist3FlowPfInstance),
312    LinDist3FlowOpfInstance(powerio_prob::LinDist3FlowOpfInstance),
313    AcScucInstance(powerio_prob::AcScucInstance),
314    DcPfSolution(powerio_prob::DcPfSolution),
315    AcPfSolution(powerio_prob::AcPfSolution),
316    DcOpfSolution(powerio_prob::DcOpfSolution),
317    AcOpfSolution(powerio_prob::AcOpfSolution),
318    AcOtsSolution(powerio_prob::AcOtsSolution),
319    SocwrOpfSolution(powerio_prob::solution::SocwrOpfSolution),
320    McAcPfSolution(powerio_prob::McAcPfSolution),
321    McAcOpfSolution(powerio_prob::McAcOpfSolution),
322    LinDist3FlowPfSolution(powerio_prob::LinDist3FlowPfSolution),
323    LinDist3FlowOpfSolution(powerio_prob::LinDist3FlowOpfSolution),
324    AcScucSolution(powerio_prob::AcScucSolution),
325}
326
327impl PioValue {
328    /// Canonical structural type name used by C and PowerIO IR.
329    #[must_use]
330    pub fn type_name(&self) -> &str {
331        match self {
332            Self::BalancedNetwork(_) => "powerio.BalancedNetwork",
333            Self::MulticonductorNetwork(_) => "powerio.MulticonductorNetwork",
334            Self::GeoLayer(_) => "powerio.GeoLayer",
335            Self::ContingencySet(_) => "powerio.ContingencySet",
336            Self::SubsystemSet(_) => "powerio.SubsystemSet",
337            Self::MonitoredSet(_) => "powerio.MonitoredSet",
338            Self::BalancedOperatingPoint(_) => "powerio.OperatingPoint<powerio.BalancedNetwork>",
339            Self::MulticonductorOperatingPoint(_) => {
340                "powerio.OperatingPoint<powerio.MulticonductorNetwork>"
341            }
342            Self::TimeSeries(series) => series.type_name(),
343            Self::ScenarioSet(scenarios) => scenarios.type_name(),
344            Self::DcPfInstance(_) => "powerio.DcPfInstance",
345            Self::AcPfInstance(_) => "powerio.AcPfInstance",
346            Self::DcOpfInstance(_) => "powerio.DcOpfInstance",
347            Self::AcOpfInstance(_) => "powerio.AcOpfInstance",
348            Self::AcOtsInstance(_) => "powerio.AcOtsInstance",
349            Self::McAcPfInstance(_) => "powerio.McAcPfInstance",
350            Self::McAcOpfInstance(_) => "powerio.McAcOpfInstance",
351            Self::LinDist3FlowPfInstance(_) => "powerio.LinDist3FlowPfInstance",
352            Self::LinDist3FlowOpfInstance(_) => "powerio.LinDist3FlowOpfInstance",
353            Self::AcScucInstance(_) => "powerio.AcScucInstance",
354            Self::DcPfSolution(_) => "powerio.DcPfSolution",
355            Self::AcPfSolution(_) => "powerio.AcPfSolution",
356            Self::DcOpfSolution(_) => "powerio.DcOpfSolution",
357            Self::AcOpfSolution(_) => "powerio.AcOpfSolution",
358            Self::AcOtsSolution(_) => "powerio.AcOtsSolution",
359            Self::SocwrOpfSolution(_) => "powerio.SocwrOpfSolution",
360            Self::McAcPfSolution(_) => "powerio.McAcPfSolution",
361            Self::McAcOpfSolution(_) => "powerio.McAcOpfSolution",
362            Self::LinDist3FlowPfSolution(_) => "powerio.LinDist3FlowPfSolution",
363            Self::LinDist3FlowOpfSolution(_) => "powerio.LinDist3FlowOpfSolution",
364            Self::AcScucSolution(_) => "powerio.AcScucSolution",
365        }
366    }
367}
368
369macro_rules! value_conversion {
370    ($ty:ty, $variant:ident) => {
371        impl From<$ty> for PioValue {
372            fn from(value: $ty) -> Self {
373                Self::$variant(value)
374            }
375        }
376    };
377}
378
379impl From<BalancedNetwork> for PioValue {
380    fn from(mut value: BalancedNetwork) -> Self {
381        value.assign_missing_component_ids();
382        Self::BalancedNetwork(value)
383    }
384}
385value_conversion!(MulticonductorNetwork, MulticonductorNetwork);
386value_conversion!(GeoLayer, GeoLayer);
387value_conversion!(ContingencySet, ContingencySet);
388value_conversion!(SubsystemSet, SubsystemSet);
389value_conversion!(MonitoredSet, MonitoredSet);
390value_conversion!(OperatingPoint<BalancedNetwork>, BalancedOperatingPoint);
391value_conversion!(
392    OperatingPoint<MulticonductorNetwork>,
393    MulticonductorOperatingPoint
394);
395value_conversion!(powerio_prob::DcPfInstance, DcPfInstance);
396value_conversion!(powerio_prob::AcPfInstance, AcPfInstance);
397value_conversion!(powerio_prob::DcOpfInstance, DcOpfInstance);
398value_conversion!(powerio_prob::AcOpfInstance, AcOpfInstance);
399value_conversion!(powerio_prob::AcOtsInstance, AcOtsInstance);
400value_conversion!(powerio_prob::McAcPfInstance, McAcPfInstance);
401value_conversion!(powerio_prob::McAcOpfInstance, McAcOpfInstance);
402value_conversion!(powerio_prob::LinDist3FlowPfInstance, LinDist3FlowPfInstance);
403value_conversion!(
404    powerio_prob::LinDist3FlowOpfInstance,
405    LinDist3FlowOpfInstance
406);
407value_conversion!(powerio_prob::AcScucInstance, AcScucInstance);
408value_conversion!(powerio_prob::DcPfSolution, DcPfSolution);
409value_conversion!(powerio_prob::AcPfSolution, AcPfSolution);
410value_conversion!(powerio_prob::DcOpfSolution, DcOpfSolution);
411value_conversion!(powerio_prob::AcOpfSolution, AcOpfSolution);
412value_conversion!(powerio_prob::AcOtsSolution, AcOtsSolution);
413value_conversion!(powerio_prob::solution::SocwrOpfSolution, SocwrOpfSolution);
414value_conversion!(powerio_prob::McAcPfSolution, McAcPfSolution);
415value_conversion!(powerio_prob::McAcOpfSolution, McAcOpfSolution);
416value_conversion!(powerio_prob::LinDist3FlowPfSolution, LinDist3FlowPfSolution);
417value_conversion!(
418    powerio_prob::LinDist3FlowOpfSolution,
419    LinDist3FlowOpfSolution
420);
421value_conversion!(powerio_prob::AcScucSolution, AcScucSolution);
422
423macro_rules! time_series_conversion {
424    ($ty:ty, $name:literal) => {
425        impl From<TimeSeries<$ty>> for PioValue {
426            fn from(value: TimeSeries<$ty>) -> Self {
427                Self::TimeSeries(PioTimeSeries::from_typed($name, value))
428            }
429        }
430    };
431}
432
433time_series_conversion!(BalancedNetwork, "powerio.BalancedNetwork");
434time_series_conversion!(MulticonductorNetwork, "powerio.MulticonductorNetwork");
435time_series_conversion!(
436    OperatingPoint<BalancedNetwork>,
437    "powerio.OperatingPoint<powerio.BalancedNetwork>"
438);
439time_series_conversion!(
440    OperatingPoint<MulticonductorNetwork>,
441    "powerio.OperatingPoint<powerio.MulticonductorNetwork>"
442);
443
444macro_rules! scenario_set_conversion {
445    ($ty:ty, $name:literal) => {
446        impl From<ScenarioSet<$ty>> for PioValue {
447            fn from(value: ScenarioSet<$ty>) -> Self {
448                Self::ScenarioSet(PioScenarioSet::from_typed($name, value))
449            }
450        }
451    };
452}
453
454scenario_set_conversion!(BalancedNetwork, "powerio.BalancedNetwork");
455scenario_set_conversion!(MulticonductorNetwork, "powerio.MulticonductorNetwork");
456scenario_set_conversion!(
457    OperatingPoint<BalancedNetwork>,
458    "powerio.OperatingPoint<powerio.BalancedNetwork>"
459);
460scenario_set_conversion!(
461    OperatingPoint<MulticonductorNetwork>,
462    "powerio.OperatingPoint<powerio.MulticonductorNetwork>"
463);
464scenario_set_conversion!(
465    TimeSeries<BalancedNetwork>,
466    "powerio.TimeSeries<powerio.BalancedNetwork>"
467);
468scenario_set_conversion!(
469    TimeSeries<MulticonductorNetwork>,
470    "powerio.TimeSeries<powerio.MulticonductorNetwork>"
471);
472scenario_set_conversion!(
473    TimeSeries<OperatingPoint<BalancedNetwork>>,
474    "powerio.TimeSeries<powerio.OperatingPoint<powerio.BalancedNetwork>>"
475);
476scenario_set_conversion!(
477    TimeSeries<OperatingPoint<MulticonductorNetwork>>,
478    "powerio.TimeSeries<powerio.OperatingPoint<powerio.MulticonductorNetwork>>"
479);
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484
485    fn small_balanced() -> BalancedNetwork {
486        use powerio_tx::{Bus, BusId, BusType};
487        BalancedNetwork::in_memory(
488            "facade",
489            100.0,
490            vec![Bus::new(BusId(1), BusType::Ref, 230.0)],
491            Vec::new(),
492        )
493    }
494
495    #[test]
496    fn rust_uses_enum_matching_and_structural_names() {
497        let value = PioValue::from(small_balanced());
498        assert!(matches!(value, PioValue::BalancedNetwork(_)));
499        assert_eq!(value.type_name(), "powerio.BalancedNetwork");
500    }
501
502    #[test]
503    fn collections_keep_generic_structural_names() {
504        let series = TimeSeries::new(
505            vec![TimePoint::new("now", None).unwrap()],
506            vec![small_balanced()],
507        )
508        .unwrap();
509        let value = PioValue::from(series);
510        let PioValue::TimeSeries(series) = &value else {
511            unreachable!();
512        };
513        assert_eq!(series.len(), 1);
514        assert_eq!(
515            value.type_name(),
516            "powerio.TimeSeries<powerio.BalancedNetwork>"
517        );
518    }
519
520    #[test]
521    fn scenario_sets_compose_with_time_series() {
522        let series = TimeSeries::new(
523            vec![TimePoint::new("now", None).unwrap()],
524            vec![small_balanced()],
525        )
526        .unwrap();
527        let set = ScenarioSet::new(vec![Scenario::new(
528            powerio_core::ScenarioId::new("base").unwrap(),
529            None,
530            series,
531        )])
532        .unwrap();
533        let value = PioValue::from(set);
534        assert_eq!(
535            value.type_name(),
536            "powerio.ScenarioSet<powerio.TimeSeries<powerio.BalancedNetwork>>"
537        );
538    }
539}