1use std::collections::HashSet;
13
14use serde_json::Value;
15
16use crate::network::{
17 Branch, Bus, BusId, BusType, Extras, Generator, Network, 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 Network {
80 #[must_use]
96 #[expect(clippy::too_many_lines)]
99 pub fn subset(&self, sel: &Selector, keep_boundary: bool) -> Network {
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 = Network {
234 name: format!("{} (subset)", self.name),
235 base_mva: self.base_mva,
236 base_frequency: self.base_frequency,
237 buses,
238 loads,
239 shunts,
240 branches,
241 switches,
242 generators,
243 storage,
244 hvdc,
245 transformers_3w,
246 areas,
247 solver: self.solver.clone(),
248 source_format: SourceFormat::InMemory,
249 source: None,
250 };
251 debug_assert!(
252 net.validate().is_ok(),
253 "subset produced a dangling reference"
254 );
255 net
256 }
257
258 pub fn merge_bus(&mut self, into: BusId, from: BusId) {
267 if into == from {
268 return;
269 }
270 let remap = |b: &mut BusId| {
271 if *b == from {
272 *b = into;
273 }
274 };
275
276 for l in &mut self.loads {
277 remap(&mut l.bus);
278 }
279 for s in &mut self.shunts {
280 remap(&mut s.bus);
281 if let Some(cb) = s.control.as_mut().and_then(|c| c.control_bus.as_mut()) {
282 remap(cb);
283 }
284 }
285 for g in &mut self.generators {
286 remap(&mut g.bus);
287 if let Some(rb) = g.regulated_bus.as_mut() {
288 remap(rb);
289 }
290 }
291 for st in &mut self.storage {
292 remap(&mut st.bus);
293 }
294 for br in &mut self.branches {
295 remap(&mut br.from);
296 remap(&mut br.to);
297 if let Some(cb) = br.control.as_mut().and_then(|c| c.controlled_bus.as_mut()) {
298 remap(cb);
299 }
300 }
301 self.branches.retain(|b| b.from != b.to);
302 for sw in &mut self.switches {
303 remap(&mut sw.from);
304 remap(&mut sw.to);
305 }
306 self.switches.retain(|s| s.from != s.to);
307 for d in &mut self.hvdc {
308 remap(&mut d.from);
309 remap(&mut d.to);
310 }
311 self.hvdc.retain(|d| d.from != d.to);
312 for t in &mut self.transformers_3w {
313 for w in &mut t.windings {
314 remap(&mut w.bus);
315 }
316 }
317 for a in &mut self.areas {
318 if let Some(slack) = a.slack_bus.as_mut() {
319 remap(slack);
320 }
321 }
322
323 let from_kind = self.buses.iter().find(|b| b.id == from).map(|b| b.kind);
325 self.buses.retain(|b| b.id != from);
326 if let (Some(fk), Some(into_bus)) =
327 (from_kind, self.buses.iter_mut().find(|b| b.id == into))
328 {
329 if kind_priority(fk) > kind_priority(into_bus.kind) {
330 into_bus.kind = fk;
331 }
332 }
333 self.invalidate_source();
335 }
336
337 pub fn reduce_zero_impedance(&mut self, threshold: f64) -> usize {
350 let before = self.branches.len();
351 while let Some((into, from)) = self.branches.iter().find_map(|b| {
353 (b.in_service
354 && !b.is_transformer()
355 && b.from != b.to
356 && b.r.hypot(b.x) <= threshold
357 && !self.shares_transformer_3w(b.from, b.to))
358 .then_some((b.from, b.to))
359 }) {
360 self.merge_bus(into, from);
361 }
362 before - self.branches.len()
363 }
364
365 fn shares_transformer_3w(&self, a: BusId, b: BusId) -> bool {
368 self.transformers_3w
369 .iter()
370 .any(|t| t.windings.iter().any(|w| w.bus == a) && t.windings.iter().any(|w| w.bus == b))
371 }
372
373 pub fn reduce_passthrough_buses(&mut self) -> usize {
389 let mut collapsed = 0;
390 while let Some(mid) = self
393 .buses
394 .iter()
395 .map(|b| b.id)
396 .find(|&m| self.is_passthrough(m))
397 {
398 self.collapse_passthrough(mid);
399 collapsed += 1;
400 }
401 collapsed
402 }
403
404 fn is_passthrough(&self, m: BusId) -> bool {
407 let Some(bus) = self.buses.iter().find(|b| b.id == m) else {
408 return false;
409 };
410 if bus.kind == BusType::Ref {
411 return false;
412 }
413 if self.loads.iter().any(|l| l.bus == m)
414 || self.generators.iter().any(|g| g.bus == m)
415 || self.shunts.iter().any(|s| s.bus == m)
416 || self.storage.iter().any(|s| s.bus == m)
417 || self.hvdc.iter().any(|d| d.from == m || d.to == m)
418 {
419 return false;
420 }
421 if self
422 .transformers_3w
423 .iter()
424 .any(|t| t.windings.iter().any(|w| w.bus == m))
425 {
426 return false;
427 }
428 if self.areas.iter().any(|a| a.slack_bus == Some(m)) {
429 return false;
430 }
431 let controlled = self
432 .branches
433 .iter()
434 .any(|b| b.control.as_ref().and_then(|c| c.controlled_bus) == Some(m));
435 let regulated = self
436 .shunts
437 .iter()
438 .any(|s| s.control.as_ref().and_then(|c| c.control_bus) == Some(m));
439 let gen_regulated = self.generators.iter().any(|g| g.regulated_bus == Some(m));
440 if controlled || regulated || gen_regulated {
441 return false;
442 }
443 let incident: Vec<&Branch> = self
444 .branches
445 .iter()
446 .filter(|b| b.from == m || b.to == m)
447 .collect();
448 if incident.len() != 2 {
449 return false;
450 }
451 let a = other_end(incident[0], m);
452 let c = other_end(incident[1], m);
453 incident.iter().all(|b| !b.is_transformer() && b.in_service) && a != m && c != m && a != c
454 }
455
456 fn collapse_passthrough(&mut self, m: BusId) {
459 let mut sections: Vec<Branch> = Vec::new();
460 self.branches.retain(|b| {
461 if b.from == m || b.to == m {
462 sections.push(b.clone());
463 false
464 } else {
465 true
466 }
467 });
468 debug_assert_eq!(sections.len(), 2, "passthrough bus must have two sections");
469 let (s1, s2) = (§ions[0], §ions[1]);
470 let mut angmin = s1.angmin.max(s2.angmin);
477 let mut angmax = s1.angmax.min(s2.angmax);
478 if angmin > angmax {
479 angmin = s1.angmin.min(s2.angmin);
480 angmax = s1.angmax.max(s2.angmax);
481 }
482 self.branches.push(Branch {
483 from: other_end(s1, m),
484 to: other_end(s2, m),
485 r: s1.r + s2.r,
486 x: s1.x + s2.x,
487 b: s1.legacy_total_charging_b() + s2.legacy_total_charging_b(),
488 charging: None,
489 rate_a: combine_rate(s1.rate_a, s2.rate_a),
490 rate_b: combine_rate(s1.rate_b, s2.rate_b),
491 rate_c: combine_rate(s1.rate_c, s2.rate_c),
492 rating_sets: Vec::new(),
493 current_ratings: None,
494 tap: 0.0,
495 shift: 0.0,
496 in_service: true,
497 angmin,
498 angmax,
499 control: None,
500 solution: None,
501 uid: None,
502 extras: Extras::new(),
503 });
504 self.buses.retain(|b| b.id != m);
505 self.invalidate_source();
507 }
508
509 pub fn retype_isolated_buses(&mut self) -> usize {
521 let mut connected: HashSet<BusId> = HashSet::new();
522 for br in self.branches.iter().filter(|b| b.in_service) {
523 connected.insert(br.from);
524 connected.insert(br.to);
525 }
526 for d in self.hvdc.iter().filter(|d| d.in_service) {
527 connected.insert(d.from);
528 connected.insert(d.to);
529 }
530 for t in self.transformers_3w.iter().filter(|t| t.in_service) {
531 for w in &t.windings {
532 connected.insert(w.bus);
533 }
534 }
535 let mut retyped = 0;
536 for b in &mut self.buses {
537 if b.kind != BusType::Isolated && !connected.contains(&b.id) {
538 b.kind = BusType::Isolated;
539 retyped += 1;
540 }
541 }
542 if retyped > 0 {
544 self.invalidate_source();
545 }
546 retyped
547 }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use crate::network::{
554 Area, BusType, Extras, Generator, Impedance, Load, Transformer3W, Winding,
555 };
556
557 fn bus(id: usize, area: usize, base_kv: f64) -> Bus {
558 Bus {
559 id: BusId(id),
560 kind: BusType::Pq,
561 vm: 1.0,
562 va: 0.0,
563 base_kv,
564 vmax: 1.1,
565 vmin: 0.9,
566 evhi: None,
567 evlo: None,
568 area,
569 zone: 1,
570 name: None,
571 uid: None,
572 extras: Extras::new(),
573 }
574 }
575
576 fn line(from: usize, to: usize) -> Branch {
577 Branch {
578 from: BusId(from),
579 to: BusId(to),
580 r: 0.0,
581 x: 0.1,
582 b: 0.0,
583 charging: None,
584 rate_a: 0.0,
585 rate_b: 0.0,
586 rate_c: 0.0,
587 rating_sets: Vec::new(),
588 current_ratings: None,
589 tap: 0.0,
590 shift: 0.0,
591 in_service: true,
592 angmin: -360.0,
593 angmax: 360.0,
594 control: None,
595 solution: None,
596 uid: None,
597 extras: Extras::new(),
598 }
599 }
600
601 fn load(bus: usize) -> Load {
602 Load {
603 bus: BusId(bus),
604 p: 10.0,
605 q: 5.0,
606 voltage_model: None,
607 in_service: true,
608 uid: None,
609 extras: Extras::new(),
610 }
611 }
612
613 fn two_area_net() -> Network {
616 let mut net = Network::in_memory(
617 "net",
618 100.0,
619 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 2, 230.0)],
620 vec![line(1, 2), line(2, 3)],
621 );
622 net.loads.push(load(1));
623 net.loads.push(load(3));
624 net
625 }
626
627 fn transformer_3w(a: usize, b: usize, c: usize) -> Transformer3W {
628 let winding = |bus| Winding {
629 bus: BusId(bus),
630 tap: 1.0,
631 shift: 0.0,
632 nominal_kv: 0.0,
633 rate_a: 0.0,
634 rate_b: 0.0,
635 rate_c: 0.0,
636 };
637 let imp = Impedance {
638 r: 0.0,
639 x: 0.1,
640 base_mva: 100.0,
641 };
642 Transformer3W {
643 windings: [winding(a), winding(b), winding(c)],
644 z: [imp, imp, imp],
645 star_vm: 1.0,
646 star_va: 0.0,
647 mag_g: 0.0,
648 mag_b: 0.0,
649 in_service: true,
650 name: None,
651 uid: None,
652 extras: Extras::new(),
653 }
654 }
655
656 fn gen_regulating(bus: usize, regulated: usize) -> Generator {
657 Generator {
658 bus: BusId(bus),
659 pg: 10.0,
660 qg: 0.0,
661 pmax: 100.0,
662 pmin: 0.0,
663 qmax: 50.0,
664 qmin: -50.0,
665 vg: 1.0,
666 mbase: 100.0,
667 in_service: true,
668 cost: None,
669 caps: Default::default(),
670 regulated_bus: Some(BusId(regulated)),
671 uid: None,
672 }
673 }
674
675 #[test]
676 fn subset_clears_a_regulated_bus_outside_the_kept_set() {
677 let mut net = two_area_net();
679 net.generators.push(gen_regulating(1, 3));
680 let sel = Selector {
681 area: Some((1, 1)),
682 ..Selector::default()
683 };
684 let sub = net.subset(&sel, false);
685 assert_eq!(sub.generators.len(), 1);
686 assert_eq!(
687 sub.generators[0].regulated_bus, None,
688 "the dropped remote regulated bus is cleared, not left dangling"
689 );
690 sub.validate().unwrap();
691 }
692
693 #[test]
694 fn merge_bus_remaps_regulated_bus_and_area_slack() {
695 let mut net = two_area_net();
696 net.generators.push(gen_regulating(1, 3)); net.areas.push(Area {
698 number: 1,
699 slack_bus: Some(BusId(3)),
700 net_interchange: 0.0,
701 tolerance: 0.0,
702 name: None,
703 });
704 net.merge_bus(BusId(2), BusId(3)); assert_eq!(
706 net.generators[0].regulated_bus,
707 Some(BusId(2)),
708 "the regulated bus follows the merge"
709 );
710 assert_eq!(
711 net.areas[0].slack_bus,
712 Some(BusId(2)),
713 "the area swing follows the merge"
714 );
715 net.validate().unwrap();
716 }
717
718 #[test]
719 fn reduce_passthrough_keeps_a_generator_regulated_bus() {
720 let mut net = Network::in_memory(
723 "net",
724 100.0,
725 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
726 vec![line(1, 2), line(2, 3)],
727 );
728 net.generators.push(gen_regulating(1, 2));
729 assert_eq!(net.reduce_passthrough_buses(), 0);
730 assert_eq!(net.buses.len(), 3);
731 net.validate().unwrap();
732 }
733
734 #[test]
735 fn subset_by_area_drops_out_of_scope_buses_and_cut_branches() {
736 let net = two_area_net();
737 let sel = Selector {
738 area: Some((1, 1)),
739 ..Selector::default()
740 };
741 let sub = net.subset(&sel, false);
742
743 assert_eq!(sub.buses.len(), 2);
745 assert!(sub.buses.iter().all(|b| b.area == 1));
746 assert_eq!(sub.branches.len(), 1, "only the intra-area line survives");
747 assert_eq!(sub.loads.len(), 1, "the area-2 load is dropped");
748 sub.validate().unwrap();
749 }
750
751 #[test]
752 fn subset_keep_boundary_pulls_in_the_tie_bus() {
753 let net = two_area_net();
754 let sel = Selector {
755 area: Some((1, 1)),
756 ..Selector::default()
757 };
758 let sub = net.subset(&sel, true);
759
760 assert_eq!(sub.buses.len(), 3);
762 assert_eq!(sub.branches.len(), 2);
763 let tie = sub.buses.iter().find(|b| b.id == BusId(3)).unwrap();
764 assert_eq!(tie.extras.get("tie_bus"), Some(&Value::Bool(true)));
765 assert_eq!(sub.loads.len(), 1);
767 sub.validate().unwrap();
768 }
769
770 #[test]
771 fn empty_selector_keeps_everything() {
772 let net = two_area_net();
773 let sub = net.subset(&Selector::default(), false);
774 assert_eq!(sub.buses.len(), net.buses.len());
775 assert_eq!(sub.branches.len(), net.branches.len());
776 }
777
778 #[test]
779 fn base_kv_range_filters_by_voltage() {
780 let mut net = two_area_net();
781 net.buses[2].base_kv = 115.0; let sel = Selector {
783 base_kv: Some((200.0, 300.0)),
784 ..Selector::default()
785 };
786 let sub = net.subset(&sel, false);
787 assert_eq!(sub.buses.len(), 2, "only the 230 kV buses match");
788 }
789
790 #[test]
791 fn merge_bus_rehomes_elements_and_drops_the_connecting_branch() {
792 let mut net = two_area_net(); net.merge_bus(BusId(2), BusId(3));
794
795 assert_eq!(net.buses.len(), 2, "bus 3 removed");
796 assert!(net.buses.iter().all(|b| b.id != BusId(3)));
797 assert_eq!(
798 net.branches.len(),
799 1,
800 "the 2-3 line collapsed to a self-loop"
801 );
802 assert_eq!(net.branches[0].from, BusId(1));
803 assert_eq!(net.branches[0].to, BusId(2));
804 assert_eq!(net.loads.len(), 2);
806 assert!(net.loads.iter().any(|l| l.bus == BusId(2)));
807 net.validate().unwrap();
808 }
809
810 #[test]
811 fn merge_bus_keeps_the_stronger_bus_kind() {
812 let mut net = two_area_net();
813 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();
816 assert_eq!(two.kind, BusType::Ref, "the slack designation is not lost");
817 }
818
819 #[test]
820 fn reduce_passthrough_folds_a_multi_section_line() {
821 let mut s1 = line(1, 2);
824 s1.rate_a = 100.0;
825 let mut s2 = line(2, 3);
826 s2.rate_a = 80.0;
827 let s3 = line(3, 4); let mut net = Network::in_memory(
829 "net",
830 100.0,
831 vec![
832 bus(1, 1, 230.0),
833 bus(2, 1, 230.0),
834 bus(3, 1, 230.0),
835 bus(4, 1, 230.0),
836 ],
837 vec![s1, s2, s3],
838 );
839
840 let removed = net.reduce_passthrough_buses();
841 assert_eq!(removed, 2, "both dummy buses collapse");
842 assert_eq!(net.buses.len(), 2);
843 assert!(
844 net.buses
845 .iter()
846 .all(|b| b.id == BusId(1) || b.id == BusId(4))
847 );
848 assert_eq!(net.branches.len(), 1, "one equivalent branch");
849 let eq = &net.branches[0];
850 assert_eq!(
851 [eq.from, eq.to].iter().copied().collect::<HashSet<_>>(),
852 [BusId(1), BusId(4)].into_iter().collect::<HashSet<_>>(),
853 );
854 assert!((eq.x - 0.3).abs() < 1e-9, "series reactance sums");
855 assert!(
856 (eq.rate_a - 80.0).abs() < 1e-9,
857 "the more limiting finite rating wins"
858 );
859 net.validate().unwrap();
860 }
861
862 #[test]
863 fn reduce_passthrough_keeps_a_bus_with_injection() {
864 let mut net = Network::in_memory(
866 "net",
867 100.0,
868 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
869 vec![line(1, 2), line(2, 3)],
870 );
871 net.loads.push(load(2));
872 assert_eq!(net.reduce_passthrough_buses(), 0);
873 assert_eq!(net.buses.len(), 3);
874 }
875
876 #[test]
877 fn reduce_passthrough_does_not_fold_across_a_transformer() {
878 let mut xfmr = line(2, 3);
880 xfmr.tap = 1.0;
881 let mut net = Network::in_memory(
882 "net",
883 100.0,
884 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
885 vec![line(1, 2), xfmr],
886 );
887 assert_eq!(net.reduce_passthrough_buses(), 0);
888 assert_eq!(net.buses.len(), 3);
889 }
890
891 #[test]
892 fn retype_isolated_marks_stranded_buses() {
893 let mut net = Network::in_memory(
895 "net",
896 100.0,
897 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
898 vec![line(1, 2)],
899 );
900 assert_eq!(net.retype_isolated_buses(), 1);
901 let three = net.buses.iter().find(|b| b.id == BusId(3)).unwrap();
902 assert_eq!(three.kind, BusType::Isolated);
903 let one = net.buses.iter().find(|b| b.id == BusId(1)).unwrap();
905 assert_eq!(one.kind, BusType::Pq);
906 net.validate().unwrap();
907 }
908
909 #[test]
910 fn retype_isolated_judges_in_service_equipment_only() {
911 let mut br = line(1, 2);
913 br.in_service = false;
914 let mut net = Network::in_memory(
915 "net",
916 100.0,
917 vec![bus(1, 1, 230.0), bus(2, 1, 230.0)],
918 vec![br],
919 );
920 assert_eq!(net.retype_isolated_buses(), 2);
921 assert!(net.buses.iter().all(|b| b.kind == BusType::Isolated));
922 }
923
924 #[test]
925 fn retype_isolated_is_idempotent() {
926 let mut net = Network::in_memory(
927 "net",
928 100.0,
929 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
930 vec![line(1, 2)],
931 );
932 assert_eq!(net.retype_isolated_buses(), 1);
933 assert_eq!(net.retype_isolated_buses(), 0, "second pass is a no-op");
934 }
935
936 #[test]
937 fn reduce_zero_impedance_collapses_jumpers_only() {
938 let mut jumper = line(2, 3);
940 jumper.x = 0.0;
941 let mut net = Network::in_memory(
942 "net",
943 100.0,
944 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
945 vec![line(1, 2), jumper],
946 );
947 net.loads.push(load(3));
948
949 let removed = net.reduce_zero_impedance(1e-9);
950 assert_eq!(removed, 1, "only the jumper is collapsed");
951 assert_eq!(net.buses.len(), 2);
952 assert_eq!(net.branches.len(), 1, "the real 1-2 line remains");
953 assert!(net.loads.iter().any(|l| l.bus == BusId(2)), "load re-homed");
954 net.validate().unwrap();
955 }
956
957 #[test]
958 fn reduce_zero_impedance_keeps_an_open_jumper() {
959 let mut jumper = line(2, 3);
962 jumper.x = 0.0;
963 jumper.in_service = false;
964 let mut net = Network::in_memory(
965 "net",
966 100.0,
967 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
968 vec![line(1, 2), jumper],
969 );
970
971 let removed = net.reduce_zero_impedance(1e-9);
972 assert_eq!(removed, 0, "an open jumper is left in place");
973 assert_eq!(net.buses.len(), 3);
974 assert_eq!(net.branches.len(), 2);
975 net.validate().unwrap();
976 }
977
978 #[test]
979 fn reduce_zero_impedance_keeps_a_3w_winding_pair() {
980 let mut jumper = line(2, 3);
984 jumper.x = 0.0;
985 let mut net = Network::in_memory(
986 "net",
987 100.0,
988 vec![bus(1, 1, 230.0), bus(2, 1, 138.0), bus(3, 1, 13.8)],
989 vec![line(1, 2), jumper],
990 );
991 net.transformers_3w.push(transformer_3w(1, 2, 3));
992
993 let removed = net.reduce_zero_impedance(1e-9);
994 assert_eq!(
995 removed, 0,
996 "a jumper across two windings of one 3W transformer is kept"
997 );
998 assert_eq!(net.buses.len(), 3);
999 net.validate().unwrap();
1000 }
1001
1002 #[test]
1003 fn in_place_mutations_invalidate_the_retained_source() {
1004 use std::sync::Arc;
1005 let retained = || Some(Arc::new("RETAINED".to_string()));
1006
1007 let mut jumper = line(2, 3);
1009 jumper.x = 0.0;
1010 let mut net = Network::in_memory(
1011 "net",
1012 100.0,
1013 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1014 vec![line(1, 2), jumper],
1015 );
1016 net.source = retained();
1017 assert_eq!(net.reduce_zero_impedance(1e-9), 1);
1018 assert!(net.source.is_none(), "a merge invalidates the source");
1019
1020 let mut net = Network::in_memory(
1022 "net",
1023 100.0,
1024 vec![bus(1, 1, 230.0), bus(2, 1, 230.0)],
1025 vec![line(1, 2)],
1026 );
1027 net.source = retained();
1028 assert_eq!(net.reduce_zero_impedance(1e-9), 0);
1029 assert!(net.source.is_some(), "a no-op leaves the source intact");
1030
1031 let mut net = Network::in_memory(
1033 "net",
1034 100.0,
1035 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1036 vec![line(1, 2), line(2, 3)],
1037 );
1038 net.source = retained();
1039 assert_eq!(net.reduce_passthrough_buses(), 1);
1040 assert!(net.source.is_none());
1041
1042 let mut net = Network::in_memory(
1044 "net",
1045 100.0,
1046 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1047 vec![line(1, 2)],
1048 );
1049 net.source = retained();
1050 assert_eq!(net.retype_isolated_buses(), 1);
1051 assert!(net.source.is_none());
1052
1053 let mut net = Network::in_memory("net", 100.0, vec![bus(1, 1, 230.0)], Vec::new());
1055 net.buses[0].vm = -1.0;
1056 net.source = retained();
1057 assert!(!net.repair().is_empty());
1058 assert!(net.source.is_none());
1059 }
1060}