Networks.java

/**
 * Copyright (c) 2016, All partners of the iTesla project (http://www.itesla-project.eu/consortium)
 * Copyright (c) 2017, 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.commons.io.table.AbstractTableFormatter;
import com.powsybl.commons.io.table.AsciiTableFormatter;
import com.powsybl.commons.io.table.Column;
import com.powsybl.commons.io.table.HorizontalAlignment;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.iidm.network.*;
import com.powsybl.math.graph.TraverseResult;
import org.slf4j.Logger;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.IntStream;

/**
 *
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
public final class Networks {

    private Networks() {
    }

    public static boolean isBusValid(int feederCount) {
        return feederCount >= 1;
    }

    public static Map<String, String> getExecutionTags(Network network) {
        return Map.of("variant", network.getVariantManager().getWorkingVariantId());
    }

    public static void dumpVariantId(Path workingDir, String variantId) throws IOException {
        try (BufferedWriter writer = Files.newBufferedWriter(workingDir.resolve("variant.txt"), StandardCharsets.UTF_8)) {
            writer.write(variantId);
            writer.newLine();
        }
    }

    public static void dumpVariantId(Path workingDir, Network network) throws IOException {
        dumpVariantId(workingDir, network.getVariantManager().getWorkingVariantId());
    }

    static class ConnectedPower {
        private int busCount = 0;

        private final List<String> connectedLoads = new ArrayList<>();
        private final List<String> disconnectedLoads = new ArrayList<>();
        private double connectedLoadVolume = 0.0;
        private double disconnectedLoadVolume = 0.0;

        private double connectedMaxGeneration = 0.0;
        private double disconnectedMaxGeneration = 0.0;
        private double connectedGeneration = 0.0;
        private double disconnectedGeneration = 0.0;
        private final List<String> connectedGenerators = new ArrayList<>();
        private final List<String> disconnectedGenerators = new ArrayList<>();

        private final List<String> connectedShunts = new ArrayList<>();
        private final List<String> disconnectedShunts = new ArrayList<>();
        private double connectedShuntPositiveVolume = 0.0;
        private double disconnectedShuntPositiveVolume = 0.0;
        private double connectedShuntNegativeVolume = 0.0;
        private double disconnectedShuntNegativeVolume = 0.0;
    }

    public static void printBalanceSummary(String title, Network network, Writer writer) throws IOException {
        Objects.requireNonNull(title);
        Objects.requireNonNull(network);
        Objects.requireNonNull(writer);

        ConnectedPower balanceMainCC = new ConnectedPower();
        ConnectedPower balanceOtherCC = new ConnectedPower();

        addBuses(network, balanceMainCC, balanceOtherCC);
        addLoads(network, balanceMainCC, balanceOtherCC);
        addDanglingLines(network, balanceMainCC, balanceOtherCC);
        addGenerators(network, balanceMainCC, balanceOtherCC);
        addShuntCompensators(network, balanceMainCC, balanceOtherCC);

        logOtherCC(writer, title, () -> writeInTable(balanceMainCC, balanceOtherCC), balanceOtherCC);
    }

    private static void addBuses(Network network, ConnectedPower balanceMainCC, ConnectedPower balanceOtherCC) {
        for (Bus b : network.getBusBreakerView().getBuses()) {
            if (b.isInMainConnectedComponent()) {
                balanceMainCC.busCount++;
            } else {
                balanceOtherCC.busCount++;
            }
        }
    }

    private static void addLoads(Network network, ConnectedPower balanceMainCC, ConnectedPower balanceOtherCC) {
        for (Load l : network.getLoads()) {
            Terminal.BusBreakerView view = l.getTerminal().getBusBreakerView();
            if (view.getBus() != null) {
                if (view.getBus().isInMainConnectedComponent()) {
                    balanceMainCC.connectedLoads.add(l.getId());
                    balanceMainCC.connectedLoadVolume += l.getP0();
                } else {
                    balanceOtherCC.connectedLoads.add(l.getId());
                    balanceOtherCC.connectedLoadVolume += l.getP0();
                }
            } else {
                if (view.getConnectableBus().isInMainConnectedComponent()) {
                    balanceMainCC.disconnectedLoads.add(l.getId());
                    balanceMainCC.disconnectedLoadVolume += l.getP0();
                } else {
                    balanceOtherCC.disconnectedLoads.add(l.getId());
                    balanceOtherCC.disconnectedLoadVolume += l.getP0();
                }
            }
        }
    }

    private static void addDanglingLines(Network network, ConnectedPower balanceMainCC, ConnectedPower balanceOtherCC) {
        for (DanglingLine dl : network.getDanglingLines(DanglingLineFilter.UNPAIRED)) {
            Terminal.BusBreakerView view = dl.getTerminal().getBusBreakerView();
            if (view.getBus() != null) {
                if (view.getBus().isInMainConnectedComponent()) {
                    balanceMainCC.connectedLoads.add(dl.getId());
                    balanceMainCC.connectedLoadVolume += dl.getP0();
                } else {
                    balanceOtherCC.connectedLoads.add(dl.getId());
                    balanceOtherCC.connectedLoadVolume += dl.getP0();
                }
            } else {
                if (view.getConnectableBus().isInMainConnectedComponent()) {
                    balanceMainCC.disconnectedLoads.add(dl.getId());
                    balanceMainCC.disconnectedLoadVolume += dl.getP0();
                } else {
                    balanceOtherCC.disconnectedLoads.add(dl.getId());
                    balanceOtherCC.disconnectedLoadVolume += dl.getP0();
                }
            }
        }
    }

    private static void addGenerators(Network network, ConnectedPower balanceMainCC, ConnectedPower balanceOtherCC) {
        for (Generator g : network.getGenerators()) {
            Terminal.BusBreakerView view = g.getTerminal().getBusBreakerView();
            if (view.getBus() != null) {
                if (view.getBus().isInMainConnectedComponent()) {
                    balanceMainCC.connectedMaxGeneration += g.getMaxP();
                    balanceMainCC.connectedGeneration += g.getTargetP();
                    balanceMainCC.connectedGenerators.add(g.getId());
                } else {
                    balanceOtherCC.connectedMaxGeneration += g.getMaxP();
                    balanceOtherCC.connectedGeneration += g.getTargetP();
                    balanceOtherCC.connectedGenerators.add(g.getId());
                }
            } else {
                if (view.getConnectableBus().isInMainConnectedComponent()) {
                    balanceMainCC.disconnectedMaxGeneration += g.getMaxP();
                    balanceMainCC.disconnectedGeneration += g.getTargetP();
                    balanceMainCC.disconnectedGenerators.add(g.getId());
                } else {
                    balanceOtherCC.disconnectedMaxGeneration += g.getMaxP();
                    balanceOtherCC.disconnectedGeneration += g.getTargetP();
                    balanceOtherCC.disconnectedGenerators.add(g.getId());
                }
            }
        }
    }

    private static void addShuntCompensators(Network network, ConnectedPower balanceMainCC, ConnectedPower balanceOtherCC) {
        for (ShuntCompensator sc : network.getShuntCompensators()) {
            Terminal.BusBreakerView view = sc.getTerminal().getBusBreakerView();
            double q = sc.getB() * Math.pow(sc.getTerminal().getVoltageLevel().getNominalV(), 2);
            if (view.getBus() != null) {
                addConnectedShunt(view, q, sc.getId(), balanceMainCC, balanceOtherCC);
            } else {
                addDisonnectedShunt(view, q, sc.getId(), balanceMainCC, balanceOtherCC);
            }
        }
    }

    private static void addConnectedShunt(Terminal.BusBreakerView view, double q, String shuntId, ConnectedPower balanceMainCC, ConnectedPower balanceOtherCC) {
        if (view.getBus().isInMainConnectedComponent()) {
            if (q > 0) {
                balanceMainCC.connectedShuntPositiveVolume += q;
            } else {
                balanceMainCC.connectedShuntNegativeVolume += q;
            }
            balanceMainCC.connectedShunts.add(shuntId);
        } else {
            if (q > 0) {
                balanceOtherCC.connectedShuntPositiveVolume += q;
            } else {
                balanceOtherCC.connectedShuntNegativeVolume += q;
            }
            balanceOtherCC.connectedShunts.add(shuntId);
        }
    }

    private static void addDisonnectedShunt(Terminal.BusBreakerView view, double q, String shuntId, ConnectedPower balanceMainCC, ConnectedPower balanceOtherCC) {
        if (view.getConnectableBus().isInMainConnectedComponent()) {
            if (q > 0) {
                balanceMainCC.disconnectedShuntPositiveVolume += q;
            } else {
                balanceMainCC.disconnectedShuntNegativeVolume += q;
            }
            balanceMainCC.disconnectedShunts.add(shuntId);
        } else {
            if (q > 0) {
                balanceOtherCC.disconnectedShuntPositiveVolume += q;
            } else {
                balanceOtherCC.disconnectedShuntNegativeVolume += q;
            }
            balanceOtherCC.disconnectedShunts.add(shuntId);
        }
    }

    private static String writeInTable(ConnectedPower balanceMainCC, ConnectedPower balanceOtherCC) {
        Writer writer = new StringWriter();
        try (AbstractTableFormatter formatter = new AsciiTableFormatter(writer, null,
                new Column("")
                        .setTitleHorizontalAlignment(HorizontalAlignment.CENTER),
                new Column("Main CC connected/disconnected")
                        .setColspan(2)
                        .setTitleHorizontalAlignment(HorizontalAlignment.CENTER),
                new Column("Others CC connected/disconnected")
                        .setColspan(2)
                        .setTitleHorizontalAlignment(HorizontalAlignment.CENTER))) {
            formatter.writeCell("Bus count")
                    .writeCell(Integer.toString(balanceMainCC.busCount), 2)
                    .writeCell(Integer.toString(balanceOtherCC.busCount), 2);
            formatter.writeCell("Load count")
                    .writeCell(Integer.toString(balanceMainCC.connectedLoads.size()))
                    .writeCell(Integer.toString(balanceMainCC.disconnectedLoads.size()))
                    .writeCell(Integer.toString(balanceOtherCC.connectedLoads.size()))
                    .writeCell(Integer.toString(balanceOtherCC.disconnectedLoads.size()));
            formatter.writeCell("Load (MW)")
                    .writeCell(Double.toString(balanceMainCC.connectedLoadVolume))
                    .writeCell(Double.toString(balanceMainCC.disconnectedLoadVolume))
                    .writeCell(Double.toString(balanceOtherCC.connectedLoadVolume))
                    .writeCell(Double.toString(balanceOtherCC.disconnectedLoadVolume));
            formatter.writeCell("Generator count")
                    .writeCell(Integer.toString(balanceMainCC.connectedGenerators.size()))
                    .writeCell(Integer.toString(balanceMainCC.disconnectedGenerators.size()))
                    .writeCell(Integer.toString(balanceOtherCC.connectedGenerators.size()))
                    .writeCell(Integer.toString(balanceOtherCC.disconnectedGenerators.size()));
            formatter.writeCell("Max generation (MW)")
                    .writeCell(Double.toString(balanceMainCC.connectedMaxGeneration))
                    .writeCell(Double.toString(balanceMainCC.disconnectedMaxGeneration))
                    .writeCell(Double.toString(balanceOtherCC.connectedMaxGeneration))
                    .writeCell(Double.toString(balanceOtherCC.disconnectedMaxGeneration));
            formatter.writeCell("Generation (MW)")
                    .writeCell(Double.toString(balanceMainCC.connectedGeneration))
                    .writeCell(Double.toString(balanceMainCC.disconnectedGeneration))
                    .writeCell(Double.toString(balanceOtherCC.connectedGeneration))
                    .writeCell(Double.toString(balanceOtherCC.disconnectedGeneration));
            formatter.writeCell("Shunt at nom V (MVar)")
                    .writeCell(balanceMainCC.connectedShuntPositiveVolume + " " +
                        balanceMainCC.connectedShuntNegativeVolume +
                            " (" + balanceMainCC.connectedShunts.size() + ")")
                    .writeCell(balanceMainCC.disconnectedShuntPositiveVolume + " " +
                        balanceMainCC.disconnectedShuntNegativeVolume +
                            " (" + balanceMainCC.disconnectedShunts.size() + ")")
                    .writeCell(balanceOtherCC.connectedShuntPositiveVolume + " " +
                        balanceOtherCC.connectedShuntNegativeVolume +
                            " (" + balanceOtherCC.connectedShunts.size() + ")")
                    .writeCell(balanceOtherCC.disconnectedShuntPositiveVolume + " " +
                        balanceOtherCC.disconnectedShuntNegativeVolume +
                            " (" + balanceOtherCC.disconnectedShunts.size() + ")");
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        return writer.toString();
    }

    private static void logOtherCC(Writer writer, String title, Supplier<String> tableSupplier, ConnectedPower balanceOtherCC) throws IOException {
        writer.write("Active balance at step '" + title + "':" + System.lineSeparator() + tableSupplier.get());

        if (!balanceOtherCC.connectedLoads.isEmpty()) {
            writer.write("Connected loads in other CC: " + balanceOtherCC.connectedLoads + System.lineSeparator());
        }
        if (!balanceOtherCC.disconnectedLoads.isEmpty()) {
            writer.write("Disconnected loads in other CC: " + balanceOtherCC.disconnectedLoads + System.lineSeparator());
        }
        if (!balanceOtherCC.connectedGenerators.isEmpty()) {
            writer.write("Connected generators in other CC: " + balanceOtherCC.connectedGenerators + System.lineSeparator());
        }
        if (!balanceOtherCC.disconnectedGenerators.isEmpty()) {
            writer.write("Disconnected generators in other CC: " + balanceOtherCC.disconnectedGenerators + System.lineSeparator());
        }
        if (!balanceOtherCC.disconnectedShunts.isEmpty()) {
            writer.write("Disconnected shunts in other CC: " + balanceOtherCC.disconnectedShunts + System.lineSeparator());
        }
    }

    public static void printGeneratorsSetpointDiff(Network network, Logger logger) {
        for (Generator g : network.getGenerators()) {
            double dp = Math.abs(g.getTerminal().getP() + g.getTargetP());
            double dq = Math.abs(g.getTerminal().getQ() + g.getTargetQ());
            double dv = Math.abs(g.getTerminal().getBusBreakerView().getConnectableBus().getV() - g.getTargetV());
            if (dp > 1 || dq > 5 || dv > 0.1) {
                logger.warn("Generator {}: ({}, {}, {}) ({}, {}, {}) -> ({}, {}, {})", g.getId(),
                        dp, dq, dv,
                        -g.getTargetP(), -g.getTargetQ(), g.getTargetV(),
                        g.getTerminal().getP(), g.getTerminal().getQ(), g.getTerminal().getBusBreakerView().getConnectableBus().getV());
            }
        }
    }

    /**
     * Return the list of nodes (N/B topology) for each bus of a the Bus view
     * If a node is not associated to a bus, it is not included in any list.
     * @param voltageLevel The voltage level to traverse
     * @return a map with the list of nodes (N/B topology) for each bus of a Bus view
     */
    public static Map<String, Set<Integer>> getNodesByBus(VoltageLevel voltageLevel) {
        checkNodeBreakerVoltageLevel(voltageLevel);

        Map<String, Set<Integer>> nodesByBus = new TreeMap<>();
        for (int i : voltageLevel.getNodeBreakerView().getNodes()) {
            Terminal terminal = voltageLevel.getNodeBreakerView().getTerminal(i);
            if (terminal != null) {
                Bus bus = terminal.getBusView().getBus();
                if (bus != null) {
                    nodesByBus.computeIfAbsent(bus.getId(), k -> new TreeSet<>()).add(i);
                }
            } else {
                // If there is no terminal for the current node, we try to find one traversing the topology
                Terminal equivalentTerminal = getEquivalentTerminal(voltageLevel, i);

                if (equivalentTerminal != null) {
                    Bus bus = equivalentTerminal.getBusView().getBus();
                    if (bus != null) {
                        nodesByBus.computeIfAbsent(bus.getId(), k -> new TreeSet<>()).add(i);
                    }
                }
            }
        }

        return nodesByBus;
    }

    private static void addBusFromTerminal(String busId, Terminal terminal, Function<Terminal, Bus> getBusFromTerminal, Set<Integer> nodes, int node) {
        Bus bus = getBusFromTerminal.apply(terminal);
        if (bus != null && bus.getId().equals(busId)) {
            nodes.add(node);
        }
    }

    public static IntStream getNodes(String busId, VoltageLevel voltageLevel, Function<Terminal, Bus> getBusFromTerminal) {
        checkNodeBreakerVoltageLevel(voltageLevel);
        Set<Integer> nodes = new TreeSet<>();
        for (int i : voltageLevel.getNodeBreakerView().getNodes()) {
            Terminal terminal = voltageLevel.getNodeBreakerView().getTerminal(i);
            if (terminal != null) {
                addBusFromTerminal(busId, terminal, getBusFromTerminal, nodes, i);
            } else {
                // If there is no terminal for the current node, we try to find one traversing the topology
                Terminal equivalentTerminal = Networks.getEquivalentTerminal(voltageLevel, i);
                if (equivalentTerminal != null) {
                    addBusFromTerminal(busId, equivalentTerminal, getBusFromTerminal, nodes, i);
                }
            }
        }
        return nodes.stream().mapToInt(Integer::intValue);
    }

    /**
     * Return a terminal for the specified node.
     * If a terminal is attached to the node, return this terminal. Otherwise, this method traverses the topology and return
     * the first equivalent terminal found.
     *
     * @param voltageLevel The voltage level to traverse
     * @param node The starting node
     * @return A terminal for the specified node or null.
     */
    public static Terminal getEquivalentTerminal(VoltageLevel voltageLevel, int node) {
        checkNodeBreakerVoltageLevel(voltageLevel);

        Terminal[] equivalentTerminal = new Terminal[1];

        VoltageLevel.NodeBreakerView.TopologyTraverser traverser = (node1, sw, node2) -> {
            if (sw != null && sw.isOpen()) {
                return TraverseResult.TERMINATE_PATH;
            }
            Terminal t = voltageLevel.getNodeBreakerView().getTerminal(node2);
            if (t != null) {
                equivalentTerminal[0] = t;
                return TraverseResult.TERMINATE_TRAVERSER;
            }
            return TraverseResult.CONTINUE;
        };

        voltageLevel.getNodeBreakerView().traverse(node, traverser);

        return equivalentTerminal[0];
    }

    private static void checkNodeBreakerVoltageLevel(VoltageLevel voltageLevel) {
        if (voltageLevel.getTopologyKind() != TopologyKind.NODE_BREAKER) {
            throw new IllegalArgumentException("The voltage level " + voltageLevel.getId() + " is not described in Node/Breaker topology");
        }
    }

    /**
     * Set a {@link ReportNode} in the reportNode context of the given network, execute a runnable then restore the reportNode context.
     *
     * @param network a network
     * @param reportNode the reportNode to use
     * @param runnable the runnable to execute
     */
    public static void executeWithReportNode(Network network, ReportNode reportNode, Runnable runnable) {
        network.getReportNodeContext().pushReportNode(reportNode);
        try {
            runnable.run();
        } finally {
            network.getReportNodeContext().popReportNode();
        }
    }

    /**
     * Returns a {@link ReportNodeContext} containing the same reportNodes as the given one,
     * but reconfigured to allow it, or not, to be accessed simultaneously by different threads.
     * When this option is activated, the reportNode context can have a different content
     * for each thread.
     *
     * @param reportNodeContext the ReportNodeContext to reconfigure
     * @param allow allow multi-thread access to the ReportNodeContext
     * @return the reconfigured ReportNodeContext
     */
    public static AbstractReportNodeContext allowReportNodeContextMultiThreadAccess(AbstractReportNodeContext reportNodeContext, boolean allow) {
        AbstractReportNodeContext newReportNodeContext = null;
        if (allow && !(reportNodeContext instanceof MultiThreadReportNodeContext)) {
            newReportNodeContext = new MultiThreadReportNodeContext(reportNodeContext);
        } else if (!allow && !(reportNodeContext instanceof SimpleReportNodeContext)) {
            newReportNodeContext = new SimpleReportNodeContext(reportNodeContext);
            if (reportNodeContext instanceof MultiThreadReportNodeContext multiThreadReportNodeContext) {
                multiThreadReportNodeContext.close(); // to avoid memory leaks
            }
        }
        return newReportNodeContext != null ? newReportNodeContext : reportNodeContext;
    }
}