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 McAcPfInstance(powerio_prob::McAcPfInstance),
309 McAcOpfInstance(powerio_prob::McAcOpfInstance),
310 LinDist3FlowOpfInstance(powerio_prob::LinDist3FlowOpfInstance),
311 AcScucInstance(powerio_prob::AcScucInstance),
312 DcPfSolution(powerio_prob::DcPfSolution),
313 AcPfSolution(powerio_prob::AcPfSolution),
314 DcOpfSolution(powerio_prob::DcOpfSolution),
315 AcOpfSolution(powerio_prob::AcOpfSolution),
316 SocwrOpfSolution(powerio_prob::solution::SocwrOpfSolution),
317 McAcPfSolution(powerio_prob::McAcPfSolution),
318 McAcOpfSolution(powerio_prob::McAcOpfSolution),
319 LinDist3FlowOpfSolution(powerio_prob::LinDist3FlowOpfSolution),
320 AcScucSolution(powerio_prob::AcScucSolution),
321}
322
323impl PioValue {
324 #[must_use]
326 pub fn type_name(&self) -> &str {
327 match self {
328 Self::BalancedNetwork(_) => "powerio.BalancedNetwork",
329 Self::MulticonductorNetwork(_) => "powerio.MulticonductorNetwork",
330 Self::GeoLayer(_) => "powerio.GeoLayer",
331 Self::ContingencySet(_) => "powerio.ContingencySet",
332 Self::SubsystemSet(_) => "powerio.SubsystemSet",
333 Self::MonitoredSet(_) => "powerio.MonitoredSet",
334 Self::BalancedOperatingPoint(_) => "powerio.OperatingPoint<powerio.BalancedNetwork>",
335 Self::MulticonductorOperatingPoint(_) => {
336 "powerio.OperatingPoint<powerio.MulticonductorNetwork>"
337 }
338 Self::TimeSeries(series) => series.type_name(),
339 Self::ScenarioSet(scenarios) => scenarios.type_name(),
340 Self::DcPfInstance(_) => "powerio.DcPfInstance",
341 Self::AcPfInstance(_) => "powerio.AcPfInstance",
342 Self::DcOpfInstance(_) => "powerio.DcOpfInstance",
343 Self::AcOpfInstance(_) => "powerio.AcOpfInstance",
344 Self::McAcPfInstance(_) => "powerio.McAcPfInstance",
345 Self::McAcOpfInstance(_) => "powerio.McAcOpfInstance",
346 Self::LinDist3FlowOpfInstance(_) => "powerio.LinDist3FlowOpfInstance",
347 Self::AcScucInstance(_) => "powerio.AcScucInstance",
348 Self::DcPfSolution(_) => "powerio.DcPfSolution",
349 Self::AcPfSolution(_) => "powerio.AcPfSolution",
350 Self::DcOpfSolution(_) => "powerio.DcOpfSolution",
351 Self::AcOpfSolution(_) => "powerio.AcOpfSolution",
352 Self::SocwrOpfSolution(_) => "powerio.SocwrOpfSolution",
353 Self::McAcPfSolution(_) => "powerio.McAcPfSolution",
354 Self::McAcOpfSolution(_) => "powerio.McAcOpfSolution",
355 Self::LinDist3FlowOpfSolution(_) => "powerio.LinDist3FlowOpfSolution",
356 Self::AcScucSolution(_) => "powerio.AcScucSolution",
357 }
358 }
359}
360
361macro_rules! value_conversion {
362 ($ty:ty, $variant:ident) => {
363 impl From<$ty> for PioValue {
364 fn from(value: $ty) -> Self {
365 Self::$variant(value)
366 }
367 }
368 };
369}
370
371impl From<BalancedNetwork> for PioValue {
372 fn from(mut value: BalancedNetwork) -> Self {
373 value.assign_missing_component_ids();
374 Self::BalancedNetwork(value)
375 }
376}
377value_conversion!(MulticonductorNetwork, MulticonductorNetwork);
378value_conversion!(GeoLayer, GeoLayer);
379value_conversion!(ContingencySet, ContingencySet);
380value_conversion!(SubsystemSet, SubsystemSet);
381value_conversion!(MonitoredSet, MonitoredSet);
382value_conversion!(OperatingPoint<BalancedNetwork>, BalancedOperatingPoint);
383value_conversion!(
384 OperatingPoint<MulticonductorNetwork>,
385 MulticonductorOperatingPoint
386);
387value_conversion!(powerio_prob::DcPfInstance, DcPfInstance);
388value_conversion!(powerio_prob::AcPfInstance, AcPfInstance);
389value_conversion!(powerio_prob::DcOpfInstance, DcOpfInstance);
390value_conversion!(powerio_prob::AcOpfInstance, AcOpfInstance);
391value_conversion!(powerio_prob::McAcPfInstance, McAcPfInstance);
392value_conversion!(powerio_prob::McAcOpfInstance, McAcOpfInstance);
393value_conversion!(
394 powerio_prob::LinDist3FlowOpfInstance,
395 LinDist3FlowOpfInstance
396);
397value_conversion!(powerio_prob::AcScucInstance, AcScucInstance);
398value_conversion!(powerio_prob::DcPfSolution, DcPfSolution);
399value_conversion!(powerio_prob::AcPfSolution, AcPfSolution);
400value_conversion!(powerio_prob::DcOpfSolution, DcOpfSolution);
401value_conversion!(powerio_prob::AcOpfSolution, AcOpfSolution);
402value_conversion!(powerio_prob::solution::SocwrOpfSolution, SocwrOpfSolution);
403value_conversion!(powerio_prob::McAcPfSolution, McAcPfSolution);
404value_conversion!(powerio_prob::McAcOpfSolution, McAcOpfSolution);
405value_conversion!(
406 powerio_prob::LinDist3FlowOpfSolution,
407 LinDist3FlowOpfSolution
408);
409value_conversion!(powerio_prob::AcScucSolution, AcScucSolution);
410
411macro_rules! time_series_conversion {
412 ($ty:ty, $name:literal) => {
413 impl From<TimeSeries<$ty>> for PioValue {
414 fn from(value: TimeSeries<$ty>) -> Self {
415 Self::TimeSeries(PioTimeSeries::from_typed($name, value))
416 }
417 }
418 };
419}
420
421time_series_conversion!(BalancedNetwork, "powerio.BalancedNetwork");
422time_series_conversion!(MulticonductorNetwork, "powerio.MulticonductorNetwork");
423time_series_conversion!(
424 OperatingPoint<BalancedNetwork>,
425 "powerio.OperatingPoint<powerio.BalancedNetwork>"
426);
427time_series_conversion!(
428 OperatingPoint<MulticonductorNetwork>,
429 "powerio.OperatingPoint<powerio.MulticonductorNetwork>"
430);
431
432macro_rules! scenario_set_conversion {
433 ($ty:ty, $name:literal) => {
434 impl From<ScenarioSet<$ty>> for PioValue {
435 fn from(value: ScenarioSet<$ty>) -> Self {
436 Self::ScenarioSet(PioScenarioSet::from_typed($name, value))
437 }
438 }
439 };
440}
441
442scenario_set_conversion!(BalancedNetwork, "powerio.BalancedNetwork");
443scenario_set_conversion!(MulticonductorNetwork, "powerio.MulticonductorNetwork");
444scenario_set_conversion!(
445 OperatingPoint<BalancedNetwork>,
446 "powerio.OperatingPoint<powerio.BalancedNetwork>"
447);
448scenario_set_conversion!(
449 OperatingPoint<MulticonductorNetwork>,
450 "powerio.OperatingPoint<powerio.MulticonductorNetwork>"
451);
452scenario_set_conversion!(
453 TimeSeries<BalancedNetwork>,
454 "powerio.TimeSeries<powerio.BalancedNetwork>"
455);
456scenario_set_conversion!(
457 TimeSeries<MulticonductorNetwork>,
458 "powerio.TimeSeries<powerio.MulticonductorNetwork>"
459);
460scenario_set_conversion!(
461 TimeSeries<OperatingPoint<BalancedNetwork>>,
462 "powerio.TimeSeries<powerio.OperatingPoint<powerio.BalancedNetwork>>"
463);
464scenario_set_conversion!(
465 TimeSeries<OperatingPoint<MulticonductorNetwork>>,
466 "powerio.TimeSeries<powerio.OperatingPoint<powerio.MulticonductorNetwork>>"
467);
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 fn small_balanced() -> BalancedNetwork {
474 use powerio_tx::{Bus, BusId, BusType};
475 BalancedNetwork::in_memory(
476 "facade",
477 100.0,
478 vec![Bus::new(BusId(1), BusType::Ref, 230.0)],
479 Vec::new(),
480 )
481 }
482
483 #[test]
484 fn rust_uses_enum_matching_and_structural_names() {
485 let value = PioValue::from(small_balanced());
486 assert!(matches!(value, PioValue::BalancedNetwork(_)));
487 assert_eq!(value.type_name(), "powerio.BalancedNetwork");
488 }
489
490 #[test]
491 fn collections_keep_generic_structural_names() {
492 let series = TimeSeries::new(
493 vec![TimePoint::new("now", None).unwrap()],
494 vec![small_balanced()],
495 )
496 .unwrap();
497 let value = PioValue::from(series);
498 let PioValue::TimeSeries(series) = &value else {
499 unreachable!();
500 };
501 assert_eq!(series.len(), 1);
502 assert_eq!(
503 value.type_name(),
504 "powerio.TimeSeries<powerio.BalancedNetwork>"
505 );
506 }
507
508 #[test]
509 fn scenario_sets_compose_with_time_series() {
510 let series = TimeSeries::new(
511 vec![TimePoint::new("now", None).unwrap()],
512 vec![small_balanced()],
513 )
514 .unwrap();
515 let set = ScenarioSet::new(vec![Scenario::new(
516 powerio_core::ScenarioId::new("base").unwrap(),
517 None,
518 series,
519 )])
520 .unwrap();
521 let value = PioValue::from(set);
522 assert_eq!(
523 value.type_name(),
524 "powerio.ScenarioSet<powerio.TimeSeries<powerio.BalancedNetwork>>"
525 );
526 }
527}