SwitchesFlow.java

/**
 * Copyright (c) 2021, 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.iidm.network.util;

import com.powsybl.iidm.network.*;
import org.jgrapht.alg.connectivity.ConnectivityInspector;
import org.jgrapht.alg.interfaces.SpanningTreeAlgorithm.SpanningTree;
import org.jgrapht.alg.spanning.KruskalMinimumSpanningTree;
import org.jgrapht.graph.AsSubgraph;
import org.jgrapht.graph.DefaultWeightedEdge;
import org.jgrapht.graph.SimpleWeightedGraph;

import java.util.*;

/**
 * Utility class to compute the flow of the switches associated to a voltageLevel
 *
 * @author Luma Zamarre��o {@literal <zamarrenolm at aia.es>}
 * @author Jos�� Antonio Marqu��s {@literal <marquesja at aia.es>}
 */
public class SwitchesFlow {
    private final VoltageLevel voltageLevel;
    private final Terminal slackTerminal;
    private final Map<String, SwFlow> switchesFlows;

    public SwitchesFlow(VoltageLevel voltageLevel) {
        this(voltageLevel, null);
    }

    public SwitchesFlow(VoltageLevel voltageLevel, Terminal slackTerminal) {
        this.voltageLevel = Objects.requireNonNull(voltageLevel);
        this.slackTerminal = slackTerminal;
        switchesFlows = new HashMap<>();

        compute();
    }

    // Compute switches flow
    private void compute() {

        Map<String, SwNode> swNodeInjection = new HashMap<>();
        SimpleWeightedGraph<SwNode, SwEdge> graph = new SimpleWeightedGraph<>(SwEdge.class);

        buildGraph(swNodeInjection, graph);
        calculateInjections(swNodeInjection);

        ConnectivityInspector<SwNode, SwEdge> ci = new ConnectivityInspector<>(graph);
        ci.connectedSets().forEach(connectedSet -> connectedComponentSwitchesFlow(swNodeInjection, graph, connectedSet));

        assignZeroFlowToEdgesOutsideAllTrees(graph);
    }

    public boolean isEmpty() {
        return switchesFlows.isEmpty();
    }

    public double getP1(String switchId) {
        if (switchesFlows.containsKey(switchId)) {
            return switchesFlows.get(switchId).p1;
        } else {
            return 0.0;
        }
    }

    public double getQ1(String switchId) {
        if (switchesFlows.containsKey(switchId)) {
            return switchesFlows.get(switchId).q1;
        } else {
            return 0.0;
        }
    }

    public double getP2(String switchId) {
        if (switchesFlows.containsKey(switchId)) {
            return switchesFlows.get(switchId).p2;
        } else {
            return 0.0;
        }
    }

    public double getQ2(String switchId) {
        if (switchesFlows.containsKey(switchId)) {
            return switchesFlows.get(switchId).q2;
        } else {
            return 0.0;
        }
    }

    private void buildGraph(Map<String, SwNode> swNodeInjection, SimpleWeightedGraph<SwNode, SwEdge> graph) {
        if (voltageLevel.getTopologyKind() == TopologyKind.NODE_BREAKER) {
            buildGraphFromNodeBreaker(swNodeInjection, graph);
        } else {
            buildGraphFromBusBreaker(swNodeInjection, graph);
        }
    }

