Skip to main content

powerio_tx/
islands.rs

1//! Electrical islands and one reference bus per island.
2//!
3//! A large case can hold several AC islands: asynchronous systems joined only
4//! by HVDC, radial pockets behind an open breaker, and stranded equipment. A
5//! power flow needs one reference bus in each island it solves, and an island
6//! with no source cannot be solved at all. [`BalancedNetwork::calc_islands`]
7//! partitions the energized buses; [`BalancedNetwork::assign_island_references`]
8//! gives each supplied island one reference and de-energizes the rest, and
9//! [`IslandReferencePolicy::PerIsland`] applies the same rule during
10//! normalization.
11
12use std::collections::{BTreeSet, HashMap};
13use std::fmt::Write as _;
14
15use crate::diagnostics::{Diagnostic, codes};
16use crate::network::{BalancedNetwork, BusId, BusType, Generator};
17
18/// How normalization and [`BalancedNetwork::assign_island_references`] treat
19/// the reference buses of a network with several islands.
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum IslandReferencePolicy {
23    /// Keep the reference buses the case states. Normalization designates
24    /// one only when none survives anywhere, and
25    /// [`BalancedNetwork::assign_island_references`] changes nothing.
26    #[default]
27    Stated,
28    /// One reference per island, on a bus that hosts an in-service
29    /// generator. A stated reference that hosts none is demoted first. An
30    /// island left with no reference takes the bus of its in-service
31    /// generator with the largest `pmax` (the smallest bus id breaking ties);
32    /// an island left with several keeps the one with the most generation and
33    /// demotes the others; an island with no in-service generator cannot be
34    /// solved and is de-energized. Normalization under this policy picks the
35    /// same reference buses.
36    PerIsland,
37}
38
39/// One AC island: energized buses joined by in-service branches, closed
40/// switches, and in-service three winding transformers.
41#[derive(Clone, Debug, PartialEq)]
42#[non_exhaustive]
43pub struct Island {
44    /// The island's buses in ascending id order.
45    pub buses: Vec<BusId>,
46    /// Its reference (slack) buses, in ascending id order.
47    pub references: Vec<BusId>,
48    /// Rows of the in-service generators on its buses.
49    pub generators: Vec<usize>,
50    /// Rows of the in-service HVDC lines that tie it to another island: one
51    /// terminal on its buses, the other on another island's.
52    pub hvdc: Vec<usize>,
53}
54
55impl Island {
56    /// Whether an in-service generator can supply the island.
57    #[must_use]
58    pub fn is_supplied(&self) -> bool {
59        !self.generators.is_empty()
60    }
61
62    /// The one reference bus [`IslandReferencePolicy::PerIsland`] gives this
63    /// island, without changing the network. Only a stated reference that
64    /// hosts an in-service generator counts: the island keeps it when it
65    /// states one, takes the bus of its largest `pmax` in-service generator
66    /// (the smallest bus id breaking ties) when it states none, and keeps the
67    /// one hosting the most in-service `pmax` (the smallest id breaking ties)
68    /// when it states several. `None` when no in-service generator supplies
69    /// the island, which that policy de-energizes. `generators` is the
70    /// network's generator table.
71    #[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    /// The stated references that host one of the island's in-service
81    /// generators, in ascending id order.
82    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/// The result of [`BalancedNetwork::calc_islands`].
96#[derive(Clone, Debug, Default, PartialEq)]
97#[non_exhaustive]
98pub struct IslandPartition {
99    /// Every island, largest first; islands of one size come in ascending
100    /// order of their smallest bus id.
101    pub islands: Vec<Island>,
102    /// Buses typed isolated, which belong to no island.
103    pub isolated: Vec<BusId>,
104    island_of: HashMap<BusId, usize>,
105}
106
107impl IslandPartition {
108    /// The position in [`islands`](Self::islands) of the island holding
109    /// `bus`, or `None` for an isolated or unknown bus.
110    #[must_use]
111    pub fn island_of(&self, bus: BusId) -> Option<usize> {
112        self.island_of.get(&bus).copied()
113    }
114}
115
116/// The result of [`BalancedNetwork::assign_island_references`].
117#[derive(Clone, Debug, Default, PartialEq)]
118#[non_exhaustive]
119pub struct IslandReferenceReport {
120    /// The islands after the assignment.
121    pub partition: IslandPartition,
122    /// Buses made the reference of their island.
123    pub designated: Vec<BusId>,
124    /// Reference buses demoted, because they host no in-service generator
125    /// or because their island kept another, in ascending id order.
126    pub demoted: Vec<BusId>,
127    /// Buses of the islands that were de-energized, now typed isolated.
128    pub de_energized: Vec<BusId>,
129    /// One finding per island the assignment changed.
130    pub diagnostics: Vec<Diagnostic>,
131}
132
133/// The `pmax` order the reference choice uses: a NaN bound never wins, and an
134/// unbounded `+Inf` wins as the largest capacity.
135fn pmax_key(generator: &Generator) -> f64 {
136    if generator.pmax.is_nan() {
137        f64::NEG_INFINITY
138    } else {
139        generator.pmax
140    }
141}
142
143/// The bus of the in-service generator with the largest `pmax` among `rows`,
144/// the smallest bus id breaking ties, so the choice does not depend on row
145/// order.
146pub(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
157/// Of several reference buses, the one hosting the most in-service `pmax`,
158/// the smallest id breaking ties.
159pub(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
177/// A short list of bus ids for a message: the first few, then a count.
178pub(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    /// Partition the energized buses into AC islands.
194    ///
195    /// Two buses share an island when a path of in-service branches, closed
196    /// switches, and in-service three winding transformers joins them. HVDC
197    /// lines do not join islands: they tie asynchronous systems. Buses typed
198    /// isolated belong to no island and are listed apart, and an element that
199    /// touches one does not join anything through it. A three winding
200    /// transformer joins the buses of its in-service windings
201    /// ([`Transformer3W::winding_in_service`](crate::Transformer3W::winding_in_service)),
202    /// so one whose tertiary sits on an isolated bus or is out of service
203    /// still joins the other two. Carve an
204    /// island out with [`subset_buses`](BalancedNetwork::subset_buses), which
205    /// keeps such a transformer.
206    #[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    /// Record on each island, keyed by its union-find root, the rows of its
287    /// in-service generators and of the in-service HVDC lines tying it to
288    /// another island.
289    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    /// Give every island one reference bus under `policy`, returning what
318    /// changed.
319    ///
320    /// Under [`IslandReferencePolicy::PerIsland`], in a supplied island:
321    ///
322    /// - A stated reference that hosts no in-service generator cannot balance
323    ///   the island and is demoted to PQ first. What follows counts only the
324    ///   references that remain.
325    /// - An island with one reference keeps it.
326    /// - An island with none gets the bus of its largest `pmax` in-service
327    ///   generator (the smallest bus id breaking ties) as its reference.
328    /// - An island with several keeps the one hosting the most in-service
329    ///   `pmax` (the smallest id breaking ties) and demotes the others to PV.
330    /// - An island with no in-service generator has no source to balance it,
331    ///   so it is de-energized: its buses are typed isolated, and the
332    ///   in-service loads, shunts, static var compensators, storage, and every
333    ///   branch and HVDC line touching them are taken out of service. A three
334    ///   winding transformer loses the windings on those buses and goes out
335    ///   whole only when fewer than two windings remain.
336    ///
337    /// Each change records one finding. Normalization under the same policy
338    /// picks the same reference buses. [`IslandReferencePolicy::Stated`]
339    /// changes nothing and reports the partition.
340    #[allow(clippy::too_many_lines)] // one stanza per island outcome
341    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    /// The finding for an island no in-service generator supplies, whose
461    /// `outcome` completes the sentence. An island an HVDC line still feeds
462    /// gets its own code, naming the lines and the power they schedule into
463    /// it, since supply counts generators only and that infeed goes too.
464    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            // MATPOWER's dcline convention: `pf` leaves the from bus and `pt`
481            // arrives at the to bus.
482            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    /// Keep `island` energized and de-energize every other bus not already
500    /// typed isolated, with [`de_energize_buses`](Self::de_energize_buses),
501    /// returning the buses de-energized in ascending order.
502    ///
503    /// This is the in-place counterpart of
504    /// [`subset_buses`](BalancedNetwork::subset_buses): every table keeps all
505    /// its rows, so a source row index still names the same element, and the
506    /// elements outside `island` stay in the network out of service. Pass the
507    /// buses of one [`Island`] of [`calc_islands`](Self::calc_islands), or
508    /// the union of several.
509    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    /// De-energize `buses`, as [`assign_island_references`] does to an island
523    /// with no source.
524    ///
525    /// Each bus is typed isolated, and the loads, shunts, static var
526    /// compensators, storage, and generators at them, and every branch and
527    /// HVDC line touching them, are taken out of service. A three winding
528    /// transformer loses the windings on those buses and goes out whole only
529    /// when fewer than two windings remain. Switches keep their state: a
530    /// closed switch joins nothing through an isolated bus. No row is added
531    /// or removed, so data keyed by source rows stays valid. Ids the network
532    /// does not declare are ignored, and only a table with an element to
533    /// change is copied.
534    ///
535    /// [`assign_island_references`]: BalancedNetwork::assign_island_references
536    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        // A winding on a de-energized bus goes out; a transformer left with
604        // fewer than two windings couples nothing and goes out whole.
605        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    /// Island A {1, 2, 9} holds the reference bus 1; island B {3, 4, 5}
646    /// holds generation and no reference, bus 5 joined by a closed switch;
647    /// island C {6, 7} holds only a load and an HVDC end; bus 8 is isolated.
648    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        // An HVDC line from island A feeds island C, which has no generator.
671        let mut tie = Hvdc::new(BusId(1), BusId(6));
672        tie.pf = 30.0;
673        tie.pt = 29.0;
674        // One inside island A ties nothing.
675        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        // The HVDC tie belongs to both islands it joins; the line inside
705        // island A ties nothing.
706        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        // Normalization takes the winding on the isolated bus out of service
729        // and keeps the transformer, so the two other windings stay joined.
730        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        // A transformer inside the dead island {6, 7}, plus one joining the
767        // live island B to a dead bus only through a winding already out.
768        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        // Island A keeps bus 1; island B takes bus 5, its largest generator.
798        assert_eq!(report.designated, [BusId(5)]);
799        assert_eq!(kind(&net, 1), BusType::Ref);
800        assert_eq!(kind(&net, 5), BusType::Ref);
801        // Island C has no generator: its buses are isolated and everything
802        // on or touching them is out of service.
803        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        // Island C was fed over HVDC, so its finding has its own code and
812        // names the line and the power it scheduled into the island.
813        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        // A second pass has nothing left to do.
837        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        // Bus 3 hosts no generator and goes first; then bus 1 yields to bus 2.
850        assert_eq!(
851            codes(&report.diagnostics),
852            [
853                "CANONICALIZE.ISLAND.REFERENCE_DEMOTED",
854                "CANONICALIZE.ISLAND.REFERENCE_DEMOTED"
855            ]
856        );
857    }
858
859    /// Buses 1-2-3-4 in a line, the reference on bus 1 with no generator,
860    /// and generators of 50 MW on bus 3 and 120 MW on bus 4.
861    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    /// Buses 1, 2, and 3 all stated references, generators on buses 1 and 2.
880    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        // Stated keeps the source's designation.
926        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        // A second 120 MW generator, on a smaller bus but a later row.
936        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    /// The raw assignment and normalization under the per island policy
942    /// pick the same reference buses. Closed switches are kept so that bus
943    /// ids survive normalization.
944    #[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        // Kept as data, the closed switch still joins island B, whose largest
1031        // generator, on bus 5, becomes its reference.
1032        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        // Merged by default, both of island B's generators sit on bus 4.
1039        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    /// Buses 1-2-3 in a line, the reference on bus 1, and a three winding
1058    /// transformer on buses 2, 4, and 5.
1059    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    /// Every source row of every table maps to its row in the cut, in order,
1144    /// or to `None` exactly when the cut leaves it out.
1145    #[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        // Island B {3, 4, 5} holds generation; de-energizing takes it out too.
1225        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        // Bus 8 was already isolated; the buses of islands A and C go.
1250        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        // The in-service elements are exactly the ones the cut keeps, at
1255        // their source rows.
1256        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}