1use std::collections::HashSet;
13
14mod subset;
15pub use subset::BusSubset;
16pub(crate) use subset::filter_connectivity;
17use subset::filter_rows;
18
19use serde_json::Value;
20
21use crate::diagnostics::Diagnostic;
22use crate::merge::{BusMergeRule, MergeSurvivor, ZeroImpedanceRule, kind_priority};
23use crate::network::{
24 BalancedNetwork, BalancedNetworkTables, Branch, Bus, BusId, BusType, Extras,
25 IMPEDANCE_CORRECTION_APPLIED_EXTRA, IMPEDANCE_CORRECTION_NOMINAL_EXTRA,
26 ImpedanceCorrectionVariable, SourceFormat, current_correction_nominal,
27};
28
29fn recorded_nominal<T>(extras: &mut Extras, current: T) -> T
33where
34 T: PartialEq + serde::Serialize + serde::de::DeserializeOwned,
35{
36 if let Some(nominal) = current_correction_nominal(extras, ¤t) {
37 return nominal;
38 }
39 let value = serde_json::to_value(¤t).expect("an impedance serializes to JSON");
40 extras.insert(IMPEDANCE_CORRECTION_NOMINAL_EXTRA.to_owned(), value);
41 current
42}
43
44fn record_applied<T: serde::Serialize>(extras: &mut Extras, applied: &T) {
47 let value = serde_json::to_value(applied).expect("an impedance serializes to JSON");
48 extras.insert(IMPEDANCE_CORRECTION_APPLIED_EXTRA.to_owned(), value);
49}
50
51fn complex_product(a: (f64, f64), b: (f64, f64)) -> (f64, f64) {
53 (a.0 * b.0 - a.1 * b.1, a.0 * b.1 + a.1 * b.0)
54}
55
56fn other_end(b: &Branch, m: BusId) -> BusId {
58 if b.from == m { b.to } else { b.from }
59}
60
61fn combine_rate(a: f64, b: f64) -> f64 {
65 match (a == 0.0, b == 0.0) {
66 (true, _) => b,
67 (_, true) => a,
68 _ => a.min(b),
69 }
70}
71
72#[derive(Debug, Clone, Default)]
76#[non_exhaustive]
77pub struct ZeroImpedanceReduction {
78 pub removed: usize,
81 pub diagnostics: Vec<Diagnostic>,
83}
84
85#[derive(Debug, Clone, Default, PartialEq)]
89pub struct Selector {
90 pub area: Option<(usize, usize)>,
92 pub zone: Option<(usize, usize)>,
94 pub base_kv: Option<(f64, f64)>,
96 pub bus: Option<(usize, usize)>,
98}
99
100impl Selector {
101 fn matches(&self, bus: &Bus) -> bool {
103 fn in_usize(range: Option<(usize, usize)>, v: usize) -> bool {
104 range.is_none_or(|(lo, hi)| lo <= v && v <= hi)
105 }
106 fn in_f64(range: Option<(f64, f64)>, v: f64) -> bool {
107 range.is_none_or(|(lo, hi)| lo <= v && v <= hi)
108 }
109 in_usize(self.area, bus.area)
110 && in_usize(self.zone, bus.zone)
111 && in_f64(self.base_kv, bus.base_kv)
112 && in_usize(self.bus, bus.id.0)
113 }
114}
115
116impl BalancedNetwork {
117 #[must_use]
136 pub fn subset(&self, sel: &Selector, keep_boundary: bool) -> BalancedNetwork {
137 let in_scope: HashSet<BusId> = self
138 .buses()
139 .iter()
140 .filter(|b| sel.matches(b))
141 .map(|b| b.id)
142 .collect();
143 self.subset_scope(&in_scope, keep_boundary, &HashSet::new())
144 .network
145 }
146
147 #[must_use]
162 pub fn subset_buses(&self, buses: &std::collections::BTreeSet<BusId>) -> BusSubset {
163 let in_scope: HashSet<BusId> = buses.iter().copied().collect();
164 let isolated: HashSet<BusId> = self
165 .buses()
166 .iter()
167 .filter(|bus| bus.kind == BusType::Isolated)
168 .map(|bus| bus.id)
169 .collect();
170 let mut stubs = HashSet::new();
171 for transformer in self.transformers_3w().iter().filter(|t| t.in_service) {
172 let windings = || {
173 transformer
174 .windings
175 .iter()
176 .zip(transformer.winding_in_service())
177 };
178 let touches = windings().any(|(w, _)| in_scope.contains(&w.bus));
179 let dead_outside = windings().all(|(w, connected)| {
180 in_scope.contains(&w.bus) || !connected || isolated.contains(&w.bus)
181 });
182 if touches && dead_outside {
183 stubs.extend(
184 windings()
185 .map(|(w, _)| w.bus)
186 .filter(|bus| !in_scope.contains(bus)),
187 );
188 }
189 }
190 self.subset_scope(&in_scope, false, &stubs)
191 }
192
193 #[expect(clippy::too_many_lines)]
198 fn subset_scope(
199 &self,
200 in_scope: &HashSet<BusId>,
201 keep_boundary: bool,
202 stubs: &HashSet<BusId>,
203 ) -> BusSubset {
204 let mut boundary: HashSet<BusId> = stubs.clone();
207 if keep_boundary {
208 let mut edge = |a: BusId, b: BusId| match (in_scope.contains(&a), in_scope.contains(&b))
209 {
210 (true, false) => {
211 boundary.insert(b);
212 }
213 (false, true) => {
214 boundary.insert(a);
215 }
216 _ => {}
217 };
218 for br in self.branches() {
219 edge(br.from, br.to);
220 }
221 for d in self.hvdc() {
222 edge(d.from, d.to);
223 }
224 }
225 let kept: HashSet<BusId> = in_scope.union(&boundary).copied().collect();
226
227 let (mut buses, bus_rows) = filter_rows(self.buses(), |b| kept.contains(&b.id));
228 for bus in &mut buses {
229 if boundary.contains(&bus.id) {
230 bus.extras.insert("tie_bus".into(), Value::Bool(true));
231 }
232 if stubs.contains(&bus.id) {
233 bus.kind = BusType::Isolated;
234 }
235 }
236
237 let (loads, load_rows) = filter_rows(self.loads(), |v| in_scope.contains(&v.bus));
240 let (mut shunts, shunt_rows) = filter_rows(self.shunts(), |v| in_scope.contains(&v.bus));
241 let (static_var_compensators, static_var_compensator_rows) =
242 filter_rows(self.static_var_compensators(), |v| {
243 in_scope.contains(&v.bus)
244 });
245 let (mut generators, generator_rows) =
246 filter_rows(self.generators(), |v| in_scope.contains(&v.bus));
247 let (storage, storage_rows) = filter_rows(self.storage(), |v| in_scope.contains(&v.bus));
248 let ends_kept = |from: BusId, to: BusId| kept.contains(&from) && kept.contains(&to);
249 let (mut branches, branch_rows) = filter_rows(self.branches(), |v| ends_kept(v.from, v.to));
250 let (switches, switch_rows) = filter_rows(self.switches(), |v| ends_kept(v.from, v.to));
251 let (hvdc, hvdc_rows) = filter_rows(self.hvdc(), |v| ends_kept(v.from, v.to));
252 let (mut transformers_3w, transformer_3w_rows) = filter_rows(self.transformers_3w(), |v| {
253 v.windings.iter().all(|w| kept.contains(&w.bus))
254 });
255
256 for br in &mut branches {
258 if let Some(c) = &mut br.control
259 && c.controlled_bus.is_some_and(|b| !kept.contains(&b))
260 {
261 c.controlled_bus = None;
262 }
263 }
264 for transformer in &mut transformers_3w {
265 for winding in &mut transformer.windings {
266 if let Some(control) = &mut winding.control
267 && control
268 .controlled_bus
269 .is_some_and(|bus| !kept.contains(&bus))
270 {
271 control.controlled_bus = None;
272 }
273 }
274 }
275 for sh in &mut shunts {
276 if let Some(c) = &mut sh.control
277 && c.control_bus.is_some_and(|b| !kept.contains(&b))
278 {
279 c.control_bus = None;
280 }
281 }
282 for g in &mut generators {
283 if g.regulated_bus.is_some_and(|b| !kept.contains(&b)) {
284 g.regulated_bus = None;
285 }
286 }
287
288 let kept_area_numbers: HashSet<usize> = buses.iter().map(|b| b.area).collect();
293 let (mut areas, area_rows) =
294 filter_rows(self.areas(), |a| kept_area_numbers.contains(&a.number));
295 for area in &mut areas {
296 if area.slack_bus.is_some_and(|b| !kept.contains(&b)) {
297 area.slack_bus = None;
298 }
299 }
300
301 let mut net = BalancedNetwork::from_tables(BalancedNetworkTables {
302 name: format!("{} (subset)", self.name()),
303 base_mva: self.base_mva(),
304 base_frequency: self.base_frequency(),
305 geo: self.geo().clone(),
306 case_metadata: self.case_metadata().clone(),
307 detailed_connectivity: self.detailed_connectivity().clone(),
308 generated_uids: self.generated_uids().clone(),
309 buses: buses.into(),
310 loads: loads.into(),
311 shunts: shunts.into(),
312 static_var_compensators: static_var_compensators.into(),
313 branches: branches.into(),
314 switches: switches.into(),
315 generators: generators.into(),
316 storage: storage.into(),
317 hvdc: hvdc.into(),
318 transformers_3w: transformers_3w.into(),
319 areas: areas.into(),
320 solver: self.solver().clone(),
321 source_format: if self.is_normalized() {
322 SourceFormat::Normalized
323 } else {
324 SourceFormat::InMemory
325 },
326 });
327 subset::filter_connectivity(self, &mut net);
328 debug_assert!(
329 net.validate().is_ok(),
330 "subset produced a dangling reference"
331 );
332 BusSubset {
333 network: net,
334 bus_rows,
335 load_rows,
336 shunt_rows,
337 static_var_compensator_rows,
338 branch_rows,
339 switch_rows,
340 generator_rows,
341 storage_rows,
342 hvdc_rows,
343 transformer_3w_rows,
344 area_rows,
345 }
346 }
347
348 pub fn merge_bus(&mut self, into: BusId, from: BusId) {
360 if into == from {
361 return;
362 }
363 self.remap_buses(|bus| if bus == from { into } else { bus });
364 self.branches_mut().retain(|b| b.from != b.to);
365 self.switches_mut().retain(|s| s.from != s.to);
366 self.hvdc_mut().retain(|d| d.from != d.to);
367
368 let from_kind = self.buses().iter().find(|b| b.id == from).map(|b| b.kind);
370 self.buses_mut().retain(|b| b.id != from);
371 if let (Some(fk), Some(into_bus)) = (
372 from_kind,
373 self.buses_mut().iter_mut().find(|b| b.id == into),
374 ) && kind_priority(fk) > kind_priority(into_bus.kind)
375 {
376 into_bus.kind = fk;
377 }
378 }
380
381 pub(crate) fn remap_buses(&mut self, map: impl Fn(BusId) -> BusId) {
389 fn remap_table<T: Clone>(
390 table: &mut std::sync::Arc<Vec<T>>,
391 touches: impl Fn(&T) -> bool,
392 apply: impl FnMut(&mut T),
393 ) {
394 if table.iter().any(touches) {
395 std::sync::Arc::make_mut(table).iter_mut().for_each(apply);
396 }
397 }
398 let moved = |bus: BusId| map(bus) != bus;
399 let moved_opt = |bus: Option<BusId>| bus.is_some_and(moved);
400 let set = |bus: &mut BusId| *bus = map(*bus);
401 let set_opt = |bus: &mut Option<BusId>| {
402 if let Some(bus) = bus {
403 *bus = map(*bus);
404 }
405 };
406 let tables = self.tables_mut();
407 remap_table(&mut tables.loads, |l| moved(l.bus), |l| set(&mut l.bus));
408 remap_table(
409 &mut tables.shunts,
410 |s| moved(s.bus) || moved_opt(s.control.as_ref().and_then(|c| c.control_bus)),
411 |s| {
412 set(&mut s.bus);
413 if let Some(control) = s.control.as_mut() {
414 set_opt(&mut control.control_bus);
415 }
416 },
417 );
418 remap_table(
419 &mut tables.static_var_compensators,
420 |svc| moved(svc.bus),
421 |svc| set(&mut svc.bus),
422 );
423 remap_table(
424 &mut tables.generators,
425 |g| moved(g.bus) || moved_opt(g.regulated_bus),
426 |g| {
427 set(&mut g.bus);
428 set_opt(&mut g.regulated_bus);
429 },
430 );
431 remap_table(&mut tables.storage, |s| moved(s.bus), |s| set(&mut s.bus));
432 remap_table(
433 &mut tables.branches,
434 |b| {
435 moved(b.from)
436 || moved(b.to)
437 || moved_opt(b.control.as_ref().and_then(|c| c.controlled_bus))
438 },
439 |b| {
440 set(&mut b.from);
441 set(&mut b.to);
442 if let Some(control) = b.control.as_mut() {
443 set_opt(&mut control.controlled_bus);
444 }
445 },
446 );
447 remap_table(
448 &mut tables.switches,
449 |s| moved(s.from) || moved(s.to),
450 |s| {
451 set(&mut s.from);
452 set(&mut s.to);
453 },
454 );
455 remap_table(
456 &mut tables.hvdc,
457 |d| moved(d.from) || moved(d.to),
458 |d| {
459 set(&mut d.from);
460 set(&mut d.to);
461 },
462 );
463 remap_table(
464 &mut tables.transformers_3w,
465 |t| {
466 t.windings.iter().any(|w| {
467 moved(w.bus) || moved_opt(w.control.as_ref().and_then(|c| c.controlled_bus))
468 })
469 },
470 |t| {
471 for w in &mut t.windings {
472 set(&mut w.bus);
473 if let Some(control) = w.control.as_mut() {
474 set_opt(&mut control.controlled_bus);
475 }
476 }
477 },
478 );
479 remap_table(
480 &mut tables.areas,
481 |a| moved_opt(a.slack_bus),
482 |a| set_opt(&mut a.slack_bus),
483 );
484 }
485
486 pub fn reduce_zero_impedance(&mut self, threshold: f64) -> usize {
508 self.reduce_zero_impedance_with_report(threshold).removed
509 }
510
511 pub fn reduce_zero_impedance_with_report(&mut self, threshold: f64) -> ZeroImpedanceReduction {
517 let rule = BusMergeRule::new(
518 false,
519 Some(ZeroImpedanceRule::ImpedanceMagnitude(threshold)),
520 )
521 .with_survivor(MergeSurvivor::FromBus);
522 match self.merge_buses(&rule) {
523 Ok(merge) => {
524 let removed = merge.removed_branches.len();
525 *self = merge.network;
526 ZeroImpedanceReduction {
527 removed,
528 diagnostics: merge.diagnostics,
529 }
530 }
531 Err(error) => ZeroImpedanceReduction {
532 removed: 0,
533 diagnostics: vec![Diagnostic::of(
534 error.code(),
535 format!("zero impedance reduction merged nothing: {error}"),
536 )],
537 },
538 }
539 }
540
541 pub fn reduce_passthrough_buses(&mut self) -> usize {
557 let mut collapsed = 0;
558 while let Some(mid) = self
561 .buses()
562 .iter()
563 .map(|b| b.id)
564 .find(|&m| self.is_passthrough(m))
565 {
566 self.collapse_passthrough(mid);
567 collapsed += 1;
568 }
569 collapsed
570 }
571
572 fn is_passthrough(&self, m: BusId) -> bool {
575 let Some(bus) = self.buses().iter().find(|b| b.id == m) else {
576 return false;
577 };
578 if bus.kind == BusType::Ref {
579 return false;
580 }
581 if self.loads().iter().any(|l| l.bus == m)
582 || self.generators().iter().any(|g| g.bus == m)
583 || self.shunts().iter().any(|s| s.bus == m)
584 || self
585 .static_var_compensators()
586 .iter()
587 .any(|svc| svc.bus == m)
588 || self.storage().iter().any(|s| s.bus == m)
589 || self.hvdc().iter().any(|d| d.from == m || d.to == m)
590 {
591 return false;
592 }
593 if self
594 .transformers_3w()
595 .iter()
596 .any(|t| t.windings.iter().any(|w| w.bus == m))
597 {
598 return false;
599 }
600 if self.areas().iter().any(|a| a.slack_bus == Some(m)) {
601 return false;
602 }
603 let controlled = self
604 .branches()
605 .iter()
606 .any(|b| b.control.as_ref().and_then(|c| c.controlled_bus) == Some(m));
607 let winding_controlled = self.transformers_3w().iter().any(|transformer| {
608 transformer.windings.iter().any(|winding| {
609 winding
610 .control
611 .as_ref()
612 .and_then(|control| control.controlled_bus)
613 == Some(m)
614 })
615 });
616 let regulated = self
617 .shunts()
618 .iter()
619 .any(|s| s.control.as_ref().and_then(|c| c.control_bus) == Some(m));
620 let gen_regulated = self.generators().iter().any(|g| g.regulated_bus == Some(m));
621 if controlled || winding_controlled || regulated || gen_regulated {
622 return false;
623 }
624 let incident: Vec<&Branch> = self
625 .branches()
626 .iter()
627 .filter(|b| b.from == m || b.to == m)
628 .collect();
629 if incident.len() != 2 {
630 return false;
631 }
632 let a = other_end(incident[0], m);
633 let c = other_end(incident[1], m);
634 incident.iter().all(|b| !b.is_transformer() && b.in_service) && a != m && c != m && a != c
635 }
636
637 fn collapse_passthrough(&mut self, m: BusId) {
640 let mut sections: Vec<Branch> = Vec::new();
641 self.branches_mut().retain(|b| {
642 if b.from == m || b.to == m {
643 sections.push(b.clone());
644 false
645 } else {
646 true
647 }
648 });
649 debug_assert_eq!(sections.len(), 2, "passthrough bus must have two sections");
650 let (s1, s2) = (§ions[0], §ions[1]);
651 let mut angmin = s1.angmin.max(s2.angmin);
658 let mut angmax = s1.angmax.min(s2.angmax);
659 if angmin > angmax {
660 angmin = s1.angmin.min(s2.angmin);
661 angmax = s1.angmax.max(s2.angmax);
662 }
663 self.branches_mut().push(Branch {
664 name: None,
665 from: other_end(s1, m),
666 to: other_end(s2, m),
667 r: s1.r + s2.r,
668 x: s1.x + s2.x,
669 b: s1.calc_total_charging_b() + s2.calc_total_charging_b(),
670 charging: None,
671 rate_a: combine_rate(s1.rate_a, s2.rate_a),
672 rate_b: combine_rate(s1.rate_b, s2.rate_b),
673 rate_c: combine_rate(s1.rate_c, s2.rate_c),
674 rating_sets: Vec::new(),
675 current_ratings: None,
676 tap: 0.0,
677 shift: 0.0,
678 in_service: true,
679 angmin,
680 angmax,
681 control: None,
682 solution: None,
683 uid: None,
684 route: None,
685 extras: Extras::new(),
686 });
687 self.buses_mut().retain(|b| b.id != m);
688 }
690
691 pub fn apply_impedance_correction(&mut self) -> usize {
713 let angle_scale = if self.is_normalized() {
716 crate::normalize::RAD_TO_DEG
717 } else {
718 1.0
719 };
720 let mut scaled = 0;
721 for branch in self.branches_mut().iter_mut() {
722 let Some(correction) = branch.impedance_correction() else {
723 continue;
724 };
725 let at = match correction.variable {
726 ImpedanceCorrectionVariable::Angle => branch.shift * angle_scale,
727 _ => branch.calc_effective_tap(),
728 };
729 let Some(factor) = correction.calc_factor(at) else {
730 continue;
731 };
732 let [r, x] = recorded_nominal(&mut branch.extras, [branch.r, branch.x]);
733 (branch.r, branch.x) = complex_product((r, x), factor);
734 record_applied(&mut branch.extras, &[branch.r, branch.x]);
735 scaled += 1;
736 }
737 for transformer in self.transformers_3w_mut().iter_mut() {
738 let pairwise_mode =
739 transformer.extras.get("psse_zcod").and_then(Value::as_i64) == Some(1);
740 let controlling = transformer.impedance_correction_control_winding();
741 if pairwise_mode && controlling.is_none() {
745 continue;
746 }
747 let factors: [Option<(f64, f64)>; 3] = std::array::from_fn(|winding| {
748 let correction = transformer.winding_impedance_correction(winding)?;
749 let setting = if pairwise_mode {
750 controlling.unwrap_or(winding)
751 } else {
752 winding
753 };
754 let w = &transformer.windings[setting];
755 let at = match correction.variable {
756 ImpedanceCorrectionVariable::Angle => w.shift * angle_scale,
757 _ => w.tap,
758 };
759 correction.calc_factor(at)
760 });
761 if factors.iter().all(Option::is_none) {
762 continue;
763 }
764 let pairwise = transformer.z.map(|z| [z.r, z.x]);
765 let nominal = recorded_nominal(&mut transformer.extras, pairwise);
766 if pairwise_mode {
767 for ((z, [r, x]), factor) in transformer.z.iter_mut().zip(nominal).zip(factors) {
768 (z.r, z.x) = if let Some(factor) = factor {
769 scaled += 1;
770 complex_product((r, x), factor)
771 } else {
772 (r, x)
773 };
774 }
775 let applied = transformer.z.map(|z| [z.r, z.x]);
776 record_applied(&mut transformer.extras, &applied);
777 continue;
778 }
779 let [z12, z23, z31] = nominal;
780 let star = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
781 ((a[0] + b[0] - c[0]) / 2.0, (a[1] + b[1] - c[1]) / 2.0)
782 };
783 let mut stars = [
784 star(z12, z31, z23),
785 star(z12, z23, z31),
786 star(z23, z31, z12),
787 ];
788 for (star, factor) in stars.iter_mut().zip(factors) {
789 if let Some(factor) = factor {
790 *star = complex_product(*star, factor);
791 scaled += 1;
792 }
793 }
794 for (pair, (a, b)) in [(0, 1), (1, 2), (2, 0)].into_iter().enumerate() {
795 transformer.z[pair].r = stars[a].0 + stars[b].0;
796 transformer.z[pair].x = stars[a].1 + stars[b].1;
797 }
798 let applied = transformer.z.map(|z| [z.r, z.x]);
799 record_applied(&mut transformer.extras, &applied);
800 }
801 scaled
802 }
803
804 pub fn retype_isolated_buses(&mut self) -> usize {
816 let mut connected: HashSet<BusId> = HashSet::new();
817 for br in self.branches().iter().filter(|b| b.in_service) {
818 connected.insert(br.from);
819 connected.insert(br.to);
820 }
821 for sw in self.switches().iter().filter(|sw| sw.closed) {
822 connected.insert(sw.from);
823 connected.insert(sw.to);
824 }
825 for d in self.hvdc().iter().filter(|d| d.in_service) {
826 connected.insert(d.from);
827 connected.insert(d.to);
828 }
829 for t in self.transformers_3w().iter().filter(|t| t.in_service) {
830 for (w, in_service) in t.windings.iter().zip(t.winding_in_service()) {
831 if in_service {
832 connected.insert(w.bus);
833 }
834 }
835 }
836 let mut retyped = 0;
837 for b in self.buses_mut() {
838 if b.kind != BusType::Isolated && !connected.contains(&b.id) {
839 b.kind = BusType::Isolated;
840 retyped += 1;
841 }
842 }
843 retyped
846 }
847}
848
849#[cfg(test)]
850mod tests {
851 use super::*;
852 use crate::network::{
853 Area, BusType, Extras, Generator, GeneratorEnergySource, Impedance, Load, Transformer3W,
854 Winding,
855 };
856
857 fn bus(id: usize, area: usize, base_kv: f64) -> Bus {
858 Bus {
859 id: BusId(id),
860 kind: BusType::Pq,
861 vm: 1.0,
862 va: 0.0,
863 base_kv,
864 vmax: 1.1,
865 vmin: 0.9,
866 evhi: None,
867 evlo: None,
868 area,
869 zone: 1,
870 name: None,
871 uid: None,
872 location: None,
873 extras: Extras::new(),
874 }
875 }
876
877 fn line(from: usize, to: usize) -> Branch {
878 Branch {
879 name: None,
880 from: BusId(from),
881 to: BusId(to),
882 r: 0.0,
883 x: 0.1,
884 b: 0.0,
885 charging: None,
886 rate_a: 0.0,
887 rate_b: 0.0,
888 rate_c: 0.0,
889 rating_sets: Vec::new(),
890 current_ratings: None,
891 tap: 0.0,
892 shift: 0.0,
893 in_service: true,
894 angmin: -360.0,
895 angmax: 360.0,
896 control: None,
897 solution: None,
898 uid: None,
899 route: None,
900 extras: Extras::new(),
901 }
902 }
903
904 fn load(bus: usize) -> Load {
905 Load {
906 bus: BusId(bus),
907 p: 10.0,
908 q: 5.0,
909 voltage_model: None,
910 in_service: true,
911 uid: None,
912 extras: Extras::new(),
913 }
914 }
915
916 fn two_area_net() -> BalancedNetwork {
919 let mut net = BalancedNetwork::in_memory(
920 "net",
921 100.0,
922 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 2, 230.0)],
923 vec![line(1, 2), line(2, 3)],
924 );
925 net.loads_mut().push(load(1));
926 net.loads_mut().push(load(3));
927 net
928 }
929
930 fn transformer_3w(a: usize, b: usize, c: usize) -> Transformer3W {
931 let winding = |bus| Winding {
932 bus: BusId(bus),
933 tap: 1.0,
934 shift: 0.0,
935 nominal_kv: 0.0,
936 rate_a: 0.0,
937 rate_b: 0.0,
938 rate_c: 0.0,
939 control: None,
940 };
941 let imp = Impedance {
942 r: 0.0,
943 x: 0.1,
944 base_mva: 100.0,
945 };
946 Transformer3W {
947 windings: [winding(a), winding(b), winding(c)],
948 z: [imp, imp, imp],
949 star_vm: 1.0,
950 star_va: 0.0,
951 mag_g: 0.0,
952 mag_b: 0.0,
953 in_service: true,
954 name: None,
955 uid: None,
956 extras: Extras::new(),
957 }
958 }
959
960 fn gen_regulating(bus: usize, regulated: usize) -> Generator {
961 Generator {
962 bus: BusId(bus),
963 energy_source: GeneratorEnergySource::default(),
964 pg: 10.0,
965 qg: 0.0,
966 pmax: 100.0,
967 pmin: 0.0,
968 qmax: 50.0,
969 qmin: -50.0,
970 vg: 1.0,
971 mbase: 100.0,
972 in_service: true,
973 cost: None,
974 caps: Default::default(),
975 voltage_regulation_on: true,
976 regulating_terminal: None,
977 regulated_bus: Some(BusId(regulated)),
978 active_power_control: None,
979 uid: None,
980 }
981 }
982
983 #[test]
984 fn subset_clears_a_regulated_bus_outside_the_kept_set() {
985 let mut net = two_area_net();
987 net.generators_mut().push(gen_regulating(1, 3));
988 let sel = Selector {
989 area: Some((1, 1)),
990 ..Selector::default()
991 };
992 let sub = net.subset(&sel, false);
993 assert_eq!(sub.generators().len(), 1);
994 assert_eq!(
995 sub.generators()[0].regulated_bus,
996 None,
997 "the dropped remote regulated bus is cleared, not left dangling"
998 );
999 sub.validate().unwrap();
1000 }
1001
1002 #[test]
1003 fn merge_bus_remaps_regulated_bus_and_area_slack() {
1004 let mut net = two_area_net();
1005 net.generators_mut().push(gen_regulating(1, 3)); net.areas_mut().push(Area {
1007 number: 1,
1008 slack_bus: Some(BusId(3)),
1009 net_interchange: 0.0,
1010 tolerance: 0.0,
1011 name: None,
1012 uid: None,
1013 area_type: None,
1014 });
1015 net.merge_bus(BusId(2), BusId(3)); assert_eq!(
1017 net.generators()[0].regulated_bus,
1018 Some(BusId(2)),
1019 "the regulated bus follows the merge"
1020 );
1021 assert_eq!(
1022 net.areas()[0].slack_bus,
1023 Some(BusId(2)),
1024 "the area swing follows the merge"
1025 );
1026 net.validate().unwrap();
1027 }
1028
1029 #[test]
1030 fn reduce_passthrough_keeps_a_generator_regulated_bus() {
1031 let mut net = BalancedNetwork::in_memory(
1034 "net",
1035 100.0,
1036 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1037 vec![line(1, 2), line(2, 3)],
1038 );
1039 net.generators_mut().push(gen_regulating(1, 2));
1040 assert_eq!(net.reduce_passthrough_buses(), 0);
1041 assert_eq!(net.buses().len(), 3);
1042 net.validate().unwrap();
1043 }
1044
1045 #[test]
1046 fn subset_by_area_drops_out_of_scope_buses_and_cut_branches() {
1047 let net = two_area_net();
1048 let sel = Selector {
1049 area: Some((1, 1)),
1050 ..Selector::default()
1051 };
1052 let sub = net.subset(&sel, false);
1053
1054 assert_eq!(sub.buses().len(), 2);
1056 assert!(sub.buses().iter().all(|b| b.area == 1));
1057 assert_eq!(sub.branches().len(), 1, "only the intra-area line survives");
1058 assert_eq!(sub.loads().len(), 1, "the area-2 load is dropped");
1059 sub.validate().unwrap();
1060 }
1061
1062 #[test]
1063 fn subset_keep_boundary_pulls_in_the_tie_bus() {
1064 let net = two_area_net();
1065 let sel = Selector {
1066 area: Some((1, 1)),
1067 ..Selector::default()
1068 };
1069 let sub = net.subset(&sel, true);
1070
1071 assert_eq!(sub.buses().len(), 3);
1073 assert_eq!(sub.branches().len(), 2);
1074 let tie = sub.buses().iter().find(|b| b.id == BusId(3)).unwrap();
1075 assert_eq!(tie.extras.get("tie_bus"), Some(&Value::Bool(true)));
1076 assert_eq!(sub.loads().len(), 1);
1078 sub.validate().unwrap();
1079 }
1080
1081 #[test]
1082 fn empty_selector_keeps_everything() {
1083 let net = two_area_net();
1084 let sub = net.subset(&Selector::default(), false);
1085 assert_eq!(sub.buses().len(), net.buses().len());
1086 assert_eq!(sub.branches().len(), net.branches().len());
1087 }
1088
1089 #[test]
1090 fn base_kv_range_filters_by_voltage() {
1091 let mut net = two_area_net();
1092 net.buses_mut()[2].base_kv = 115.0; let sel = Selector {
1094 base_kv: Some((200.0, 300.0)),
1095 ..Selector::default()
1096 };
1097 let sub = net.subset(&sel, false);
1098 assert_eq!(sub.buses().len(), 2, "only the 230 kV buses match");
1099 }
1100
1101 #[test]
1102 fn merge_bus_rehomes_elements_and_drops_the_connecting_branch() {
1103 let mut net = two_area_net(); net.merge_bus(BusId(2), BusId(3));
1105
1106 assert_eq!(net.buses().len(), 2, "bus 3 removed");
1107 assert!(net.buses().iter().all(|b| b.id != BusId(3)));
1108 assert_eq!(
1109 net.branches().len(),
1110 1,
1111 "the 2-3 line collapsed to a self-loop"
1112 );
1113 assert_eq!(net.branches()[0].from, BusId(1));
1114 assert_eq!(net.branches()[0].to, BusId(2));
1115 assert_eq!(net.loads().len(), 2);
1117 assert!(net.loads().iter().any(|l| l.bus == BusId(2)));
1118 net.validate().unwrap();
1119 }
1120
1121 #[test]
1122 fn merge_bus_keeps_the_stronger_bus_kind() {
1123 let mut net = two_area_net();
1124 net.buses_mut()[2].kind = BusType::Ref; net.merge_bus(BusId(2), BusId(3)); let two = net.buses().iter().find(|b| b.id == BusId(2)).unwrap();
1127 assert_eq!(two.kind, BusType::Ref, "the slack designation is not lost");
1128 }
1129
1130 #[test]
1131 fn reduce_passthrough_folds_a_multi_section_line() {
1132 let mut s1 = line(1, 2);
1135 s1.rate_a = 100.0;
1136 let mut s2 = line(2, 3);
1137 s2.rate_a = 80.0;
1138 let s3 = line(3, 4); let mut net = BalancedNetwork::in_memory(
1140 "net",
1141 100.0,
1142 vec![
1143 bus(1, 1, 230.0),
1144 bus(2, 1, 230.0),
1145 bus(3, 1, 230.0),
1146 bus(4, 1, 230.0),
1147 ],
1148 vec![s1, s2, s3],
1149 );
1150
1151 let removed = net.reduce_passthrough_buses();
1152 assert_eq!(removed, 2, "both dummy buses collapse");
1153 assert_eq!(net.buses().len(), 2);
1154 assert!(
1155 net.buses()
1156 .iter()
1157 .all(|b| b.id == BusId(1) || b.id == BusId(4))
1158 );
1159 assert_eq!(net.branches().len(), 1, "one equivalent branch");
1160 let eq = &net.branches()[0];
1161 assert_eq!(
1162 [eq.from, eq.to].iter().copied().collect::<HashSet<_>>(),
1163 [BusId(1), BusId(4)].into_iter().collect::<HashSet<_>>(),
1164 );
1165 assert!((eq.x - 0.3).abs() < 1e-9, "series reactance sums");
1166 assert!(
1167 (eq.rate_a - 80.0).abs() < 1e-9,
1168 "the more limiting finite rating wins"
1169 );
1170 net.validate().unwrap();
1171 }
1172
1173 #[test]
1174 fn reduce_passthrough_keeps_a_bus_with_injection() {
1175 let mut net = BalancedNetwork::in_memory(
1177 "net",
1178 100.0,
1179 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1180 vec![line(1, 2), line(2, 3)],
1181 );
1182 net.loads_mut().push(load(2));
1183 assert_eq!(net.reduce_passthrough_buses(), 0);
1184 assert_eq!(net.buses().len(), 3);
1185 }
1186
1187 #[test]
1188 fn reduce_passthrough_does_not_fold_across_a_transformer() {
1189 let mut xfmr = line(2, 3);
1191 xfmr.tap = 1.0;
1192 let mut net = BalancedNetwork::in_memory(
1193 "net",
1194 100.0,
1195 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1196 vec![line(1, 2), xfmr],
1197 );
1198 assert_eq!(net.reduce_passthrough_buses(), 0);
1199 assert_eq!(net.buses().len(), 3);
1200 }
1201
1202 #[test]
1203 fn retype_isolated_marks_stranded_buses() {
1204 let mut net = BalancedNetwork::in_memory(
1206 "net",
1207 100.0,
1208 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1209 vec![line(1, 2)],
1210 );
1211 assert_eq!(net.retype_isolated_buses(), 1);
1212 let three = net.buses().iter().find(|b| b.id == BusId(3)).unwrap();
1213 assert_eq!(three.kind, BusType::Isolated);
1214 let one = net.buses().iter().find(|b| b.id == BusId(1)).unwrap();
1216 assert_eq!(one.kind, BusType::Pq);
1217 net.validate().unwrap();
1218 }
1219
1220 #[test]
1221 fn retype_isolated_judges_in_service_equipment_only() {
1222 let mut br = line(1, 2);
1224 br.in_service = false;
1225 let mut net = BalancedNetwork::in_memory(
1226 "net",
1227 100.0,
1228 vec![bus(1, 1, 230.0), bus(2, 1, 230.0)],
1229 vec![br],
1230 );
1231 assert_eq!(net.retype_isolated_buses(), 2);
1232 assert!(net.buses().iter().all(|b| b.kind == BusType::Isolated));
1233 }
1234
1235 #[test]
1236 fn retype_isolated_counts_a_closed_switch_as_a_connection() {
1237 let mut net = BalancedNetwork::in_memory(
1240 "net",
1241 100.0,
1242 vec![
1243 bus(1, 1, 230.0),
1244 bus(2, 1, 230.0),
1245 bus(3, 1, 230.0),
1246 bus(4, 1, 230.0),
1247 ],
1248 vec![line(1, 2)],
1249 );
1250 net.switches_mut().extend([
1251 crate::network::Switch::new(BusId(2), BusId(3), true),
1252 crate::network::Switch::new(BusId(2), BusId(4), false),
1253 ]);
1254 assert_eq!(net.retype_isolated_buses(), 1);
1255 let kind = |id| net.buses().iter().find(|b| b.id == BusId(id)).unwrap().kind;
1256 assert_eq!(kind(3), BusType::Pq);
1257 assert_eq!(kind(4), BusType::Isolated);
1258 }
1259
1260 #[test]
1261 fn retype_isolated_is_idempotent() {
1262 let mut net = BalancedNetwork::in_memory(
1263 "net",
1264 100.0,
1265 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1266 vec![line(1, 2)],
1267 );
1268 assert_eq!(net.retype_isolated_buses(), 1);
1269 assert_eq!(net.retype_isolated_buses(), 0, "second pass is a no-op");
1270 }
1271
1272 #[test]
1273 fn reduce_zero_impedance_collapses_jumpers_only() {
1274 let mut jumper = line(2, 3);
1276 jumper.x = 0.0;
1277 let mut net = BalancedNetwork::in_memory(
1278 "net",
1279 100.0,
1280 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1281 vec![line(1, 2), jumper],
1282 );
1283 net.loads_mut().push(load(3));
1284
1285 let removed = net.reduce_zero_impedance(1e-9);
1286 assert_eq!(removed, 1, "only the jumper is collapsed");
1287 assert_eq!(net.buses().len(), 2);
1288 assert_eq!(net.branches().len(), 1, "the real 1-2 line remains");
1289 assert!(
1290 net.loads().iter().any(|l| l.bus == BusId(2)),
1291 "load re-homed"
1292 );
1293 net.validate().unwrap();
1294 }
1295
1296 #[test]
1297 fn reduce_zero_impedance_keeps_the_from_bus() {
1298 let mut jumper = line(3, 2);
1301 jumper.x = 0.0;
1302 let mut net = BalancedNetwork::in_memory(
1303 "net",
1304 100.0,
1305 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1306 vec![line(1, 2), jumper],
1307 );
1308 net.loads_mut().push(load(2));
1309
1310 assert_eq!(net.reduce_zero_impedance(1e-9), 1);
1311 let ids: Vec<BusId> = net.buses().iter().map(|b| b.id).collect();
1312 assert_eq!(ids, [BusId(1), BusId(3)]);
1313 assert_eq!(net.branches()[0].to, BusId(3));
1314 assert_eq!(net.loads()[0].bus, BusId(3));
1315 net.validate().unwrap();
1316 }
1317
1318 #[test]
1319 fn reduce_zero_impedance_keeps_an_open_jumper() {
1320 let mut jumper = line(2, 3);
1323 jumper.x = 0.0;
1324 jumper.in_service = false;
1325 let mut net = BalancedNetwork::in_memory(
1326 "net",
1327 100.0,
1328 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1329 vec![line(1, 2), jumper],
1330 );
1331
1332 let removed = net.reduce_zero_impedance(1e-9);
1333 assert_eq!(removed, 0, "an open jumper is left in place");
1334 assert_eq!(net.buses().len(), 3);
1335 assert_eq!(net.branches().len(), 2);
1336 net.validate().unwrap();
1337 }
1338
1339 #[test]
1340 fn a_failed_zero_impedance_reduction_is_reported() {
1341 let mut jumper = line(1, 2);
1345 jumper.x = 0.0;
1346 let mut ideal = line(1, 2);
1347 ideal.x = 0.0;
1348 ideal.tap = 1.05;
1349 let mut second = line(2, 3);
1350 second.x = 0.0;
1351 let network = BalancedNetwork::in_memory(
1352 "net",
1353 100.0,
1354 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1355 vec![jumper, ideal, second],
1356 );
1357
1358 let mut old = network.clone();
1359 assert_eq!(old.reduce_zero_impedance(1e-9), 0);
1360 assert_eq!(old.buses().len(), 3);
1361 assert_eq!(old.branches().len(), 3);
1362
1363 let mut reported = network.clone();
1364 let report = reported.reduce_zero_impedance_with_report(1e-9);
1365 assert_eq!(report.removed, 0);
1366 assert_eq!(reported.buses().len(), 3, "nothing is merged");
1367 assert_eq!(reported.branches().len(), 3);
1368 let codes: Vec<&str> = report.diagnostics.iter().map(Diagnostic::code).collect();
1369 assert_eq!(codes, ["BUILD.BRANCH.ZERO_IMPEDANCE"]);
1370 assert!(
1371 report.diagnostics[0]
1372 .message()
1373 .contains("zero impedance reduction merged nothing"),
1374 "{}",
1375 report.diagnostics[0].message()
1376 );
1377 }
1378
1379 #[test]
1380 fn a_zero_impedance_reduction_reports_the_merge() {
1381 let mut jumper = line(2, 3);
1382 jumper.x = 0.0;
1383 let mut net = BalancedNetwork::in_memory(
1384 "net",
1385 100.0,
1386 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1387 vec![line(1, 2), jumper],
1388 );
1389 let report = net.reduce_zero_impedance_with_report(1e-9);
1390 assert_eq!(report.removed, 1);
1391 let codes: Vec<&str> = report.diagnostics.iter().map(Diagnostic::code).collect();
1392 assert_eq!(codes, ["CANONICALIZE.MERGE.ZERO_IMPEDANCE"]);
1393 }
1394
1395 #[test]
1396 fn reduce_zero_impedance_keeps_a_3w_winding_pair() {
1397 let mut jumper = line(2, 3);
1401 jumper.x = 0.0;
1402 let mut net = BalancedNetwork::in_memory(
1403 "net",
1404 100.0,
1405 vec![bus(1, 1, 230.0), bus(2, 1, 138.0), bus(3, 1, 13.8)],
1406 vec![line(1, 2), jumper],
1407 );
1408 net.transformers_3w_mut().push(transformer_3w(1, 2, 3));
1409
1410 let removed = net.reduce_zero_impedance(1e-9);
1411 assert_eq!(
1412 removed, 0,
1413 "a jumper across two windings of one 3W transformer is kept"
1414 );
1415 assert_eq!(net.buses().len(), 3);
1416 net.validate().unwrap();
1417 }
1418}