    private void buildGraphFromNodeBreaker(Map<String, SwNode> swNodeInjection, SimpleWeightedGraph<SwNode, SwEdge> graph) {
        voltageLevel.getNodeBreakerView().getSwitches().forEach(sw -> {
            if (sw.isOpen()) {
                return;
            }
            SwNode swNode1 = addSwNode(swNodeInjection, voltageLevel.getNodeBreakerView().getNode1(sw.getId()));
            SwNode swNode2 = addSwNode(swNodeInjection, voltageLevel.getNodeBreakerView().getNode2(sw.getId()));
            // Discard loops
            if (swNode1 == swNode2) {
                return;
            }
            graph.addVertex(swNode1);
            graph.addVertex(swNode2);
            graph.addEdge(swNode1, swNode2, new SwEdge(sw, swNode1, swNode2));
        });

        voltageLevel.getNodeBreakerView().getInternalConnections().forEach(ic -> {
            SwNode swNode1 = addSwNode(swNodeInjection, ic.getNode1());
            SwNode swNode2 = addSwNode(swNodeInjection, ic.getNode2());
            SwEdge swEdge = new SwEdge(swNode1, swNode2);
            // Discard loops
            if (swNode1 == swNode2) {
                return;
            }
            graph.addVertex(swNode1);
            graph.addVertex(swNode2);
            graph.addEdge(swNode1, swNode2, swEdge);
        });
    }

    private void buildGraphFromBusBreaker(Map<String, SwNode> swNodeInjection, SimpleWeightedGraph<SwNode, SwEdge> graph) {
        voltageLevel.getBusBreakerView().getSwitches().forEach(sw -> {
            if (sw.isOpen()) {
                return;
            }
            SwNode swNode1 = addSwNode(swNodeInjection, voltageLevel.getBusBreakerView().getBus1(sw.getId()));
            SwNode swNode2 = addSwNode(swNodeInjection, voltageLevel.getBusBreakerView().getBus2(sw.getId()));
            // Discard loops
            if (swNode1 == swNode2) {
                return;
            }
            graph.addVertex(swNode1);
            graph.addVertex(swNode2);
            graph.addEdge(swNode1, swNode2, new SwEdge(sw, swNode1, swNode2));
        });
    }

    private static SwNode addSwNode(Map<String, SwNode> swNodeInjection, int node) {
        if (swNodeInjection.containsKey(getKey(node))) {
            return swNodeInjection.get(getKey(node));
        }
        SwNode swNode = new SwNode(node);
        swNodeInjection.put(getKey(node), swNode);
        return swNode;
    }

    private static SwNode addSwNode(Map<String, SwNode> swNodeInjection, Bus bus) {
        if (swNodeInjection.containsKey(getKey(bus))) {
            return swNodeInjection.get(getKey(bus));
        }
        SwNode swNode = new SwNode(bus);
        swNodeInjection.put(getKey(bus), swNode);
        return swNode;
    }

    private void calculateInjections(Map<String, SwNode> swNodeInjection) {
        if (voltageLevel.getTopologyKind() == TopologyKind.NODE_BREAKER) {
            calculateInjectionsNodeBreaker(voltageLevel, swNodeInjection);
        } else {
            calculateInjectionsBusBreaker(voltageLevel, swNodeInjection);
        }
    }

    private static void calculateInjectionsNodeBreaker(VoltageLevel voltageLevel, Map<String, SwNode> swNodeInjection) {
        int[] nodes = voltageLevel.getNodeBreakerView().getNodes();
        for (int node : nodes) {
            Terminal terminal = voltageLevel.getNodeBreakerView().getTerminal(node);
            if (terminal != null) {
                double p = getTerminalP(terminal);
                double q = getTerminalQ(terminal);
                swNodeInjection.computeIfPresent(getKey(node), (key, value) -> value.addPQ(p, q));
            }
        }
    }

    private static void calculateInjectionsBusBreaker(VoltageLevel voltageLevel, Map<String, SwNode> swNodeInjection) {
        voltageLevel.getBusBreakerView().getBuses().forEach(bus ->
            bus.getConnectedTerminals().forEach(terminal -> {
                double p = getTerminalP(terminal);
                double q = getTerminalQ(terminal);
                swNodeInjection.computeIfPresent(getKey(bus), (key, value) -> value.addPQ(p, q));
            }));
    }

