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
262 .hvdc()
263 .iter()
264 .filter(|d| d.in_service || !in_service_only)
265 {
266 let from = map.entry(d.from.0).or_default();
267 from[0] += d.pf;
268 from[2] += d.qf;
269 let to = map.entry(d.to.0).or_default();
270 to[1] += d.pt;
271 to[3] += d.qt;
272 }
273 map
274}
275
276#[must_use]
291pub fn dc_terminal_change(
292 before: &BalancedNetwork,
293 after: &BalancedNetwork,
294) -> Option<InjectionChange> {
295 let change = first_moved(
296 &per_bus_dc(before, true),
297 &per_bus_dc(after, true),
298 DC_TERMINALS,
299 )?;
300 let status_only = first_moved(
301 &per_bus_dc(before, false),
302 &per_bus_dc(after, false),
303 DC_TERMINALS,
304 )
305 .is_none();
306 Some(InjectionChange {
307 status_only,
308 ..change
309 })
310}
311
312fn first_moved<const N: usize>(
318 a: &BTreeMap<usize, [f64; N]>,
319 b: &BTreeMap<usize, [f64; N]>,
320 quantities: [Injection; N],
321) -> Option<InjectionChange> {
322 for key in a.keys().chain(b.keys().filter(|k| !a.contains_key(*k))) {
323 let x = a.get(key).copied().unwrap_or([0.0; N]);
324 let y = b.get(key).copied().unwrap_or([0.0; N]);
325 for (i, quantity) in quantities.into_iter().enumerate() {
326 if beyond_tol(x[i], y[i], ELECTRICAL_TOL) {
327 return Some(InjectionChange {
328 bus: *key,
329 quantity,
330 before: x[i],
331 after: y[i],
332 status_only: false,
333 });
334 }
335 }
336 }
337 None
338}
339
340fn beyond_tol(x: f64, y: f64, tol: f64) -> bool {
348 if !x.is_finite() || !y.is_finite() {
349 #[allow(clippy::float_cmp)]
354 return !(x.is_nan() && y.is_nan()) && x != y;
355 }
356 (x - y).abs() > tol * x.abs().max(y.abs()).max(1.0)
357}
358
359#[must_use]
368pub fn status_disagreements(a: &BalancedNetwork, b: &BalancedNetwork) -> usize {
369 let off = |net: &BalancedNetwork| {
370 [
371 net.generators().iter().filter(|g| !g.in_service).count(),
372 net.branches().iter().filter(|x| !x.in_service).count(),
373 net.loads().iter().filter(|l| !l.in_service).count(),
374 net.shunts().iter().filter(|s| !s.in_service).count(),
375 net.switches().iter().filter(|x| !x.closed).count(),
376 net.storage().iter().filter(|x| !x.in_service).count(),
377 net.hvdc().iter().filter(|d| !d.in_service).count(),
378 net.transformers_3w()
379 .iter()
380 .filter(|t| !t.in_service)
381 .count(),
382 ]
383 };
384 off(a)
385 .iter()
386 .zip(off(b).iter())
387 .map(|(x, y)| x.abs_diff(*y))
388 .sum()
389}
390
391#[derive(Debug, Clone, PartialEq, Eq)]
393pub struct TransmissionCore {
394 pub buses: usize,
395 pub branches: usize,
396 pub generators: usize,
397 pub loads: usize,
398 pub shunts: usize,
399 pub load_p: i64,
400 pub load_q: i64,
401 pub gen_p: i64,
402 pub base_mva: i64,
403}
404
405fn milli(x: f64) -> i64 {
406 (x * 1e3).round() as i64
407}
408
409#[must_use]
410pub fn transmission_core(net: &BalancedNetwork) -> TransmissionCore {
411 TransmissionCore {
412 buses: net.buses().len(),
413 branches: net.branches().len(),
414 generators: net.generators().len(),
415 loads: net.loads().len(),
416 shunts: net.shunts().len(),
417 load_p: milli(net.loads().iter().map(|load| load.p).sum()),
418 load_q: milli(net.loads().iter().map(|load| load.q).sum()),
419 gen_p: milli(net.generators().iter().map(|generator| generator.pg).sum()),
420 base_mva: milli(net.base_mva()),
421 }
422}
423
424#[derive(Debug, Clone, PartialEq, Eq)]
426pub struct DistributionCore {
427 pub buses: usize,
428 pub loads: usize,
429 pub generators: usize,
430 pub shunts: usize,
431 pub load_p: i64,
432 pub load_q: i64,
433}
434
435#[must_use]
436pub fn distribution_core(net: &MulticonductorNetwork) -> DistributionCore {
437 DistributionCore {
438 buses: net.buses().len(),
439 loads: net.loads().len(),
440 generators: net.generators().len(),
441 shunts: net.shunts().len(),
442 load_p: milli(net.loads().iter().flat_map(|load| load.p_nom.iter()).sum()),
443 load_q: milli(net.loads().iter().flat_map(|load| load.q_nom.iter()).sum()),
444 }
445}
446
447#[must_use]
461pub fn transmission_value(net: &BalancedNetwork) -> serde_json::Value {
462 let mut net = net.clone();
463 *net.name_mut() = String::new();
464 *net.source_format_mut() = powerio_matrix::SourceFormat::Matpower;
465 for br in net.branches_mut() {
466 br.charging = Some(br.terminal_charging());
467 }
468 net.buses_mut().sort_by_key(|b| b.id);
469 net.branches_mut().sort_by_key(|a| (a.from, a.to));
470 net.loads_mut().sort_by_key(|l| l.bus);
471 net.shunts_mut().sort_by_key(|s| s.bus);
472 net.generators_mut().sort_by_key(|g| g.bus);
473 net.storage_mut().sort_by_key(|s| s.bus);
474 net.hvdc_mut().sort_by_key(|d| (d.from, d.to));
475 serde_json::to_value(&net).unwrap()
476}
477
478#[must_use]
484pub fn distribution_value(net: &MulticonductorNetwork) -> serde_json::Value {
485 let mut net = net.clone();
486 *net.name_mut() = None;
487 *net.source_format_mut() = None;
488 serde_json::to_value(&net).unwrap()
489}
490
491#[derive(Debug, Clone, PartialEq)]
493pub struct ModelDiff {
494 pub path: String,
497 pub before: DiffSide,
498 pub after: DiffSide,
499}
500
501#[derive(Debug, Clone, PartialEq)]
502pub enum DiffSide {
503 Number(f64),
504 Value(String),
507 Len(usize),
509 Absent,
510}
511
512impl std::fmt::Display for DiffSide {
513 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
514 match self {
515 Self::Number(x) => write!(f, "{x}"),
516 Self::Value(v) => write!(f, "{v}"),
517 Self::Len(n) => write!(f, "{n} entries"),
518 Self::Absent => write!(f, "absent"),
519 }
520 }
521}
522
523impl std::fmt::Display for ModelDiff {
524 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525 match (&self.before, &self.after) {
528 (DiffSide::Len(x), DiffSide::Len(y)) => {
529 write!(f, "{}: {x} entries -> {y}", self.path)
530 }
531 (before, after) => write!(f, "{}: {before} -> {after}", self.path),
532 }
533 }
534}
535
536#[must_use]
543pub fn model_diffs(before: &serde_json::Value, after: &serde_json::Value) -> Vec<ModelDiff> {
544 let mut out = Vec::new();
545 diff_values(before, after, "", &mut out);
546 out
547}
548
549fn diff_values(a: &serde_json::Value, b: &serde_json::Value, path: &str, out: &mut Vec<ModelDiff>) {
550 use serde_json::Value;
551 if out.len() >= MAX_MODEL_DIFFS {
552 return;
553 }
554 match (a, b) {
555 (Value::Number(x), Value::Number(y)) => {
556 let (x, y) = (
557 x.as_f64().unwrap_or(f64::NAN),
558 y.as_f64().unwrap_or(f64::NAN),
559 );
560 let within = (x - y).abs() <= MODEL_TOL * x.abs().max(y.abs()).max(1.0);
564 if !(within || (x.is_nan() && y.is_nan())) {
565 out.push(ModelDiff {
566 path: path.to_string(),
567 before: DiffSide::Number(x),
568 after: DiffSide::Number(y),
569 });
570 }
571 }
572 (Value::Array(xs), Value::Array(ys)) => {
573 if xs.len() != ys.len() {
574 out.push(ModelDiff {
575 path: path.to_string(),
576 before: DiffSide::Len(xs.len()),
577 after: DiffSide::Len(ys.len()),
578 });
579 return;
580 }
581 for (i, (x, y)) in xs.iter().zip(ys).enumerate() {
582 diff_values(x, y, &format!("{path}[{i}]"), out);
583 }
584 }
585 (Value::Object(xs), Value::Object(ys)) => {
586 for key in xs.keys().chain(ys.keys().filter(|k| !xs.contains_key(*k))) {
587 if out.len() >= MAX_MODEL_DIFFS {
591 return;
592 }
593 let sub = format!("{path}.{key}");
594 match (xs.get(key), ys.get(key)) {
595 (Some(x), Some(y)) => diff_values(x, y, &sub, out),
596 (Some(x), None) => out.push(ModelDiff {
597 path: sub,
598 before: DiffSide::Value(x.to_string()),
599 after: DiffSide::Absent,
600 }),
601 (None, Some(y)) => out.push(ModelDiff {
602 path: sub,
603 before: DiffSide::Absent,
604 after: DiffSide::Value(y.to_string()),
605 }),
606 (None, None) => unreachable!(),
607 }
608 }
609 }
610 (x, y) if x == y => {}
611 (x, y) => out.push(ModelDiff {
612 path: path.to_string(),
613 before: DiffSide::Value(x.to_string()),
614 after: DiffSide::Value(y.to_string()),
615 }),
616 }
617}