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