1use std::sync::Arc;
5
6use powerio_core::{Error, TimePoint, TimeSeries};
7use powerio_dist::MulticonductorNetwork;
8
9use super::{
10 OperatingPointColumns, OperatingPointFlags, OperatingPointValues, QuantityLayout,
11 SharedColumns, dense_quantity, sparse_quantity,
12};
13use crate::diagnostics::codes;
14
15const TERMINAL_VOLTAGE_MAGNITUDE: &str = "terminal_voltage_magnitude";
20const TERMINAL_VOLTAGE_ANGLE: &str = "terminal_voltage_angle";
21pub(crate) const LOAD_ACTIVE_POWER: &str = "load_active_power";
22pub(crate) const LOAD_REACTIVE_POWER: &str = "load_reactive_power";
23pub(crate) const GENERATOR_ACTIVE_POWER: &str = "generator_active_power";
24pub(crate) const GENERATOR_REACTIVE_POWER: &str = "generator_reactive_power";
25pub(crate) const SWITCH_CLOSED: &str = "switch_closed";
26const TRANSFORMER_TAP: &str = "transformer_tap";
27const CAPACITOR_STEPS: &str = "capacitor_steps";
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31#[non_exhaustive]
32pub enum MulticonductorOperatingPointQuantity {
33 TerminalVoltageMagnitude,
34 TerminalVoltageAngle,
35 LoadActivePower,
36 LoadReactivePower,
37 GeneratorActivePower,
38 GeneratorReactivePower,
39 TransformerTap,
40 CapacitorSteps,
41}
42
43impl MulticonductorOperatingPointQuantity {
44 #[must_use]
46 pub const fn name(self) -> &'static str {
47 match self {
48 Self::TerminalVoltageMagnitude => TERMINAL_VOLTAGE_MAGNITUDE,
49 Self::TerminalVoltageAngle => TERMINAL_VOLTAGE_ANGLE,
50 Self::LoadActivePower => LOAD_ACTIVE_POWER,
51 Self::LoadReactivePower => LOAD_REACTIVE_POWER,
52 Self::GeneratorActivePower => GENERATOR_ACTIVE_POWER,
53 Self::GeneratorReactivePower => GENERATOR_REACTIVE_POWER,
54 Self::TransformerTap => TRANSFORMER_TAP,
55 Self::CapacitorSteps => CAPACITOR_STEPS,
56 }
57 }
58}
59
60#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
62#[non_exhaustive]
63pub enum MulticonductorOperatingPointFlag {
64 SwitchClosed,
65}
66
67impl MulticonductorOperatingPointFlag {
68 #[must_use]
70 pub const fn name(self) -> &'static str {
71 match self {
72 Self::SwitchClosed => SWITCH_CLOSED,
73 }
74 }
75}
76
77pub use super::OperatingPoint;
78
79impl OperatingPoint<MulticonductorNetwork> {
80 pub(crate) fn rebind_network(mut self, network: MulticonductorNetwork) -> Result<Self, Error> {
83 let layout = MulticonductorOperatingPointBuilder::new(network.clone(), Vec::new());
84 for quantity in self.columns.quantities.keys() {
85 let expected: Vec<String> = layout
86 .layout_for(quantity)?
87 .order()
88 .map(str::to_owned)
89 .collect();
90 let actual: Vec<&str> = self
91 .identity_order(quantity)
92 .expect("the quantity came from this point")
93 .collect();
94 if actual.len() != expected.len()
95 || actual
96 .iter()
97 .zip(&expected)
98 .any(|(left, right)| *left != right)
99 {
100 return Err(Error::new(
101 &codes::BUILD_OPERATING_POINT_SHAPE_MISMATCH,
102 format!(
103 "{quantity}: edited network changes the initial point's component identity order"
104 ),
105 ));
106 }
107 }
108 self.network = network;
109 Ok(self)
110 }
111
112 #[must_use]
114 pub fn values(
115 &self,
116 quantity: MulticonductorOperatingPointQuantity,
117 ) -> Option<OperatingPointValues<'_>> {
118 self.iter_values(quantity.name())
119 }
120
121 #[must_use]
123 pub fn flags(
124 &self,
125 quantity: MulticonductorOperatingPointFlag,
126 ) -> Option<OperatingPointFlags<'_>> {
127 self.iter_flags(quantity.name())
128 }
129
130 #[must_use]
133 pub fn terminal_voltage_magnitude(&self, bus: &str, terminal: &str) -> Option<f64> {
134 self.value_pair(TERMINAL_VOLTAGE_MAGNITUDE, bus, terminal)
135 }
136
137 #[must_use]
139 pub fn terminal_voltage_angle(&self, bus: &str, terminal: &str) -> Option<f64> {
140 self.value_pair(TERMINAL_VOLTAGE_ANGLE, bus, terminal)
141 }
142
143 #[must_use]
146 pub fn load_active_power(&self, load: &str, terminal: &str) -> Option<f64> {
147 self.value_pair(LOAD_ACTIVE_POWER, load, terminal)
148 }
149
150 #[must_use]
152 pub fn load_reactive_power(&self, load: &str, terminal: &str) -> Option<f64> {
153 self.value_pair(LOAD_REACTIVE_POWER, load, terminal)
154 }
155
156 #[must_use]
158 pub fn generator_active_power(&self, generator: &str, terminal: &str) -> Option<f64> {
159 self.value_pair(GENERATOR_ACTIVE_POWER, generator, terminal)
160 }
161
162 #[must_use]
164 pub fn generator_reactive_power(&self, generator: &str, terminal: &str) -> Option<f64> {
165 self.value_pair(GENERATOR_REACTIVE_POWER, generator, terminal)
166 }
167
168 #[must_use]
170 pub fn switch_closed(&self, switch: &str) -> Option<bool> {
171 self.value_single(SWITCH_CLOSED, switch)
172 .map(|value| value != 0.0)
173 }
174
175 #[must_use]
177 pub fn transformer_tap(&self, transformer: &str) -> Option<f64> {
178 self.value_single(TRANSFORMER_TAP, transformer)
179 }
180
181 #[must_use]
183 pub fn capacitor_steps(&self, capacitor: &str) -> Option<f64> {
184 self.value_single(CAPACITOR_STEPS, capacitor)
185 }
186
187 fn value_pair(&self, quantity: &'static str, element: &str, terminal: &str) -> Option<f64> {
188 self.columns
189 .quantities
190 .get(quantity)?
191 .value(self.index, &format!("{element}/{terminal}"))
192 }
193
194 fn value_single(&self, quantity: &'static str, element: &str) -> Option<f64> {
195 self.columns
196 .quantities
197 .get(quantity)?
198 .value(self.index, element)
199 }
200}
201
202#[derive(Debug)]
208pub struct MulticonductorOperatingPointBuilder {
209 network: MulticonductorNetwork,
210 time_points: Vec<TimePoint>,
211 quantities: Vec<(&'static str, ColumnsInput)>,
212}
213
214#[derive(Debug)]
215enum ColumnsInput {
216 Dense(Vec<f64>),
217 Sparse {
218 base: Vec<f64>,
219 changes: Vec<Vec<(String, f64)>>,
220 },
221}
222
223impl MulticonductorOperatingPointBuilder {
224 #[must_use]
225 pub fn new(network: MulticonductorNetwork, time_points: Vec<TimePoint>) -> Self {
226 Self {
227 network,
228 time_points,
229 quantities: Vec::new(),
230 }
231 }
232
233 #[must_use]
235 pub fn for_point(network: MulticonductorNetwork) -> Self {
236 Self::new(network, Vec::new())
237 }
238
239 fn dense(mut self, quantity: &'static str, values: Vec<f64>) -> Self {
240 self.quantities
241 .push((quantity, ColumnsInput::Dense(values)));
242 self
243 }
244
245 fn sparse(
246 mut self,
247 quantity: &'static str,
248 base: Vec<f64>,
249 changes: Vec<Vec<(String, f64)>>,
250 ) -> Self {
251 self.quantities
252 .push((quantity, ColumnsInput::Sparse { base, changes }));
253 self
254 }
255
256 fn sparse_flags(
257 self,
258 quantity: &'static str,
259 base: Vec<bool>,
260 changes: Vec<Vec<(String, bool)>>,
261 ) -> Self {
262 self.sparse(
263 quantity,
264 encode_flags(base),
265 changes
266 .into_iter()
267 .map(|point| {
268 point
269 .into_iter()
270 .map(|(identity, value)| (identity, encode_flag(value)))
271 .collect()
272 })
273 .collect(),
274 )
275 }
276
277 #[must_use]
280 pub fn terminal_voltage_magnitudes(self, values: Vec<f64>) -> Self {
281 self.dense(TERMINAL_VOLTAGE_MAGNITUDE, values)
282 }
283
284 #[must_use]
285 pub fn terminal_voltage_angles(self, values: Vec<f64>) -> Self {
286 self.dense(TERMINAL_VOLTAGE_ANGLE, values)
287 }
288
289 #[must_use]
292 pub fn load_active_powers(self, values: Vec<f64>) -> Self {
293 self.dense(LOAD_ACTIVE_POWER, values)
294 }
295
296 #[must_use]
297 pub fn load_reactive_powers(self, values: Vec<f64>) -> Self {
298 self.dense(LOAD_REACTIVE_POWER, values)
299 }
300
301 #[must_use]
302 pub fn generator_active_powers(self, values: Vec<f64>) -> Self {
303 self.dense(GENERATOR_ACTIVE_POWER, values)
304 }
305
306 #[must_use]
307 pub fn generator_reactive_powers(self, values: Vec<f64>) -> Self {
308 self.dense(GENERATOR_REACTIVE_POWER, values)
309 }
310
311 #[must_use]
312 pub fn switch_closed(self, values: Vec<bool>) -> Self {
313 self.dense(SWITCH_CLOSED, encode_flags(values))
314 }
315
316 #[must_use]
317 pub fn transformer_taps(self, values: Vec<f64>) -> Self {
318 self.dense(TRANSFORMER_TAP, values)
319 }
320
321 #[must_use]
322 pub fn capacitor_steps(self, values: Vec<f64>) -> Self {
323 self.dense(CAPACITOR_STEPS, values)
324 }
325
326 #[must_use]
329 pub fn sparse_terminal_voltage_magnitudes(
330 self,
331 base: Vec<f64>,
332 changes: Vec<Vec<(String, f64)>>,
333 ) -> Self {
334 self.sparse(TERMINAL_VOLTAGE_MAGNITUDE, base, changes)
335 }
336
337 #[must_use]
338 pub fn sparse_terminal_voltage_angles(
339 self,
340 base: Vec<f64>,
341 changes: Vec<Vec<(String, f64)>>,
342 ) -> Self {
343 self.sparse(TERMINAL_VOLTAGE_ANGLE, base, changes)
344 }
345
346 #[must_use]
349 pub fn sparse_load_active_powers(
350 self,
351 base: Vec<f64>,
352 changes: Vec<Vec<(String, f64)>>,
353 ) -> Self {
354 self.sparse(LOAD_ACTIVE_POWER, base, changes)
355 }
356
357 #[must_use]
358 pub fn sparse_load_reactive_powers(
359 self,
360 base: Vec<f64>,
361 changes: Vec<Vec<(String, f64)>>,
362 ) -> Self {
363 self.sparse(LOAD_REACTIVE_POWER, base, changes)
364 }
365
366 #[must_use]
367 pub fn sparse_generator_active_powers(
368 self,
369 base: Vec<f64>,
370 changes: Vec<Vec<(String, f64)>>,
371 ) -> Self {
372 self.sparse(GENERATOR_ACTIVE_POWER, base, changes)
373 }
374
375 #[must_use]
376 pub fn sparse_generator_reactive_powers(
377 self,
378 base: Vec<f64>,
379 changes: Vec<Vec<(String, f64)>>,
380 ) -> Self {
381 self.sparse(GENERATOR_REACTIVE_POWER, base, changes)
382 }
383
384 #[must_use]
385 pub fn sparse_switch_closed(self, base: Vec<bool>, changes: Vec<Vec<(String, bool)>>) -> Self {
386 self.sparse_flags(SWITCH_CLOSED, base, changes)
387 }
388
389 #[must_use]
390 pub fn sparse_transformer_taps(self, base: Vec<f64>, changes: Vec<Vec<(String, f64)>>) -> Self {
391 self.sparse(TRANSFORMER_TAP, base, changes)
392 }
393
394 #[must_use]
395 pub fn sparse_capacitor_steps(self, base: Vec<f64>, changes: Vec<Vec<(String, f64)>>) -> Self {
396 self.sparse(CAPACITOR_STEPS, base, changes)
397 }
398
399 fn layout_for(&self, quantity: &'static str) -> Result<QuantityLayout, Error> {
400 let network = &self.network;
401 match quantity {
402 TERMINAL_VOLTAGE_MAGNITUDE | TERMINAL_VOLTAGE_ANGLE => QuantityLayout::from_order(
403 quantity,
404 network.buses().iter().flat_map(|bus| {
405 bus.terminals
406 .iter()
407 .map(move |terminal| format!("{}/{terminal}", bus.id))
408 }),
409 ),
410 LOAD_ACTIVE_POWER | LOAD_REACTIVE_POWER => QuantityLayout::from_order(
411 quantity,
412 network.loads().iter().flat_map(|load| {
413 load.terminal_map
414 .iter()
415 .map(move |terminal| format!("{}/{terminal}", load.name))
416 }),
417 ),
418 GENERATOR_ACTIVE_POWER | GENERATOR_REACTIVE_POWER => QuantityLayout::from_order(
419 quantity,
420 network.generators().iter().flat_map(|generator| {
421 generator
422 .terminal_map
423 .iter()
424 .map(move |terminal| format!("{}/{terminal}", generator.name))
425 }),
426 ),
427 SWITCH_CLOSED => QuantityLayout::from_order(
428 quantity,
429 network.switches().iter().map(|s| s.name.clone()),
430 ),
431 TRANSFORMER_TAP => QuantityLayout::from_order(
432 quantity,
433 network.transformers().iter().map(|t| t.name.clone()),
434 ),
435 CAPACITOR_STEPS => QuantityLayout::from_order(
436 quantity,
437 network.capacitors().iter().map(|c| c.name.clone()),
438 ),
439 _ => unreachable!("builder methods name registered quantities"),
440 }
441 }
442
443 fn build_points(
444 &self,
445 point_count: usize,
446 ) -> Result<Vec<OperatingPoint<MulticonductorNetwork>>, Error> {
447 let mut quantities = std::collections::HashMap::new();
448 for (quantity, input) in &self.quantities {
449 let layout = self.layout_for(quantity)?;
450 let built = match input {
451 ColumnsInput::Dense(values) => {
452 dense_quantity(quantity, layout, point_count, values.clone())?
453 }
454 ColumnsInput::Sparse { base, changes } => {
455 sparse_quantity(quantity, layout, point_count, base.clone(), changes.clone())?
456 }
457 };
458 if quantities.insert(*quantity, built).is_some() {
459 return Err(Error::new(
460 &codes::BUILD_OPERATING_POINT_SHAPE_MISMATCH,
461 format!("{quantity} was supplied twice"),
462 ));
463 }
464 }
465 let columns: SharedColumns = Arc::new(OperatingPointColumns {
466 point_count,
467 quantities,
468 });
469 Ok((0..point_count)
470 .map(|index| OperatingPoint {
471 network: self.network.clone(),
472 columns: Arc::clone(&columns),
473 index,
474 })
475 .collect())
476 }
477
478 pub fn build(self) -> Result<TimeSeries<OperatingPoint<MulticonductorNetwork>>, Error> {
487 let point_count = self.time_points.len();
488 if point_count == 0 {
489 return Err(Error::new(
490 &codes::BUILD_OPERATING_POINT_SHAPE_MISMATCH,
491 "an operating point series needs at least one time point",
492 ));
493 }
494 let points = self.build_points(point_count)?;
495 TimeSeries::new(self.time_points, points)
496 }
497
498 pub fn build_point(self) -> Result<OperatingPoint<MulticonductorNetwork>, Error> {
503 if self.time_points.len() > 1 {
504 return Err(Error::new(
505 &codes::BUILD_OPERATING_POINT_SHAPE_MISMATCH,
506 "a scalar operating point builder cannot contain several time points",
507 ));
508 }
509 self.build_points(1)?.pop().ok_or_else(|| {
510 Error::new(
511 &codes::BUILD_OPERATING_POINT_SHAPE_MISMATCH,
512 "the scalar operating point builder produced no point",
513 )
514 })
515 }
516}
517
518fn encode_flags(values: Vec<bool>) -> Vec<f64> {
519 values.into_iter().map(encode_flag).collect()
520}
521
522fn encode_flag(value: bool) -> f64 {
523 if value { 1.0 } else { 0.0 }
524}