ReplaceTeePointByVoltageLevelOnLine.java

/**
 * Copyright (c) 2022, 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.modification.topology;

import com.powsybl.commons.report.ReportNode;
import com.powsybl.computation.ComputationManager;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.extensions.BusbarSectionPosition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.Objects;

import static com.powsybl.iidm.modification.topology.TopologyModificationUtils.*;
import static com.powsybl.iidm.modification.util.ModificationLogs.logOrThrow;
import static com.powsybl.iidm.modification.util.ModificationReports.*;

/**
 * This method transform the action done in the CreateLineOnLine class into the action done in the ConnectVoltageLevelOnLine class :
 * it replaces 3 existing lines (with the same voltage level at one of their side (tee point)) with two new lines,
 * and removes the tee point
 * <p>
 * Before modification:
 * <pre>
 * VL1 ----------------- tee point ----------------- VL2
 *      (teePointLine1)     |       (teePointLine2)
 *                          |
 *                          | (teePointLineToRemove)
 *                          |
 *                      VL3 tapped
 *                (contains bbsOrBusId)</pre>
 * After modification:
 * <pre>
 * VL1 ------------ VL3 switching ------------ VL2
 *      (newLine1)                (newLine2)</pre>
 *
 * @author Franck Lecuyer {@literal <franck.lecuyer at rte-france.com>}
 */
public class ReplaceTeePointByVoltageLevelOnLine extends AbstractLineDisconnectionModification<ReplaceTeePointByVoltageLevelOnLine> {

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

    private final String bbsOrBusId;
    private final String newLine1Id;
    private final String newLine1Name;
    private final String newLine2Id;
    private final String newLine2Name;

    private static final String LINE_NOT_FOUND_REPORT_MESSAGE = "Line %s is not found";
    private static final String LINE_REMOVED_LOG_MESSAGE = "Line {} removed";

    /**
     * Constructor.
     *
     * @param teePointLine1Id        The non-null ID of the existing line connecting the tee point to the first voltage level
     * @param teePointLine2Id        The non-null ID of the existing line connecting the tee point to the second voltage level
     * @param teePointLineToRemoveId The non-null ID of the existing line connecting the tee point to the tapped voltage level
     * @param bbsOrBusId             The non-null ID of the existing bus or bus bar section in the tapped voltage level, where we want to connect the new lines newLine1 and newLine2
     * @param newLine1Id             The non-null ID of the new line connecting the first voltage level to the formerly tapped voltage level
     * @param newLine1Name           The optional name of the new line connecting the first voltage level to the formerly tapped voltage level
     * @param newLine2Id             The non-null ID of the new line connecting the second voltage level to the formerly tapped voltage level
     * @param newLine2Name           The optional name of the new line connecting the second voltage level to the formerly tapped voltage level
     * <p>
     * NB: This constructor is package-private, Please use {@link ReplaceTeePointByVoltageLevelOnLineBuilder} instead.
     */
    ReplaceTeePointByVoltageLevelOnLine(String teePointLine1Id, String teePointLine2Id, String teePointLineToRemoveId, String bbsOrBusId,
                                        String newLine1Id, String newLine1Name, String newLine2Id, String newLine2Name) {
        super(teePointLine1Id, teePointLine2Id, teePointLineToRemoveId);
        this.bbsOrBusId = Objects.requireNonNull(bbsOrBusId);
        this.newLine1Id = Objects.requireNonNull(newLine1Id);
        this.newLine1Name = newLine1Name;
        this.newLine2Id = Objects.requireNonNull(newLine2Id);
        this.newLine2Name = newLine2Name;
    }

    @Override
    public String getName() {
        return "ReplaceTeePointByVoltageLevelOnLine";
    }

    public String getTeePointLine1Id() {
        return oldLine1Id;
    }

    public String getTeePointLine2Id() {
        return oldLine2Id;
    }

