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 static_var_compensators = self
159 .static_var_compensators()
160 .iter()
161 .filter(|svc| in_scope.contains(&svc.bus))
162 .cloned()
163 .collect::<Vec<_>>()
164 .into();
165 let mut generators: Vec<Generator> = self
166 .generators()
167 .iter()
168 .filter(|g| in_scope.contains(&g.bus))
169 .cloned()
170 .collect();
171 let storage = self
172 .storage()
173 .iter()
174 .filter(|s| in_scope.contains(&s.bus))
175 .cloned()
176 .collect::<Vec<_>>()
177 .into();
178
179 let mut branches: Vec<Branch> = self
180 .branches()
181 .iter()
182 .filter(|br| kept.contains(&br.from) && kept.contains(&br.to))
183 .cloned()
184 .collect();
185 let switches = self
186 .switches()
187 .iter()
188 .filter(|sw| kept.contains(&sw.from) && kept.contains(&sw.to))
189 .cloned()
190 .collect::<Vec<_>>()
191 .into();
192 let hvdc = self
193 .hvdc()
194 .iter()
195 .filter(|d| kept.contains(&d.from) && kept.contains(&d.to))
196 .cloned()
197 .collect::<Vec<_>>()
198 .into();
199 let mut transformers_3w = self
200 .transformers_3w()
201 .iter()
202 .filter(|t| t.windings.iter().all(|w| kept.contains(&w.bus)))
203 .cloned()
204 .collect::<Vec<_>>();
205
206 for br in &mut branches {
208 if let Some(c) = &mut br.control
209 && c.controlled_bus.is_some_and(|b| !kept.contains(&b))
210 {
211 c.controlled_bus = None;
212 }
213 }
214 for transformer in &mut transformers_3w {
215 for winding in &mut transformer.windings {
216 if let Some(control) = &mut winding.control
217 && control
218 .controlled_bus
219 .is_some_and(|bus| !kept.contains(&bus))
220 {
221 control.controlled_bus = None;
222 }
223 }
224 }
225 for sh in &mut shunts {
226 if let Some(c) = &mut sh.control
227 && c.control_bus.is_some_and(|b| !kept.contains(&b))
228 {
229 c.control_bus = None;
230 }
231 }
232 for g in &mut generators {
233 if g.regulated_bus.is_some_and(|b| !kept.contains(&b)) {
234 g.regulated_bus = None;
235 }
236 }
237
238 let kept_area_numbers: HashSet<usize> = buses.iter().map(|b| b.area).collect();
243 let areas = self
244 .areas()
245 .iter()
246 .filter(|a| kept_area_numbers.contains(&a.number))
247 .cloned()
248 .map(|mut a| {
249 if a.slack_bus.is_some_and(|b| !kept.contains(&b)) {
250 a.slack_bus = None;
251 }
252 a
253 })
254 .collect::<Vec<_>>();
255
256 let net = BalancedNetwork::from_tables(BalancedNetworkTables {
257 name: format!("{} (subset)", self.name()),
258 base_mva: self.base_mva(),
259 base_frequency: self.base_frequency(),
260 geo: self.geo().clone(),
261 case_metadata: self.case_metadata().clone(),
262 detailed_connectivity: self.detailed_connectivity().clone(),
263 generated_uids: self.generated_uids().clone(),
264 buses: buses.into(),
265 loads,
266 shunts: shunts.into(),
267 static_var_compensators,
268 branches: branches.into(),
269 switches,
270 generators: generators.into(),
271 storage,
272 hvdc,
273 transformers_3w: transformers_3w.into(),
274 areas: areas.into(),
275 solver: self.solver().clone(),
276 source_format: SourceFormat::InMemory,
277 });
278 debug_assert!(
279 net.validate().is_ok(),
280 "subset produced a dangling reference"
281 );
282 net
283 }
284
285 pub fn merge_bus(&mut self, into: BusId, from: BusId) {
294 if into == from {
295 return;
296 }
297 let remap = |b: &mut BusId| {
298 if *b == from {
299 *b = into;
300 }
301 };
302
303 for l in self.loads_mut() {
304 remap(&mut l.bus);
305 }
306 for s in self.shunts_mut() {
307 remap(&mut s.bus);
308 if let Some(cb) = s.control.as_mut().and_then(|c| c.control_bus.as_mut()) {
309 remap(cb);
310 }
311 }
312 for svc in self.static_var_compensators_mut() {
313 remap(&mut svc.bus);
314 }
315 for g in self.generators_mut() {
316 remap(&mut g.bus);
317 if let Some(rb) = g.regulated_bus.as_mut() {
318 remap(rb);
319 }
320 }
321 for st in self.storage_mut() {
322 remap(&mut st.bus);
323 }
324 for br in self.branches_mut() {
325 remap(&mut br.from);
326 remap(&mut br.to);
327 if let Some(cb) = br.control.as_mut().and_then(|c| c.controlled_bus.as_mut()) {
328 remap(cb);
329 }
330 }
331 self.branches_mut().retain(|b| b.from != b.to);
332 for sw in self.switches_mut() {
333 remap(&mut sw.from);
334 remap(&mut sw.to);
335 }
336 self.switches_mut().retain(|s| s.from != s.to);
337 for d in self.hvdc_mut() {
338 remap(&mut d.from);
339 remap(&mut d.to);
340 }
341 self.hvdc_mut().retain(|d| d.from != d.to);
342 for t in self.transformers_3w_mut() {
343 for w in &mut t.windings {
344 remap(&mut w.bus);
345 if let Some(controlled_bus) = w
346 .control
347 .as_mut()
348 .and_then(|control| control.controlled_bus.as_mut())
349 {
350 remap(controlled_bus);
351 }
352 }
353 }
354 for a in self.areas_mut() {
355 if let Some(slack) = a.slack_bus.as_mut() {
356 remap(slack);
357 }
358 }
359
360 let from_kind = self.buses().iter().find(|b| b.id == from).map(|b| b.kind);
362 self.buses_mut().retain(|b| b.id != from);
363 if let (Some(fk), Some(into_bus)) = (
364 from_kind,
365 self.buses_mut().iter_mut().find(|b| b.id == into),
366 ) && kind_priority(fk) > kind_priority(into_bus.kind)
367 {
368 into_bus.kind = fk;
369 }
370 }
372
373 pub fn reduce_zero_impedance(&mut self, threshold: f64) -> usize {
386 let before = self.branches().len();
387 while let Some((into, from)) = self.branches().iter().find_map(|b| {
389 (b.in_service
390 && !b.is_transformer()
391 && b.from != b.to
392 && b.r.hypot(b.x) <= threshold
393 && !self.shares_transformer_3w(b.from, b.to))
394 .then_some((b.from, b.to))
395 }) {
396 self.merge_bus(into, from);
397 }
398 before - self.branches().len()
399 }
400
401 fn shares_transformer_3w(&self, a: BusId, b: BusId) -> bool {
404 self.transformers_3w()
405 .iter()
406 .any(|t| t.windings.iter().any(|w| w.bus == a) && t.windings.iter().any(|w| w.bus == b))
407 }
408
409 pub fn reduce_passthrough_buses(&mut self) -> usize {
425 let mut collapsed = 0;
426 while let Some(mid) = self
429 .buses()
430 .iter()
431 .map(|b| b.id)
432 .find(|&m| self.is_passthrough(m))
433 {
434 self.collapse_passthrough(mid);
435 collapsed += 1;
436 }
437 collapsed
438 }
439
440 fn is_passthrough(&self, m: BusId) -> bool {
443 let Some(bus) = self.buses().iter().find(|b| b.id == m) else {
444 return false;
445 };
446 if bus.kind == BusType::Ref {
447 return false;
448 }
449 if self.loads().iter().any(|l| l.bus == m)
450 || self.generators().iter().any(|g| g.bus == m)
451 || self.shunts().iter().any(|s| s.bus == m)
452 || self
453 .static_var_compensators()
454 .iter()
455 .any(|svc| svc.bus == m)
456 || self.storage().iter().any(|s| s.bus == m)
457 || self.hvdc().iter().any(|d| d.from == m || d.to == m)
458 {
459 return false;
460 }
461 if self
462 .transformers_3w()
463 .iter()
464 .any(|t| t.windings.iter().any(|w| w.bus == m))
465 {
466 return false;
467 }
468 if self.areas().iter().any(|a| a.slack_bus == Some(m)) {
469 return false;
470 }
471 let controlled = self
472 .branches()
473 .iter()
474 .any(|b| b.control.as_ref().and_then(|c| c.controlled_bus) == Some(m));
475 let winding_controlled = self.transformers_3w().iter().any(|transformer| {
476 transformer.windings.iter().any(|winding| {
477 winding
478 .control
479 .as_ref()
480 .and_then(|control| control.controlled_bus)
481 == Some(m)
482 })
483 });
484 let regulated = self
485 .shunts()
486 .iter()
487 .any(|s| s.control.as_ref().and_then(|c| c.control_bus) == Some(m));
488 let gen_regulated = self.generators().iter().any(|g| g.regulated_bus == Some(m));
489 if controlled || winding_controlled || regulated || gen_regulated {
490 return false;
491 }
492 let incident: Vec<&Branch> = self
493 .branches()
494 .iter()
495 .filter(|b| b.from == m || b.to == m)
496 .collect();
497 if incident.len() != 2 {
498 return false;
499 }
500 let a = other_end(incident[0], m);
501 let c = other_end(incident[1], m);
502 incident.iter().all(|b| !b.is_transformer() && b.in_service) && a != m && c != m && a != c
503 }
504
505 fn collapse_passthrough(&mut self, m: BusId) {
508 let mut sections: Vec<Branch> = Vec::new();
509 self.branches_mut().retain(|b| {
510 if b.from == m || b.to == m {
511 sections.push(b.clone());
512 false
513 } else {
514 true
515 }
516 });
517 debug_assert_eq!(sections.len(), 2, "passthrough bus must have two sections");
518 let (s1, s2) = (§ions[0], §ions[1]);
519 let mut angmin = s1.angmin.max(s2.angmin);
526 let mut angmax = s1.angmax.min(s2.angmax);
527 if angmin > angmax {
528 angmin = s1.angmin.min(s2.angmin);
529 angmax = s1.angmax.max(s2.angmax);
530 }
531 self.branches_mut().push(Branch {
532 name: None,
533 from: other_end(s1, m),
534 to: other_end(s2, m),
535 r: s1.r + s2.r,
536 x: s1.x + s2.x,
537 b: s1.calc_total_charging_b() + s2.calc_total_charging_b(),
538 charging: None,
539 rate_a: combine_rate(s1.rate_a, s2.rate_a),
540 rate_b: combine_rate(s1.rate_b, s2.rate_b),
541 rate_c: combine_rate(s1.rate_c, s2.rate_c),
542 rating_sets: Vec::new(),
543 current_ratings: None,
544 tap: 0.0,
545 shift: 0.0,
546 in_service: true,
547 angmin,
548 angmax,
549 control: None,
550 solution: None,
551 uid: None,
552 route: None,
553 extras: Extras::new(),
554 });
555 self.buses_mut().retain(|b| b.id != m);
556 }
558
559 pub fn retype_isolated_buses(&mut self) -> usize {
571 let mut connected: HashSet<BusId> = HashSet::new();
572 for br in self.branches().iter().filter(|b| b.in_service) {
573 connected.insert(br.from);
574 connected.insert(br.to);
575 }
576 for d in self.hvdc().iter().filter(|d| d.in_service) {
577 connected.insert(d.from);
578 connected.insert(d.to);
579 }
580 for t in self.transformers_3w().iter().filter(|t| t.in_service) {
581 for w in &t.windings {
582 connected.insert(w.bus);
583 }
584 }
585 let mut retyped = 0;
586 for b in self.buses_mut() {
587 if b.kind != BusType::Isolated && !connected.contains(&b.id) {
588 b.kind = BusType::Isolated;
589 retyped += 1;
590 }
591 }
592 retyped
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601 use crate::network::{
602 Area, BusType, Extras, Generator, GeneratorEnergySource, Impedance, Load, Transformer3W,
603 Winding,
604 };
605
606 fn bus(id: usize, area: usize, base_kv: f64) -> Bus {
607 Bus {
608 id: BusId(id),
609 kind: BusType::Pq,
610 vm: 1.0,
611 va: 0.0,
612 base_kv,
613 vmax: 1.1,
614 vmin: 0.9,
615 evhi: None,
616 evlo: None,
617 area,
618 zone: 1,
619 name: None,
620 uid: None,
621 location: None,
622 extras: Extras::new(),
623 }
624 }
625
626 fn line(from: usize, to: usize) -> Branch {
627 Branch {
628 name: None,
629 from: BusId(from),
630 to: BusId(to),
631 r: 0.0,
632 x: 0.1,
633 b: 0.0,
634 charging: None,
635 rate_a: 0.0,
636 rate_b: 0.0,
637 rate_c: 0.0,
638 rating_sets: Vec::new(),
639 current_ratings: None,
640 tap: 0.0,
641 shift: 0.0,
642 in_service: true,
643 angmin: -360.0,
644 angmax: 360.0,
645 control: None,
646 solution: None,
647 uid: None,
648 route: None,
649 extras: Extras::new(),
650 }
651 }
652
653 fn load(bus: usize) -> Load {
654 Load {
655 bus: BusId(bus),
656 p: 10.0,
657 q: 5.0,
658 voltage_model: None,
659 in_service: true,
660 uid: None,
661 extras: Extras::new(),
662 }
663 }
664
665 fn two_area_net() -> BalancedNetwork {
668 let mut net = BalancedNetwork::in_memory(
669 "net",
670 100.0,
671 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 2, 230.0)],
672 vec![line(1, 2), line(2, 3)],
673 );
674 net.loads_mut().push(load(1));
675 net.loads_mut().push(load(3));
676 net
677 }
678
679 fn transformer_3w(a: usize, b: usize, c: usize) -> Transformer3W {
680 let winding = |bus| Winding {
681 bus: BusId(bus),
682 tap: 1.0,
683 shift: 0.0,
684 nominal_kv: 0.0,
685 rate_a: 0.0,
686 rate_b: 0.0,
687 rate_c: 0.0,
688 control: None,
689 };
690 let imp = Impedance {
691 r: 0.0,
692 x: 0.1,
693 base_mva: 100.0,
694 };
695 Transformer3W {
696 windings: [winding(a), winding(b), winding(c)],
697 z: [imp, imp, imp],
698 star_vm: 1.0,
699 star_va: 0.0,
700 mag_g: 0.0,
701 mag_b: 0.0,
702 in_service: true,
703 name: None,
704 uid: None,
705 extras: Extras::new(),
706 }
707 }
708
709 fn gen_regulating(bus: usize, regulated: usize) -> Generator {
710 Generator {
711 bus: BusId(bus),
712 energy_source: GeneratorEnergySource::default(),
713 pg: 10.0,
714 qg: 0.0,
715 pmax: 100.0,
716 pmin: 0.0,
717 qmax: 50.0,
718 qmin: -50.0,
719 vg: 1.0,
720 mbase: 100.0,
721 in_service: true,
722 cost: None,
723 caps: Default::default(),
724 voltage_regulation_on: true,
725 regulating_terminal: None,
726 regulated_bus: Some(BusId(regulated)),
727 active_power_control: None,
728 uid: None,
729 }
730 }
731
732 #[test]
733 fn subset_clears_a_regulated_bus_outside_the_kept_set() {
734 let mut net = two_area_net();
736 net.generators_mut().push(gen_regulating(1, 3));
737 let sel = Selector {
738 area: Some((1, 1)),
739 ..Selector::default()
740 };
741 let sub = net.subset(&sel, false);
742 assert_eq!(sub.generators().len(), 1);
743 assert_eq!(
744 sub.generators()[0].regulated_bus,
745 None,
746 "the dropped remote regulated bus is cleared, not left dangling"
747 );
748 sub.validate().unwrap();
749 }
750
751 #[test]
752 fn merge_bus_remaps_regulated_bus_and_area_slack() {
753 let mut net = two_area_net();
754 net.generators_mut().push(gen_regulating(1, 3)); net.areas_mut().push(Area {
756 number: 1,
757 slack_bus: Some(BusId(3)),
758 net_interchange: 0.0,
759 tolerance: 0.0,
760 name: None,
761 uid: None,
762 area_type: None,
763 });
764 net.merge_bus(BusId(2), BusId(3)); assert_eq!(
766 net.generators()[0].regulated_bus,
767 Some(BusId(2)),
768 "the regulated bus follows the merge"
769 );
770 assert_eq!(
771 net.areas()[0].slack_bus,
772 Some(BusId(2)),
773 "the area swing follows the merge"
774 );
775 net.validate().unwrap();
776 }
777
778 #[test]
779 fn reduce_passthrough_keeps_a_generator_regulated_bus() {
780 let mut net = BalancedNetwork::in_memory(
783 "net",
784 100.0,
785 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
786 vec![line(1, 2), line(2, 3)],
787 );
788 net.generators_mut().push(gen_regulating(1, 2));
789 assert_eq!(net.reduce_passthrough_buses(), 0);
790 assert_eq!(net.buses().len(), 3);
791 net.validate().unwrap();
792 }
793
794 #[test]
795 fn subset_by_area_drops_out_of_scope_buses_and_cut_branches() {
796 let net = two_area_net();
797 let sel = Selector {
798 area: Some((1, 1)),
799 ..Selector::default()
800 };
801 let sub = net.subset(&sel, false);
802
803 assert_eq!(sub.buses().len(), 2);
805 assert!(sub.buses().iter().all(|b| b.area == 1));
806 assert_eq!(sub.branches().len(), 1, "only the intra-area line survives");
807 assert_eq!(sub.loads().len(), 1, "the area-2 load is dropped");
808 sub.validate().unwrap();
809 }
810
811 #[test]
812 fn subset_keep_boundary_pulls_in_the_tie_bus() {
813 let net = two_area_net();
814 let sel = Selector {
815 area: Some((1, 1)),
816 ..Selector::default()
817 };
818 let sub = net.subset(&sel, true);
819
820 assert_eq!(sub.buses().len(), 3);
822 assert_eq!(sub.branches().len(), 2);
823 let tie = sub.buses().iter().find(|b| b.id == BusId(3)).unwrap();
824 assert_eq!(tie.extras.get("tie_bus"), Some(&Value::Bool(true)));
825 assert_eq!(sub.loads().len(), 1);
827 sub.validate().unwrap();
828 }
829
830 #[test]
831 fn empty_selector_keeps_everything() {
832 let net = two_area_net();
833 let sub = net.subset(&Selector::default(), false);
834 assert_eq!(sub.buses().len(), net.buses().len());
835 assert_eq!(sub.branches().len(), net.branches().len());
836 }
837
838 #[test]
839 fn base_kv_range_filters_by_voltage() {
840 let mut net = two_area_net();
841 net.buses_mut()[2].base_kv = 115.0; let sel = Selector {
843 base_kv: Some((200.0, 300.0)),
844 ..Selector::default()
845 };
846 let sub = net.subset(&sel, false);
847 assert_eq!(sub.buses().len(), 2, "only the 230 kV buses match");
848 }
849
850 #[test]
851 fn merge_bus_rehomes_elements_and_drops_the_connecting_branch() {
852 let mut net = two_area_net(); net.merge_bus(BusId(2), BusId(3));
854
855 assert_eq!(net.buses().len(), 2, "bus 3 removed");
856 assert!(net.buses().iter().all(|b| b.id != BusId(3)));
857 assert_eq!(
858 net.branches().len(),
859 1,
860 "the 2-3 line collapsed to a self-loop"
861 );
862 assert_eq!(net.branches()[0].from, BusId(1));
863 assert_eq!(net.branches()[0].to, BusId(2));
864 assert_eq!(net.loads().len(), 2);
866 assert!(net.loads().iter().any(|l| l.bus == BusId(2)));
867 net.validate().unwrap();
868 }
869
870 #[test]
871 fn merge_bus_keeps_the_stronger_bus_kind() {
872 let mut net = two_area_net();
873 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();
876 assert_eq!(two.kind, BusType::Ref, "the slack designation is not lost");
877 }
878
879 #[test]
880 fn reduce_passthrough_folds_a_multi_section_line() {
881 let mut s1 = line(1, 2);
884 s1.rate_a = 100.0;
885 let mut s2 = line(2, 3);
886 s2.rate_a = 80.0;
887 let s3 = line(3, 4); let mut net = BalancedNetwork::in_memory(
889 "net",
890 100.0,
891 vec![
892 bus(1, 1, 230.0),
893 bus(2, 1, 230.0),
894 bus(3, 1, 230.0),
895 bus(4, 1, 230.0),
896 ],
897 vec![s1, s2, s3],
898 );
899
900 let removed = net.reduce_passthrough_buses();
901 assert_eq!(removed, 2, "both dummy buses collapse");
902 assert_eq!(net.buses().len(), 2);
903 assert!(
904 net.buses()
905 .iter()
906 .all(|b| b.id == BusId(1) || b.id == BusId(4))
907 );
908 assert_eq!(net.branches().len(), 1, "one equivalent branch");
909 let eq = &net.branches()[0];
910 assert_eq!(
911 [eq.from, eq.to].iter().copied().collect::<HashSet<_>>(),
912 [BusId(1), BusId(4)].into_iter().collect::<HashSet<_>>(),
913 );
914 assert!((eq.x - 0.3).abs() < 1e-9, "series reactance sums");
915 assert!(
916 (eq.rate_a - 80.0).abs() < 1e-9,
917 "the more limiting finite rating wins"
918 );
919 net.validate().unwrap();
920 }
921
922 #[test]
923 fn reduce_passthrough_keeps_a_bus_with_injection() {
924 let mut net = BalancedNetwork::in_memory(
926 "net",
927 100.0,
928 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
929 vec![line(1, 2), line(2, 3)],
930 );
931 net.loads_mut().push(load(2));
932 assert_eq!(net.reduce_passthrough_buses(), 0);
933 assert_eq!(net.buses().len(), 3);
934 }
935
936 #[test]
937 fn reduce_passthrough_does_not_fold_across_a_transformer() {
938 let mut xfmr = line(2, 3);
940 xfmr.tap = 1.0;
941 let mut net = BalancedNetwork::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), xfmr],
946 );
947 assert_eq!(net.reduce_passthrough_buses(), 0);
948 assert_eq!(net.buses().len(), 3);
949 }
950
951 #[test]
952 fn retype_isolated_marks_stranded_buses() {
953 let mut net = BalancedNetwork::in_memory(
955 "net",
956 100.0,
957 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
958 vec![line(1, 2)],
959 );
960 assert_eq!(net.retype_isolated_buses(), 1);
961 let three = net.buses().iter().find(|b| b.id == BusId(3)).unwrap();
962 assert_eq!(three.kind, BusType::Isolated);
963 let one = net.buses().iter().find(|b| b.id == BusId(1)).unwrap();
965 assert_eq!(one.kind, BusType::Pq);
966 net.validate().unwrap();
967 }
968
969 #[test]
970 fn retype_isolated_judges_in_service_equipment_only() {
971 let mut br = line(1, 2);
973 br.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)],
978 vec![br],
979 );
980 assert_eq!(net.retype_isolated_buses(), 2);
981 assert!(net.buses().iter().all(|b| b.kind == BusType::Isolated));
982 }
983
984 #[test]
985 fn retype_isolated_is_idempotent() {
986 let mut net = BalancedNetwork::in_memory(
987 "net",
988 100.0,
989 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
990 vec![line(1, 2)],
991 );
992 assert_eq!(net.retype_isolated_buses(), 1);
993 assert_eq!(net.retype_isolated_buses(), 0, "second pass is a no-op");
994 }
995
996 #[test]
997 fn reduce_zero_impedance_collapses_jumpers_only() {
998 let mut jumper = line(2, 3);
1000 jumper.x = 0.0;
1001 let mut net = BalancedNetwork::in_memory(
1002 "net",
1003 100.0,
1004 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1005 vec![line(1, 2), jumper],
1006 );
1007 net.loads_mut().push(load(3));
1008
1009 let removed = net.reduce_zero_impedance(1e-9);
1010 assert_eq!(removed, 1, "only the jumper is collapsed");
1011 assert_eq!(net.buses().len(), 2);
1012 assert_eq!(net.branches().len(), 1, "the real 1-2 line remains");
1013 assert!(
1014 net.loads().iter().any(|l| l.bus == BusId(2)),
1015 "load re-homed"
1016 );
1017 net.validate().unwrap();
1018 }
1019
1020 #[test]
1021 fn reduce_zero_impedance_keeps_an_open_jumper() {
1022 let mut jumper = line(2, 3);
1025 jumper.x = 0.0;
1026 jumper.in_service = false;
1027 let mut net = BalancedNetwork::in_memory(
1028 "net",
1029 100.0,
1030 vec![bus(1, 1, 230.0), bus(2, 1, 230.0), bus(3, 1, 230.0)],
1031 vec![line(1, 2), jumper],
1032 );
1033
1034 let removed = net.reduce_zero_impedance(1e-9);
1035 assert_eq!(removed, 0, "an open jumper is left in place");
1036 assert_eq!(net.buses().len(), 3);
1037 assert_eq!(net.branches().len(), 2);
1038 net.validate().unwrap();
1039 }
1040
1041 #[test]
1042 fn reduce_zero_impedance_keeps_a_3w_winding_pair() {
1043 let mut jumper = line(2, 3);
1047 jumper.x = 0.0;
1048 let mut net = BalancedNetwork::in_memory(
1049 "net",
1050 100.0,
1051 vec![bus(1, 1, 230.0), bus(2, 1, 138.0), bus(3, 1, 13.8)],
1052 vec![line(1, 2), jumper],
1053 );
1054 net.transformers_3w_mut().push(transformer_3w(1, 2, 3));
1055
1056 let removed = net.reduce_zero_impedance(1e-9);
1057 assert_eq!(
1058 removed, 0,
1059 "a jumper across two windings of one 3W transformer is kept"
1060 );
1061 assert_eq!(net.buses().len(), 3);
1062 net.validate().unwrap();
1063 }
1064}