1use std::collections::HashSet;
13
14use serde_json::Value;
15
16use crate::network::{
17 BalancedNetwork, Branch, Bus, BusId, BusType, Extras, Generator, Shunt, SourceFormat,
18};
19
20fn other_end(b: &Branch, m: BusId) -> BusId {
22 if b.from == m { b.to } else { b.from }
23}
24
25fn combine_rate(a: f64, b: f64) -> f64 {
29 match (a == 0.0, b == 0.0) {
30 (true, _) => b,
31 (_, true) => a,
32 _ => a.min(b),
33 }
34}
35
36fn kind_priority(kind: BusType) -> u8 {
40 match kind {
41 BusType::Ref => 3,
42 BusType::Pv => 2,
43 BusType::Pq => 1,
44 BusType::Isolated => 0,
45 }
46}
47
48#[derive(Debug, Clone, Default, PartialEq)]
52pub struct Selector {
53 pub area: Option<(usize, usize)>,
55 pub zone: Option<(usize, usize)>,
57 pub base_kv: Option<(f64, f64)>,
59 pub bus: Option<(usize, usize)>,
61}
62
63impl Selector {
64 fn matches(&self, bus: &Bus) -> bool {
66 fn in_usize(range: Option<(usize, usize)>, v: usize) -> bool {
67 range.is_none_or(|(lo, hi)| lo <= v && v <= hi)
68 }
69 fn in_f64(range: Option<(f64, f64)>, v: f64) -> bool {
70 range.is_none_or(|(lo, hi)| lo <= v && v <= hi)
71 }
72 in_usize(self.area, bus.area)
73 && in_usize(self.zone, bus.zone)
74 && in_f64(self.base_kv, bus.base_kv)
75 && in_usize(self.bus, bus.id.0)
76 }
77}
78
79impl BalancedNetwork {
80 #[must_use]
96 #[expect(clippy::too_many_lines)]
99 pub fn subset(&self, sel: &Selector, keep_boundary: bool) -> BalancedNetwork {
100 let in_scope: HashSet<BusId> = self
101 .buses
102 .iter()
103 .filter(|b| sel.matches(b))
104 .map(|b| b.id)
105 .collect();
106
107 let mut boundary: HashSet<BusId> = HashSet::new();
110 if keep_boundary {
111 let mut edge = |a: BusId, b: BusId| match (in_scope.contains(&a), in_scope.contains(&b))
112 {
113 (true, false) => {
114 boundary.insert(b);
115 }
116 (false, true) => {
117 boundary.insert(a);
118 }
119 _ => {}
120 };
121 for br in &self.branches {
122 edge(br.from, br.to);
123 }
124 for d in &self.hvdc {
125 edge(d.from, d.to);
126 }
127 }
128 let kept: HashSet<BusId> = in_scope.union(&boundary).copied().collect();
129
130 let buses: Vec<Bus> = self
131 .buses
132 .iter()
133 .filter(|b| kept.contains(&b.id))
134 .map(|b| {
135 let mut b = b.clone();
136 if boundary.contains(&b.id) {
137 b.extras.insert("tie_bus".into(), Value::Bool(true));
138 }
139 b
140 })
141 .collect();
142
143 let loads = self
145 .loads
146 .iter()
147 .filter(|l| in_scope.contains(&l.bus))
148 .cloned()
149 .collect();
150 let mut shunts: Vec<Shunt> = self
151 .shunts
152 .iter()
153 .filter(|s| in_scope.contains(&s.bus))
154 .cloned()
155 .collect();
156 let mut generators: Vec<Generator> = self
157 .generators
158 .iter()
159 .filter(|g| in_scope.contains(&g.bus))
160 .cloned()
161 .collect();
162 let storage = self
163 .storage
164 .iter()
165 .filter(|s| in_scope.contains(&s.bus))
166 .cloned()
167 .collect();
168
169 let mut branches: Vec<Branch> = self
170 .branches
171 .iter()
172 .filter(|br| kept.contains(&br.from) && kept.contains(&br.to))
173 .cloned()
174 .collect();
175 let switches = self
176 .switches
177 .iter()
178 .filter(|sw| kept.contains(&sw.from) && kept.contains(&sw.to))
179 .cloned()
180 .collect();
181 let hvdc = self
182 .hvdc
183 .iter()
184 .filter(|d| kept.contains(&d.from) && kept.contains(&d.to))
185 .cloned()
186 .collect();
187 let transformers_3w = self
188 .transformers_3w
189 .iter()
190 .filter(|t| t.windings.iter().all(|w| kept.contains(&w.bus)))
191 .cloned()
192 .collect();
193
194 for br in &mut branches {
196 if let Some(c) = &mut br.control {
197 if c.controlled_bus.is_some_and(|b| !kept.contains(&b)) {
198 c.controlled_bus = None;
199 }
200 }
201 }
202 for sh in &mut shunts {
203 if let Some(c) = &mut sh.control {
204 if c.control_bus.is_some_and(|b| !kept.contains(&b)) {
205 c.control_bus = None;
206 }
207 }
208 }
209 for g in &mut generators {
210 if g.regulated_bus.is_some_and(|b| !kept.contains(&b)) {
211 g.regulated_bus = None;
212 }
213 }
214
215 let kept_area_numbers: HashSet<usize> = buses.iter().map(|b| b.area).collect();
220 let areas = self
221 .areas
222 .iter()
223 .filter(|a| kept_area_numbers.contains(&a.number))
224 .cloned()
225 .map(|mut a| {
226 if a.slack_bus.is_some_and(|b| !kept.contains(&b)) {
227 a.slack_bus = None;
228 }
229 a
230 })
231 .collect::<Vec<_>>();
232
233 let net = BalancedNetwork {
234 name: format!("{} (subset)", self.name),
235 base_mva: self.base_mva,
236 base_frequency: self.base_frequency,
237 geo: self.geo.clone(),
238 buses,
239 loads,
240 shunts,
241 branches,
242 switches,
243 generators,
244 storage,
245 hvdc,
246 transformers_3w,
247 areas,
248 solver: self.solver.clone(),
249 source_format: SourceFormat::InMemory,
250 source: None,
251 };
252 debug_assert!(
253 net.validate().is_ok(),
254 "subset produced a dangling reference"
255 );
256 net
257 }
258
259 pub fn merge_bus(&mut self, into: BusId, from: BusId) {
268 if into == from {
269 return;
270 }
271 let remap = |b: &mut BusId| {
272 if *b == from {
273 *b = into;
274 }
275 };
276
277 for l in &mut self.loads {
278 remap(&mut l.bus);
279 }
280 for s in &mut self.shunts {
281 remap(&mut s.bus);
282 if let Some(cb) = s.control.as_mut().and_then(|c| c.control_bus.as_mut()) {
283 remap(cb);
284 }
285 }
286 for g in &mut self.generators {
287 remap(&mut g.bus);
288 if let Some(rb) = g.regulated_bus.as_mut() {
289 remap(rb);
290 }
291 }
292 for st in &mut self.storage {
293 remap(&mut st.bus);
294 }
295 for br in &mut self.branches {
296 remap(&mut br.from);
297 remap(&mut br.to);
298 if let Some(cb) = br.control.as_mut().and_then(|c| c.controlled_bus.as_mut()) {
299 remap(cb);
300 }
301 }
302 self.branches.retain(|b| b.from != b.to);
303 for sw in &mut self.switches {
304 remap(&mut sw.from);
305 remap(&mut sw.to);
306 }
307 self.switches.retain(|s| s.from != s.to);
308 for d in &mut self.hvdc {
309 remap(&mut d.from);
310 remap(&mut d.to);
311 }
312 self.hvdc.retain(|d| d.from != d.to);
313 for t in &mut self.transformers_3w {
314 for w in &mut t.windings {
315 remap(&mut w.bus);
316 }
317 }
318 for a in &mut self.areas {
319 if let Some(slack) = a.slack_bus.as_mut() {
320 remap(slack);
321 }
322 }
323
324 let from_kind = self.buses.iter().find(|b| b.id == from).map(|b| b.kind);
326 self.buses.retain(|b| b.id != from);
327 if let (Some(fk), Some(into_bus)) =
328 (from_kind, self.buses.iter_mut().find(|b| b.id == into))
329 {
330 if kind_priority(fk) > kind_priority(into_bus.kind) {
331 into_bus.kind = fk;
332 }
333 }
334 self.invalidate_source();
336 }
337
338 pub fn reduce_zero_impedance(&mut self, threshold: f64) -> usize {
351 let before = self.branches.len();
352 while let Some((into, from)) = self.branches.iter().find_map(|b| {
354 (b.in_service
355 && !b.is_transformer()
356 && b.from != b.to
357 && b.r.hypot(b.x) <= threshold
358 && !self.shares_transformer_3w(b.from, b.to))
359 .then_some((b.from, b.to))
360 }) {
361 self.merge_bus(into, from);
362 }
363 before - self.branches.len()
364 }
365
366 fn shares_transformer_3w(&self, a: BusId, b: BusId) -> bool {
369 self.transformers_3w
370 .iter()
371 .any(|t| t.windings.iter().any(|w| w.bus == a) && t.windings.iter().any(|w| w.bus == b))
372 }
373
374 pub fn reduce_passthrough_buses(&mut self) -> usize {
390 let mut collapsed = 0;
391 while let Some(mid) = self
394 .buses
395 .iter()
396 .map(|b| b.id)
397 .find(|&m| self.is_passthrough(m))
398 {
399 self.collapse_passthrough(mid);
400 collapsed += 1;
401 }
402 collapsed
403 }
404
405 fn is_passthrough(&self, m: BusId) -> bool {
408 let Some(bus) = self.buses.iter().find(|b| b.id == m) else {
409 return false;
410 };
411 if bus.kind == BusType::Ref {
412 return false;
413 }
414 if self.loads.iter().any(|l| l.bus == m)
415 || self.generators.iter().any(|g| g.bus == m)
416 || self.shunts.iter().any(|s| s.bus == m)
417 || self.storage.iter().any(|s| s.bus == m)
418 || self.hvdc.iter().any(|d| d.from == m || d.to == m)
419 {
420 return false;
421 }
422 if self
423 .transformers_3w
424 .iter()
425 .any(|t| t.windings.iter().any(|w| w.bus == m))
426 {
427 return false;
428 }
429 if self.areas.iter().any(|a| a.slack_bus == Some(m)) {
430 return false;
431 }
432 let controlled = self
433 .branches
434 .iter()
435 .any(|b| b.control.as_ref().and_then(|c| c.controlled_bus) == Some(m));
436 let regulated = self
437 .shunts
438 .iter()
439 .any(|s| s.control.as_ref().and_then(|c| c.control_bus) == Some(m));
440 let gen_regulated = self.generators.iter().any(|g| g.regulated_bus == Some(m));
441 if controlled || regulated || gen_regulated {
442 return false;
443 }
444 let incident: Vec<&Branch> = self
445 .branches
446 .iter()
447 .filter(|b| b.from == m || b.to == m)
448 .collect();
449 if incident.len() != 2 {
450 return false;
451 }
452 let a = other_end(incident[0], m);
453 let c = other_end(incident[1], m);
454 incident.iter().all(|b| !b.is_transformer() && b.in_service) && a != m && c != m && a != c
455 }
456
457 fn collapse_passthrough(&mut self, m: BusId) {
460 let mut sections: Vec<Branch> = Vec::new();
461 self.branches.retain(|b| {
462 if b.from == m || b.to == m {
463 sections.push(b.clone());
464 false
465 } else {
466 true
467 }
468 });
469 debug_assert_eq!(sections.len(), 2, "passthrough bus must have two sections");
470 let (s1, s2) = (§ions[0], §ions[1]);
471 let mut angmin = s1.angmin.max(s2.angmin);
478 let mut angmax = s1.angmax.min(s2.angmax);
479 if angmin > angmax {
480 angmin = s1.angmin.min(s2.angmin);
481 angmax = s1.angmax.max(s2.angmax);
482 }
483 self.branches.push(Branch {
484 from: other_end(s1, m),
485 to: other_end(s2, m),
486 r: s1.r + s2.r,
487 x: s1.x + s2.x,
488 b: s1.total_charging_b() + s2.total_charging_b(),
489 charging: None,
490 rate_a: combine_rate(s1.rate_a, s2.rate_a),
491 rate_b: combine_rate(s1.rate_b, s2.rate_b),
492 rate_c: combine_rate(s1.rate_c, s2.rate_c),
493 rating_sets: Vec::new(),
494 current_ratings: None,
495 tap: 0.0,
496 shift: 0.0,
497 in_service: true,
498 angmin,
499 angmax,
500 control: None,
501 solution: None,
502 uid: None,
503 route: None,
504 extras: Extras::new(),
505 });
506 self.buses.retain(|b| b.id != m);
507 self.invalidate_source();
509 }
510
511 pub fn retype_isolated_buses(&mut self) -> usize {
523 let mut connected: HashSet<BusId> = HashSet::new();
524 for br in self.branches.iter().filter(|b| b.in_service) {
525 connected.insert(br.from);
526 connected.insert(br.to);
527 }
528 for d in self.hvdc.iter().filter(|d| d.in_service) {
529 connected.insert(d.from);
530 connected.insert(d.to);
531 }
532 for t in self.transformers_3w.iter().filter(|t| t.in_service) {
533 for w in &t.windings {
534 connected.insert(w.bus);
535 }
536 }
537 let mut retyped = 0;
538 for b in &mut self.buses {
539 if b.kind != BusType::Isolated && !connected.contains(&b.id) {
540 b.kind = BusType::Isolated;
541 retyped += 1;
542 }
543 }
544 if retyped > 0 {
546 self.invalidate_source();
547 }
548 retyped
549 }
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555 use crate::network::{
556 Area, BusType, Extras, Generator, Impedance, Load, Transformer3W, Winding,
557 };
558
559 fn bus(id: usize, area: usize, base_kv: f64) -> Bus {
560 Bus {
561 id: BusId(id),
562 kind: BusType::Pq,
563 vm: 1.0,
564 va: 0.0,
565 base_kv,
566 vmax: 1.1,
567 vmin: 0.9,
568 evhi: None,
569 evlo: None,
570 area,
571 zone: 1,
572 name: None,
573 uid: None,
574 location: None,
575 extras: Extras::new(),
576 }
577 }
578
579 fn line(from: usize, to: usize) -> Branch {
580 Branch {
581 from: BusId(from),
582 to: BusId(to),
583 r: 0.0,
584 x: 0.1,
585 b: 0.0,
586 charging: None,
587 rate_a: 0.0,
588 rate_b: 0.0,
589 rate_c: 0.0,
590 rating_sets: Vec::new(),
591 current_ratings: None,
592 tap: 0.0,
593 shift: 0.0,
594 in_service: true,
595 angmin: -360.0,
596 angmax: 360.0,
597 control: None,
598 solution: None,
599 uid: None,
600 route: None,
601 extras: Extras::new(),
602 }
603 }
604
605 fn load(bus: usize) -> Load {
606 Load {
607 bus: BusId(bus),
608 p: 10.0,
609 q: 5.0,
610 voltage_model: None,
611 in_service: true,
612 uid: None,
613 extras: Extras::new(),
614 }
615 }
616
617 fn two_area_net() -> BalancedNetwork {
620 let mut net = BalancedNetwork::in_memory(
621 "net",
622 100.0,
623 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 2, 230.0)],
624 vec![line(1, 2), line(2, 3)],
625 );
626 net.loads.push(load(1));
627 net.loads.push(load(3));
628 net
629 }
630
631 fn transformer_3w(a: usize, b: usize, c: usize) -> Transformer3W {
632 let winding = |bus| Winding {
633 bus: BusId(bus),
634 tap: 1.0,
635 shift: 0.0,
636 nominal_kv: 0.0,
637 rate_a: 0.0,
638 rate_b: 0.0,
639 rate_c: 0.0,
640 };
641 let imp = Impedance {
642 r: 0.0,
643 x: 0.1,
644 base_mva: 100.0,
645 };
646 Transformer3W {
647 windings: [winding(a), winding(b), winding(c)],
648 z: [imp, imp, imp],
649 star_vm: 1.0,
650 star_va: 0.0,
651 mag_g: 0.0,
652 mag_b: 0.0,
653 in_service: true,
654 name: None,
655 uid: None,
656 extras: Extras::new(),
657 }
658 }
659
660 fn gen_regulating(bus: usize, regulated: usize) -> Generator {
661 Generator {
662 bus: BusId(bus),
663 pg: 10.0,
664 qg: 0.0,
665 pmax: 100.0,
666 pmin: 0.0,
667 qmax: 50.0,
668 qmin: -50.0,
669 vg: 1.0,
670 mbase: 100.0,
671 in_service: true,
672 cost: None,
673 caps: Default::default(),
674 regulated_bus: Some(BusId(regulated)),
675 uid: None,
676 }
677 }
678
679 #[test]
680 fn subset_clears_a_regulated_bus_outside_the_kept_set() {
681 let mut net = two_area_net();
683 net.generators.push(gen_regulating(1, 3));
684 let sel = Selector {
685 area: Some((1, 1)),
686 ..Selector::default()
687 };
688 let sub = net.subset(&sel, false);
689 assert_eq!(sub.generators.len(), 1);
690 assert_eq!(
691 sub.generators[0].regulated_bus, None,
692 "the dropped remote regulated bus is cleared, not left dangling"
693 );
694 sub.validate().unwrap();
695 }
696
697 #[test]
698 fn merge_bus_remaps_regulated_bus_and_area_slack() {
699 let mut net = two_area_net();
700 net.generators.push(gen_regulating(1, 3)); net.areas.push(Area {
702 number: 1,
703 slack_bus: Some(BusId(3)),
704 net_interchange: 0.0,
705 tolerance: 0.0,
706 name: None,
707 });
708 net.merge_bus(BusId(2), BusId(3)); assert_eq!(
710 net.generators[0].regulated_bus,
711 Some(BusId(2)),
712 "the regulated bus follows the merge"
713 );
714 assert_eq!(
715 net.areas[0].slack_bus,
716 Some(BusId(2)),
717 "the area swing follows the merge"
718 );
719 net.validate().unwrap();
720 }
721
722 #[test]
723 fn reduce_passthrough_keeps_a_generator_regulated_bus() {
724 let mut net = BalancedNetwork::in_memory(
727 "net",
728 100.0,
729 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
730 vec![line(1, 2), line(2, 3)],
731 );
732 net.generators.push(gen_regulating(1, 2));
733 assert_eq!(net.reduce_passthrough_buses(), 0);
734 assert_eq!(net.buses.len(), 3);
735 net.validate().unwrap();
736 }
737
738 #[test]
739 fn subset_by_area_drops_out_of_scope_buses_and_cut_branches() {
740 let net = two_area_net();
741 let sel = Selector {
742 area: Some((1, 1)),
743 ..Selector::default()
744 };
745 let sub = net.subset(&sel, false);
746
747 assert_eq!(sub.buses.len(), 2);
749 assert!(sub.buses.iter().all(|b| b.area == 1));
750 assert_eq!(sub.branches.len(), 1, "only the intra-area line survives");
751 assert_eq!(sub.loads.len(), 1, "the area-2 load is dropped");
752 sub.validate().unwrap();
753 }
754
755 #[test]
756 fn subset_keep_boundary_pulls_in_the_tie_bus() {
757 let net = two_area_net();
758 let sel = Selector {
759 area: Some((1, 1)),
760 ..Selector::default()
761 };
762 let sub = net.subset(&sel, true);
763
764 assert_eq!(sub.buses.len(), 3);
766 assert_eq!(sub.branches.len(), 2);
767 let tie = sub.buses.iter().find(|b| b.id == BusId(3)).unwrap();
768 assert_eq!(tie.extras.get("tie_bus"), Some(&Value::Bool(true)));
769 assert_eq!(sub.loads.len(), 1);
771 sub.validate().unwrap();
772 }
773
774 #[test]
775 fn empty_selector_keeps_everything() {
776 let net = two_area_net();
777 let sub = net.subset(&Selector::default(), false);
778 assert_eq!(sub.buses.len(), net.buses.len());
779 assert_eq!(sub.branches.len(), net.branches.len());
780 }
781
782 #[test]
783 fn base_kv_range_filters_by_voltage() {
784 let mut net = two_area_net();
785 net.buses[2].base_kv = 115.0; let sel = Selector {
787 base_kv: Some((200.0, 300.0)),
788 ..Selector::default()
789 };
790 let sub = net.subset(&sel, false);
791 assert_eq!(sub.buses.len(), 2, "only the 230 kV buses match");
792 }
793
794 #[test]
795 fn merge_bus_rehomes_elements_and_drops_the_connecting_branch() {
796 let mut net = two_area_net(); net.merge_bus(BusId(2), BusId(3));
798
799 assert_eq!(net.buses.len(), 2, "bus 3 removed");
800 assert!(net.buses.iter().all(|b| b.id != BusId(3)));
801 assert_eq!(
802 net.branches.len(),
803 1,
804 "the 2-3 line collapsed to a self-loop"
805 );
806 assert_eq!(net.branches[0].from, BusId(1));
807 assert_eq!(net.branches[0].to, BusId(2));
808 assert_eq!(net.loads.len(), 2);
810 assert!(net.loads.iter().any(|l| l.bus == BusId(2)));
811 net.validate().unwrap();
812 }
813
814 #[test]
815 fn merge_bus_keeps_the_stronger_bus_kind() {
816 let mut net = two_area_net();
817 net.buses[2].kind = BusType::Ref; net.merge_bus(BusId(2), BusId(3)); let two = net.buses.iter().find(|b| b.id == BusId(2)).unwrap();
820 assert_eq!(two.kind, BusType::Ref, "the slack designation is not lost");
821 }
822
823 #[test]
824 fn reduce_passthrough_folds_a_multi_section_line() {
825 let mut s1 = line(1, 2);
828 s1.rate_a = 100.0;
829 let mut s2 = line(2, 3);
830 s2.rate_a = 80.0;
831 let s3 = line(3, 4); let mut net = BalancedNetwork::in_memory(
833 "net",
834 100.0,
835 vec![
836 bus(1, 1, 230.0),
837 bus(2, 1, 230.0),
838 bus(3, 1, 230.0),
839 bus(4, 1, 230.0),
840 ],
841 vec![s1, s2, s3],
842 );
843
844 let removed = net.reduce_passthrough_buses();
845 assert_eq!(removed, 2, "both dummy buses collapse");
846 assert_eq!(net.buses.len(), 2);
847 assert!(
848 net.buses
849 .iter()
850 .all(|b| b.id == BusId(1) || b.id == BusId(4))
851 );
852 assert_eq!(net.branches.len(), 1, "one equivalent branch");
853 let eq = &net.branches[0];
854 assert_eq!(
855 [eq.from, eq.to].iter().copied().collect::<HashSet<_>>(),
856 [BusId(1), BusId(4)].into_iter().collect::<HashSet<_>>(),
857 );
858 assert!((eq.x - 0.3).abs() < 1e-9, "series reactance sums");
859 assert!(
860 (eq.rate_a - 80.0).abs() < 1e-9,
861 "the more limiting finite rating wins"
862 );
863 net.validate().unwrap();
864 }
865
866 #[test]
867 fn reduce_passthrough_keeps_a_bus_with_injection() {
868 let mut net = BalancedNetwork::in_memory(
870 "net",
871 100.0,
872 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
873 vec![line(1, 2), line(2, 3)],
874 );
875 net.loads.push(load(2));
876 assert_eq!(net.reduce_passthrough_buses(), 0);
877 assert_eq!(net.buses.len(), 3);
878 }
879
880 #[test]
881 fn reduce_passthrough_does_not_fold_across_a_transformer() {
882 let mut xfmr = line(2, 3);
884 xfmr.tap = 1.0;
885 let mut net = BalancedNetwork::in_memory(
886 "net",
887 100.0,
888 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
889 vec![line(1, 2), xfmr],
890 );
891 assert_eq!(net.reduce_passthrough_buses(), 0);
892 assert_eq!(net.buses.len(), 3);
893 }
894
895 #[test]
896 fn retype_isolated_marks_stranded_buses() {
897 let mut net = BalancedNetwork::in_memory(
899 "net",
900 100.0,
901 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
902 vec![line(1, 2)],
903 );
904 assert_eq!(net.retype_isolated_buses(), 1);
905 let three = net.buses.iter().find(|b| b.id == BusId(3)).unwrap();
906 assert_eq!(three.kind, BusType::Isolated);
907 let one = net.buses.iter().find(|b| b.id == BusId(1)).unwrap();
909 assert_eq!(one.kind, BusType::Pq);
910 net.validate().unwrap();
911 }
912
913 #[test]
914 fn retype_isolated_judges_in_service_equipment_only() {
915 let mut br = line(1, 2);
917 br.in_service = false;
918 let mut net = BalancedNetwork::in_memory(
919 "net",
920 100.0,
921 vec![bus(1, 1, 230.0), bus(2, 1, 230.0)],
922 vec![br],
923 );
924 assert_eq!(net.retype_isolated_buses(), 2);
925 assert!(net.buses.iter().all(|b| b.kind == BusType::Isolated));
926 }
927
928 #[test]
929 fn retype_isolated_is_idempotent() {
930 let mut net = BalancedNetwork::in_memory(
931 "net",
932 100.0,
933 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
934 vec![line(1, 2)],
935 );
936 assert_eq!(net.retype_isolated_buses(), 1);
937 assert_eq!(net.retype_isolated_buses(), 0, "second pass is a no-op");
938 }
939
940 #[test]
941 fn reduce_zero_impedance_collapses_jumpers_only() {
942 let mut jumper = line(2, 3);
944 jumper.x = 0.0;
945 let mut net = BalancedNetwork::in_memory(
946 "net",
947 100.0,
948 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
949 vec![line(1, 2), jumper],
950 );
951 net.loads.push(load(3));
952
953 let removed = net.reduce_zero_impedance(1e-9);
954 assert_eq!(removed, 1, "only the jumper is collapsed");
955 assert_eq!(net.buses.len(), 2);
956 assert_eq!(net.branches.len(), 1, "the real 1-2 line remains");
957 assert!(net.loads.iter().any(|l| l.bus == BusId(2)), "load re-homed");
958 net.validate().unwrap();
959 }
960
961 #[test]
962 fn reduce_zero_impedance_keeps_an_open_jumper() {
963 let mut jumper = line(2, 3);
966 jumper.x = 0.0;
967 jumper.in_service = false;
968 let mut net = BalancedNetwork::in_memory(
969 "net",
970 100.0,
971 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
972 vec![line(1, 2), jumper],
973 );
974
975 let removed = net.reduce_zero_impedance(1e-9);
976 assert_eq!(removed, 0, "an open jumper is left in place");
977 assert_eq!(net.buses.len(), 3);
978 assert_eq!(net.branches.len(), 2);
979 net.validate().unwrap();
980 }
981
982 #[test]
983 fn reduce_zero_impedance_keeps_a_3w_winding_pair() {
984 let mut jumper = line(2, 3);
988 jumper.x = 0.0;
989 let mut net = BalancedNetwork::in_memory(
990 "net",
991 100.0,
992 vec![bus(1, 1, 230.0), bus(2, 1, 138.0), bus(3, 1, 13.8)],
993 vec![line(1, 2), jumper],
994 );
995 net.transformers_3w.push(transformer_3w(1, 2, 3));
996
997 let removed = net.reduce_zero_impedance(1e-9);
998 assert_eq!(
999 removed, 0,
1000 "a jumper across two windings of one 3W transformer is kept"
1001 );
1002 assert_eq!(net.buses.len(), 3);
1003 net.validate().unwrap();
1004 }
1005
1006 #[test]
1007 fn in_place_mutations_invalidate_the_retained_source() {
1008 use std::sync::Arc;
1009 let retained = || Some(Arc::new("RETAINED".to_string()));
1010
1011 let mut jumper = line(2, 3);
1013 jumper.x = 0.0;
1014 let mut net = BalancedNetwork::in_memory(
1015 "net",
1016 100.0,
1017 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1018 vec![line(1, 2), jumper],
1019 );
1020 net.source = retained();
1021 assert_eq!(net.reduce_zero_impedance(1e-9), 1);
1022 assert!(net.source.is_none(), "a merge invalidates the source");
1023
1024 let mut net = BalancedNetwork::in_memory(
1026 "net",
1027 100.0,
1028 vec![bus(1, 1, 230.0), bus(2, 1, 230.0)],
1029 vec![line(1, 2)],
1030 );
1031 net.source = retained();
1032 assert_eq!(net.reduce_zero_impedance(1e-9), 0);
1033 assert!(net.source.is_some(), "a no-op leaves the source intact");
1034
1035 let mut net = BalancedNetwork::in_memory(
1037 "net",
1038 100.0,
1039 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1040 vec![line(1, 2), line(2, 3)],
1041 );
1042 net.source = retained();
1043 assert_eq!(net.reduce_passthrough_buses(), 1);
1044 assert!(net.source.is_none());
1045
1046 let mut net = BalancedNetwork::in_memory(
1048 "net",
1049 100.0,
1050 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1051 vec![line(1, 2)],
1052 );
1053 net.source = retained();
1054 assert_eq!(net.retype_isolated_buses(), 1);
1055 assert!(net.source.is_none());
1056
1057 let mut net = BalancedNetwork::in_memory("net", 100.0, vec![bus(1, 1, 230.0)], Vec::new());
1059 net.buses[0].vm = -1.0;
1060 net.source = retained();
1061 assert!(!net.repair().is_empty());
1062 assert!(net.source.is_none());
1063 }
1064}