NodeBreakerTraverser.java

/**
 * Copyright (c) 2020, RTE (http://www.rte-france.com)
 * 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.openloadflow.network.impl;

import com.powsybl.iidm.network.*;
import com.powsybl.math.graph.TraverseResult;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
 */
public class NodeBreakerTraverser implements VoltageLevel.NodeBreakerView.TopologyTraverser {

    private final Set<Switch> switchesToOpen;
    private final Set<Terminal> traversedTerminals;
    private final List<Terminal> nextTerminals;
    private final VoltageLevel.NodeBreakerView nodeBreakerView;

    public NodeBreakerTraverser(Set<Switch> switchesToOpen, Set<Terminal> traversedTerminals, List<Terminal> nextTerminals,
                                VoltageLevel.NodeBreakerView nodeBreakerView) {
        this.switchesToOpen = switchesToOpen;
        this.traversedTerminals = traversedTerminals;
        this.nextTerminals = nextTerminals;
        this.nodeBreakerView = nodeBreakerView;
    }

    @Override
    public TraverseResult traverse(int nodeBefore, Switch sw, int nodeAfter) {
        if (sw != null && sw.isOpen()) {
            return TraverseResult.TERMINATE_PATH;
        }

        if (hasAttachedEquipment(nodeBefore) && traverserStopsAtOtherEdges(sw, nodeBefore, nodeAfter)) {
            // Switch is just after a traversed terminal which will be disconnected, and traverser stops at other start edges
            if (isOpenable(sw)) {
                // The traverser can stop now and no need to retain current switch
                return TraverseResult.TERMINATE_PATH;
            }
            if (!hasAttachedEquipment(nodeAfter) && traverserStopsAtOtherEdges(sw, nodeAfter, nodeBefore)) {
                // As the traverser would stop just after, it can stop now (without retaining current switch)
                return TraverseResult.TERMINATE_PATH;
            }
        }

        if (isOpenable(sw)) {
            // The current switch is openable: the traverser could stop and the switch could be retained,
            // but, to avoid unnecessary retained switches, the traverser does not retain it in two cases
            if (!hasAttachedEquipment(nodeAfter) && traverserStopsAtOtherEdges(sw, nodeAfter, nodeBefore)) {
                // Continuing traversing might lead in some cases to more retained switches, but in practice the
                // switches after are often opened and sometimes followed by an end node
                return TraverseResult.CONTINUE;
            }
            if (isEquivalentToStopAfterSwitch(sw, nodeAfter)) {
                // Retaining the switch is equivalent to stop at the node after if the node after the switch is an end node (e.g. load or generator)
                nodeBreakerView.getOptionalTerminal(nodeAfter).ifPresent(traversedTerminals::add);
                return TraverseResult.TERMINATE_PATH;
            }
            switchesToOpen.add(sw);
            return TraverseResult.TERMINATE_PATH;
        }

        // The traverser continues, hence nodeAfter is traversed
        nodeBreakerView.getOptionalTerminal(nodeAfter).ifPresent(this::terminalTraversed);
        return TraverseResult.CONTINUE;
    }

    private boolean hasAttachedEquipment(int nodeBefore) {
        return nodeBreakerView.getOptionalTerminal(nodeBefore).isPresent();
    }

    private void terminalTraversed(Terminal terminal) {
        traversedTerminals.add(terminal);
        ((Connectable<?>) terminal.getConnectable()).getTerminals().stream()
                .filter(t -> t != terminal)
                .forEach(nextTerminals::add);
    }

    private boolean isEquivalentToStopAfterSwitch(Switch sw, int nodeAfter) {
        Terminal terminal2 = nodeBreakerView.getTerminal(nodeAfter);
        if (terminal2 != null) {
            IdentifiableType connectableAfter = terminal2.getConnectable().getType();
            boolean endNodeAfter = connectableAfter == IdentifiableType.GENERATOR
                || connectableAfter == IdentifiableType.LOAD
                || connectableAfter == IdentifiableType.STATIC_VAR_COMPENSATOR
                || connectableAfter == IdentifiableType.BATTERY
                || connectableAfter == IdentifiableType.SHUNT_COMPENSATOR
                || connectableAfter == IdentifiableType.DANGLING_LINE
                || connectableAfter == IdentifiableType.LINE
                || connectableAfter == IdentifiableType.TWO_WINDINGS_TRANSFORMER
                || connectableAfter == IdentifiableType.THREE_WINDINGS_TRANSFORMER;

            if (endNodeAfter) { // check that there isn't another (closed) switch or internal connection at node after
                return noInternalConnectionAtNode(nodeAfter)
                    && nodeBreakerView.getSwitchStream(nodeAfter).noneMatch(s -> s != sw && !s.isOpen());
            }
        }
        return false;
    }

    /**
     * Return if propagation would stop at all edges starting from nodeStart, but different from the edge defined by
     * nodeStart ---aSwitch--- nodeEnd,
     */
    private boolean traverserStopsAtOtherEdges(Switch aSwitch, int nodeStart, int nodeEnd) {
        // The traverser stops at other start edges if node is a direct or indirect junction of switches only, with all
        // other switches either opened or openable.
        // An indirect junction means through internal connections.
        return internalConnectionsEndOnOpenOrOpenableSwitches(nodeStart, nodeEnd)
            && allOtherSwitchesOpenOrOpenable(aSwitch, nodeStart);
    }

    private boolean allOtherSwitchesOpenOrOpenable(Switch aSwitch, int node) {
        return nodeBreakerView.getSwitchStream(node).filter(s -> s != aSwitch).allMatch(NodeBreakerTraverser::isOpenOrOpenable);
    }

    private boolean noInternalConnectionAtNode(int node) {
        return nodeBreakerView.getNodeInternalConnectedToStream(node).findFirst().isEmpty();
    }

    private boolean internalConnectionsEndOnOpenOrOpenableSwitches(int node, int dismissedNode) {
        Set<Integer> visitedNodes = new HashSet<>();
        visitedNodes.add(dismissedNode);
        return internalConnectionsEndOnOpenOrOpenableSwitches(node, visitedNodes);
    }

    private boolean internalConnectionsEndOnOpenOrOpenableSwitches(int nStart, Set<Integer> visitedNodes) {
        if (!visitedNodes.contains(nStart)) {
            visitedNodes.add(nStart);
            for (int n : nodeBreakerView.getNodesInternalConnectedTo(nStart)) {
                if (!visitedNodes.contains(n) && (hasAttachedEquipment(n)
                                || nodeBreakerView.getSwitchStream(n).anyMatch(s -> !NodeBreakerTraverser.isOpenOrOpenable(s))
                                || !internalConnectionsEndOnOpenOrOpenableSwitches(n, visitedNodes))) {
                    return false;
                }
            }
        }
        return true;
    }

    private static boolean isOpenable(Switch aSwitch) {
        return aSwitch != null && !aSwitch.isFictitious() && aSwitch.getKind() == SwitchKind.BREAKER;
    }

    private static boolean isOpenOrOpenable(Switch aSwitch) {
        return aSwitch.isOpen() || isOpenable(aSwitch);
    }
}