    public String getTeePointLineToRemoveId() {
        return lineToRemoveId;
    }

    public String getBbsOrBusId() {
        return bbsOrBusId;
    }

    public String getNewLine1Id() {
        return newLine1Id;
    }

    public String getNewLine1Name() {
        return newLine1Name;
    }

    public String getNewLine2Id() {
        return newLine2Id;
    }

    public String getNewLine2Name() {
        return newLine2Name;
    }

    @Override
    public void apply(Network network, NamingStrategy namingStrategy, boolean throwException,
                      ComputationManager computationManager, ReportNode reportNode) {
        Line tpLine1 = getLineFromNetwork(network, oldLine1Id, reportNode, throwException);
        Line tpLine2 = getLineFromNetwork(network, oldLine2Id, reportNode, throwException);

        if (tpLine1 == null || tpLine2 == null) {
            return;
        }

        Line tpLineToRemove = getLineFromNetwork(network, lineToRemoveId, reportNode, throwException);
        if (tpLineToRemove == null) {
            return;
        }

        // tee point is the voltage level in common with tpLine1, tpLine2 and tpLineToRemove
        VoltageLevel teePoint = TopologyModificationUtils.findTeePoint(tpLine1, tpLine2, tpLineToRemove);

        if (teePoint == null) {
            noTeePointAndOrTappedVoltageLevelReport(reportNode, oldLine1Id, oldLine2Id, lineToRemoveId);
            logOrThrow(throwException, String.format("Unable to find the tee point and the tapped voltage level from lines %s, %s and %s", oldLine1Id, oldLine2Id, lineToRemoveId));
            return;
        }

        // tapped voltage level is the voltage level of tpLineToRemove, at the opposite side of the tee point
        VoltageLevel tappedVoltageLevel = tpLineToRemove.getTerminal1().getVoltageLevel() == teePoint
                ? tpLineToRemove.getTerminal2().getVoltageLevel()
                : tpLineToRemove.getTerminal1().getVoltageLevel();

        TwoSides tpLine1OtherVlSide = tpLine1.getTerminal1().getVoltageLevel() == teePoint ? TwoSides.TWO : TwoSides.ONE;
        TwoSides tpLine2OtherVlSide = tpLine2.getTerminal1().getVoltageLevel() == teePoint ? TwoSides.TWO : TwoSides.ONE;

        // Set parameters of the new lines newLine1 and newLine2
        LineAdder newLine1Adder = createLineAdder(newLine1Id, newLine1Name, tpLine1.getTerminal(tpLine1OtherVlSide).getVoltageLevel().getId(), tappedVoltageLevel.getId(), network, tpLine1, tpLineToRemove);
        LineAdder newLine2Adder = createLineAdder(newLine2Id, newLine2Name, tappedVoltageLevel.getId(), tpLine2.getTerminal(tpLine2OtherVlSide).getVoltageLevel().getId(), network, tpLine2, tpLineToRemove);

        // Create the topology inside the existing tapped voltage level and attach lines newLine1 and newLine2
        attachLine(tpLine1.getTerminal(tpLine1OtherVlSide), newLine1Adder, (bus, adder) -> adder.setConnectableBus1(bus.getId()), (bus, adder) -> adder.setBus1(bus.getId()), (node, adder) -> adder.setNode1(node));
        attachLine(tpLine2.getTerminal(tpLine2OtherVlSide), newLine2Adder, (bus, adder) -> adder.setConnectableBus2(bus.getId()), (bus, adder) -> adder.setBus2(bus.getId()), (node, adder) -> adder.setNode2(node));

        // Create the breaker topology
        if (!createTopology(newLine1Adder, newLine2Adder, tappedVoltageLevel, namingStrategy, reportNode, throwException)) {
            return;
        }

        // get line tpLine1 limits
        TwoSides tpLine1Limits1Side = tpLine1OtherVlSide;
        TwoSides tpLine1Limits2Side = tpLine1OtherVlSide == TwoSides.ONE ? TwoSides.TWO : TwoSides.ONE;
        LoadingLimitsBags limits1TpLine1 = new LoadingLimitsBags(() -> tpLine1.getActivePowerLimits(tpLine1Limits1Side),
                () -> tpLine1.getApparentPowerLimits(tpLine1Limits1Side),
                () -> tpLine1.getCurrentLimits(tpLine1Limits1Side));
        LoadingLimitsBags limits2TpLine1 = new LoadingLimitsBags(() -> tpLine1.getActivePowerLimits(tpLine1Limits2Side),
                () -> tpLine1.getApparentPowerLimits(tpLine1Limits2Side),
                () -> tpLine1.getCurrentLimits(tpLine1Limits2Side));

        // get line tpLine2 limits
        TwoSides tpLine2Limits1Side = tpLine2OtherVlSide == TwoSides.ONE ? TwoSides.TWO : TwoSides.ONE;
        TwoSides tpLine2Limits2Side = tpLine2OtherVlSide;

        LoadingLimitsBags limits1TpLine2 = new LoadingLimitsBags(() -> tpLine2.getActivePowerLimits(tpLine2Limits1Side),
                () -> tpLine2.getApparentPowerLimits(tpLine2Limits1Side),
                () -> tpLine2.getCurrentLimits(tpLine2Limits1Side));
        LoadingLimitsBags limits2TpLine2 = new LoadingLimitsBags(() -> tpLine2.getActivePowerLimits(tpLine2Limits2Side),
                () -> tpLine2.getApparentPowerLimits(tpLine2Limits2Side),
                () -> tpLine2.getCurrentLimits(tpLine2Limits2Side));

        // Remove the three existing lines
        tpLine1.remove();
        removedLineReport(reportNode, oldLine1Id);
        LOGGER.info(LINE_REMOVED_LOG_MESSAGE, oldLine1Id);
        tpLine2.remove();
        removedLineReport(reportNode, oldLine2Id);
        LOGGER.info(LINE_REMOVED_LOG_MESSAGE, oldLine2Id);
        new RemoveFeederBay(tpLineToRemove.getId()).apply(network, namingStrategy, throwException, computationManager, reportNode);
        removedLineReport(reportNode, lineToRemoveId);
        LOGGER.info(LINE_REMOVED_LOG_MESSAGE, lineToRemoveId);

        // Create the two new lines
        Line newLine1 = newLine1Adder.add();
        addLoadingLimits(newLine1, limits1TpLine1, TwoSides.ONE);
        addLoadingLimits(newLine1, limits2TpLine1, TwoSides.TWO);
        createdLineReport(reportNode, newLine1Id);
        LOGGER.info("Line {} created", newLine1Id);

        Line newLine2 = newLine2Adder.add();
        addLoadingLimits(newLine2, limits1TpLine2, TwoSides.ONE);
        addLoadingLimits(newLine2, limits2TpLine2, TwoSides.TWO);
        createdLineReport(reportNode, newLine2Id);
        LOGGER.info("Line {} created", newLine2Id);

        // remove tee point
        removeVoltageLevelAndSubstation(teePoint, reportNode);
    }

