1use std::collections::BTreeMap;
15
16use powerio_dist::MulticonductorNetwork;
17use powerio_matrix::BalancedNetwork;
18
19const ELECTRICAL_TOL: f64 = 1e-8;
22
23const MODEL_TOL: f64 = 1e-9;
26
27pub const MAX_MODEL_DIFFS: usize = 24;
30
31#[derive(Debug, Clone, PartialEq)]
34pub struct YbusChange {
35 pub from_bus: usize,
36 pub to_bus: usize,
37 pub before: (f64, f64),
38 pub after: (f64, f64),
39}
40
41impl std::fmt::Display for YbusChange {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 write!(
44 f,
45 "Y[{}, {}] was {}+j{}, now {}+j{}",
46 self.from_bus, self.to_bus, self.before.0, self.before.1, self.after.0, self.after.1
47 )
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum Injection {
59 LoadP,
60 LoadQ,
61 GenP,
62 GenQ,
63 DcRectifierP,
65 DcInverterP,
67 DcRectifierQ,
68 DcInverterQ,
69}
70
71impl Injection {
72 #[must_use]
73 pub const fn label(self) -> &'static str {
74 match self {
75 Self::LoadP => "load p",
76 Self::LoadQ => "load q",
77 Self::GenP => "gen p",
78 Self::GenQ => "gen q",
79 Self::DcRectifierP => "dc rectifier p",
80 Self::DcInverterP => "dc inverter p",
81 Self::DcRectifierQ => "dc rectifier q",
82 Self::DcInverterQ => "dc inverter q",
83 }
84 }
85}
86
87#[derive(Debug, Clone, PartialEq)]
89pub struct InjectionChange {
90 pub bus: usize,
91 pub quantity: Injection,
92 pub before: f64,
93 pub after: f64,
94 pub status_only: bool,
103}
104
105impl std::fmt::Display for InjectionChange {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 write!(
108 f,
109 "bus {} {}: {} -> {}",
110 self.bus,
111 self.quantity.label(),
112 self.before,
113 self.after
114 )
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum YbusUnavailable {
125 Before,
126 After,
127}
128
129pub fn ybus_change(
141 before: &BalancedNetwork,
142 after: &BalancedNetwork,
143) -> Result<Option<YbusChange>, YbusUnavailable> {
144 let entries = |net: &BalancedNetwork| -> Option<BTreeMap<(usize, usize), (f64, f64)>> {
145 let view = powerio_matrix::IndexedNetwork::new(net);
146 let parts =
147 powerio_matrix::build_ybus(&view, &powerio_matrix::BuildOptions::default()).ok()?;
148 let mut map = BTreeMap::new();
149 for (value, (i, j)) in &parts.g {
150 map.entry((view.bus_id(i).0, view.bus_id(j).0))
151 .or_insert((0.0, 0.0))
152 .0 = *value;
153 }
154 for (value, (i, j)) in &parts.b {
155 map.entry((view.bus_id(i).0, view.bus_id(j).0))
156 .or_insert((0.0, 0.0))
157 .1 = *value;
158 }
159 Some(map)
160 };
161 let a = entries(before).ok_or(YbusUnavailable::Before)?;
162 let b = entries(after).ok_or(YbusUnavailable::After)?;
163 for key in a.keys().chain(b.keys().filter(|k| !a.contains_key(*k))) {
164 let (ga, ba) = a.get(key).copied().unwrap_or((0.0, 0.0));
165 let (gb, bb) = b.get(key).copied().unwrap_or((0.0, 0.0));
166 if beyond_tol(ga, gb, ELECTRICAL_TOL) || beyond_tol(ba, bb, ELECTRICAL_TOL) {
167 return Ok(Some(YbusChange {
168 from_bus: key.0,
169 to_bus: key.1,
170 before: (ga, ba),
171 after: (gb, bb),
172 }));
173 }
174 }
175 Ok(None)
176}
177
178#[must_use]
196pub fn injection_change(
197 before: &BalancedNetwork,
198 after: &BalancedNetwork,
199) -> Option<InjectionChange> {
200 let change = first_moved(&per_bus(before, true), &per_bus(after, true), AC_INJECTIONS)?;
201 let status_only = first_moved(
202 &per_bus(before, false),
203 &per_bus(after, false),
204 AC_INJECTIONS,
205 )
206 .is_none();
207 Some(InjectionChange {
208 status_only,
209 ..change
210 })
211}
212
213const AC_INJECTIONS: [Injection; 4] = [
215 Injection::LoadP,
216 Injection::LoadQ,
217 Injection::GenP,
218 Injection::GenQ,
219];
220
221const DC_TERMINALS: [Injection; 4] = [
223 Injection::DcRectifierP,
224 Injection::DcInverterP,
225 Injection::DcRectifierQ,
226 Injection::DcInverterQ,
227];
228
229fn per_bus(net: &BalancedNetwork, in_service_only: bool) -> BTreeMap<usize, [f64; 4]> {
230 let mut map: BTreeMap<usize, [f64; 4]> = BTreeMap::new();
231 for l in net
232 .loads
233 .iter()
234 .filter(|l| l.in_service || !in_service_only)
235 {
236 let e = map.entry(l.bus.0).or_default();
237 e[0] += l.p;
238 e[1] += l.q;
239 }
240 for g in net
241 .generators
242 .iter()
243 .filter(|g| g.in_service || !in_service_only)
244 {
245 let e = map.entry(g.bus.0).or_default();
246 e[2] += g.pg;
247 e[3] += g.qg;
248 }
249 map
250}
251
252fn per_bus_dc(net: &BalancedNetwork, in_service_only: bool) -> BTreeMap<usize, [f64; 4]> {
260 let mut map: BTreeMap<usize, [f64; 4]> = BTreeMap::new();
261 for d in net.hvdc.iter().filter(|d| d.in_service || !in_service_only) {
262 let from = map.entry(d.from.0).or_default();
263 from[0] += d.pf;
264 from[2] += d.qf;
265 let to = map.entry(d.to.0).or_default();
266 to[1] += d.pt;
267 to[3] += d.qt;
268 }
269 map
270}
271
272#[must_use]
287pub fn dc_terminal_change(
288 before: &BalancedNetwork,
289 after: &BalancedNetwork,
290) -> Option<InjectionChange> {
291 let change = first_moved(
292 &per_bus_dc(before, true),
293 &per_bus_dc(after, true),
294 DC_TERMINALS,
295 )?;
296 let status_only = first_moved(
297 &per_bus_dc(before, false),
298 &per_bus_dc(after, false),
299 DC_TERMINALS,
300 )
301 .is_none();
302 Some(InjectionChange {
303 status_only,
304 ..change
305 })
306}
307
308fn first_moved<const N: usize>(
314 a: &BTreeMap<usize, [f64; N]>,
315 b: &BTreeMap<usize, [f64; N]>,
316 quantities: [Injection; N],
317) -> Option<InjectionChange> {
318 for key in a.keys().chain(b.keys().filter(|k| !a.contains_key(*k))) {
319 let x = a.get(key).copied().unwrap_or([0.0; N]);
320 let y = b.get(key).copied().unwrap_or([0.0; N]);
321 for (i, quantity) in quantities.into_iter().enumerate() {
322 if beyond_tol(x[i], y[i], ELECTRICAL_TOL) {
323 return Some(InjectionChange {
324 bus: *key,
325 quantity,
326 before: x[i],
327 after: y[i],
328 status_only: false,
329 });
330 }
331 }
332 }
333 None
334}
335
336fn beyond_tol(x: f64, y: f64, tol: f64) -> bool {
344 if !x.is_finite() || !y.is_finite() {
345 #[allow(clippy::float_cmp)]
350 return !(x.is_nan() && y.is_nan()) && x != y;
351 }
352 (x - y).abs() > tol * x.abs().max(y.abs()).max(1.0)
353}
354
355#[must_use]
364pub fn status_disagreements(a: &BalancedNetwork, b: &BalancedNetwork) -> usize {
365 let off = |net: &BalancedNetwork| {
366 [
367 net.generators.iter().filter(|g| !g.in_service).count(),
368 net.branches.iter().filter(|x| !x.in_service).count(),
369 net.loads.iter().filter(|l| !l.in_service).count(),
370 net.shunts.iter().filter(|s| !s.in_service).count(),
371 net.switches.iter().filter(|x| !x.closed).count(),
372 net.storage.iter().filter(|x| !x.in_service).count(),
373 net.hvdc.iter().filter(|d| !d.in_service).count(),
374 net.transformers_3w.iter().filter(|t| !t.in_service).count(),
375 ]
376 };
377 off(a)
378 .iter()
379 .zip(off(b).iter())
380 .map(|(x, y)| x.abs_diff(*y))
381 .sum()
382}
383
384#[derive(Debug, Clone, PartialEq, Eq)]
386pub struct TransmissionCore {
387 pub buses: usize,
388 pub branches: usize,
389 pub generators: usize,
390 pub loads: usize,
391 pub shunts: usize,
392 pub load_p: i64,
393 pub load_q: i64,
394 pub gen_p: i64,
395 pub base_mva: i64,
396}
397
398fn milli(x: f64) -> i64 {
399 (x * 1e3).round() as i64
400}
401
402#[must_use]
403pub fn transmission_core(net: &BalancedNetwork) -> TransmissionCore {
404 TransmissionCore {
405 buses: net.buses.len(),
406 branches: net.branches.len(),
407 generators: net.generators.len(),
408 loads: net.loads.len(),
409 shunts: net.shunts.len(),
410 load_p: milli(net.loads.iter().map(|load| load.p).sum()),
411 load_q: milli(net.loads.iter().map(|load| load.q).sum()),
412 gen_p: milli(net.generators.iter().map(|generator| generator.pg).sum()),
413 base_mva: milli(net.base_mva),
414 }
415}
416
417#[derive(Debug, Clone, PartialEq, Eq)]
419pub struct DistributionCore {
420 pub buses: usize,
421 pub loads: usize,
422 pub generators: usize,
423 pub shunts: usize,
424 pub load_p: i64,
425 pub load_q: i64,
426}
427
428#[must_use]
429pub fn distribution_core(net: &MulticonductorNetwork) -> DistributionCore {
430 DistributionCore {
431 buses: net.buses.len(),
432 loads: net.loads.len(),
433 generators: net.generators.len(),
434 shunts: net.shunts.len(),
435 load_p: milli(net.loads.iter().flat_map(|load| load.p_nom.iter()).sum()),
436 load_q: milli(net.loads.iter().flat_map(|load| load.q_nom.iter()).sum()),
437 }
438}
439
440#[must_use]
454pub fn transmission_value(net: &BalancedNetwork) -> serde_json::Value {
455 let mut net = net.clone();
456 net.name = String::new();
457 net.source = None;
458 net.source_format = powerio_matrix::SourceFormat::Matpower;
459 for br in &mut net.branches {
460 br.charging = Some(br.terminal_charging());
461 }
462 net.buses.sort_by_key(|b| b.id);
463 net.branches.sort_by_key(|a| (a.from, a.to));
464 net.loads.sort_by_key(|l| l.bus);
465 net.shunts.sort_by_key(|s| s.bus);
466 net.generators.sort_by_key(|g| g.bus);
467 net.storage.sort_by_key(|s| s.bus);
468 net.hvdc.sort_by_key(|d| (d.from, d.to));
469 serde_json::to_value(&net).unwrap()
470}
471
472#[must_use]
478pub fn distribution_value(net: &MulticonductorNetwork) -> serde_json::Value {
479 let mut net = net.clone();
480 net.name = None;
481 net.source = None;
482 net.source_format = None;
483 net.warnings = Vec::new();
484 serde_json::to_value(&net).unwrap()
485}
486
487#[derive(Debug, Clone, PartialEq)]
489pub struct ModelDiff {
490 pub path: String,
493 pub before: DiffSide,
494 pub after: DiffSide,
495}
496
497#[derive(Debug, Clone, PartialEq)]
498pub enum DiffSide {
499 Number(f64),
500 Value(String),
503 Len(usize),
505 Absent,
506}
507
508impl std::fmt::Display for DiffSide {
509 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
510 match self {
511 Self::Number(x) => write!(f, "{x}"),
512 Self::Value(v) => write!(f, "{v}"),
513 Self::Len(n) => write!(f, "{n} entries"),
514 Self::Absent => write!(f, "absent"),
515 }
516 }
517}
518
519impl std::fmt::Display for ModelDiff {
520 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521 match (&self.before, &self.after) {
524 (DiffSide::Len(x), DiffSide::Len(y)) => {
525 write!(f, "{}: {x} entries -> {y}", self.path)
526 }
527 (before, after) => write!(f, "{}: {before} -> {after}", self.path),
528 }
529 }
530}
531
532#[must_use]
539pub fn model_diffs(before: &serde_json::Value, after: &serde_json::Value) -> Vec<ModelDiff> {
540 let mut out = Vec::new();
541 diff_values(before, after, "", &mut out);
542 out
543}
544
545fn diff_values(a: &serde_json::Value, b: &serde_json::Value, path: &str, out: &mut Vec<ModelDiff>) {
546 use serde_json::Value;
547 if out.len() >= MAX_MODEL_DIFFS {
548 return;
549 }
550 match (a, b) {
551 (Value::Number(x), Value::Number(y)) => {
552 let (x, y) = (
553 x.as_f64().unwrap_or(f64::NAN),
554 y.as_f64().unwrap_or(f64::NAN),
555 );
556 let within = (x - y).abs() <= MODEL_TOL * x.abs().max(y.abs()).max(1.0);
560 if !(within || (x.is_nan() && y.is_nan())) {
561 out.push(ModelDiff {
562 path: path.to_string(),
563 before: DiffSide::Number(x),
564 after: DiffSide::Number(y),
565 });
566 }
567 }
568 (Value::Array(xs), Value::Array(ys)) => {
569 if xs.len() != ys.len() {
570 out.push(ModelDiff {
571 path: path.to_string(),
572 before: DiffSide::Len(xs.len()),
573 after: DiffSide::Len(ys.len()),
574 });
575 return;
576 }
577 for (i, (x, y)) in xs.iter().zip(ys).enumerate() {
578 diff_values(x, y, &format!("{path}[{i}]"), out);
579 }
580 }
581 (Value::Object(xs), Value::Object(ys)) => {
582 for key in xs.keys().chain(ys.keys().filter(|k| !xs.contains_key(*k))) {
583 if out.len() >= MAX_MODEL_DIFFS {
587 return;
588 }
589 let sub = format!("{path}.{key}");
590 match (xs.get(key), ys.get(key)) {
591 (Some(x), Some(y)) => diff_values(x, y, &sub, out),
592 (Some(x), None) => out.push(ModelDiff {
593 path: sub,
594 before: DiffSide::Value(x.to_string()),
595 after: DiffSide::Absent,
596 }),
597 (None, Some(y)) => out.push(ModelDiff {
598 path: sub,
599 before: DiffSide::Absent,
600 after: DiffSide::Value(y.to_string()),
601 }),
602 (None, None) => unreachable!(),
603 }
604 }
605 }
606 (x, y) if x == y => {}
607 (x, y) => out.push(ModelDiff {
608 path: path.to_string(),
609 before: DiffSide::Value(x.to_string()),
610 after: DiffSide::Value(y.to_string()),
611 }),
612 }
613}