NodeBreakerTopologyModel.java

/**
 * Copyright (c) 2016, 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.base.Predicates;
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.VoltageLevel.NodeBreakerView.InternalConnectionAdder;
import com.powsybl.iidm.network.VoltageLevel.NodeBreakerView.SwitchAdder;
import com.powsybl.iidm.network.util.Identifiables;
import com.powsybl.iidm.network.util.ShortIdDictionary;
import com.powsybl.iidm.network.util.SwitchPredicates;
import com.powsybl.math.graph.*;
import gnu.trove.TCollections;
import gnu.trove.list.array.TDoubleArrayList;
import gnu.trove.list.array.TIntArrayList;
import gnu.trove.map.TIntObjectMap;
import gnu.trove.map.hash.TIntObjectHashMap;
import gnu.trove.set.TIntSet;
import gnu.trove.set.hash.TIntHashSet;
import org.anarres.graphviz.builder.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.concurrent.CopyOnWriteArrayList;
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 NodeBreakerTopologyModel extends AbstractTopologyModel {

    private static final Logger LOGGER = LoggerFactory.getLogger(NodeBreakerTopologyModel.class);

    private static final String WRONG_TERMINAL_TYPE_EXCEPTION_MESSAGE = "Given TerminalExt not supported: ";

    private static final boolean DRAW_SWITCH_ID = true;

    private static final BusChecker CALCULATED_BUS_CHECKER = new CalculatedBusChecker();

    private static final BusChecker CALCULATED_BUS_BREAKER_CHECKER = new CalculatedBusBreakerChecker();

    private static final BusNamingStrategy NAMING_STRATEGY = new LowestNodeNumberBusNamingStrategy();

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

    private final Map<String, Integer> switches = new HashMap<>();

    private final class VariantImpl implements Variant {

        final CalculatedBusTopology calculatedBusTopology
                = new CalculatedBusTopology();

        final CalculatedBusBreakerTopology calculatedBusBreakerTopology
                = new CalculatedBusBreakerTopology();

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

    }

    private final VariantArray<VariantImpl> variants;

    private final class SwitchAdderImpl extends AbstractIdentifiableAdder<SwitchAdderImpl> implements SwitchAdder {

        private Integer node1;

        private Integer node2;

        private SwitchKind kind;

        private boolean open = false;

        private boolean retained = false;

        private SwitchAdderImpl() {
            this(null);
        }

        private SwitchAdderImpl(SwitchKind kind) {
            this.kind = kind;
        }

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

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

        @Override
        public SwitchAdder setNode1(int node1) {
            this.node1 = node1;
            return this;
        }

        @Override
        public SwitchAdder setNode2(int node2) {
            this.node2 = node2;
            return this;
        }

        @Override
        public SwitchAdder setKind(SwitchKind kind) {
            if (kind == null) {
                throw new NullPointerException("kind is null");
            }
            this.kind = kind;
            return this;
        }

        @Override
        public SwitchAdder setKind(String kind) {
            return setKind(SwitchKind.valueOf(kind));
        }

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

        @Override
        public SwitchAdder setRetained(boolean retained) {
            this.retained = retained;
            return this;
        }

        @Override
        public Switch add() {
            String id = checkAndGetUniqueId();
            if (node1 == null) {
                throw new ValidationException(this, "first connection node is not set");
            }
            if (node2 == null) {
                throw new ValidationException(this, "second connection node is not set");
            }
            if (node1.equals(node2)) {
                throw new ValidationException(this, "same node at both ends");
            }
            if (kind == null) {
                throw new ValidationException(this, "kind is not set");
            }
            SwitchImpl aSwitch = new SwitchImpl(voltageLevel, id, getName(), isFictitious(), kind, open, retained);
            graph.addVertexIfNotPresent(node1);
            graph.addVertexIfNotPresent(node2);
            graph.addEdge(node1, node2, aSwitch);
            return aSwitch;
        }

    }

    private final class InternalConnectionAdderImpl implements InternalConnectionAdder {

        private Integer node1;

        private Integer node2;

        private InternalConnectionAdderImpl() {
        }

        @Override
        public InternalConnectionAdder setNode1(int node1) {
            this.node1 = node1;
            return this;
        }

        @Override
        public InternalConnectionAdder setNode2(int node2) {
            this.node2 = node2;
            return this;
        }

        @Override
        public void add() {
            if (node1 == null) {
                throw new ValidationException(voltageLevel, "first connection node is not set");
            }
            if (node2 == null) {
                throw new ValidationException(voltageLevel, "second connection node is not set");
            }
            graph.addVertexIfNotPresent(node1);
            graph.addVertexIfNotPresent(node2);
            graph.addEdge(node1, node2, null);
        }

    }

    /**
     * Cached data for buses
     */
    private static final class BusCache {

        private final CalculatedBus[] node2bus;

        private final Map<String, CalculatedBus> id2bus;

        private BusCache(CalculatedBus[] node2bus, Map<String, CalculatedBus> id2bus) {
            this.node2bus = node2bus;
            this.id2bus = id2bus;
        }

        private Collection<CalculatedBus> getBuses() {
            return id2bus.values();
        }

        private int getBusCount() {
            return id2bus.size();
        }

        private CalculatedBus getBus(int node) {
            return node2bus[node];
        }

        private CalculatedBus getBus(String id) {
            return id2bus.get(id);
        }
    }

    /**
     * Bus topology calculated from node breaker topology
     */
    class CalculatedBusTopology {

        protected BusCache busCache;

        protected void updateCache() {
            updateCache(Switch::isOpen);
        }

        protected BusChecker getBusChecker() {
            return CALCULATED_BUS_CHECKER;
        }

        private void traverse(int n, boolean[] encountered, Predicate<SwitchImpl> terminate, Map<String, CalculatedBus> id2bus, CalculatedBus[] node2bus) {
            if (!encountered[n]) {
                final TIntArrayList nodes = new TIntArrayList(1);
                nodes.add(n);
                Traverser traverser = (n1, e, n2) -> {
                    SwitchImpl aSwitch = graph.getEdgeObject(e);
                    if (aSwitch != null && terminate.test(aSwitch)) {
                        return TraverseResult.TERMINATE_PATH;
                    }

                    if (!encountered[n2]) {
                        // We need to check this as the traverser might be called twice with the same n2 but with different edges.
                        // Note that the "encountered" array is used and maintained inside graph::traverse method, hence we should not update it.
                        nodes.add(n2);
                    }
                    return TraverseResult.CONTINUE;
                };
                graph.traverse(n, TraversalType.DEPTH_FIRST, traverser, encountered);

                // check that the component is a bus
                String busId = Identifiables.getUniqueId(NAMING_STRATEGY.getId(voltageLevel, nodes), getNetwork().getIndex()::contains);
                CopyOnWriteArrayList<NodeTerminal> terminals = new CopyOnWriteArrayList<>();
                for (int i = 0; i < nodes.size(); i++) {
                    int n2 = nodes.getQuick(i);
                    NodeTerminal terminal2 = graph.getVertexObject(n2);
                    if (terminal2 != null) {
                        terminals.add(terminal2);
                    }
                }
                if (getBusChecker().isValid(graph, nodes, terminals)) {
                    addBus(nodes, id2bus, node2bus, busId, terminals);
                }
            }
        }

        private void addBus(TIntArrayList nodes, Map<String, CalculatedBus> id2bus, CalculatedBus[] node2bus,
                            String busId, CopyOnWriteArrayList<NodeTerminal> terminals) {
            String busName = NAMING_STRATEGY.getName(voltageLevel, nodes);
            Function<Terminal, Bus> getBusFromTerminal = getBusChecker() == CALCULATED_BUS_CHECKER ? t -> t.getBusView().getBus() : t -> t.getBusBreakerView().getBus();
            CalculatedBusImpl bus = new CalculatedBusImpl(busId, busName, voltageLevel.isFictitious(), voltageLevel, nodes, terminals, getBusFromTerminal);
            id2bus.put(busId, bus);
            for (int i = 0; i < nodes.size(); i++) {
                node2bus[nodes.getQuick(i)] = bus;
            }
        }

        protected void updateCache(final Predicate<SwitchImpl> terminate) {
            if (busCache != null) {
                return;
            }
            LOGGER.trace("Update bus topology of voltage level {}", voltageLevel.getId());
            Map<String, CalculatedBus> id2bus = new LinkedHashMap<>();
            CalculatedBus[] node2bus = new CalculatedBus[graph.getVertexCapacity()];
            boolean[] encountered = new boolean[graph.getVertexCapacity()];
            for (int v : graph.getVertices()) {
                traverse(v, encountered, terminate, id2bus, node2bus);
            }
            busCache = new BusCache(node2bus, id2bus);
            LOGGER.trace("Found buses {}", id2bus.values());
        }

        protected void invalidateCache() {
            // detach buses
            if (busCache != null) {
                for (CalculatedBus bus : busCache.id2bus.values()) {
                    bus.invalidate();
                }
                busCache = null;
            }
        }

        Collection<CalculatedBus> getBuses() {
            updateCache();
            return busCache.getBuses();
        }

        int getBusCount() {
            updateCache();
            return busCache.getBusCount();
        }

        CalculatedBus getBus(int node) {
            updateCache();
            return busCache.getBus(node);
        }

        CalculatedBus getBus(String id, boolean throwException) {
            updateCache();
            CalculatedBus bus = busCache.getBus(id);
            if (throwException && bus == null) {
                throw new PowsyblException(getExceptionMessageElementNotFound("Bus", id));
            }
            return bus;
        }

        BusExt getConnectableBus(int node) {
            // check id the node is associated to a bus
            BusExt connectableBus = getBus(node);
            if (connectableBus != null) {
                return connectableBus;
            }
            // if not traverse the graph starting from the node (without stopping at open switches) until finding another
            // node associated to a bus
            BusExt[] connectableBus2 = new BusExt[1];
            graph.traverse(node, TraversalType.DEPTH_FIRST, (v1, e, v2) -> {
                if (connectableBus2[0] != null) {
                    // traverse does not stop the algorithm when TERMINATE, it only stops searching in a given direction
                    // this condition insures that while checking all the edges (in every direction) of a node, if a bus is found, it will not be lost
                    return TraverseResult.TERMINATE_PATH;
                }
                connectableBus2[0] = getBus(v2);
                if (connectableBus2[0] != null) {
                    return TraverseResult.TERMINATE_PATH;
                }
                return TraverseResult.CONTINUE;
            });
            // if nothing found, just take the first bus
            if (connectableBus2[0] == null) {
                Collection<CalculatedBus> buses = getBuses();
                if (buses.isEmpty()) { // if the whole voltage level is disconnected, return null
                    return null;
                }
                return buses.iterator().next();
            }
            return connectableBus2[0];
        }
    }

    /**
     * Bus breaker topology calculated from node breaker topology
     */
    class CalculatedBusBreakerTopology extends CalculatedBusTopology {

        @Override
        protected void updateCache() {
            updateCache(sw -> sw.isOpen() || sw.isRetained());
        }

        @Override
        protected BusChecker getBusChecker() {
            return CALCULATED_BUS_BREAKER_CHECKER;
        }

        Bus getBus1(String switchId, boolean throwException) {
            int edge = getEdge(switchId, throwException);
            SwitchImpl aSwitch = graph.getEdgeObject(edge);
            if (!aSwitch.isRetained()) {
                if (throwException) {
                    throw createSwitchNotFoundException(switchId);
                }
                return null;
            }
            int node1 = graph.getEdgeVertex1(edge);
            return getBus(node1);
        }

        Bus getBus2(String switchId, boolean throwException) {
            int edge = getEdge(switchId, throwException);
            SwitchImpl aSwitch = graph.getEdgeObject(edge);
            if (!aSwitch.isRetained()) {
                if (throwException) {
                    throw createSwitchNotFoundException(switchId);
                }
                return null;
            }
            int node2 = graph.getEdgeVertex2(edge);
            return getBus(node2);
        }

        Iterable<SwitchImpl> getSwitches() {
            return Iterables.filter(graph.getEdgesObject(), sw -> sw != null && sw.isRetained());
        }

        Stream<Switch> getSwitchStream() {
            return graph.getEdgeObjectStream().filter(Objects::nonNull).filter(Switch::isRetained).map(Function.identity());
        }

        int getSwitchCount() {
            return (int) graph.getEdgeObjectStream().filter(Objects::nonNull).filter(SwitchImpl::isRetained).count();
        }

        SwitchImpl getSwitch(String switchId, boolean throwException) {
            Integer edge = getEdge(switchId, false);
            if (edge != null) {
                SwitchImpl aSwitch = graph.getEdgeObject(edge);
                if (aSwitch.isRetained()) {
                    return aSwitch;
                }
            }
            if (throwException) {
                throw createSwitchNotFoundException(switchId);
            }
            return null;
        }
    }

    private interface BusChecker {

        boolean isValid(UndirectedGraph<? extends TerminalExt, SwitchImpl> graph, TIntArrayList nodes, List<NodeTerminal> terminals);
    }

    private static final class CalculatedBusChecker implements BusChecker {

        @Override
        public boolean isValid(UndirectedGraph<? extends TerminalExt, SwitchImpl> graph, TIntArrayList nodes, List<NodeTerminal> terminals) {
            int feederCount = 0;
            int branchCount = 0;
            int busbarSectionCount = 0;
            for (int i = 0; i < nodes.size(); i++) {
                int node = nodes.get(i);
                TerminalExt terminal = graph.getVertexObject(node);
                if (terminal != null) {
                    AbstractConnectable connectable = terminal.getConnectable();
                    switch (connectable.getType()) {
                        case LINE, TWO_WINDINGS_TRANSFORMER, THREE_WINDINGS_TRANSFORMER, HVDC_CONVERTER_STATION, DANGLING_LINE -> {
                            branchCount++;
                            feederCount++;
                        }
                        case LOAD, GENERATOR, BATTERY, SHUNT_COMPENSATOR, STATIC_VAR_COMPENSATOR -> feederCount++;
                        case BUSBAR_SECTION -> busbarSectionCount++;
                        case GROUND -> {
                            // Do nothing
                        }
                        default -> throw new IllegalStateException();
                    }
                }
            }
            return busbarSectionCount >= 1 && feederCount >= 1
                    || branchCount >= 1 && feederCount >= 2;
        }
    }

    private static final class CalculatedBusBreakerChecker implements BusChecker {
        @Override
        public boolean isValid(UndirectedGraph<? extends TerminalExt, SwitchImpl> graph, TIntArrayList nodes, List<NodeTerminal> terminals) {
            return !nodes.isEmpty();
        }
    }

    private interface BusNamingStrategy {

        String getId(VoltageLevel voltageLevel, TIntArrayList nodes);

        String getName(VoltageLevel voltageLevel, TIntArrayList nodes);
    }

    private static final class LowestNodeNumberBusNamingStrategy implements BusNamingStrategy {

        @Override
        public String getId(VoltageLevel voltageLevel, TIntArrayList nodes) {
            return voltageLevel.getId() + "_" + nodes.min();
        }

        @Override
        public String getName(VoltageLevel voltageLevel, TIntArrayList nodes) {
            return voltageLevel.getOptionalName().map(name -> name + "_" + nodes.min()).orElse(null);
        }
    }

    NodeBreakerTopologyModel(VoltageLevelExt voltageLevel) {
        super(voltageLevel);
        variants = new VariantArray<>(voltageLevel.getNetworkRef(), VariantImpl::new);
        graph.addListener(new DefaultUndirectedGraphListener<>() {

            private static final String INTERNAL_CONNECTION = "internalConnection";

            @Override
            public void edgeAdded(int e, SwitchImpl aSwitch) {
                NetworkImpl network = getNetwork();
                if (aSwitch != null) {
                    network.getIndex().checkAndAdd(aSwitch);
                    switches.put(aSwitch.getId(), e);
                    network.getListeners().notifyCreation(aSwitch);
                } else {
                    network.getListeners().notifyPropertyAdded(voltageLevel, INTERNAL_CONNECTION, null);
                }
                invalidateCache();
            }

            @Override
            public void edgeBeforeRemoval(int e, SwitchImpl aSwitch) {
                NetworkImpl network = getNetwork();
                if (aSwitch != null) {
                    network.getListeners().notifyBeforeRemoval(aSwitch);
                }
            }

            @Override
            public void edgeRemoved(int e, SwitchImpl aSwitch) {
                NetworkImpl network = getNetwork();
                if (aSwitch != null) {
                    String switchId = aSwitch.getId();
                    network.getIndex().remove(aSwitch);
                    switches.remove(switchId);
                    network.getListeners().notifyAfterRemoval(switchId);
                } else {
                    network.getListeners().notifyPropertyRemoved(voltageLevel, INTERNAL_CONNECTION, null);
                }
            }

            @Override
            public void allEdgesBeforeRemoval(Collection<SwitchImpl> aSwitches) {
                NetworkImpl network = getNetwork();
                aSwitches.stream().filter(Objects::nonNull).forEach(ss -> network.getListeners().notifyBeforeRemoval(ss));
            }

            @Override
            public void allEdgesRemoved(Collection<SwitchImpl> aSwitches) {
                NetworkImpl network = getNetwork();
                aSwitches.forEach(ss -> {
                    if (ss != null) {
                        network.getIndex().remove(ss);
                    } else {
                        network.getListeners().notifyPropertyRemoved(voltageLevel, INTERNAL_CONNECTION, null);
                    }
                });
                switches.clear();
                aSwitches.stream().filter(Objects::nonNull).forEach(ss -> network.getListeners().notifyAfterRemoval(ss.getId()));
            }
        });
    }

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

    private Integer getEdge(String switchId, boolean throwException) {
        Integer edge = switches.get(switchId);
        if (throwException && edge == null) {
            throw createSwitchNotFoundException(switchId);
        }
        return edge;
    }

    @Override
    public Iterable<Terminal> getTerminals() {
        return FluentIterable.from(graph.getVerticesObj())
                .filter(Predicates.notNull())
                .transform(Functions.identity());
    }

    @Override
    public Stream<Terminal> getTerminalStream() {
        return graph.getVertexObjectStream().filter(Objects::nonNull).map(Function.identity());
    }

    static PowsyblException createNotSupportedNodeBreakerTopologyException() {
        return new PowsyblException("Not supported in a node/breaker topology");
    }

    private static PowsyblException createSwitchNotFoundException(String switchId) {
        return new PowsyblException(getExceptionMessageElementNotFound("Switch", switchId));
    }

    CalculatedBusBreakerTopology getCalculatedBusBreakerTopology() {
        return variants.get().calculatedBusBreakerTopology;
    }

    CalculatedBusTopology getCalculatedBusTopology() {
        return variants.get().calculatedBusTopology;
    }

    void removeSwitchFromTopology(String switchId, boolean notify) {
        Integer e = switches.get(switchId);
        if (e == null) {
            throw new PowsyblException("Switch '" + switchId
                    + "' not found in voltage level '" + voltageLevel.getId() + "'");
        }
        graph.removeEdge(e, notify);
        graph.removeIsolatedVertices(notify);
    }

    private final VoltageLevelExt.NodeBreakerViewExt nodeBreakerView = new VoltageLevelExt.NodeBreakerViewExt() {

        private final TIntObjectMap<TDoubleArrayList> fictitiousP0ByNode = TCollections.synchronizedMap(new TIntObjectHashMap<>());
        private final TIntObjectMap<TDoubleArrayList> fictitiousQ0ByNode = TCollections.synchronizedMap(new TIntObjectHashMap<>());

        @Override
        public double getFictitiousP0(int node) {
            TDoubleArrayList fictP0 = fictitiousP0ByNode.get(node);
            if (fictP0 != null) {
                return fictP0.get(getNetwork().getVariantIndex());
            }
            return 0.0;
        }

        @Override
        public VoltageLevel.NodeBreakerView setFictitiousP0(int node, double p0) {
            if (Double.isNaN(p0)) {
                throw new ValidationException(voltageLevel, "undefined value cannot be set as fictitious p0");
            }
            TDoubleArrayList p0ByVariant = fictitiousP0ByNode.get(node);
            if (p0ByVariant == null) {
                int variantArraySize = getNetwork().getVariantManager().getVariantArraySize();
                p0ByVariant = new TDoubleArrayList(variantArraySize);
                for (int i = 0; i < variantArraySize; i++) {
                    p0ByVariant.add(0.0);
                }
                synchronized (fictitiousP0ByNode) {
                    fictitiousP0ByNode.put(node, p0ByVariant);
                }
            }
            int variantIndex = getNetwork().getVariantIndex();
            double oldValue = p0ByVariant.set(getNetwork().getVariantIndex(), p0);
            String variantId = getNetwork().getVariantManager().getVariantId(variantIndex);
            getNetwork().getListeners().notifyUpdate(voltageLevel, "fictitiousP0", variantId, oldValue, p0);
            TIntSet toRemove = clearFictitiousInjections(fictitiousP0ByNode);
            synchronized (fictitiousP0ByNode) {
                toRemove.forEach(n -> {
                    fictitiousP0ByNode.remove(n);
                    return true;
                });
            }
            return this;
        }

        @Override
        public double getFictitiousQ0(int node) {
            TDoubleArrayList fictQ0 = fictitiousQ0ByNode.get(node);
            if (fictQ0 != null) {
                return fictQ0.get(getNetwork().getVariantIndex());
            }
            return 0.0;
        }

        @Override
        public VoltageLevel.NodeBreakerView setFictitiousQ0(int node, double q0) {
            if (Double.isNaN(q0)) {
                throw new ValidationException(voltageLevel, "undefined value cannot be set as fictitious q0");
            }
            TDoubleArrayList q0ByVariant = fictitiousQ0ByNode.get(node);
            if (q0ByVariant == null) {
                int variantArraySize = getNetwork().getVariantManager().getVariantArraySize();
                q0ByVariant = new TDoubleArrayList(variantArraySize);
                for (int i = 0; i < variantArraySize; i++) {
                    q0ByVariant.add(0.0);
                }
                synchronized (fictitiousQ0ByNode) {
                    fictitiousQ0ByNode.put(node, q0ByVariant);
                }
            }
            int variantIndex = getNetwork().getVariantIndex();
            double oldValue = q0ByVariant.set(getNetwork().getVariantIndex(), q0);
            String variantId = getNetwork().getVariantManager().getVariantId(variantIndex);
            getNetwork().getListeners().notifyUpdate(voltageLevel, "fictitiousQ0", variantId, oldValue, q0);
            TIntSet toRemove = clearFictitiousInjections(fictitiousQ0ByNode);
            synchronized (fictitiousQ0ByNode) {
                toRemove.forEach(n -> {
                    fictitiousQ0ByNode.remove(n);
                    return true;
                });
            }
            return this;
        }

        @Override
        public int getMaximumNodeIndex() {
            return graph.getVertexCapacity() - 1;
        }

        @Override
        public int[] getNodes() {
            return graph.getVertices();
        }

        @Override
        public int getNode1(String switchId) {
            int edge = getEdge(switchId, true);
            return graph.getEdgeVertex1(edge);
        }

        @Override
        public int getNode2(String switchId) {
            int edge = getEdge(switchId, true);
            return graph.getEdgeVertex2(edge);
        }

        @Override
        public Terminal getTerminal(int node) {
            return graph.getVertexObject(node);
        }

        @Override
        public Stream<Switch> getSwitchStream(int node) {
            return graph.getEdgeObjectConnectedToVertexStream(node).filter(Objects::nonNull).map(Switch.class::cast);
        }

        @Override
        public List<Switch> getSwitches(int node) {
            return getSwitchStream(node).collect(Collectors.toList());
        }

        @Override
        public IntStream getNodeInternalConnectedToStream(int node) {
            return graph.getEdgeConnectedToVertexStream(node).filter(e -> graph.getEdgeObject(e) == null)
                .map(e -> {
                    int vertex1 = graph.getEdgeVertex1(e);
                    return vertex1 != node ? vertex1 : graph.getEdgeVertex2(e);
                });
        }

        @Override
        public List<Integer> getNodesInternalConnectedTo(int node) {
            return getNodeInternalConnectedToStream(node).boxed().collect(Collectors.toList());
        }

        @Override
        public Optional<Terminal> getOptionalTerminal(int node) {
            if (graph.vertexExists(node)) {
                return Optional.ofNullable(graph.getVertexObject(node));
            }
            return Optional.empty();
        }

        @Override
        public boolean hasAttachedEquipment(int node) {
            return graph.vertexExists(node);
        }

        @Override
        public Terminal getTerminal1(String switchId) {
            return getTerminal(getNode1(switchId));
        }

        @Override
        public Terminal getTerminal2(String switchId) {
            return getTerminal(getNode2(switchId));
        }

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

        @Override
        public InternalConnectionAdder newInternalConnection() {
            return new InternalConnectionAdderImpl();
        }

        @Override
        public int getInternalConnectionCount() {
            return (int) getInternalConnectionStream().count();
        }

        @Override
        public Iterable<InternalConnection> getInternalConnections() {
            return getInternalConnectionStream().collect(Collectors.toList());
        }

        @Override
        public Stream<InternalConnection> getInternalConnectionStream() {
            return Arrays.stream(graph.getEdges())
                    .filter(e -> graph.getEdgeObject(e) == null)
                    .mapToObj(e -> new InternalConnection() {
                        @Override
                        public int getNode1() {
                            return graph.getEdgeVertex1(e);
                        }

                        @Override
                        public int getNode2() {
                            return graph.getEdgeVertex2(e);
                        }
                    });
        }

        @Override
        public void removeInternalConnections(int node1, int node2) {
            int[] internalConnectionsToBeRemoved = Arrays.stream(graph.getEdges())
                    .filter(e -> graph.getEdgeObject(e) == null)
                    .filter(e -> graph.getEdgeVertex1(e) == node1 && graph.getEdgeVertex2(e) == node2
                            || graph.getEdgeVertex1(e) == node2 && graph.getEdgeVertex2(e) == node1)
                    .toArray();
            if (internalConnectionsToBeRemoved.length == 0) {
                throw new PowsyblException("Internal connection not found between " + node1 + " and " + node2);
            }
            for (int ic : internalConnectionsToBeRemoved) {
                graph.removeEdge(ic);
            }
            graph.removeIsolatedVertices();
            invalidateCache();
        }

        @Override
        public SwitchAdder newBreaker() {
            return new SwitchAdderImpl(SwitchKind.BREAKER);
        }

        @Override
        public SwitchAdder newDisconnector() {
            return new SwitchAdderImpl(SwitchKind.DISCONNECTOR);
        }

        @Override
        public SwitchImpl getSwitch(String switchId) {
            Integer edge = getEdge(switchId, false);
            if (edge != null) {
                return graph.getEdgeObject(edge);
            }
            return null;
        }

        @Override
        public Iterable<Switch> getSwitches() {
            return Iterables.filter(graph.getEdgesObject(), Switch.class); // just to upcast and return an unmodifiable iterable
        }

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

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

        @Override
        public void removeSwitch(String switchId) {
            NodeBreakerTopologyModel.this.removeSwitchFromTopology(switchId, true);
        }

        @Override
        public BusbarSectionAdder newBusbarSection() {
            return new BusbarSectionAdderImpl(voltageLevel);
        }

        @Override
        public Iterable<BusbarSection> getBusbarSections() {
            return getConnectables(BusbarSection.class);
        }

        @Override
        public Stream<BusbarSection> getBusbarSectionStream() {
            return getConnectableStream(BusbarSection.class);
        }

        @Override
        public int getBusbarSectionCount() {
            return getConnectableCount(BusbarSection.class);
        }

        @Override
        public BusbarSection getBusbarSection(String id) {
            BusbarSection bbs = getNetwork().getIndex().get(id, BusbarSection.class);
            if (bbs != null && bbs.getTerminal().getVoltageLevel() != voltageLevel) {
                return null;
            }
            return bbs;
        }

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

        @Override
        public void traverse(int node, TopologyTraverser t) {
            graph.traverse(node, TraversalType.DEPTH_FIRST, adapt(t));
        }

        @Override
        public void traverse(int[] nodes, TopologyTraverser t) {
            graph.traverse(nodes, TraversalType.DEPTH_FIRST, adapt(t));
        }
    };

    private static TIntSet clearFictitiousInjections(TIntObjectMap<TDoubleArrayList> fictitiousInjectionsByNode) {
        TIntSet toRemove = new TIntHashSet(fictitiousInjectionsByNode.keySet());
        fictitiousInjectionsByNode.forEachEntry((node, value) -> {
            value.forEach(inj -> {
                if (inj != 0.0) {
                    toRemove.remove(node);
                }
                return true;
            });
            return true;
        });
        return toRemove;
    }

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

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

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

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

        @Override
        public CalculatedBus getBus(String id) {
            return variants.get().calculatedBusTopology.getBus(id, false);
        }

        @Override
        public Bus getMergedBus(String busBarId) {
            NodeTerminal nt = (NodeTerminal) Objects.requireNonNull(nodeBreakerView.getBusbarSection(busBarId)).getTerminal();
            int node = nt.getNode();
            return variants.get().calculatedBusTopology.getBus(node);
        }
    };

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

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

        @Override
        public Iterable<Bus> getBuses() {
            return Collections.unmodifiableCollection(variants.get().calculatedBusBreakerTopology.getBuses());
        }

        @Override
        public Stream<Bus> getBusStream() {
            return variants.get().calculatedBusBreakerTopology.getBuses().stream().map(Function.identity());
        }

        @Override
        public int getBusCount() {
            return variants.get().calculatedBusBreakerTopology.getBusCount();
        }

        @Override
        public CalculatedBus getBus(String id) {
            return variants.get().calculatedBusBreakerTopology.getBus(id, false);
        }

        @Override
        public BusAdder newBus() {
            throw createNotSupportedNodeBreakerTopologyException();
        }

        @Override
        public void removeBus(String busId) {
            throw createNotSupportedNodeBreakerTopologyException();
        }

        @Override
        public void removeAllBuses() {
            throw createNotSupportedNodeBreakerTopologyException();
        }

        @Override
        public Iterable<Switch> getSwitches() {
            return Iterables.filter(variants.get().calculatedBusBreakerTopology.getSwitches(), Switch.class); // just to upcast and return an unmodifiable iterable
        }

        @Override
        public Stream<Switch> getSwitchStream() {
            return variants.get().calculatedBusBreakerTopology.getSwitchStream();
        }

        @Override
        public int getSwitchCount() {
            return variants.get().calculatedBusBreakerTopology.getSwitchCount();
        }

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

        @Override
        public void removeAllSwitches() {
            throw createNotSupportedNodeBreakerTopologyException();
        }

        @Override
        public Bus getBus1(String switchId) {
            return variants.get().calculatedBusBreakerTopology.getBus1(switchId, true);
        }

        @Override
        public Bus getBus2(String switchId) {
            return variants.get().calculatedBusBreakerTopology.getBus2(switchId, true);
        }

        @Override
        public Collection<Bus> getBusesFromBusViewBusId(String mergedBusId) {
            Set<Bus> buses = new HashSet<>();
            for (int i = 0; i < graph.getVertexCapacity(); i++) {
                Bus b = variants.get().calculatedBusTopology.getBus(i);
                if (b != null && b.getId().equals(mergedBusId)) {
                    buses.add(variants.get().calculatedBusBreakerTopology.getBus(i));
                }
            }
            if (buses.isEmpty()) {
                throw new PowsyblException(getExceptionMessageElementNotFound("Bus", mergedBusId));
            }
            return buses;
        }

        @Override
        public Stream<Bus> getBusStreamFromBusViewBusId(String mergedBusId) {
            return getBusesFromBusViewBusId(mergedBusId).stream();
        }

        @Override
        public Switch getSwitch(String switchId) {
            return variants.get().calculatedBusBreakerTopology.getSwitch(switchId, true);
        }

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

        @Override
        public void traverse(Bus bus, TopologyTraverser traverser) {
            throw createNotSupportedNodeBreakerTopologyException();
        }
    };

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

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

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

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

    private void checkTerminal(TerminalExt terminal) {
        if (!(terminal instanceof NodeTerminal)) {
            throw new ValidationException(terminal.getConnectable(),
                    "voltage level " + voltageLevel.getId() + " has a node/breaker topology"
                            + ", a node connection should be specified instead of a bus connection");
        }
        int node = ((NodeTerminal) terminal).getNode();
        graph.addVertexIfNotPresent(node);
        if (graph.getVertexObject(node) != null) {
            throw new ValidationException(terminal.getConnectable(),
                    "an equipment (" + graph.getVertexObject(node).getConnectable().getId()
                            + ") is already connected to node " + node + " of voltage level "
                            + voltageLevel.getId());
        }
    }

    @Override
    public void attach(TerminalExt terminal, boolean test) {
        checkTerminal(terminal);
        if (test) {
            return;
        }
        int node = ((NodeTerminal) terminal).getNode();

        // create the link terminal <-> voltage level
        terminal.setVoltageLevel(voltageLevel);

        // create the link terminal <-> graph vertex
        graph.setVertexObject(node, (NodeTerminal) terminal);

        getNetwork().getVariantManager().forEachVariant(NodeBreakerTopologyModel.this::invalidateCache);
    }

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

        int node = ((NodeTerminal) terminal).getNode();

        graph.setVertexObject(node, null);

        getNetwork().getVariantManager().forEachVariant(NodeBreakerTopologyModel.this::invalidateCache);

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

        graph.removeIsolatedVertices();
    }

    private static boolean isBusbarSection(Terminal t) {
        return t != null && t.getConnectable().getType() == IdentifiableType.BUSBAR_SECTION;
    }

    /**
     * Check if a switch is open and cannot be operated (according to the given predicate)
     * @param sw the switch to test
     * @param isSwitchOperable the predicate defining if a switch can be operated
     * @return <code>true</code> if the switch is open and cannot be operated
     */
    private boolean checkNonClosableSwitch(SwitchImpl sw, Predicate<? super SwitchImpl> isSwitchOperable) {
        return SwitchPredicates.IS_OPEN.test(sw) && isSwitchOperable.negate().test(sw);
    }

    private void checkTopologyKind(Terminal terminal) {
        if (!(terminal instanceof NodeTerminal)) {
            throw new IllegalStateException(WRONG_TERMINAL_TYPE_EXCEPTION_MESSAGE + terminal.getClass().getName());
        }
    }

    /**
     * Connect the terminal to a busbar section in its voltage level.
     * The switches that can be operated are the non-fictional breakers.
     * @param terminal Terminal to connect
     * @return <code>true</code> if the terminal has been connected, <code>false</code> if it hasn't or if it was already connected
     */
    boolean connect(TerminalExt terminal) {
        // Only keep the closed non-fictional breakers in the nominal case
        return connect(terminal, SwitchPredicates.IS_NONFICTIONAL_BREAKER);
    }

    /**
     * Connect the terminal to a busbar section in its voltage level.
     * @param terminal Terminal to connect
     * @param isSwitchOperable Predicate used to identify the switches that can be operated on. <b>Warning:</b> do not include a test to see if the switch is opened, it is already done in this method
     * @return <code>true</code> if the terminal has been connected, <code>false</code> if it hasn't or if it was already connected
     */
    @Override
    public boolean connect(TerminalExt terminal, Predicate<? super SwitchImpl> isSwitchOperable) {
        // Check the topology kind
        checkTopologyKind(terminal);

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

        // Initialisation of a list to open in case some terminals are in node-breaker view
        Set<SwitchImpl> switchForConnection = new HashSet<>();

        // Get the list of switches to close
        if (getConnectingSwitches(terminal, isSwitchOperable, switchForConnection)) {
            // Close the switches
            switchForConnection.forEach(sw -> sw.setOpen(false));
            return true;
        } else {
            return false;
        }
    }

    boolean getConnectingSwitches(Terminal terminal, Predicate<? super SwitchImpl> isSwitchOperable, Set<SwitchImpl> switchForConnection) {
        // Check the topology kind
        checkTopologyKind(terminal);

        int node = ((NodeTerminal) terminal).getNode();
        // find all paths starting from the current terminal to a busbar section that does not contain an open switch
        // that is not of the type of switch the user wants to operate
        // Paths are already sorted by the number of open switches and by the size of the paths
        List<TIntArrayList> paths = graph.findAllPaths(node, NodeBreakerTopologyModel::isBusbarSection, sw -> checkNonClosableSwitch(sw, isSwitchOperable),
            Comparator.comparing((TIntArrayList o) -> o.grep(idx -> SwitchPredicates.IS_OPEN.test(graph.getEdgeObject(idx))).size())
                .thenComparing(TIntArrayList::size));
        if (!paths.isEmpty()) {
            // the shortest path is the best
            TIntArrayList shortestPath = paths.get(0);

            // close all open switches on the path
            for (int i = 0; i < shortestPath.size(); i++) {
                int e = shortestPath.get(i);
                SwitchImpl sw = graph.getEdgeObject(e);
                if (SwitchPredicates.IS_OPEN.test(sw)) {
                    // Since the paths were constructed using the method checkNonClosableSwitches, only operable switches can be open
                    switchForConnection.add(sw);
                }
            }
            return true;
        }
        return false;
    }

    boolean disconnect(TerminalExt terminal) {
        // Only keep the closed non-fictional breakers in the nominal case
        return disconnect(terminal, SwitchPredicates.IS_CLOSED_BREAKER);
    }

    @Override
    public boolean disconnect(TerminalExt terminal, Predicate<? super SwitchImpl> isSwitchOpenable) {
        // Check the topology kind
        checkTopologyKind(terminal);

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

        // Set of switches that are to be opened
        Set<SwitchImpl> switchesToOpen = new HashSet<>();

        // Get the list of switches to open
        if (getDisconnectingSwitches(terminal, isSwitchOpenable, switchesToOpen)) {
            // Open the switches
            switchesToOpen.forEach(sw -> sw.setOpen(true));
            return true;
        } else {
            return false;
        }
    }

    boolean getDisconnectingSwitches(Terminal terminal, Predicate<? super SwitchImpl> isSwitchOpenable, Set<SwitchImpl> switchForDisconnection) {
        // Check the topology kind
        checkTopologyKind(terminal);

        int node = ((NodeTerminal) terminal).getNode();
        // find all paths starting from the current terminal to a terminal that does not contain an open switch
        List<TIntArrayList> paths = graph.findAllPaths(node, Objects::nonNull, SwitchPredicates.IS_OPEN);
        if (paths.isEmpty()) {
            return false;
        }

        // Each path is visited and for each, the first openable switch found is added in the set of switches to open
        for (TIntArrayList path : paths) {
            // Identify the first openable switch on the path
            if (!identifySwitchToOpenPath(path, isSwitchOpenable, switchForDisconnection)) {
                // If no such switch was found, return false immediately
                return false;
            }
        }
        return true;
    }

    /**
     * Add the first openable switch in the given path to the set of switches to open
     * @param path the path to open
     * @param isSwitchOpenable predicate used to know if a switch can be opened
     * @param switchesToOpen set of switches to be opened
     * @return true if the path can be opened, else false
     */
    boolean identifySwitchToOpenPath(TIntArrayList path, Predicate<? super SwitchImpl> isSwitchOpenable, Set<SwitchImpl> switchesToOpen) {
        for (int i = 0; i < path.size(); i++) {
            int e = path.get(i);
            SwitchImpl sw = graph.getEdgeObject(e);
            if (isSwitchOpenable.test(sw)) {
                switchesToOpen.add(sw);
                // just one open breaker is enough to disconnect the terminal, so we can stop
                return true;
            }
        }
        return false;
    }

    boolean isConnected(TerminalExt terminal) {
        // Check the topology kind
        checkTopologyKind(terminal);

        return terminal.getBusView().getBus() != null;
    }

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

    /**
     * Traverse from given node 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(NodeTerminal terminal, Terminal.TopologyTraverser traverser, Set<Terminal> visitedTerminals, TraversalType traversalType) {
        Objects.requireNonNull(terminal);
        Objects.requireNonNull(traverser);
        Objects.requireNonNull(visitedTerminals);

        TraverseResult termTraverseResult = getTraverseResult(visitedTerminals, terminal, traverser);
        if (termTraverseResult == TraverseResult.TERMINATE_TRAVERSER) {
            return false;
        } else if (termTraverseResult == TraverseResult.CONTINUE) {
            List<TerminalExt> nextTerminals = new ArrayList<>();
            addNextTerminals(terminal, nextTerminals);

            int node = terminal.getNode();
            boolean traverseTerminated = traverseOtherNodes(node, nextTerminals, traverser, visitedTerminals, traversalType);
            if (traverseTerminated) {
                return false;
            }

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

        return true;
    }

    private boolean traverseOtherNodes(int node, List<TerminalExt> nextTerminals,
                                       Terminal.TopologyTraverser traverser, Set<Terminal> visitedTerminals, TraversalType traversalType) {
        return !graph.traverse(node, traversalType, (v1, e, v2) -> {
            SwitchImpl aSwitch = graph.getEdgeObject(e);
            NodeTerminal otherTerminal = graph.getVertexObject(v2);
            TraverseResult edgeTraverseResult = aSwitch != null ? traverser.traverse(aSwitch)
                : TraverseResult.CONTINUE; // internal connection case
            if (edgeTraverseResult == TraverseResult.CONTINUE && otherTerminal != null) {
                TraverseResult otherTermTraverseResult = getTraverseResult(visitedTerminals, otherTerminal, traverser);
                if (otherTermTraverseResult == TraverseResult.CONTINUE) {
                    addNextTerminals(otherTerminal, nextTerminals);
                }
                return otherTermTraverseResult;
            }
            return edgeTraverseResult;
        });
    }

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

    @Override
    public void extendVariantArraySize(int initVariantArraySize, int number, int sourceIndex) {
        variants.push(number, VariantImpl::new);
    }

    @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, VariantImpl::new);
    }

    @Override
    protected void removeTopology() {
        removeAllEdges();
    }

    private void removeAllEdges() {
        graph.removeAllEdges();
    }

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

    @Override
    public void printTopology(PrintStream out, ShortIdDictionary dict) {
        out.println("-------------------------------------------------------------");
        out.println("Topology of " + voltageLevel.getId());
        graph.print(out, terminal -> terminal != null ? terminal.getConnectable().toString() : null, null);
    }

    @Override
    public void exportTopology(Path file) {
        try (Writer writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
            exportTopology(writer);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @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();

        exportNodes(random, gvGraph, scope);
        exportEdges(gvGraph, scope);

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

    private void exportNodes(Random random, GraphVizGraph gvGraph, GraphVizScope scope) {
        // create bus color scale
        Map<String, String> busColor = new HashMap<>();
        List<CalculatedBus> buses = new ArrayList<>(getCalculatedBusBreakerTopology().getBuses());
        String[] colors = Colors.generateColorScale(buses.size(), random);
        for (int i = 0; i < buses.size(); i++) {
            CalculatedBus bus = buses.get(i);
            busColor.put(bus.getId(), colors[i]);
        }

        for (int n = 0; n < graph.getVertexCapacity(); n++) {
            if (!graph.vertexExists(n)) {
                continue;
            }
            Bus bus = getCalculatedBusBreakerTopology().getBus(n);
            String label = "" + n;
            TerminalExt terminal = graph.getVertexObject(n);
            if (terminal != null) {
                AbstractConnectable connectable = terminal.getConnectable();
                label += System.lineSeparator() + connectable.getType().toString()
                        + System.lineSeparator() + connectable.getId()
                        + connectable.getOptionalName().map(name -> System.lineSeparator() + name).orElse("");
            }
            GraphVizNode gvNode = gvGraph.node(scope, n)
                    .label(label)
                    .shape("ellipse");
            if (bus != null) {
                gvNode.style("filled")
                        .attr(GraphVizAttribute.fillcolor, busColor.get(bus.getId()));
                gvGraph.cluster(scope, bus).add(gvNode)
                        .attr(GraphVizAttribute.pencolor, "transparent");
            }
        }
    }

    private void exportEdges(GraphVizGraph gvGraph, GraphVizScope scope) {
        // Iterate over non-removed edges
        for (int e : graph.getEdges()) {
            GraphVizEdge edge = gvGraph.edge(scope, graph.getEdgeVertex1(e), graph.getEdgeVertex2(e));
            SwitchImpl aSwitch = graph.getEdgeObject(e);
            if (aSwitch != null) {
                if (DRAW_SWITCH_ID) {
                    edge.label(aSwitch.getKind().toString()
                            + System.lineSeparator() + aSwitch.getId()
                            + aSwitch.getOptionalName().map(n -> System.lineSeparator() + n).orElse(""))
                            .attr(GraphVizAttribute.fontsize, "10");
                }
                edge.style(aSwitch.isOpen() ? "dotted" : "solid");
            }
        }
    }

    private static String getExceptionMessageElementNotFound(String element, String id) {
        return element + " " + id + " not found";
    }
}