    private boolean isSlack(SwNode swNode) {
        if (voltageLevel.getTopologyKind() == TopologyKind.NODE_BREAKER) {
            return slackTerminal != null && swNode.node == slackTerminal.getNodeBreakerView().getNode();
        } else {
            return slackTerminal != null && swNode.bus.equals(slackTerminal.getBusBreakerView().getBus());
        }
    }

    private void connectedComponentSwitchesFlow(Map<String, SwNode> swNodeInjection,
        SimpleWeightedGraph<SwNode, SwEdge> graph, Set<SwNode> connectedSet) {
        // Discard isolated nodes
        if (connectedSet.size() <= 1) {
            return;
        }
        // We chose the slack bus (if there is one) as the root of the tree,
        // This way, the potential mismatch of the whole busview bus will be assigned entirely
        // to the bus/breaker view that is labeled as slack
        // We always look in the sorted nodes list to ensure a deterministic selection of the root
        List<SwNode> sortedNodes = connectedSet.stream().sorted(Comparator.comparing(SwitchesFlow::getKey)).toList();
        Optional<SwNode> swRoot = sortedNodes.stream()
                .filter(this::isSlack)
                .findFirst()
                .or(() -> sortedNodes.stream().findFirst());
        if (swRoot.isEmpty()) {
            return;
        }

        AsSubgraph<SwNode, SwEdge> subGraph = new AsSubgraph<>(graph, connectedSet);
        SpanningTree<SwEdge> tree = new KruskalMinimumSpanningTree<>(subGraph).getSpanningTree();

        List<List<SwNode>> levels = new ArrayList<>();
        Map<SwNode, SwEdge> parent = new HashMap<>();
        buildLevels(tree, swRoot.get(), parent, levels);

        completeFlowsForEdgesInsideTree(swNodeInjection, parent, levels);
    }

    private static void buildLevels(SpanningTree<SwEdge> tree, SwNode swRoot, Map<SwNode, SwEdge> parent,
        List<List<SwNode>> levels) {
        Set<SwEdge> processed = new HashSet<>();
        Map<SwNode, List<SwEdge>> swEdgesInNode = new HashMap<>();

        // Map to iterate once over the edges of the bus
        tree.getEdges().forEach(e -> {
            swEdgesInNode.computeIfAbsent(e.swNode1, b -> new ArrayList<>()).add(e);
            swEdgesInNode.computeIfAbsent(e.swNode2, b -> new ArrayList<>()).add(e);
        });

        // Add root level
        levels.add(new ArrayList<>(Collections.singleton(swRoot)));

        // Build levels of the tree
        int level = 0;
        while (level < levels.size()) {
            List<SwNode> nextLevel = new ArrayList<>();
            levels.get(level).forEach(swNode -> swEdgesInNode.get(swNode).forEach(e -> {
                SwNode other = otherSwNode(e, swNode);
                if (other == null) {
                    return;
                }
                if (processed.contains(e)) {
                    return;
                }
                nextLevel.add(other);
                parent.put(other, e);
                processed.add(e);
            }));
            if (!nextLevel.isEmpty()) {
                levels.add(nextLevel);
            }
            level++;
        }
    }

    private void completeFlowsForEdgesInsideTree(Map<String, SwNode> swNodeInjection, Map<SwNode, SwEdge> parent,
        List<List<SwNode>> levels) {
        // Traverse the tree from leaves to root
        // (The root itself does not need to be processed)
        int level = levels.size() - 1;
        while (level >= 1) {
            levels.get(level).forEach(swNode -> {
                double p = swNode.pflow + swNode.p;
                double q = swNode.qflow + swNode.q;
                SwEdge swEdge = parent.get(swNode);
                addFlowToParentSwNode(swNodeInjection, otherSwNode(swEdge, swNode), p, q);
                if (swEdge.isSwitch()) {
                    switchesFlows.computeIfAbsent(swEdge.getSwitchId(), k -> calculateSwFlow(swEdge, swNode, p, q));
                }
            });
            level--;
        }
    }

