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 a_reference_without_a_generator_is_not_a_supplied_slack_source() {
843        let buses = vec![
844            Bus::new(BusId(1), BusType::Ref, 230.0),
845            Bus::new(BusId(2), BusType::Pq, 230.0),
846        ];
847        let line = Branch::new(BusId(1), BusId(2), 0.01, 0.1);
848        let mut net = BalancedNetwork::in_memory("reference-only", 100.0, buses, vec![line]);
849        net.loads_mut().push(Load::new(BusId(2), 10.0, 0.0));
850
851        let partition = net.calc_islands();
852        assert_eq!(partition.islands[0].references, [BusId(1)]);
853        assert!(!partition.islands[0].is_supplied());
854
855        // Stated preserves the source's angle-reference designation.
856        let stated = net.assign_island_references(IslandReferencePolicy::Stated);
857        assert_eq!(stated.diagnostics.as_slice(), []);
858        assert_eq!(kind(&net, 1), BusType::Ref);
859        assert!(
860            !net.calc_islands().islands[0].is_supplied(),
861            "preserving a reference must not imply that the island has a balancing source"
862        );
863
864        // PerIsland is a solver-readiness policy: without an in-service
865        // generator, the island is de-energized rather than treated as solved.
866        let report = net.assign_island_references(IslandReferencePolicy::PerIsland);
867        assert_eq!(report.de_energized, [BusId(1), BusId(2)]);
868        assert_eq!(kind(&net, 1), BusType::Isolated);
869        assert_eq!(kind(&net, 2), BusType::Isolated);
870    }
871
872    #[test]
873    fn of_two_references_in_one_island_the_larger_source_stays() {
874        let mut net = three_references();
875        let report = net.assign_island_references(IslandReferencePolicy::PerIsland);
876        assert_eq!(report.demoted, [BusId(1), BusId(3)]);
877        assert_eq!(kind(&net, 2), BusType::Ref);
878        assert_eq!(kind(&net, 1), BusType::Pv, "it hosts a generator");
879        assert_eq!(kind(&net, 3), BusType::Pq);
880        // Bus 3 hosts no generator and goes first; then bus 1 yields to bus 2.
881        assert_eq!(
882            codes(&report.diagnostics),
883            [
884                "CANONICALIZE.ISLAND.REFERENCE_DEMOTED",
885                "CANONICALIZE.ISLAND.REFERENCE_DEMOTED"
886            ]
887        );
888    }
889
890    /// Buses 1-2-3-4 in a line, the reference on bus 1 with no generator,
891    /// and generators of 50 MW on bus 3 and 120 MW on bus 4.
892    fn lone_generator_less_reference() -> BalancedNetwork {
893        let mut buses: Vec<Bus> = (1..=4)
894            .map(|id| Bus::new(BusId(id), BusType::Pq, 230.0))
895            .collect();
896        buses[0].kind = BusType::Ref;
897        let line = |from, to| Branch::new(BusId(from), BusId(to), 0.01, 0.1);
898        let mut net = BalancedNetwork::in_memory(
899            "generator-less-reference",
900            100.0,
901            buses,
902            vec![line(1, 2), line(2, 3), line(3, 4)],
903        );
904        net.generators_mut()
905            .extend([generator(3, 50.0), generator(4, 120.0)]);
906        net.loads_mut().push(Load::new(BusId(2), 60.0, 0.0));
907        net
908    }
909
910    /// Buses 1, 2, and 3 all stated references, generators on buses 1 and 2.
911    fn three_references() -> BalancedNetwork {
912        let buses = (1..=3)
913            .map(|id| Bus::new(BusId(id), BusType::Ref, 230.0))
914            .collect();
915        let line = |from, to| Branch::new(BusId(from), BusId(to), 0.01, 0.1);
916        let mut net =
917            BalancedNetwork::in_memory("refs", 100.0, buses, vec![line(1, 2), line(2, 3)]);
918        net.generators_mut()
919            .extend([generator(1, 100.0), generator(2, 300.0)]);
920        net
921    }
922
923    #[test]
924    fn a_lone_generator_less_reference_yields_to_the_largest_generator() {
925        let mut net = lone_generator_less_reference();
926        let island = &net.calc_islands().islands[0];
927        assert_eq!(island.references, [BusId(1)]);
928        assert_eq!(
929            island.calc_reference_choice(net.generators()),
930            Some(BusId(4))
931        );
932
933        let report = net.assign_island_references(IslandReferencePolicy::PerIsland);
934        assert_eq!(report.demoted, [BusId(1)]);
935        assert_eq!(report.designated, [BusId(4)]);
936        assert_eq!(report.de_energized, []);
937        assert_eq!(kind(&net, 1), BusType::Pq);
938        assert_eq!(kind(&net, 4), BusType::Ref);
939        assert_eq!(
940            kind(&net, 3),
941            BusType::Pq,
942            "the smaller generator stays put"
943        );
944        assert_eq!(
945            codes(&report.diagnostics),
946            [
947                "CANONICALIZE.ISLAND.REFERENCE_DEMOTED",
948                "CANONICALIZE.ISLAND.REFERENCE_DESIGNATED"
949            ]
950        );
951        assert_eq!(report.partition.islands[0].references, [BusId(4)]);
952        net.validate().unwrap();
953        let again = net.assign_island_references(IslandReferencePolicy::PerIsland);
954        assert_eq!(again.diagnostics.as_slice(), []);
955
956        // Stated keeps the source's designation.
957        let mut stated = lone_generator_less_reference();
958        let report = stated.assign_island_references(IslandReferencePolicy::Stated);
959        assert_eq!(report.diagnostics.as_slice(), []);
960        assert_eq!(kind(&stated, 1), BusType::Ref);
961    }
962
963    #[test]
964    fn equal_generators_go_to_the_smallest_bus_id() {
965        let mut net = lone_generator_less_reference();
966        // A second 120 MW generator, on a smaller bus but a later row.
967        net.generators_mut().push(generator(2, 120.0));
968        let report = net.assign_island_references(IslandReferencePolicy::PerIsland);
969        assert_eq!(report.designated, [BusId(2)]);
970    }
971
972    /// The raw assignment and normalization under the per island policy
973    /// pick the same reference buses. Closed switches are kept so that bus
974    /// ids survive normalization.
975    #[test]
976    fn raw_and_normalized_per_island_references_agree() {
977        let references = |network: &BalancedNetwork| -> Vec<BusId> {
978            let mut ids: Vec<BusId> = network
979                .buses()
980                .iter()
981                .filter(|bus| bus.kind == BusType::Ref)
982                .map(|bus| bus.id)
983                .collect();
984            ids.sort_unstable();
985            ids
986        };
987        let settings = NormalizeSettings::default()
988            .with_island_references(IslandReferencePolicy::PerIsland)
989            .with_closed_switches(ClosedSwitchPolicy::Keep);
990        let fixtures = [
991            ("three islands", three_islands(), vec![1, 5]),
992            (
993                "dead tertiary",
994                line_with_a_tertiary(BusType::Isolated),
995                vec![1],
996            ),
997            ("live tertiary", line_with_a_tertiary(BusType::Pq), vec![1]),
998            (
999                "generator-less reference",
1000                lone_generator_less_reference(),
1001                vec![4],
1002            ),
1003            ("three references", three_references(), vec![2]),
1004        ];
1005        for (name, net, expected) in fixtures {
1006            let expected: Vec<BusId> = expected.into_iter().map(BusId).collect();
1007            let (normalized, _) = net.to_normalized_with(&settings).unwrap();
1008            assert_eq!(
1009                references(&normalized.network),
1010                expected,
1011                "{name}: normalized"
1012            );
1013            let mut raw = net.clone();
1014            raw.assign_island_references(IslandReferencePolicy::PerIsland);
1015            assert_eq!(references(&raw), expected, "{name}: raw");
1016            let choices: Vec<BusId> = net
1017                .calc_islands()
1018                .islands
1019                .iter()
1020                .filter_map(|island| island.calc_reference_choice(net.generators()))
1021                .collect();
1022            let mut choices = choices;
1023            choices.sort_unstable();
1024            assert_eq!(choices, expected, "{name}: calc_reference_choice");
1025        }
1026    }
1027
1028    #[test]
1029    fn normalization_per_island_references_every_island_and_drops_the_unsupplied() {
1030        let net = three_islands();
1031        let stated = net
1032            .to_normalized_with_options(&NormalizeOptions::default())
1033            .unwrap();
1034        assert_eq!(
1035            stated.network.buses().len(),
1036            7,
1037            "the typed isolated bus goes, and the closed switch merges bus 5 into bus 4"
1038        );
1039        let references: Vec<BusId> = stated
1040            .network
1041            .buses()
1042            .iter()
1043            .filter(|bus| bus.kind == BusType::Ref)
1044            .map(|bus| bus.id)
1045            .collect();
1046        assert_eq!(references, [BusId(1)]);
1047        assert!(
1048            codes(&stated.diagnostics).contains(&"CANONICALIZE.NORMALIZE.GENERATOR_BUS_RETYPED")
1049        );
1050
1051        let settings =
1052            NormalizeSettings::default().with_island_references(IslandReferencePolicy::PerIsland);
1053        let references = |network: &BalancedNetwork| -> Vec<BusId> {
1054            network
1055                .buses()
1056                .iter()
1057                .filter(|bus| bus.kind == BusType::Ref)
1058                .map(|bus| bus.id)
1059                .collect()
1060        };
1061        // Kept as data, the closed switch still joins island B, whose largest
1062        // generator, on bus 5, becomes its reference.
1063        let (kept, _) = net
1064            .to_normalized_with(&settings.with_closed_switches(ClosedSwitchPolicy::Keep))
1065            .unwrap();
1066        let ids: Vec<usize> = kept.network.buses().iter().map(|b| b.id.0).collect();
1067        assert_eq!(ids, [1, 2, 3, 4, 5, 9]);
1068        assert_eq!(references(&kept.network), [BusId(1), BusId(5)]);
1069        // Merged by default, both of island B's generators sit on bus 4.
1070        let (per_island, _) = net.to_normalized_with(&settings).unwrap();
1071        let ids: Vec<usize> = per_island.network.buses().iter().map(|b| b.id.0).collect();
1072        assert_eq!(ids, [1, 2, 3, 4, 9]);
1073        assert_eq!(references(&per_island.network), [BusId(1), BusId(4)]);
1074        assert_eq!(
1075            per_island.network.hvdc().len(),
1076            1,
1077            "only the line inside island A"
1078        );
1079        let found = codes(&per_island.diagnostics);
1080        assert!(found.contains(&"CANONICALIZE.ISLAND.HVDC_FED_DE_ENERGIZED"));
1081        assert!(!found.contains(&"CANONICALIZE.ISLAND.DE_ENERGIZED"));
1082        assert!(found.contains(&"CANONICALIZE.ISLAND.REFERENCE_DESIGNATED"));
1083        crate::IndexedNetwork::new(&per_island.network)
1084            .check_reference_coverage()
1085            .unwrap();
1086    }
1087
1088    /// Buses 1-2-3 in a line, the reference on bus 1, and a three winding
1089    /// transformer on buses 2, 4, and 5.
1090    fn line_with_a_tertiary(tertiary: BusType) -> BalancedNetwork {
1091        let mut buses: Vec<Bus> = (1..=5)
1092            .map(|id| Bus::new(BusId(id), BusType::Pq, 230.0))
1093            .collect();
1094        buses[0].kind = BusType::Ref;
1095        buses[4].kind = tertiary;
1096        let line = |from, to| Branch::new(BusId(from), BusId(to), 0.01, 0.1);
1097        let mut net =
1098            BalancedNetwork::in_memory("tertiary", 100.0, buses, vec![line(1, 2), line(2, 3)]);
1099        net.transformers_3w_mut().push(Transformer3W::new(
1100            [2, 4, 5].map(|bus| Winding::new(BusId(bus))),
1101            [Impedance::new(0.0, 0.1, 100.0); 3],
1102        ));
1103        net.generators_mut().push(generator(1, 100.0));
1104        net
1105    }
1106
1107    #[test]
1108    fn an_island_joined_through_a_dead_tertiary_carves_out_whole() {
1109        let net = line_with_a_tertiary(BusType::Isolated);
1110        let partition = net.calc_islands();
1111        assert_eq!(partition.islands.len(), 1);
1112        assert_eq!(
1113            partition.islands[0].buses,
1114            [BusId(1), BusId(2), BusId(3), BusId(4)]
1115        );
1116        assert_eq!(partition.isolated, [BusId(5)]);
1117
1118        let island: BTreeSet<BusId> = partition.islands[0].buses.iter().copied().collect();
1119        let mut sub = net.subset_buses(&island).network;
1120        assert_eq!(sub.transformers_3w().len(), 1, "the transformer is kept");
1121        let stub = sub.buses().iter().find(|b| b.id == BusId(5)).unwrap();
1122        assert_eq!(stub.kind, BusType::Isolated);
1123        assert_eq!(
1124            stub.extras.get("tie_bus"),
1125            Some(&serde_json::Value::Bool(true))
1126        );
1127        sub.validate().unwrap();
1128        let again = sub.calc_islands();
1129        assert_eq!(again.islands.len(), 1);
1130        assert_eq!(again.islands[0].buses, partition.islands[0].buses);
1131        let report = sub.assign_island_references(IslandReferencePolicy::PerIsland);
1132        assert_eq!(report.diagnostics.as_slice(), []);
1133    }
1134
1135    #[test]
1136    fn a_winding_stated_out_of_service_joins_nothing() {
1137        let mut net = line_with_a_tertiary(BusType::Pq);
1138        net.transformers_3w_mut()[0].extras.insert(
1139            "winding_in_service".into(),
1140            serde_json::json!([true, true, false]),
1141        );
1142        let partition = net.calc_islands();
1143        assert_eq!(partition.islands.len(), 2);
1144        assert_eq!(partition.islands[1].buses, [BusId(5)]);
1145
1146        let island: BTreeSet<BusId> = partition.islands[0].buses.iter().copied().collect();
1147        let sub = net.subset_buses(&island).network;
1148        assert_eq!(sub.transformers_3w().len(), 1);
1149        assert_eq!(sub.calc_islands().islands.len(), 1);
1150        sub.validate().unwrap();
1151    }
1152
1153    #[test]
1154    fn one_island_carves_out_with_its_elements() {
1155        let net = three_islands();
1156        let partition = net.calc_islands();
1157        let island: BTreeSet<BusId> = partition.islands[1].buses.iter().copied().collect();
1158        let cut = net.subset_buses(&island);
1159        assert_eq!(cut.branch_rows, [None, None, Some(0), None, None]);
1160        assert_eq!(cut.switch_rows, [Some(0)]);
1161        assert_eq!(cut.generator_rows, [None, Some(0), Some(1)]);
1162        assert_eq!(cut.load_rows, [None, Some(0), None]);
1163        assert_eq!(cut.hvdc_rows, [None, None]);
1164        let sub = cut.network;
1165        assert_eq!(sub.buses().len(), 3);
1166        assert_eq!(sub.branches().len(), 1);
1167        assert_eq!(sub.switches().len(), 1);
1168        assert_eq!(sub.generators().len(), 2);
1169        assert_eq!(sub.loads().len(), 1);
1170        assert_eq!(sub.hvdc().as_slice(), []);
1171        sub.validate().unwrap();
1172    }
1173
1174    /// Every source row of every table maps to its row in the cut, in order,
1175    /// or to `None` exactly when the cut leaves it out.
1176    #[test]
1177    fn island_cut_row_maps_agree_with_the_cut_network() {
1178        let fixtures = [
1179            three_islands(),
1180            line_with_a_tertiary(BusType::Isolated),
1181            line_with_a_tertiary(BusType::Pq),
1182        ];
1183        for net in fixtures {
1184            for island in net.calc_islands().islands {
1185                let island: BTreeSet<BusId> = island.buses.iter().copied().collect();
1186                let cut = net.subset_buses(&island);
1187                let kept: BTreeSet<BusId> = cut.network.buses().iter().map(|b| b.id).collect();
1188                assert!(island.is_subset(&kept));
1189                macro_rules! check {
1190                    ($map:ident, $table:ident, $kept:expr) => {{
1191                        let source = net.$table();
1192                        let result = cut.network.$table();
1193                        assert_eq!(cut.$map.len(), source.len(), stringify!($map));
1194                        let rows: Vec<usize> = cut.$map.iter().flatten().copied().collect();
1195                        assert_eq!(rows, (0..result.len()).collect::<Vec<_>>());
1196                        for (row, target) in cut.$map.iter().enumerate() {
1197                            let element = &source[row];
1198                            #[allow(clippy::redundant_closure_call)]
1199                            let expected = ($kept)(element);
1200                            assert_eq!(
1201                                target.is_some(),
1202                                expected,
1203                                "{} row {row}",
1204                                stringify!($map)
1205                            );
1206                            if let Some(target) = target {
1207                                assert_eq!(&result[*target], element);
1208                            }
1209                        }
1210                    }};
1211                }
1212                for (row, target) in cut.bus_rows.iter().enumerate() {
1213                    let id = net.buses()[row].id;
1214                    assert_eq!(target.is_some(), kept.contains(&id));
1215                    if let Some(target) = target {
1216                        assert_eq!(cut.network.buses()[*target].id, id);
1217                    }
1218                }
1219                let at = |bus: &BusId| island.contains(bus);
1220                let spans = |from: &BusId, to: &BusId| kept.contains(from) && kept.contains(to);
1221                check!(load_rows, loads, |e: &Load| at(&e.bus));
1222                check!(generator_rows, generators, |e: &Generator| at(&e.bus));
1223                check!(branch_rows, branches, |e: &Branch| spans(&e.from, &e.to));
1224                check!(switch_rows, switches, |e: &Switch| spans(&e.from, &e.to));
1225                check!(hvdc_rows, hvdc, |e: &Hvdc| spans(&e.from, &e.to));
1226                check!(transformer_3w_rows, transformers_3w, |e: &Transformer3W| e
1227                    .windings
1228                    .iter()
1229                    .all(|w| kept.contains(&w.bus))
1230                    && e.windings.iter().any(|w| at(&w.bus)));
1231                assert_eq!(cut.shunt_rows.len(), net.shunts().len());
1232                assert_eq!(cut.storage_rows.len(), net.storage().len());
1233                assert_eq!(
1234                    cut.static_var_compensator_rows.len(),
1235                    net.static_var_compensators().len()
1236                );
1237            }
1238        }
1239    }
1240
1241    #[test]
1242    fn de_energizing_buses_keeps_every_row() {
1243        let mut net = three_islands();
1244        let rows = |net: &BalancedNetwork| {
1245            [
1246                net.buses().len(),
1247                net.branches().len(),
1248                net.switches().len(),
1249                net.generators().len(),
1250                net.loads().len(),
1251                net.hvdc().len(),
1252            ]
1253        };
1254        let before = rows(&net);
1255        // Island B {3, 4, 5} holds generation; de-energizing takes it out too.
1256        net.de_energize_buses(&[BusId(3), BusId(4), BusId(5), BusId(99)].into());
1257        assert_eq!(rows(&net), before, "no row is added or removed");
1258        for bus in [3, 4, 5] {
1259            assert_eq!(kind(&net, bus), BusType::Isolated);
1260        }
1261        assert_eq!(kind(&net, 2), BusType::Pq);
1262        assert!(!net.branches()[2].in_service, "the line 3-4");
1263        assert!(net.switches()[0].closed, "switches keep their state");
1264        assert!(net.generators()[0].in_service);
1265        assert!(!net.generators()[1].in_service && !net.generators()[2].in_service);
1266        assert!(!net.loads()[1].in_service && net.loads()[0].in_service);
1267        let partition = net.calc_islands();
1268        assert_eq!(partition.island_of(BusId(4)), None);
1269        assert_eq!(partition.islands.len(), 2);
1270        net.validate().unwrap();
1271    }
1272
1273    #[test]
1274    fn keeping_one_island_de_energizes_the_others_in_place() {
1275        let mut net = three_islands();
1276        let partition = net.calc_islands();
1277        let island: BTreeSet<BusId> = partition.islands[1].buses.iter().copied().collect();
1278        let cut = net.subset_buses(&island);
1279        let dead = net.keep_island(&island);
1280        // Bus 8 was already isolated; the buses of islands A and C go.
1281        assert_eq!(dead, [1, 2, 6, 7, 9].map(BusId));
1282        let after = net.calc_islands();
1283        assert_eq!(after.islands.len(), 1);
1284        assert_eq!(after.islands[0].buses, partition.islands[1].buses);
1285        // The in-service elements are exactly the ones the cut keeps, at
1286        // their source rows.
1287        for (row, target) in cut.branch_rows.iter().enumerate() {
1288            assert_eq!(net.branches()[row].in_service, target.is_some());
1289        }
1290        for (row, target) in cut.generator_rows.iter().enumerate() {
1291            assert_eq!(net.generators()[row].in_service, target.is_some());
1292        }
1293        for (row, target) in cut.load_rows.iter().enumerate() {
1294            assert_eq!(net.loads()[row].in_service, target.is_some());
1295        }
1296        for (row, target) in cut.hvdc_rows.iter().enumerate() {
1297            assert_eq!(net.hvdc()[row].in_service, target.is_some());
1298        }
1299        assert_eq!(
1300            net.keep_island(&island),
1301            [],
1302            "a second call changes nothing"
1303        );
1304        let report = net.assign_island_references(IslandReferencePolicy::PerIsland);
1305        assert_eq!(report.de_energized, []);
1306        net.validate().unwrap();
1307    }
1308}