1use std::collections::{BTreeSet, HashMap};
13use std::fmt::Write as _;
14
15use crate::diagnostics::{Diagnostic, codes};
16use crate::network::{BalancedNetwork, BusId, BusType, Generator};
17
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum IslandReferencePolicy {
23 #[default]
27 Stated,
28 PerIsland,
37}
38
39#[derive(Clone, Debug, PartialEq)]
42#[non_exhaustive]
43pub struct Island {
44 pub buses: Vec<BusId>,
46 pub references: Vec<BusId>,
48 pub generators: Vec<usize>,
50 pub hvdc: Vec<usize>,
53}
54
55impl Island {
56 #[must_use]
58 pub fn is_supplied(&self) -> bool {
59 !self.generators.is_empty()
60 }
61
62 #[must_use]
72 pub fn calc_reference_choice(&self, generators: &[Generator]) -> Option<BusId> {
73 match self.supplied_references(generators).as_slice() {
74 [bus] => Some(*bus),
75 [] => largest_generator_bus(generators, &self.generators),
76 references => kept_reference(references, generators, &self.generators),
77 }
78 }
79
80 fn supplied_references(&self, generators: &[Generator]) -> Vec<BusId> {
83 self.references
84 .iter()
85 .copied()
86 .filter(|&bus| {
87 self.generators
88 .iter()
89 .any(|&row| generators[row].bus == bus)
90 })
91 .collect()
92 }
93}
94
95#[derive(Clone, Debug, Default, PartialEq)]
97#[non_exhaustive]
98pub struct IslandPartition {
99 pub islands: Vec<Island>,
102 pub isolated: Vec<BusId>,
104 island_of: HashMap<BusId, usize>,
105}
106
107impl IslandPartition {
108 #[must_use]
111 pub fn island_of(&self, bus: BusId) -> Option<usize> {
112 self.island_of.get(&bus).copied()
113 }
114}
115
116#[derive(Clone, Debug, Default, PartialEq)]
118#[non_exhaustive]
119pub struct IslandReferenceReport {
120 pub partition: IslandPartition,
122 pub designated: Vec<BusId>,
124 pub demoted: Vec<BusId>,
127 pub de_energized: Vec<BusId>,
129 pub diagnostics: Vec<Diagnostic>,
131}
132
133fn pmax_key(generator: &Generator) -> f64 {
136 if generator.pmax.is_nan() {
137 f64::NEG_INFINITY
138 } else {
139 generator.pmax
140 }
141}
142
143pub(crate) fn largest_generator_bus(generators: &[Generator], rows: &[usize]) -> Option<BusId> {
147 rows.iter()
148 .copied()
149 .max_by(|&a, &b| {
150 pmax_key(&generators[a])
151 .total_cmp(&pmax_key(&generators[b]))
152 .then(generators[b].bus.cmp(&generators[a].bus))
153 })
154 .map(|row| generators[row].bus)
155}
156
157pub(crate) fn kept_reference(
160 references: &[BusId],
161 generators: &[Generator],
162 rows: &[usize],
163) -> Option<BusId> {
164 let capacity = |bus: BusId| -> f64 {
165 rows.iter()
166 .map(|&row| &generators[row])
167 .filter(|generator| generator.bus == bus && !generator.pmax.is_nan())
168 .map(|generator| generator.pmax)
169 .sum()
170 };
171 references
172 .iter()
173 .copied()
174 .max_by(|&a, &b| capacity(a).total_cmp(&capacity(b)).then(b.cmp(&a)))
175}
176
177pub(crate) fn bus_list(buses: &[BusId]) -> String {
179 const SHOWN: usize = 5;
180 let mut text = buses
181 .iter()
182 .take(SHOWN)
183 .map(ToString::to_string)
184 .collect::<Vec<_>>()
185 .join(", ");
186 if buses.len() > SHOWN {
187 let _ = write!(text, " and {} more", buses.len() - SHOWN);
188 }
189 text
190}
191
192impl BalancedNetwork {
193 #[must_use]
207 pub fn calc_islands(&self) -> IslandPartition {
208 let buses = self.buses();
209 let index_of: HashMap<BusId, usize> = buses
210 .iter()
211 .enumerate()
212 .filter(|(_, bus)| bus.kind != BusType::Isolated)
213 .map(|(index, bus)| (bus.id, index))
214 .collect();
215 let mut sets = petgraph::unionfind::UnionFind::new(buses.len());
216 let mut join = |a: BusId, b: BusId| {
217 if let (Some(&i), Some(&j)) = (index_of.get(&a), index_of.get(&b)) {
218 sets.union(i, j);
219 }
220 };
221 for branch in self.branches().iter().filter(|branch| branch.in_service) {
222 join(branch.from, branch.to);
223 }
224 for switch in self.switches().iter().filter(|switch| switch.closed) {
225 join(switch.from, switch.to);
226 }
227 for transformer in self.transformers_3w().iter().filter(|t| t.in_service) {
228 let connected: Vec<BusId> = transformer
229 .windings
230 .iter()
231 .zip(transformer.winding_in_service())
232 .filter(|(_, connected)| *connected)
233 .map(|(winding, _)| winding.bus)
234 .collect();
235 for (i, &a) in connected.iter().enumerate() {
236 for &b in &connected[i + 1..] {
237 join(a, b);
238 }
239 }
240 }
241 let labels = sets.into_labeling();
242
243 let mut by_root: HashMap<usize, Island> = HashMap::new();
244 let mut isolated = Vec::new();
245 for (index, bus) in buses.iter().enumerate() {
246 if bus.kind == BusType::Isolated {
247 isolated.push(bus.id);
248 continue;
249 }
250 let island = by_root.entry(labels[index]).or_insert_with(|| Island {
251 buses: Vec::new(),
252 references: Vec::new(),
253 generators: Vec::new(),
254 hvdc: Vec::new(),
255 });
256 island.buses.push(bus.id);
257 if bus.kind == BusType::Ref {
258 island.references.push(bus.id);
259 }
260 }
261 self.attach_generators_and_hvdc(&mut by_root, &labels, &index_of);
262 let mut islands: Vec<Island> = by_root.into_values().collect();
263 for island in &mut islands {
264 island.buses.sort_unstable();
265 island.references.sort_unstable();
266 }
267 islands.sort_by(|a, b| {
268 b.buses
269 .len()
270 .cmp(&a.buses.len())
271 .then(a.buses[0].cmp(&b.buses[0]))
272 });
273 isolated.sort_unstable();
274 let island_of = islands
275 .iter()
276 .enumerate()
277 .flat_map(|(position, island)| island.buses.iter().map(move |&bus| (bus, position)))
278 .collect();
279 IslandPartition {
280 islands,
281 isolated,
282 island_of,
283 }
284 }
285
286 fn attach_generators_and_hvdc(
290 &self,
291 by_root: &mut HashMap<usize, Island>,
292 labels: &[usize],
293 index_of: &HashMap<BusId, usize>,
294 ) {
295 for (row, generator) in self.generators().iter().enumerate() {
296 if let Some(&index) = index_of.get(&generator.bus)
297 && generator.in_service
298 && let Some(island) = by_root.get_mut(&labels[index])
299 {
300 island.generators.push(row);
301 }
302 }
303 for (row, line) in self.hvdc().iter().enumerate() {
304 if let (Some(&from), Some(&to)) = (index_of.get(&line.from), index_of.get(&line.to))
305 && line.in_service
306 && labels[from] != labels[to]
307 {
308 for end in [from, to] {
309 if let Some(island) = by_root.get_mut(&labels[end]) {
310 island.hvdc.push(row);
311 }
312 }
313 }
314 }
315 }
316
317 #[allow(clippy::too_many_lines)] pub fn assign_island_references(
342 &mut self,
343 policy: IslandReferencePolicy,
344 ) -> IslandReferenceReport {
345 let partition = self.calc_islands();
346 if policy == IslandReferencePolicy::Stated {
347 return IslandReferenceReport {
348 partition,
349 ..IslandReferenceReport::default()
350 };
351 }
352
353 let mut designated = Vec::new();
354 let mut demoted: Vec<(BusId, BusType)> = Vec::new();
355 let mut dead: BTreeSet<BusId> = BTreeSet::new();
356 let mut diagnostics = Vec::new();
357 for island in &partition.islands {
358 if !island.is_supplied() {
359 let load: f64 = self
360 .loads()
361 .iter()
362 .filter(|load| load.in_service && island.buses.binary_search(&load.bus).is_ok())
363 .map(|load| load.p)
364 .sum();
365 diagnostics.push(self.unsupplied_island_finding(
366 island,
367 &format!(
368 "was de-energized; its {load} MW of in-service load is no longer served"
369 ),
370 ));
371 dead.extend(island.buses.iter().copied());
372 continue;
373 }
374 let supplied = island.supplied_references(self.generators());
375 let unsupplied: Vec<BusId> = island
376 .references
377 .iter()
378 .copied()
379 .filter(|bus| !supplied.contains(bus))
380 .collect();
381 if !unsupplied.is_empty() {
382 diagnostics.push(Diagnostic::of(
383 &codes::CANONICALIZE_ISLAND_REFERENCE_DEMOTED,
384 format!(
385 "the island of {} bus(es) ({}) states reference bus(es) {} that host no \
386 in-service generator and cannot balance it; they were demoted to PQ",
387 island.buses.len(),
388 bus_list(&island.buses),
389 bus_list(&unsupplied)
390 ),
391 ));
392 demoted.extend(unsupplied.iter().map(|&bus| (bus, BusType::Pq)));
393 }
394 let Some(kept) = island.calc_reference_choice(self.generators()) else {
395 continue;
396 };
397 match supplied.as_slice() {
398 [_] => {}
399 [] => {
400 diagnostics.push(Diagnostic::of(
401 &codes::CANONICALIZE_ISLAND_REFERENCE_DESIGNATED,
402 format!(
403 "the island of {} bus(es) ({}) states no reference bus that hosts an \
404 in-service generator; bus {kept} hosts its largest pmax in-service \
405 generator and was designated the reference",
406 island.buses.len(),
407 bus_list(&island.buses)
408 ),
409 ));
410 designated.push(kept);
411 }
412 references => {
413 let others: Vec<BusId> = references
414 .iter()
415 .copied()
416 .filter(|&bus| bus != kept)
417 .collect();
418 diagnostics.push(Diagnostic::of(
419 &codes::CANONICALIZE_ISLAND_REFERENCE_DEMOTED,
420 format!(
421 "the island of {} bus(es) states {} reference buses that host \
422 generation; bus {kept} hosts the most and stays the reference, and \
423 bus(es) {} were demoted to PV",
424 island.buses.len(),
425 references.len(),
426 bus_list(&others)
427 ),
428 ));
429 demoted.extend(others.into_iter().map(|bus| (bus, BusType::Pv)));
430 }
431 }
432 }
433
434 if !designated.is_empty() || !demoted.is_empty() {
435 let designated_set: BTreeSet<BusId> = designated.iter().copied().collect();
436 let demoted_kind: HashMap<BusId, BusType> = demoted.iter().copied().collect();
437 for bus in self.buses_mut() {
438 if designated_set.contains(&bus.id) {
439 bus.kind = BusType::Ref;
440 } else if let Some(&kind) = demoted_kind.get(&bus.id) {
441 bus.kind = kind;
442 }
443 }
444 }
445 if !dead.is_empty() {
446 self.de_energize_buses(&dead);
447 }
448
449 let mut demoted: Vec<BusId> = demoted.into_iter().map(|(bus, _)| bus).collect();
450 demoted.sort_unstable();
451 IslandReferenceReport {
452 partition: self.calc_islands(),
453 designated,
454 demoted,
455 de_energized: dead.into_iter().collect(),
456 diagnostics,
457 }
458 }
459
460 pub(crate) fn unsupplied_island_finding(&self, island: &Island, outcome: &str) -> Diagnostic {
465 let size = island.buses.len();
466 let buses = bus_list(&island.buses);
467 if island.hvdc.is_empty() {
468 return Diagnostic::of(
469 &codes::CANONICALIZE_ISLAND_DE_ENERGIZED,
470 format!(
471 "the island of {size} bus(es) ({buses}) has no in-service generator and {outcome}"
472 ),
473 );
474 }
475 let inside = |bus: BusId| island.buses.binary_search(&bus).is_ok();
476 let mut infeed = 0.0;
477 let mut lines = Vec::with_capacity(island.hvdc.len());
478 for &row in &island.hvdc {
479 let line = &self.hvdc()[row];
480 infeed += if inside(line.to) { line.pt } else { -line.pf };
483 lines.push(format!("{}-{}", line.from, line.to));
484 }
485 Diagnostic::of(
486 &codes::CANONICALIZE_ISLAND_HVDC_FED_DE_ENERGIZED,
487 format!(
488 "the island of {size} bus(es) ({buses}) has no in-service generator and {outcome}; \
489 {} in-service HVDC line(s) ({}) schedule {infeed} MW into it, and only \
490 generators count as supply, so they were taken out of service with it and the \
491 other island(s) no longer exchange that power. To keep the island, model the \
492 infeed as a generator at its converter bus",
493 lines.len(),
494 lines.join(", ")
495 ),
496 )
497 }
498
499 pub fn keep_island(&mut self, island: &BTreeSet<BusId>) -> Vec<BusId> {
510 let others: BTreeSet<BusId> = self
511 .buses()
512 .iter()
513 .filter(|bus| bus.kind != BusType::Isolated && !island.contains(&bus.id))
514 .map(|bus| bus.id)
515 .collect();
516 if !others.is_empty() {
517 self.de_energize_buses(&others);
518 }
519 others.into_iter().collect()
520 }
521
522 pub fn de_energize_buses(&mut self, buses: &BTreeSet<BusId>) {
537 let at = |bus: BusId| buses.contains(&bus);
538 if self
539 .buses()
540 .iter()
541 .any(|b| b.kind != BusType::Isolated && at(b.id))
542 {
543 for bus in self.buses_mut().iter_mut().filter(|b| at(b.id)) {
544 bus.kind = BusType::Isolated;
545 }
546 }
547 if self.generators().iter().any(|g| g.in_service && at(g.bus)) {
548 for generator in self.generators_mut().iter_mut().filter(|g| at(g.bus)) {
549 generator.in_service = false;
550 }
551 }
552 if self.loads().iter().any(|l| l.in_service && at(l.bus)) {
553 for load in self.loads_mut().iter_mut().filter(|l| at(l.bus)) {
554 load.in_service = false;
555 }
556 }
557 if self.shunts().iter().any(|s| s.in_service && at(s.bus)) {
558 for shunt in self.shunts_mut().iter_mut().filter(|s| at(s.bus)) {
559 shunt.in_service = false;
560 }
561 }
562 if self
563 .static_var_compensators()
564 .iter()
565 .any(|s| s.in_service && at(s.bus))
566 {
567 for svc in self
568 .static_var_compensators_mut()
569 .iter_mut()
570 .filter(|s| at(s.bus))
571 {
572 svc.in_service = false;
573 }
574 }
575 if self.storage().iter().any(|s| s.in_service && at(s.bus)) {
576 for storage in self.storage_mut().iter_mut().filter(|s| at(s.bus)) {
577 storage.in_service = false;
578 }
579 }
580 let touches = |from: BusId, to: BusId| at(from) || at(to);
581 if self
582 .branches()
583 .iter()
584 .any(|b| b.in_service && touches(b.from, b.to))
585 {
586 for branch in self
587 .branches_mut()
588 .iter_mut()
589 .filter(|b| touches(b.from, b.to))
590 {
591 branch.in_service = false;
592 }
593 }
594 if self
595 .hvdc()
596 .iter()
597 .any(|d| d.in_service && touches(d.from, d.to))
598 {
599 for line in self.hvdc_mut().iter_mut().filter(|d| touches(d.from, d.to)) {
600 line.in_service = false;
601 }
602 }
603 let live_after = |t: &crate::network::Transformer3W| -> [bool; 3] {
606 let mut live = t.winding_in_service();
607 for (state, winding) in live.iter_mut().zip(&t.windings) {
608 *state &= !at(winding.bus);
609 }
610 live
611 };
612 if self
613 .transformers_3w()
614 .iter()
615 .any(|t| t.in_service && live_after(t) != t.winding_in_service())
616 {
617 for transformer in self.transformers_3w_mut().iter_mut() {
618 let live = live_after(transformer);
619 if live == transformer.winding_in_service() {
620 continue;
621 }
622 if live.iter().filter(|&&on| on).count() < 2 {
623 transformer.in_service = false;
624 } else {
625 transformer.set_winding_in_service(live);
626 }
627 }
628 }
629 }
630}
631
632#[cfg(test)]
633mod tests {
634 use super::*;
635 use crate::network::{Branch, Bus, Hvdc, Impedance, Load, Switch, Transformer3W, Winding};
636 use crate::{ClosedSwitchPolicy, IslandReferencePolicy, NormalizeOptions, NormalizeSettings};
637
638 fn generator(bus: usize, pmax: f64) -> Generator {
639 let mut generator = Generator::new(BusId(bus));
640 generator.pmax = pmax;
641 generator.pg = pmax / 2.0;
642 generator
643 }
644
645 fn three_islands() -> BalancedNetwork {
649 let mut buses: Vec<Bus> = (1..=9)
650 .map(|id| Bus::new(BusId(id), BusType::Pq, 230.0))
651 .collect();
652 buses[0].kind = BusType::Ref;
653 buses[7].kind = BusType::Isolated;
654 let line = |from, to| Branch::new(BusId(from), BusId(to), 0.01, 0.1);
655 let mut net = BalancedNetwork::in_memory(
656 "islands",
657 100.0,
658 buses,
659 vec![line(1, 2), line(2, 9), line(3, 4), line(6, 7), line(7, 8)],
660 );
661 net.switches_mut()
662 .push(Switch::new(BusId(4), BusId(5), true));
663 net.generators_mut()
664 .extend([generator(1, 200.0), generator(4, 50.0), generator(5, 80.0)]);
665 net.loads_mut().extend([
666 Load::new(BusId(2), 60.0, 0.0),
667 Load::new(BusId(3), 40.0, 0.0),
668 Load::new(BusId(7), 25.0, 0.0),
669 ]);
670 let mut tie = Hvdc::new(BusId(1), BusId(6));
672 tie.pf = 30.0;
673 tie.pt = 29.0;
674 let inside = Hvdc::new(BusId(1), BusId(2));
676 net.hvdc_mut().extend([tie, inside]);
677 net
678 }
679
680 fn kind(net: &BalancedNetwork, id: usize) -> BusType {
681 net.buses().iter().find(|b| b.id == BusId(id)).unwrap().kind
682 }
683
684 fn codes(diagnostics: &[Diagnostic]) -> Vec<&str> {
685 diagnostics.iter().map(Diagnostic::code).collect()
686 }
687
688 #[test]
689 fn islands_follow_branches_closed_switches_and_not_hvdc() {
690 let net = three_islands();
691 let partition = net.calc_islands();
692 let buses: Vec<Vec<usize>> = partition
693 .islands
694 .iter()
695 .map(|island| island.buses.iter().map(|b| b.0).collect())
696 .collect();
697 assert_eq!(buses, [vec![1, 2, 9], vec![3, 4, 5], vec![6, 7]]);
698 assert_eq!(partition.isolated, [BusId(8)]);
699 assert_eq!(partition.islands[0].references, [BusId(1)]);
700 assert_eq!(partition.islands[1].generators, [1, 2]);
701 assert!(!partition.islands[2].is_supplied());
702 assert_eq!(partition.island_of(BusId(5)), Some(1));
703 assert_eq!(partition.island_of(BusId(8)), None);
704 assert_eq!(partition.islands[0].hvdc, [0]);
707 assert_eq!(partition.islands[1].hvdc, [] as [usize; 0]);
708 assert_eq!(partition.islands[2].hvdc, [0]);
709 }
710
711 #[test]
712 fn a_three_winding_transformer_joins_its_windings_while_in_service() {
713 let buses = (1..=3)
714 .map(|id| Bus::new(BusId(id), BusType::Pq, 230.0))
715 .collect();
716 let mut net = BalancedNetwork::in_memory("t3w", 100.0, buses, Vec::new());
717 net.transformers_3w_mut().push(Transformer3W::new(
718 [1, 2, 3].map(|bus| Winding::new(BusId(bus))),
719 [Impedance::new(0.0, 0.1, 100.0); 3],
720 ));
721 assert_eq!(net.calc_islands().islands.len(), 1);
722 net.transformers_3w_mut()[0].in_service = false;
723 assert_eq!(net.calc_islands().islands.len(), 3);
724 }
725
726 #[test]
727 fn a_winding_on_an_isolated_bus_leaves_the_other_two_joined() {
728 let mut buses: Vec<Bus> = (1..=3)
731 .map(|id| Bus::new(BusId(id), BusType::Pq, 230.0))
732 .collect();
733 buses[0].kind = BusType::Ref;
734 buses[1].kind = BusType::Isolated;
735 let mut net = BalancedNetwork::in_memory("t3w", 100.0, buses, Vec::new());
736 net.transformers_3w_mut().push(Transformer3W::new(
737 [1, 2, 3].map(|bus| Winding::new(BusId(bus))),
738 [Impedance::new(0.0, 0.1, 100.0); 3],
739 ));
740 net.generators_mut().push(generator(1, 100.0));
741 net.loads_mut().push(Load::new(BusId(3), 10.0, 0.0));
742 let partition = net.calc_islands();
743 assert_eq!(partition.islands.len(), 1);
744 assert_eq!(partition.islands[0].buses, [BusId(1), BusId(3)]);
745
746 let settings =
747 NormalizeSettings::default().with_island_references(IslandReferencePolicy::PerIsland);
748 let (normalized, _) = net.to_normalized_with(&settings).unwrap();
749 assert_eq!(normalized.network.buses().len(), 2);
750 let transformer = &normalized.network.transformers_3w()[0];
751 assert_eq!(transformer.winding_in_service(), [true, false, true]);
752 assert!(transformer.in_service);
753 assert_eq!(normalized.network.calc_islands().islands.len(), 1);
754 let found = codes(&normalized.diagnostics);
755 assert!(found.contains(&"CANONICALIZE.NORMALIZE.WINDING_TAKEN_OUT"));
756 assert!(!found.contains(&"CANONICALIZE.ISLAND.DE_ENERGIZED"));
757 crate::IndexedNetwork::new(&normalized.network)
758 .check_reference_coverage()
759 .unwrap();
760 normalized.network.validate().unwrap();
761 }
762
763 #[test]
764 fn de_energizing_takes_out_the_windings_on_the_dead_island() {
765 let mut net = three_islands();
766 net.transformers_3w_mut().push(Transformer3W::new(
769 [6, 7, 6].map(|bus| Winding::new(BusId(bus))),
770 [Impedance::new(0.0, 0.1, 100.0); 3],
771 ));
772 let mut partial = Transformer3W::new(
773 [3, 4, 7].map(|bus| Winding::new(BusId(bus))),
774 [Impedance::new(0.0, 0.1, 100.0); 3],
775 );
776 partial.set_winding_in_service([true, true, false]);
777 net.transformers_3w_mut().push(partial);
778 net.assign_island_references(IslandReferencePolicy::PerIsland);
779 assert!(
780 !net.transformers_3w()[0].in_service,
781 "nothing left to couple"
782 );
783 let partial = &net.transformers_3w()[1];
784 assert!(partial.in_service, "its live windings stay");
785 assert_eq!(partial.winding_in_service(), [true, true, false]);
786 net.validate().unwrap();
787 }
788
789 #[test]
790 fn each_island_gets_one_reference_and_an_unsupplied_one_is_de_energized() {
791 let mut net = three_islands();
792 let unchanged = net.assign_island_references(IslandReferencePolicy::Stated);
793 assert_eq!(unchanged.diagnostics.as_slice(), []);
794 assert_eq!(kind(&net, 5), BusType::Pq);
795
796 let report = net.assign_island_references(IslandReferencePolicy::PerIsland);
797 assert_eq!(report.designated, [BusId(5)]);
799 assert_eq!(kind(&net, 1), BusType::Ref);
800 assert_eq!(kind(&net, 5), BusType::Ref);
801 assert_eq!(report.de_energized, [BusId(6), BusId(7)]);
804 assert_eq!(kind(&net, 6), BusType::Isolated);
805 assert!(!net.loads()[2].in_service);
806 assert!(!net.branches()[3].in_service);
807 assert!(!net.branches()[4].in_service, "the branch onto bus 8");
808 assert!(!net.hvdc()[0].in_service);
809 assert!(net.hvdc()[1].in_service, "the line inside island A");
810 assert!(net.branches()[0].in_service && net.loads()[0].in_service);
811 assert_eq!(
814 codes(&report.diagnostics),
815 [
816 "CANONICALIZE.ISLAND.REFERENCE_DESIGNATED",
817 "CANONICALIZE.ISLAND.HVDC_FED_DE_ENERGIZED"
818 ]
819 );
820 let message = report.diagnostics[1].message();
821 assert!(message.contains("25 MW of in-service load"), "{message}");
822 assert!(
823 message.contains("(1-6) schedule 29 MW into it"),
824 "{message}"
825 );
826 assert_eq!(report.partition.islands.len(), 2);
827 assert!(
828 report
829 .partition
830 .islands
831 .iter()
832 .all(|island| island.references.len() == 1)
833 );
834 net.validate().unwrap();
835
836 let again = net.assign_island_references(IslandReferencePolicy::PerIsland);
838 assert_eq!(again.diagnostics.as_slice(), []);
839 }
840
841 #[test]
842 fn of_two_references_in_one_island_the_larger_source_stays() {
843 let mut net = three_references();
844 let report = net.assign_island_references(IslandReferencePolicy::PerIsland);
845 assert_eq!(report.demoted, [BusId(1), BusId(3)]);
846 assert_eq!(kind(&net, 2), BusType::Ref);
847 assert_eq!(kind(&net, 1), BusType::Pv, "it hosts a generator");
848 assert_eq!(kind(&net, 3), BusType::Pq);
849 assert_eq!(
851 codes(&report.diagnostics),
852 [
853 "CANONICALIZE.ISLAND.REFERENCE_DEMOTED",
854 "CANONICALIZE.ISLAND.REFERENCE_DEMOTED"
855 ]
856 );
857 }
858
859 fn lone_generator_less_reference() -> BalancedNetwork {
862 let mut buses: Vec<Bus> = (1..=4)
863 .map(|id| Bus::new(BusId(id), BusType::Pq, 230.0))
864 .collect();
865 buses[0].kind = BusType::Ref;
866 let line = |from, to| Branch::new(BusId(from), BusId(to), 0.01, 0.1);
867 let mut net = BalancedNetwork::in_memory(
868 "generator-less-reference",
869 100.0,
870 buses,
871 vec![line(1, 2), line(2, 3), line(3, 4)],
872 );
873 net.generators_mut()
874 .extend([generator(3, 50.0), generator(4, 120.0)]);
875 net.loads_mut().push(Load::new(BusId(2), 60.0, 0.0));
876 net
877 }
878
879 fn three_references() -> BalancedNetwork {
881 let buses = (1..=3)
882 .map(|id| Bus::new(BusId(id), BusType::Ref, 230.0))
883 .collect();
884 let line = |from, to| Branch::new(BusId(from), BusId(to), 0.01, 0.1);
885 let mut net =
886 BalancedNetwork::in_memory("refs", 100.0, buses, vec![line(1, 2), line(2, 3)]);
887 net.generators_mut()
888 .extend([generator(1, 100.0), generator(2, 300.0)]);
889 net
890 }
891
892 #[test]
893 fn a_lone_generator_less_reference_yields_to_the_largest_generator() {
894 let mut net = lone_generator_less_reference();
895 let island = &net.calc_islands().islands[0];
896 assert_eq!(island.references, [BusId(1)]);
897 assert_eq!(
898 island.calc_reference_choice(net.generators()),
899 Some(BusId(4))
900 );
901
902 let report = net.assign_island_references(IslandReferencePolicy::PerIsland);
903 assert_eq!(report.demoted, [BusId(1)]);
904 assert_eq!(report.designated, [BusId(4)]);
905 assert_eq!(report.de_energized, []);
906 assert_eq!(kind(&net, 1), BusType::Pq);
907 assert_eq!(kind(&net, 4), BusType::Ref);
908 assert_eq!(
909 kind(&net, 3),
910 BusType::Pq,
911 "the smaller generator stays put"
912 );
913 assert_eq!(
914 codes(&report.diagnostics),
915 [
916 "CANONICALIZE.ISLAND.REFERENCE_DEMOTED",
917 "CANONICALIZE.ISLAND.REFERENCE_DESIGNATED"
918 ]
919 );
920 assert_eq!(report.partition.islands[0].references, [BusId(4)]);
921 net.validate().unwrap();
922 let again = net.assign_island_references(IslandReferencePolicy::PerIsland);
923 assert_eq!(again.diagnostics.as_slice(), []);
924
925 let mut stated = lone_generator_less_reference();
927 let report = stated.assign_island_references(IslandReferencePolicy::Stated);
928 assert_eq!(report.diagnostics.as_slice(), []);
929 assert_eq!(kind(&stated, 1), BusType::Ref);
930 }
931
932 #[test]
933 fn equal_generators_go_to_the_smallest_bus_id() {
934 let mut net = lone_generator_less_reference();
935 net.generators_mut().push(generator(2, 120.0));
937 let report = net.assign_island_references(IslandReferencePolicy::PerIsland);
938 assert_eq!(report.designated, [BusId(2)]);
939 }
940
941 #[test]
945 fn raw_and_normalized_per_island_references_agree() {
946 let references = |network: &BalancedNetwork| -> Vec<BusId> {
947 let mut ids: Vec<BusId> = network
948 .buses()
949 .iter()
950 .filter(|bus| bus.kind == BusType::Ref)
951 .map(|bus| bus.id)
952 .collect();
953 ids.sort_unstable();
954 ids
955 };
956 let settings = NormalizeSettings::default()
957 .with_island_references(IslandReferencePolicy::PerIsland)
958 .with_closed_switches(ClosedSwitchPolicy::Keep);
959 let fixtures = [
960 ("three islands", three_islands(), vec![1, 5]),
961 (
962 "dead tertiary",
963 line_with_a_tertiary(BusType::Isolated),
964 vec![1],
965 ),
966 ("live tertiary", line_with_a_tertiary(BusType::Pq), vec![1]),
967 (
968 "generator-less reference",
969 lone_generator_less_reference(),
970 vec![4],
971 ),
972 ("three references", three_references(), vec![2]),
973 ];
974 for (name, net, expected) in fixtures {
975 let expected: Vec<BusId> = expected.into_iter().map(BusId).collect();
976 let (normalized, _) = net.to_normalized_with(&settings).unwrap();
977 assert_eq!(
978 references(&normalized.network),
979 expected,
980 "{name}: normalized"
981 );
982 let mut raw = net.clone();
983 raw.assign_island_references(IslandReferencePolicy::PerIsland);
984 assert_eq!(references(&raw), expected, "{name}: raw");
985 let choices: Vec<BusId> = net
986 .calc_islands()
987 .islands
988 .iter()
989 .filter_map(|island| island.calc_reference_choice(net.generators()))
990 .collect();
991 let mut choices = choices;
992 choices.sort_unstable();
993 assert_eq!(choices, expected, "{name}: calc_reference_choice");
994 }
995 }
996
997 #[test]
998 fn normalization_per_island_references_every_island_and_drops_the_unsupplied() {
999 let net = three_islands();
1000 let stated = net
1001 .to_normalized_with_options(&NormalizeOptions::default())
1002 .unwrap();
1003 assert_eq!(
1004 stated.network.buses().len(),
1005 7,
1006 "the typed isolated bus goes, and the closed switch merges bus 5 into bus 4"
1007 );
1008 let references: Vec<BusId> = stated
1009 .network
1010 .buses()
1011 .iter()
1012 .filter(|bus| bus.kind == BusType::Ref)
1013 .map(|bus| bus.id)
1014 .collect();
1015 assert_eq!(references, [BusId(1)]);
1016 assert!(
1017 codes(&stated.diagnostics).contains(&"CANONICALIZE.NORMALIZE.GENERATOR_BUS_RETYPED")
1018 );
1019
1020 let settings =
1021 NormalizeSettings::default().with_island_references(IslandReferencePolicy::PerIsland);
1022 let references = |network: &BalancedNetwork| -> Vec<BusId> {
1023 network
1024 .buses()
1025 .iter()
1026 .filter(|bus| bus.kind == BusType::Ref)
1027 .map(|bus| bus.id)
1028 .collect()
1029 };
1030 let (kept, _) = net
1033 .to_normalized_with(&settings.with_closed_switches(ClosedSwitchPolicy::Keep))
1034 .unwrap();
1035 let ids: Vec<usize> = kept.network.buses().iter().map(|b| b.id.0).collect();
1036 assert_eq!(ids, [1, 2, 3, 4, 5, 9]);
1037 assert_eq!(references(&kept.network), [BusId(1), BusId(5)]);
1038 let (per_island, _) = net.to_normalized_with(&settings).unwrap();
1040 let ids: Vec<usize> = per_island.network.buses().iter().map(|b| b.id.0).collect();
1041 assert_eq!(ids, [1, 2, 3, 4, 9]);
1042 assert_eq!(references(&per_island.network), [BusId(1), BusId(4)]);
1043 assert_eq!(
1044 per_island.network.hvdc().len(),
1045 1,
1046 "only the line inside island A"
1047 );
1048 let found = codes(&per_island.diagnostics);
1049 assert!(found.contains(&"CANONICALIZE.ISLAND.HVDC_FED_DE_ENERGIZED"));
1050 assert!(!found.contains(&"CANONICALIZE.ISLAND.DE_ENERGIZED"));
1051 assert!(found.contains(&"CANONICALIZE.ISLAND.REFERENCE_DESIGNATED"));
1052 crate::IndexedNetwork::new(&per_island.network)
1053 .check_reference_coverage()
1054 .unwrap();
1055 }
1056
1057 fn line_with_a_tertiary(tertiary: BusType) -> BalancedNetwork {
1060 let mut buses: Vec<Bus> = (1..=5)
1061 .map(|id| Bus::new(BusId(id), BusType::Pq, 230.0))
1062 .collect();
1063 buses[0].kind = BusType::Ref;
1064 buses[4].kind = tertiary;
1065 let line = |from, to| Branch::new(BusId(from), BusId(to), 0.01, 0.1);
1066 let mut net =
1067 BalancedNetwork::in_memory("tertiary", 100.0, buses, vec![line(1, 2), line(2, 3)]);
1068 net.transformers_3w_mut().push(Transformer3W::new(
1069 [2, 4, 5].map(|bus| Winding::new(BusId(bus))),
1070 [Impedance::new(0.0, 0.1, 100.0); 3],
1071 ));
1072 net.generators_mut().push(generator(1, 100.0));
1073 net
1074 }
1075
1076 #[test]
1077 fn an_island_joined_through_a_dead_tertiary_carves_out_whole() {
1078 let net = line_with_a_tertiary(BusType::Isolated);
1079 let partition = net.calc_islands();
1080 assert_eq!(partition.islands.len(), 1);
1081 assert_eq!(
1082 partition.islands[0].buses,
1083 [BusId(1), BusId(2), BusId(3), BusId(4)]
1084 );
1085 assert_eq!(partition.isolated, [BusId(5)]);
1086
1087 let island: BTreeSet<BusId> = partition.islands[0].buses.iter().copied().collect();
1088 let mut sub = net.subset_buses(&island).network;
1089 assert_eq!(sub.transformers_3w().len(), 1, "the transformer is kept");
1090 let stub = sub.buses().iter().find(|b| b.id == BusId(5)).unwrap();
1091 assert_eq!(stub.kind, BusType::Isolated);
1092 assert_eq!(
1093 stub.extras.get("tie_bus"),
1094 Some(&serde_json::Value::Bool(true))
1095 );
1096 sub.validate().unwrap();
1097 let again = sub.calc_islands();
1098 assert_eq!(again.islands.len(), 1);
1099 assert_eq!(again.islands[0].buses, partition.islands[0].buses);
1100 let report = sub.assign_island_references(IslandReferencePolicy::PerIsland);
1101 assert_eq!(report.diagnostics.as_slice(), []);
1102 }
1103
1104 #[test]
1105 fn a_winding_stated_out_of_service_joins_nothing() {
1106 let mut net = line_with_a_tertiary(BusType::Pq);
1107 net.transformers_3w_mut()[0].extras.insert(
1108 "winding_in_service".into(),
1109 serde_json::json!([true, true, false]),
1110 );
1111 let partition = net.calc_islands();
1112 assert_eq!(partition.islands.len(), 2);
1113 assert_eq!(partition.islands[1].buses, [BusId(5)]);
1114
1115 let island: BTreeSet<BusId> = partition.islands[0].buses.iter().copied().collect();
1116 let sub = net.subset_buses(&island).network;
1117 assert_eq!(sub.transformers_3w().len(), 1);
1118 assert_eq!(sub.calc_islands().islands.len(), 1);
1119 sub.validate().unwrap();
1120 }
1121
1122 #[test]
1123 fn one_island_carves_out_with_its_elements() {
1124 let net = three_islands();
1125 let partition = net.calc_islands();
1126 let island: BTreeSet<BusId> = partition.islands[1].buses.iter().copied().collect();
1127 let cut = net.subset_buses(&island);
1128 assert_eq!(cut.branch_rows, [None, None, Some(0), None, None]);
1129 assert_eq!(cut.switch_rows, [Some(0)]);
1130 assert_eq!(cut.generator_rows, [None, Some(0), Some(1)]);
1131 assert_eq!(cut.load_rows, [None, Some(0), None]);
1132 assert_eq!(cut.hvdc_rows, [None, None]);
1133 let sub = cut.network;
1134 assert_eq!(sub.buses().len(), 3);
1135 assert_eq!(sub.branches().len(), 1);
1136 assert_eq!(sub.switches().len(), 1);
1137 assert_eq!(sub.generators().len(), 2);
1138 assert_eq!(sub.loads().len(), 1);
1139 assert_eq!(sub.hvdc().as_slice(), []);
1140 sub.validate().unwrap();
1141 }
1142
1143 #[test]
1146 fn island_cut_row_maps_agree_with_the_cut_network() {
1147 let fixtures = [
1148 three_islands(),
1149 line_with_a_tertiary(BusType::Isolated),
1150 line_with_a_tertiary(BusType::Pq),
1151 ];
1152 for net in fixtures {
1153 for island in net.calc_islands().islands {
1154 let island: BTreeSet<BusId> = island.buses.iter().copied().collect();
1155 let cut = net.subset_buses(&island);
1156 let kept: BTreeSet<BusId> = cut.network.buses().iter().map(|b| b.id).collect();
1157 assert!(island.is_subset(&kept));
1158 macro_rules! check {
1159 ($map:ident, $table:ident, $kept:expr) => {{
1160 let source = net.$table();
1161 let result = cut.network.$table();
1162 assert_eq!(cut.$map.len(), source.len(), stringify!($map));
1163 let rows: Vec<usize> = cut.$map.iter().flatten().copied().collect();
1164 assert_eq!(rows, (0..result.len()).collect::<Vec<_>>());
1165 for (row, target) in cut.$map.iter().enumerate() {
1166 let element = &source[row];
1167 #[allow(clippy::redundant_closure_call)]
1168 let expected = ($kept)(element);
1169 assert_eq!(
1170 target.is_some(),
1171 expected,
1172 "{} row {row}",
1173 stringify!($map)
1174 );
1175 if let Some(target) = target {
1176 assert_eq!(&result[*target], element);
1177 }
1178 }
1179 }};
1180 }
1181 for (row, target) in cut.bus_rows.iter().enumerate() {
1182 let id = net.buses()[row].id;
1183 assert_eq!(target.is_some(), kept.contains(&id));
1184 if let Some(target) = target {
1185 assert_eq!(cut.network.buses()[*target].id, id);
1186 }
1187 }
1188 let at = |bus: &BusId| island.contains(bus);
1189 let spans = |from: &BusId, to: &BusId| kept.contains(from) && kept.contains(to);
1190 check!(load_rows, loads, |e: &Load| at(&e.bus));
1191 check!(generator_rows, generators, |e: &Generator| at(&e.bus));
1192 check!(branch_rows, branches, |e: &Branch| spans(&e.from, &e.to));
1193 check!(switch_rows, switches, |e: &Switch| spans(&e.from, &e.to));
1194 check!(hvdc_rows, hvdc, |e: &Hvdc| spans(&e.from, &e.to));
1195 check!(transformer_3w_rows, transformers_3w, |e: &Transformer3W| e
1196 .windings
1197 .iter()
1198 .all(|w| kept.contains(&w.bus))
1199 && e.windings.iter().any(|w| at(&w.bus)));
1200 assert_eq!(cut.shunt_rows.len(), net.shunts().len());
1201 assert_eq!(cut.storage_rows.len(), net.storage().len());
1202 assert_eq!(
1203 cut.static_var_compensator_rows.len(),
1204 net.static_var_compensators().len()
1205 );
1206 }
1207 }
1208 }
1209
1210 #[test]
1211 fn de_energizing_buses_keeps_every_row() {
1212 let mut net = three_islands();
1213 let rows = |net: &BalancedNetwork| {
1214 [
1215 net.buses().len(),
1216 net.branches().len(),
1217 net.switches().len(),
1218 net.generators().len(),
1219 net.loads().len(),
1220 net.hvdc().len(),
1221 ]
1222 };
1223 let before = rows(&net);
1224 net.de_energize_buses(&[BusId(3), BusId(4), BusId(5), BusId(99)].into());
1226 assert_eq!(rows(&net), before, "no row is added or removed");
1227 for bus in [3, 4, 5] {
1228 assert_eq!(kind(&net, bus), BusType::Isolated);
1229 }
1230 assert_eq!(kind(&net, 2), BusType::Pq);
1231 assert!(!net.branches()[2].in_service, "the line 3-4");
1232 assert!(net.switches()[0].closed, "switches keep their state");
1233 assert!(net.generators()[0].in_service);
1234 assert!(!net.generators()[1].in_service && !net.generators()[2].in_service);
1235 assert!(!net.loads()[1].in_service && net.loads()[0].in_service);
1236 let partition = net.calc_islands();
1237 assert_eq!(partition.island_of(BusId(4)), None);
1238 assert_eq!(partition.islands.len(), 2);
1239 net.validate().unwrap();
1240 }
1241
1242 #[test]
1243 fn keeping_one_island_de_energizes_the_others_in_place() {
1244 let mut net = three_islands();
1245 let partition = net.calc_islands();
1246 let island: BTreeSet<BusId> = partition.islands[1].buses.iter().copied().collect();
1247 let cut = net.subset_buses(&island);
1248 let dead = net.keep_island(&island);
1249 assert_eq!(dead, [1, 2, 6, 7, 9].map(BusId));
1251 let after = net.calc_islands();
1252 assert_eq!(after.islands.len(), 1);
1253 assert_eq!(after.islands[0].buses, partition.islands[1].buses);
1254 for (row, target) in cut.branch_rows.iter().enumerate() {
1257 assert_eq!(net.branches()[row].in_service, target.is_some());
1258 }
1259 for (row, target) in cut.generator_rows.iter().enumerate() {
1260 assert_eq!(net.generators()[row].in_service, target.is_some());
1261 }
1262 for (row, target) in cut.load_rows.iter().enumerate() {
1263 assert_eq!(net.loads()[row].in_service, target.is_some());
1264 }
1265 for (row, target) in cut.hvdc_rows.iter().enumerate() {
1266 assert_eq!(net.hvdc()[row].in_service, target.is_some());
1267 }
1268 assert_eq!(
1269 net.keep_island(&island),
1270 [],
1271 "a second call changes nothing"
1272 );
1273 let report = net.assign_island_references(IslandReferencePolicy::PerIsland);
1274 assert_eq!(report.de_energized, []);
1275 net.validate().unwrap();
1276 }
1277}