    private static void addFlowToParentSwNode(Map<String, SwNode> swNodeInjection, SwNode swNode, double p, double q) {
        swNodeInjection.computeIfPresent(getKey(swNode), (key, value) -> value.addFlow(p, q));
    }

    private void assignZeroFlowToEdgesOutsideAllTrees(SimpleWeightedGraph<SwNode, SwEdge> graph) {
        graph.edgeSet().forEach(e -> {
            if (e.isSwitch() && !switchesFlows.containsKey(e.getSwitchId())) {
                switchesFlows.put(e.getSwitchId(), new SwFlow(0.0, 0.0, 0.0, 0.0));
            }
        });
    }

    private static SwFlow calculateSwFlow(SwEdge swEdge, SwNode swNode, double pOtherNode, double qOtherNode) {
        if (swEdge.swNode1 == swNode) {
            return new SwFlow(-pOtherNode, -qOtherNode, pOtherNode, qOtherNode);
        } else {
            return new SwFlow(pOtherNode, qOtherNode, -pOtherNode, -qOtherNode);
        }
    }

    private static SwNode otherSwNode(SwEdge swEdge, SwNode swNode) {
        if (swEdge.swNode1 == swNode) {
            return swEdge.swNode2;
        } else if (swEdge.swNode2 == swNode) {
            return swEdge.swNode1;
        } else {
            return null;
        }
    }

    private static String getKey(int node) {
        return String.format("N-%d", node);
    }

    private static String getKey(Bus bus) {
        return String.format("B-%s", bus.getId());
    }

    private static String getKey(SwNode swNode) {
        if (swNode.isNodeBreaker()) {
            return getKey(swNode.node);
        } else {
            return getKey(swNode.bus);
        }
    }

    private static double getTerminalP(Terminal terminal) {
        if (Double.isNaN(terminal.getP())) {
            return 0.0;
        } else {
            return terminal.getP();
        }
    }

    private static double getTerminalQ(Terminal terminal) {
        if (Double.isNaN(terminal.getQ())) {
            return 0.0;
        } else {
            return terminal.getQ();
        }
    }

    private static final class SwNode {
        private final Integer node;
        private final Bus bus;
        private double p;
        private double q;
        private double pflow;
        private double qflow;

        private SwNode(int node) {
            this.node = node;
            bus = null;
        }

        private SwNode(Bus bus) {
            node = null;
            this.bus = bus;
        }

        private boolean isNodeBreaker() {
            return node != null;
        }

        private SwNode addPQ(double p, double q) {
            this.p = this.p + p;
            this.q = this.q + q;

            return this;
        }

        private SwNode addFlow(double pFlow, double qFlow) {
            this.pflow = this.pflow + pFlow;
            this.qflow = this.qflow + qFlow;

            return this;
        }
    }

    private static final class SwEdge extends DefaultWeightedEdge {
        private final transient Switch sw;
        private final transient SwNode swNode1;
        private final transient SwNode swNode2;

        private SwEdge(Switch sw, SwNode swNode1, SwNode swNode2) {
            this.sw = sw;
            this.swNode1 = swNode1;
            this.swNode2 = swNode2;
        }

        private SwEdge(SwNode swNode1, SwNode swNode2) {
            sw = null;
            this.swNode1 = swNode1;
            this.swNode2 = swNode2;
        }

        private boolean isSwitch() {
            return sw != null;
        }

        private String getSwitchId() {
            if (sw != null) {
                return sw.getId();
            } else {
                return null;
            }
        }

        @Override
        protected double getWeight() {
            return 0.0;
        }
    }

    private static final class SwFlow {
        private final double p1;
        private final double q1;
        private final double p2;
        private final double q2;

        private SwFlow(double p1, double q1, double p2, double q2) {
            this.p1 = p1;
            this.q1 = q1;
            this.p2 = p2;
            this.q2 = q2;
        }
    }
}