BusBreakerTopologyModel.java

/**
 * Copyright (c) 2016-2018, All partners of the iTesla project (http://www.itesla-project.eu/consortium)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.iidm.network.impl;

import com.google.common.base.Functions;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Iterables;
import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.util.Colors;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.util.Identifiables;
import com.powsybl.iidm.network.util.Networks;
import com.powsybl.iidm.network.util.ShortIdDictionary;
import com.powsybl.math.graph.TraversalType;
import com.powsybl.math.graph.TraverseResult;
import com.powsybl.math.graph.UndirectedGraphImpl;
import com.powsybl.math.graph.UndirectedGraphListener;
import org.anarres.graphviz.builder.GraphVizAttribute;
import org.anarres.graphviz.builder.GraphVizEdge;
import org.anarres.graphviz.builder.GraphVizGraph;
import org.anarres.graphviz.builder.GraphVizScope;

import java.io.IOException;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

/**
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
class BusBreakerTopologyModel extends AbstractTopologyModel {

    private static final boolean DRAW_SWITCH_ID = true;

    private final class SwitchAdderImpl extends AbstractIdentifiableAdder<SwitchAdderImpl> implements VoltageLevel.BusBreakerView.SwitchAdder {

        private String busId1;

        private String busId2;

        private boolean open = false;

        private SwitchAdderImpl() {
        }

        @Override
        protected NetworkImpl getNetwork() {
            return BusBreakerTopologyModel.this.getNetwork();
        }

        @Override
        protected String getTypeDescription() {
            return "Switch";
        }

        @Override
        public VoltageLevel.BusBreakerView.SwitchAdder setBus1(String bus1) {
            this.busId1 = bus1;
            return this;
        }

        @Override
        public VoltageLevel.BusBreakerView.SwitchAdder setBus2(String bus2) {
            this.busId2 = bus2;
            return this;
        }

        @Override
        public VoltageLevel.BusBreakerView.SwitchAdder setOpen(boolean open) {
            this.open = open;
            return this;
        }

        @Override
        public Switch add() {
            String id = checkAndGetUniqueId();
            if (busId1 == null) {
                throw new ValidationException(this, "first connection bus is not set");
            }
            if (busId2 == null) {
                throw new ValidationException(this, "second connection bus is not set");
            }
            if (busId1.equals(busId2)) {
                throw new ValidationException(this, "same bus at both ends");
            }

            SwitchImpl aSwitch = new SwitchImpl(voltageLevel, id, getName(), isFictitious(), SwitchKind.BREAKER, open, true);
            getNetwork().getIndex().checkAndAdd(aSwitch);
            addSwitchToTopology(aSwitch, busId1, busId2);
            getNetwork().getListeners().notifyCreation(aSwitch);
            return aSwitch;
        }

    }

    private final UndirectedGraphImpl<ConfiguredBus, SwitchImpl> graph = new UndirectedGraphImpl<>(NODE_INDEX_LIMIT);

    /* buses indexed by vertex number */
    private final Map<String, Integer> buses = new HashMap<>();

    /* switches indexed by edge number */
    private final Map<String, Integer> switches = new HashMap<>();

    private Integer getVertex(String busId, boolean throwException) {
        Objects.requireNonNull(busId, "bus id is null");
        Integer v = buses.get(busId);
        if (throwException && v == null) {
            throw new PowsyblException("Bus " + busId
                    + " not found in voltage level "
                    + voltageLevel.getId());
        }
        return v;
    }

    ConfiguredBus getBus(String busId, boolean throwException) {
        Integer v = getVertex(busId, throwException);
        if (v != null) {
            ConfiguredBus bus = graph.getVertexObject(v);
            if (!bus.getId().equals(busId)) {
                throw new IllegalStateException("Invalid bus id (expected: " + busId + ", actual: " + bus.getId() + ")");
            }
            return bus;
        }
        return null;
    }

    private Integer getEdge(String switchId, boolean throwException) {
        Objects.requireNonNull(switchId, "switch id is null");
        Integer e = switches.get(switchId);
        if (throwException && e == null) {
            throw new PowsyblException("Switch " + switchId
                    + " not found in voltage level"
                    + voltageLevel.getId());
        }
        return e;
    }

    private SwitchImpl getSwitch(String switchId, boolean throwException) {
        Integer e = getEdge(switchId, throwException);
        if (e != null) {
            SwitchImpl aSwitch = graph.getEdgeObject(e);
            if (!aSwitch.getId().equals(switchId)) {
                throw new IllegalStateException("Invalid switch id (expected: " + switchId + ", actual: " + aSwitch.getId() + ")");
            }
            return aSwitch;
        }
        return null;
    }

    /**
     * Bus only topology cache
     */
    private static final class BusCache {

        /* merged bus by id */
        private final Map<String, MergedBus> mergedBus;

        /* bus to merged bus mapping */
        private final Map<ConfiguredBus, MergedBus> mapping;

        private BusCache(Map<String, MergedBus> mergedBus, Map<ConfiguredBus, MergedBus> mapping) {
            this.mergedBus = mergedBus;
            this.mapping = mapping;
        }

        private Collection<MergedBus> getMergedBuses() {
            return mergedBus.values();
        }

        private MergedBus getMergedBus(String id) {
            return mergedBus.get(id);
        }

        private MergedBus getMergedBus(ConfiguredBus cfgBus) {
            return mapping.get(cfgBus);
        }

    }

    /**
     * Bus only topology calculated from bus/breaker topology
     */
    class CalculatedBusTopology {

        protected boolean isBusValid(Set<ConfiguredBus> busSet) {
            int feederCount = 0;
            for (TerminalExt terminal : FluentIterable.from(busSet).transformAndConcat(ConfiguredBus::getConnectedTerminals)) {
                AbstractConnectable connectable = terminal.getConnectable();
                switch (connectable.getType()) {
                    case LINE, TWO_WINDINGS_TRANSFORMER, THREE_WINDINGS_TRANSFORMER, HVDC_CONVERTER_STATION,
                         DANGLING_LINE, LOAD, GENERATOR, BATTERY, SHUNT_COMPENSATOR, STATIC_VAR_COMPENSATOR -> feederCount++;
                    case GROUND -> {
                        // Do nothing
                    }
                    default -> throw new IllegalStateException();
                }
            }
            return Networks.isBusValid(feederCount);
        }

        private MergedBus createMergedBus(int busNum, Set<ConfiguredBus> busSet) {
            String suffix = "_" + busNum;
            String mergedBusId = Identifiables.getUniqueId(voltageLevel.getId() + suffix, getNetwork().getIndex()::contains);
            String mergedBusName = voltageLevel.getOptionalName().map(name -> name + suffix).orElse(null);
            return new MergedBus(mergedBusId, mergedBusName, voltageLevel.isFictitious(), busSet);
        }

        private void updateCache() {
            if (variants.get().cache != null) {
                return;
            }

            Map<String, MergedBus> mergedBuses = new LinkedHashMap<>();

            // mapping between configured buses and merged buses
            Map<ConfiguredBus, MergedBus> mapping = new IdentityHashMap<>();

            boolean[] encountered = new boolean[graph.getVertexCapacity()];
            Arrays.fill(encountered, false);
            int busNum = 0;
            for (int v : graph.getVertices()) {
                if (!encountered[v]) {
                    final Set<ConfiguredBus> busSet = new LinkedHashSet<>(1);
                    busSet.add(graph.getVertexObject(v));
                    graph.traverse(v, TraversalType.DEPTH_FIRST, (v1, e, v2) -> {
                        SwitchImpl aSwitch = graph.getEdgeObject(e);
                        if (aSwitch.isOpen()) {
                            return TraverseResult.TERMINATE_PATH;
                        } else {
                            busSet.add(graph.getVertexObject(v2));
                            return TraverseResult.CONTINUE;
                        }
                    }, encountered);
                    if (isBusValid(busSet)) {
                        MergedBus mergedBus = createMergedBus(busNum++, busSet);
                        mergedBuses.put(mergedBus.getId(), mergedBus);
                        busSet.forEach(bus -> mapping.put(bus, mergedBus));
                    }
                }
            }

            variants.get().cache = new BusCache(mergedBuses, mapping);
        }

        private void invalidateCache() {
            // detach buses
            if (variants.get().cache != null) {
                for (MergedBus bus : variants.get().cache.getMergedBuses()) {
                    bus.invalidate();
                }
                variants.get().cache = null;
            }
        }

        private Collection<MergedBus> getMergedBuses() {
            updateCache();
            return variants.get().cache.getMergedBuses();
        }

        private MergedBus getMergedBus(String mergedBusId, boolean throwException) {
            updateCache();
            MergedBus bus = variants.get().cache.getMergedBus(mergedBusId);
            if (throwException && bus == null) {
                throw new PowsyblException("Bus " + mergedBusId
                        + " not found in voltage level "
                        + voltageLevel.getId());
            }
            return bus;
        }

        MergedBus getMergedBus(ConfiguredBus bus) {
            Objects.requireNonNull(bus, "bus is null");
            updateCache();
            return variants.get().cache.getMergedBus(bus);
        }

    }

    final CalculatedBusTopology calculatedBusTopology
            = new CalculatedBusTopology();

    private static final class VariantImpl implements Variant {

        private BusCache cache;

        private VariantImpl() {
        }

        @Override
        public VariantImpl copy() {
            return new VariantImpl();
        }
    }

    protected final VariantArray<VariantImpl> variants;

    BusBreakerTopologyModel(VoltageLevelExt voltageLevel) {
        super(voltageLevel);
        // the ref object of the variant array is the same as the current object
        variants = new VariantArray<>(voltageLevel.getNetworkRef(), VariantImpl::new);
        // invalidate topology and connected components
        graph.addListener(new UndirectedGraphListener<>() {
            @Override
            public void vertexAdded(int v) {
                invalidateCache();
            }

            @Override
            public void vertexObjectSet(int v, ConfiguredBus obj) {
                invalidateCache();
            }

            @Override
            public void vertexRemoved(int v, ConfiguredBus obj) {
                invalidateCache();
            }

            @Override
            public void allVerticesRemoved() {
                invalidateCache();
            }

            @Override
            public void edgeAdded(int e, SwitchImpl obj) {
                invalidateCache();
            }

            @Override
            public void edgeBeforeRemoval(int e, SwitchImpl obj) {
                // Nothing to do, notifications are handled properly in removeSwitch
            }

            @Override
            public void edgeRemoved(int e, SwitchImpl obj) {
                invalidateCache();
            }

            @Override
            public void allEdgesBeforeRemoval(Collection<SwitchImpl> obj) {
                // Nothing to do, notifications are handled properly in removeAllSwitches
            }

            @Override
            public void allEdgesRemoved(Collection<SwitchImpl> obj) {
                invalidateCache();
            }
        });
    }

    @Override
    public void invalidateCache(boolean exceptBusBreakerView) {
        calculatedBusTopology.invalidateCache();
        getNetwork().getBusView().invalidateCache();
        getNetwork().getBusBreakerView().invalidateCache();
        getNetwork().getConnectedComponentsManager().invalidate();
        getNetwork().getSynchronousComponentsManager().invalidate();
    }

    @Override
    public Iterable<Terminal> getTerminals() {
        return FluentIterable.from(graph.getVerticesObj())
                .transformAndConcat(ConfiguredBus::getTerminals)
                .transform(Terminal.class::cast);
    }

    @Override
    public Stream<Terminal> getTerminalStream() {
        return graph.getVertexObjectStream().flatMap(bus -> bus.getTerminals().stream());
    }

    static PowsyblException createNotSupportedBusBreakerTopologyException() {
        return new PowsyblException("Not supported in a bus breaker topology");
    }

    private final VoltageLevelExt.NodeBreakerViewExt nodeBreakerView = new VoltageLevelExt.NodeBreakerViewExt() {
        @Override
        public double getFictitiousP0(int node) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public VoltageLevel.NodeBreakerView setFictitiousP0(int node, double p0) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public double getFictitiousQ0(int node) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public VoltageLevel.NodeBreakerView setFictitiousQ0(int node, double q0) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public int getMaximumNodeIndex() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public int[] getNodes() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public int getNode1(String switchId) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public int getNode2(String switchId) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public Terminal getTerminal(int node) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public Stream<Switch> getSwitchStream(int node) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public List<Switch> getSwitches(int node) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public IntStream getNodeInternalConnectedToStream(int node) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public List<Integer> getNodesInternalConnectedTo(int node) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public Optional<Terminal> getOptionalTerminal(int node) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public boolean hasAttachedEquipment(int node) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public Terminal getTerminal1(String switchId) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public Terminal getTerminal2(String switchId) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public SwitchAdder newSwitch() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public InternalConnectionAdder newInternalConnection() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public int getInternalConnectionCount() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public Iterable<InternalConnection> getInternalConnections() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public Stream<InternalConnection> getInternalConnectionStream() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public void removeInternalConnections(int node1, int node2) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public SwitchAdder newBreaker() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public SwitchAdder newDisconnector() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public Switch getSwitch(String switchId) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public Stream<Switch> getSwitchStream() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public Iterable<Switch> getSwitches() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public int getSwitchCount() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public void removeSwitch(String switchId) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public BusbarSectionAdder newBusbarSection() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public Iterable<BusbarSection> getBusbarSections() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public Stream<BusbarSection> getBusbarSectionStream() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public int getBusbarSectionCount() {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public BusbarSection getBusbarSection(String id) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public void traverse(int node, TopologyTraverser traverser) {
            throw createNotSupportedBusBreakerTopologyException();
        }

        @Override
        public void traverse(int[] node, TopologyTraverser traverser) {
            throw createNotSupportedBusBreakerTopologyException();
        }
    };

    @Override
    public VoltageLevelExt.NodeBreakerViewExt getNodeBreakerView() {
        return nodeBreakerView;
    }

    private final VoltageLevelExt.BusBreakerViewExt busBreakerView = new VoltageLevelExt.BusBreakerViewExt() {

        @Override
        public Iterable<Bus> getBuses() {
            return Iterables.unmodifiableIterable(Iterables.transform(graph.getVerticesObj(), Functions.identity()));
        }

        @Override
        public Stream<Bus> getBusStream() {
            return graph.getVertexObjectStream().map(Function.identity());
        }

        @Override
        public int getBusCount() {
            return graph.getVertexCount();
        }

        @Override
        public ConfiguredBus getBus(String id) {
            return BusBreakerTopologyModel.this.getBus(id, false);
        }

        @Override
        public BusAdder newBus() {
            return new BusAdderImpl(voltageLevel);
        }

        @Override
        public void removeBus(String busId) {
            BusBreakerTopologyModel.this.removeBus(busId);
        }

        @Override
        public void removeAllBuses() {
            BusBreakerTopologyModel.this.removeAllBuses();
        }

        @Override
        public Iterable<Switch> getSwitches() {
            return Iterables.unmodifiableIterable(Iterables.transform(graph.getEdgesObject(), Functions.identity()));
        }

        @Override
        public Stream<Switch> getSwitchStream() {
            return graph.getEdgeObjectStream().map(Function.identity());
        }

        @Override
        public int getSwitchCount() {
            return graph.getEdgeCount();
        }

        @Override
        public void removeSwitch(String switchId) {
            BusBreakerTopologyModel.this.removeSwitch(switchId);
        }

        @Override
        public void removeAllSwitches() {
            BusBreakerTopologyModel.this.removeAllSwitches();
        }

        @Override
        public ConfiguredBus getBus1(String switchId) {
            int e = getEdge(switchId, true);
            int v1 = graph.getEdgeVertex1(e);
            return graph.getVertexObject(v1);
        }

        @Override
        public ConfiguredBus getBus2(String switchId) {
            int e = getEdge(switchId, true);
            int v2 = graph.getEdgeVertex2(e);
            return graph.getVertexObject(v2);
        }

        @Override
        public Collection<Bus> getBusesFromBusViewBusId(String mergedBusId) {
            return getBusStreamFromBusViewBusId(mergedBusId).collect(Collectors.toSet());
        }

        @Override
        public Stream<Bus> getBusStreamFromBusViewBusId(String mergedBusId) {
            MergedBus bus = (MergedBus) busView.getBus(mergedBusId);
            Objects.requireNonNull(bus, "bus is null");
            calculatedBusTopology.updateCache();
            return variants.get().cache.mapping.entrySet().stream().filter(e -> e.getValue() == bus).map(e -> (Bus) e.getKey()).distinct();
        }

        @Override
        public SwitchImpl getSwitch(String switchId) {
            return BusBreakerTopologyModel.this.getSwitch(switchId, false);
        }

        @Override
        public VoltageLevel.BusBreakerView.SwitchAdder newSwitch() {
            return new SwitchAdderImpl();
        }

        private com.powsybl.math.graph.Traverser adapt(TopologyTraverser t) {
            return (vertex1, e, vertex2) -> t.traverse(graph.getVertexObject(vertex1), graph.getEdgeObject(e), graph.getVertexObject(vertex2));
        }

        @Override
        public void traverse(Bus bus, TopologyTraverser traverser) {
            graph.traverse(getVertex(bus.getId(), true), TraversalType.DEPTH_FIRST, adapt(traverser));
        }
    };

    @Override
    public VoltageLevelExt.BusBreakerViewExt getBusBreakerView() {
        return busBreakerView;
    }

    private final VoltageLevelExt.BusViewExt busView = new VoltageLevelExt.BusViewExt() {

        @Override
        public Iterable<Bus> getBuses() {
            return Collections.unmodifiableCollection(calculatedBusTopology.getMergedBuses());
        }

        @Override
        public Stream<Bus> getBusStream() {
            return calculatedBusTopology.getMergedBuses().stream().map(Function.identity());
        }

        @Override
        public MergedBus getBus(String id) {
            return calculatedBusTopology.getMergedBus(id, false);
        }

        @Override
        public Bus getMergedBus(String configuredBusId) {
            ConfiguredBus b = (ConfiguredBus) busBreakerView.getBus(configuredBusId);
            return calculatedBusTopology.getMergedBus(b);
        }
    };

    @Override
    public VoltageLevelExt.BusViewExt getBusView() {
        return busView;
    }

    @Override
    public Iterable<Switch> getSwitches() {
        return getBusBreakerView().getSwitches();
    }

    @Override
    public int getSwitchCount() {
        return getBusBreakerView().getSwitchCount();
    }

    @Override
    public TopologyKind getTopologyKind() {
        return TopologyKind.BUS_BREAKER;
    }

    void addBus(ConfiguredBus bus) {
        getNetwork().getIndex().checkAndAdd(bus);
        int v = graph.addVertex();
        graph.setVertexObject(v, bus);
        buses.put(bus.getId(), v);
    }

    private void removeBus(String busId) {
        ConfiguredBus bus = getBus(busId, true);
        if (bus.getTerminalCount() > 0) {
            throw new ValidationException(voltageLevel, "Cannot remove bus "
                    + bus.getId() + " because of connectable equipments");
        }
        // TODO improve check efficency
        for (Map.Entry<String, Integer> entry : switches.entrySet()) {
            String switchId = entry.getKey();
            int e = entry.getValue();
            int v1 = graph.getEdgeVertex1(e);
            int v2 = graph.getEdgeVertex2(e);
            ConfiguredBus b1 = graph.getVertexObject(v1);
            ConfiguredBus b2 = graph.getVertexObject(v2);
            if (bus == b1 || bus == b2) {
                throw new PowsyblException("Cannot remove bus '" + bus.getId()
                        + "' because switch '" + switchId + "' is connected to it");
            }
        }

        NetworkImpl network = getNetwork();
        network.getListeners().notifyBeforeRemoval(bus);

        network.getIndex().remove(bus);
        int v = buses.remove(bus.getId());
        graph.removeVertex(v);

        network.getListeners().notifyAfterRemoval(busId);
    }

    private void removeAllBuses() {
        if (graph.getEdgeCount() > 0) {
            throw new ValidationException(voltageLevel, "Cannot remove all buses because there is still some switches");
        }
        for (ConfiguredBus bus : graph.getVerticesObj()) {
            if (bus.getTerminalCount() > 0) {
                throw new ValidationException(voltageLevel, "Cannot remove bus "
                        + bus.getId() + " because of connected equipments");
            }
        }
        NetworkImpl network = getNetwork();
        List<String> removedBusesIds = new ArrayList<>(graph.getVertexCount());
        for (ConfiguredBus bus : graph.getVerticesObj()) {
            removedBusesIds.add(bus.getId());
            network.getListeners().notifyBeforeRemoval(bus);

            network.getIndex().remove(bus);
        }
        graph.removeAllVertices();
        buses.clear();

        removedBusesIds.forEach(id -> network.getListeners().notifyAfterRemoval(id));
    }

    void addSwitchToTopology(SwitchImpl aSwitch, String busId1, String busId2) {
        int v1 = getVertex(busId1, true);
        int v2 = getVertex(busId2, true);
        int e = graph.addEdge(v1, v2, aSwitch);
        switches.put(aSwitch.getId(), e);
    }

    private void removeSwitch(String switchId) {
        Integer e = switches.get(switchId);
        if (e == null) {
            throw new PowsyblException("Switch '" + switchId
                    + "' not found in voltage level '" + voltageLevel.getId() + "'");
        }
        NetworkImpl network = getNetwork();
        SwitchImpl aSwitch = graph.getEdgeObject(e);
        network.getListeners().notifyBeforeRemoval(aSwitch);

        switches.remove(switchId);
        graph.removeEdge(e);
        network.getIndex().remove(aSwitch);

        network.getListeners().notifyAfterRemoval(switchId);
    }

    private void removeAllSwitches() {
        NetworkImpl network = getNetwork();

        List<String> removedSwitchesIds = new ArrayList<>(graph.getEdgeCount());
        for (SwitchImpl s : graph.getEdgesObject()) {
            removedSwitchesIds.add(s.getId());
            network.getListeners().notifyBeforeRemoval(s);

            network.getIndex().remove(s);
        }
        graph.removeAllEdges();
        switches.clear();

        for (String removedSwitchId : removedSwitchesIds) {
            network.getListeners().notifyAfterRemoval(removedSwitchId);
        }
    }

    private void checkTerminal(TerminalExt terminal) {
        if (!(terminal instanceof BusTerminal)) {
            throw new ValidationException(terminal.getConnectable(),
                    "voltage level " + voltageLevel.getId() + " has a bus/breaker topology"
                            + ", a bus connection should be specified instead of a node connection");
        }

        // check connectable buses exist
        String connectableBusId = ((BusTerminal) terminal).getConnectableBusId();
        if (connectableBusId != null) {
            getBus(connectableBusId, true);
        }
    }

    @Override
    public void attach(final TerminalExt terminal, boolean test) {
        checkTerminal(terminal);
        if (test) {
            return;
        }
        // create the link terminal -> voltage level
        terminal.setVoltageLevel(voltageLevel);

        // create the link bus -> terminal
        String connectableBusId = ((BusTerminal) terminal).getConnectableBusId();

        final ConfiguredBus connectableBus = getBus(connectableBusId, true);

        getNetwork().getVariantManager().forEachVariant(() -> {
            connectableBus.addTerminal((BusTerminal) terminal);

            // invalidate connected components
            invalidateCache();
        });
    }

    @Override
    public void detach(final TerminalExt terminal) {
        if (!(terminal instanceof BusTerminal)) {
            throw new IllegalArgumentException("Incorrect terminal type");
        }

        // remove the link bus -> terminal
        String connectableBusId = ((BusTerminal) terminal).getConnectableBusId();

        final ConfiguredBus connectableBus = getBus(connectableBusId, true);

        getNetwork().getVariantManager().forEachVariant(() -> {
            connectableBus.removeTerminal((BusTerminal) terminal);
            ((BusTerminal) terminal).unsetConnectableBusId();

            invalidateCache();
        });
        // remove the link terminal -> voltage level
        terminal.setVoltageLevel(null);
    }

    boolean connect(TerminalExt terminal) {
        if (!(terminal instanceof BusTerminal)) {
            throw new IllegalStateException("Given TerminalExt not supported: " + terminal.getClass().getName());
        }

        // already connected?
        if (terminal.isConnected()) {
            return false;
        }

        ((BusTerminal) terminal).setConnected(true);

        // invalidate connected components
        invalidateCache();

        return true;
    }

    @Override
    public boolean connect(TerminalExt terminal, Predicate<? super SwitchImpl> isTypeSwitchToOperate) {
        return connect(terminal);
    }

    boolean disconnect(TerminalExt terminal) {
        if (!(terminal instanceof BusTerminal)) {
            throw new IllegalStateException("Given TerminalExt not supported: " + terminal.getClass().getName());
        }
        // already disconnected?
        if (!terminal.isConnected()) {
            return false;
        }

        ((BusTerminal) terminal).setConnected(false);

        // invalidate connected components
        invalidateCache();

        return true;
    }

    @Override
    public boolean disconnect(TerminalExt terminal, Predicate<? super SwitchImpl> isSwitchOpenable) {
        return disconnect(terminal);
    }

    void traverse(BusTerminal terminal, Terminal.TopologyTraverser traverser, TraversalType traversalType) {
        traverse(terminal, traverser, new HashSet<>(), traversalType);
    }

    /**
     * Traverse from given bus terminal using the given topology traverser, using the fact that the terminals in the
     * given set have already been traversed.
     * @return false if the traverser has to stop, meaning that a {@link TraverseResult#TERMINATE_TRAVERSER}
     * has been returned from the traverser, true otherwise
     */
    boolean traverse(BusTerminal terminal, Terminal.TopologyTraverser traverser, Set<Terminal> visitedTerminals, TraversalType traversalType) {
        Objects.requireNonNull(terminal);
        Objects.requireNonNull(traverser);
        Objects.requireNonNull(visitedTerminals);

        // check if we are allowed to traverse the terminal itself
        TraverseResult termTraverseResult = getTraverserResult(visitedTerminals, terminal, traverser);
        if (termTraverseResult == TraverseResult.TERMINATE_TRAVERSER) {
            return false;
        } else if (termTraverseResult == TraverseResult.CONTINUE) {
            List<TerminalExt> nextTerminals = new ArrayList<>();
            addNextTerminals(terminal, nextTerminals);

            // then check we can traverse terminals connected to same bus
            int v = getVertex(terminal.getConnectableBusId(), true);
            ConfiguredBus bus = graph.getVertexObject(v);
            for (BusTerminal t : bus.getTerminals()) {
                TraverseResult tTraverseResult = getTraverserResult(visitedTerminals, t, traverser);
                if (tTraverseResult == TraverseResult.TERMINATE_TRAVERSER) {
                    return false;
                } else if (tTraverseResult == TraverseResult.CONTINUE) {
                    addNextTerminals(t, nextTerminals);
                }
            }

            // then go through other buses of the voltage level
            boolean traversalTerminated = traverseOtherBuses(v, nextTerminals, traverser, visitedTerminals, traversalType);
            if (traversalTerminated) {
                return false;
            }

            for (TerminalExt t : nextTerminals) {
                if (!t.traverse(traverser, visitedTerminals, traversalType)) {
                    return false;
                }
            }
        }

        return true;
    }

    private boolean traverseOtherBuses(int v, List<TerminalExt> nextTerminals,
                                       Terminal.TopologyTraverser traverser, Set<Terminal> visitedTerminals, TraversalType traversalType) {
        return !graph.traverse(v, traversalType, (v1, e, v2) -> {
            SwitchImpl aSwitch = graph.getEdgeObject(e);
            List<BusTerminal> otherBusTerminals = graph.getVertexObject(v2).getTerminals();
            TraverseResult switchTraverseResult = traverser.traverse(aSwitch);
            if (switchTraverseResult == TraverseResult.CONTINUE && !otherBusTerminals.isEmpty()) {
                BusTerminal otherTerminal = otherBusTerminals.get(0);
                TraverseResult otherTermTraverseResult = getTraverserResult(visitedTerminals, otherTerminal, traverser);
                if (otherTermTraverseResult == TraverseResult.CONTINUE) {
                    addNextTerminals(otherTerminal, nextTerminals);
                }
                return otherTermTraverseResult;
            }
            return switchTraverseResult;
        });
    }

    private static TraverseResult getTraverserResult(Set<Terminal> visitedTerminals, BusTerminal terminal, Terminal.TopologyTraverser traverser) {
        return visitedTerminals.add(terminal) ? traverser.traverse(terminal, terminal.isConnected()) : TraverseResult.TERMINATE_PATH;
    }

    @Override
    public void extendVariantArraySize(int initVariantArraySize, int number, int sourceIndex) {
        variants.push(number, () -> variants.copy(sourceIndex));
    }

    @Override
    public void reduceVariantArraySize(int number) {
        variants.pop(number);
    }

    @Override
    public void deleteVariantArrayElement(int index) {
        variants.delete(index);
    }

    @Override
    public void allocateVariantArrayElement(int[] indexes, final int sourceIndex) {
        variants.allocate(indexes, () -> variants.copy(sourceIndex));
    }

    @Override
    protected void removeTopology() {
        removeAllSwitches();
        removeAllBuses();
    }

    @Override
    public void printTopology() {
        printTopology(System.out, null);
    }

    @Override
    public void printTopology(PrintStream out, ShortIdDictionary dict) {
        out.println("-------------------------------------------------------------");
        out.println("Topology of " + voltageLevel.getId());

        Function<ConfiguredBus, String> vertexToString = bus -> {
            StringBuilder builder = new StringBuilder();
            builder.append(bus.getId())
                    .append(" [");
            for (Iterator<TerminalExt> it = bus.getConnectedTerminals().iterator(); it.hasNext(); ) {
                TerminalExt terminal = it.next();
                builder.append(dict != null ? dict.getShortId(terminal.getConnectable().getId()) : terminal.getConnectable().getId());
                if (it.hasNext()) {
                    builder.append(", ");
                }
            }
            builder.append("]");
            return builder.toString();
        };

        Function<SwitchImpl, String> edgeToString = aSwitch -> {
            StringBuilder builder = new StringBuilder();
            builder.append("id=").append(aSwitch.getId())
                    .append(" status=").append(aSwitch.isOpen() ? "open" : "closed");
            return builder.toString();
        };

        graph.print(out, vertexToString, edgeToString);
    }

    @Override
    public void exportTopology(Path file) throws IOException {
        try (Writer writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
            exportTopology(writer);
        }
    }

    @Override
    public void exportTopology(Writer writer) {
        exportTopology(writer, new SecureRandom());
    }

    @Override
    public void exportTopology(Writer writer, Random random) {
        Objects.requireNonNull(writer);
        Objects.requireNonNull(random);

        GraphVizScope scope = new GraphVizScope.Impl();
        GraphVizGraph gvGraph = new GraphVizGraph();

        String[] colors = Colors.generateColorScale(graph.getVertexCount(), random);
        int i = 0;
        for (ConfiguredBus bus : graph.getVerticesObj()) {
            gvGraph.node(scope, bus.getId())
                    .label("BUS" + System.lineSeparator() + bus.getId())
                    .shape("ellipse")
                    .style("filled")
                    .attr(GraphVizAttribute.fillcolor, colors[i]);
            for (TerminalExt terminal : bus.getTerminals()) {
                AbstractConnectable connectable = terminal.getConnectable();
                String label = connectable.getType().toString()
                    + System.lineSeparator() + connectable.getId()
                    + connectable.getOptionalName().map(name -> System.lineSeparator() + name).orElse("");
                gvGraph.node(scope, connectable.getId())
                        .label(label)
                        .shape("ellipse")
                        .style("filled")
                        .attr(GraphVizAttribute.fillcolor, colors[i]);
            }
            i++;
        }

        for (ConfiguredBus bus : graph.getVerticesObj()) {
            for (TerminalExt terminal : bus.getTerminals()) {
                AbstractConnectable connectable = terminal.getConnectable();
                gvGraph.edge(scope, bus.getId(), connectable.getId())
                        .style(terminal.isConnected() ? "solid" : "dotted");
            }
        }
        for (int e = 0; e < graph.getEdgeCount(); e++) {
            int v1 = graph.getEdgeVertex1(e);
            int v2 = graph.getEdgeVertex2(e);
            SwitchImpl sw = graph.getEdgeObject(e);
            ConfiguredBus bus1 = graph.getVertexObject(v1);
            ConfiguredBus bus2 = graph.getVertexObject(v2);
            // Assign an id to the edge to allow parallel edges (multigraph)
            GraphVizEdge edge = gvGraph.edge(scope, bus1.getId(), bus2.getId(), sw.getId())
                    .style(sw.isOpen() ? "dotted" : "solid");
            if (DRAW_SWITCH_ID) {
                String label = sw.getKind().toString()
                    + System.lineSeparator() + sw.getId()
                    + sw.getOptionalName().map(n -> System.lineSeparator() + n).orElse("");
                edge.label(label)
                        .attr(GraphVizAttribute.fontsize, "10");
            }
        }

        try {
            gvGraph.writeTo(writer);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

}