    private boolean createTopology(LineAdder newLine1Adder, LineAdder newLine2Adder, VoltageLevel tappedVoltageLevel, NamingStrategy namingStrategy, ReportNode reportNode, boolean throwException) {
        TopologyKind topologyKind = tappedVoltageLevel.getTopologyKind();
        if (topologyKind == TopologyKind.BUS_BREAKER) {
            Bus bus = tappedVoltageLevel.getBusBreakerView().getBus(bbsOrBusId);
            if (bus == null) {
                return errorWhenBusNull(reportNode, tappedVoltageLevel, throwException);
            }
            Bus bus1 = tappedVoltageLevel.getBusBreakerView()
                .newBus()
                .setId(namingStrategy.getBusId(newLine1Id))
                .add();
            Bus bus2 = tappedVoltageLevel.getBusBreakerView()
                .newBus()
                .setId(namingStrategy.getBusId(newLine2Id))
                .add();
            createBusBreakerSwitch(bus1.getId(), bus.getId(), namingStrategy.getSwitchId(newLine1Id, 1), tappedVoltageLevel.getBusBreakerView());
            createBusBreakerSwitch(bus.getId(), bus2.getId(), namingStrategy.getSwitchId(newLine2Id, 2), tappedVoltageLevel.getBusBreakerView());
            newLine1Adder.setBus2(bus1.getId());
            newLine1Adder.setConnectableBus2(bus1.getId());
            newLine2Adder.setBus1(bus2.getId());
            newLine2Adder.setConnectableBus1(bus2.getId());
        } else if (topologyKind == TopologyKind.NODE_BREAKER) {
            BusbarSection bbs = tappedVoltageLevel.getNodeBreakerView().getBusbarSection(bbsOrBusId);
            if (bbs == null) {
                return errorWhenBusbarSectionNull(reportNode, tappedVoltageLevel, throwException);
            }
            // New node
            int firstAvailableNode = tappedVoltageLevel.getNodeBreakerView().getMaximumNodeIndex() + 1;
            newLine1Adder.setNode2(firstAvailableNode);
            newLine2Adder.setNode1(firstAvailableNode + 3);

            // Busbar section properties
            BusbarSectionPosition position = bbs.getExtension(BusbarSectionPosition.class);

            // Topology creation
            if (position == null) {
                // No position extension is present so only one disconnector is needed
                createNodeBreakerSwitchesTopology(tappedVoltageLevel, firstAvailableNode, firstAvailableNode + 1, namingStrategy, newLine1Id, bbs);
                createNodeBreakerSwitchesTopology(tappedVoltageLevel, firstAvailableNode + 3, firstAvailableNode + 2, namingStrategy, newLine2Id, bbs);
                LOGGER.warn("No busbar section position extension found on {}, only one disconnector is created.", bbs.getId());
                noBusbarSectionPositionExtensionReport(reportNode, bbs);
            } else {
                List<BusbarSection> bbsList = getParallelBusbarSections(tappedVoltageLevel, position);
                createNodeBreakerSwitchesTopology(tappedVoltageLevel, firstAvailableNode, firstAvailableNode + 1, namingStrategy, newLine1Id, bbsList, bbs);
                createNodeBreakerSwitchesTopology(tappedVoltageLevel, firstAvailableNode + 3, firstAvailableNode + 2, namingStrategy, newLine2Id, bbsList, bbs);
            }
        }
        return true;
    }

    private Line getLineFromNetwork(Network network, String lineId, ReportNode reportNode, boolean throwException) {
        Line line = network.getLine(lineId);
        if (line == null) {
            notFoundLineReport(reportNode, lineId);
            logOrThrow(throwException, String.format(LINE_NOT_FOUND_REPORT_MESSAGE, lineId));
            return null;
        }
        return line;
    }

    private boolean errorWhenBusNull(ReportNode reportNode, VoltageLevel voltageLevel, boolean throwException) {
        notFoundBusInVoltageLevelReport(reportNode, bbsOrBusId, voltageLevel.getId());
        logOrThrow(throwException, String.format("Bus %s is not found in voltage level %s", bbsOrBusId, voltageLevel.getId()));
        return false;
    }

    private boolean errorWhenBusbarSectionNull(ReportNode reportNode, VoltageLevel voltageLevel, boolean throwException) {
        notFoundBusbarSectionInVoltageLevelReport(reportNode, bbsOrBusId, voltageLevel.getId());
        logOrThrow(throwException, String.format("Busbar section %s is not found in voltage level %s", bbsOrBusId, voltageLevel.getId()));
        return false;
    }
}