1use powerio_core::{Scenario, ScenarioSet, TimePoint, TimeSeries};
4use powerio_dist::MulticonductorNetwork;
5use powerio_prob::OperatingPoint;
6use powerio_tx::{BalancedNetwork, ContingencySet, GeoLayer, MonitoredSet, SubsystemSet};
7
8#[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 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 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 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#[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 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 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 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 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#[derive(Clone, Debug)]
281#[non_exhaustive]
282#[allow(clippy::large_enum_variant)]
283pub enum PioValue {
284 BalancedNetwork(BalancedNetwork),
285 MulticonductorNetwork(MulticonductorNetwork),
286 GeoLayer(GeoLayer),
290 ContingencySet(ContingencySet),
294 SubsystemSet(SubsystemSet),
297 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 #[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}