CreateVoltageLevelSections.java

/**
 * Copyright (c) 2025, 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.modification.AbstractNetworkModification;
import com.powsybl.iidm.modification.NetworkModificationImpact;
import com.powsybl.iidm.network.BusbarSection;
import com.powsybl.iidm.network.IdentifiableType;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.Switch;
import com.powsybl.iidm.network.SwitchKind;
import com.powsybl.iidm.network.Terminal;
import com.powsybl.iidm.network.VoltageLevel;
import com.powsybl.iidm.network.extensions.BusbarSectionPosition;
import com.powsybl.iidm.network.extensions.BusbarSectionPositionAdder;
import com.powsybl.math.graph.TraverseResult;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;

import static com.powsybl.iidm.modification.topology.TopologyModificationUtils.createNBBreaker;
import static com.powsybl.iidm.modification.topology.TopologyModificationUtils.createNBDisconnector;
import static com.powsybl.iidm.modification.topology.TopologyModificationUtils.getParallelBusbarSections;
import static com.powsybl.iidm.modification.util.ModificationLogs.busbarSectionDoesNotExist;
import static com.powsybl.iidm.modification.util.ModificationLogs.logOrThrow;
import static com.powsybl.iidm.modification.util.ModificationReports.busbarSectionsWithoutPositionReport;
import static com.powsybl.iidm.modification.util.ModificationReports.failToInsertBusbarSectionReport;
import static java.lang.Math.abs;

/**
 * Create new busbar sections and new switches between existing busbar sections in a voltage level in NODE_BREAKER topology.
 * <p>
 *     The new busbar sections can be created before the busbar sections at the first position, between
 *     busbar sections, or after the busbar sections at the last position.
 * </p>
 * <p>
 *     All busbar sections in the voltage level must have the extension BusBarSectionPosition,
 *     corresponding to the position (busbar index and section index) in the voltage level.
 * </p>
 * @author Franck Lecuyer {@literal <franck.lecuyer_externe at rte-france.com>}
 */
public class CreateVoltageLevelSections extends AbstractNetworkModification {

    private final String referenceBusbarSectionId;  // Reference busbar section id

    private final boolean createTheBusbarSectionsAfterTheReferenceBusbarSection;   // create the new busbar sections after(true) or before(false) the reference busbar section

    private final boolean createOnAllParallelBusbars;  // Create the new busbar sections on all busbars(true) or only on the busbar of the reference busbar section(false)

    private final SwitchKind leftSwitchKind;  // Create only a disconnector(SwitchKind.DISCONNECTOR) or a breaker surrounded by 2 disconnectors(SwitchKind.BREAKER), left to the new busbar sections created

    private final SwitchKind rightSwitchKind;  // Create only a disconnector(SwitchKind.DISCONNECTOR) or a breaker surrounded by 2 disconnectors(SwitchKind.BREAKER), right to the new busbar sections created

    private final boolean leftSwitchFictitious;  // Fictitious(true) or not(false) for the new switches created, left to the new busbar sections created

    private final boolean rightSwitchFictitious; // Fictitious(true) or not(false) for the new switches created, right to the new busbar sections created

    private final String switchPrefixId;

    private final String busbarSectionPrefixId;

    CreateVoltageLevelSections(String referenceBusbarSectionId,
                               boolean createTheBusbarSectionsAfterTheReferenceBusbarSection,
                               boolean createOnAllParallelBusbars,
                               SwitchParameters leftSwitchParameters,
                               SwitchParameters rightSwitchParameters,
                               String switchPrefixId,
                               String busbarSectionPrefixId) {
        this.referenceBusbarSectionId = Objects.requireNonNull(referenceBusbarSectionId, "Reference busbar section id not defined");
        this.createTheBusbarSectionsAfterTheReferenceBusbarSection = createTheBusbarSectionsAfterTheReferenceBusbarSection;
        this.createOnAllParallelBusbars = createOnAllParallelBusbars;
        this.leftSwitchKind = leftSwitchParameters.switchKind;
        this.rightSwitchKind = rightSwitchParameters.switchKind;
        this.leftSwitchFictitious = leftSwitchParameters.fictitious;
        this.rightSwitchFictitious = rightSwitchParameters.fictitious;
        this.switchPrefixId = Objects.requireNonNull(switchPrefixId, "Undefined switch prefix ID");
        this.busbarSectionPrefixId = Objects.requireNonNull(busbarSectionPrefixId, "Undefined busbar section prefix ID");
    }

    record SwitchParameters(SwitchKind switchKind, boolean fictitious) { }

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

    public String getReferenceBusbarSectionId() {
        return referenceBusbarSectionId;
    }

    public boolean isCreateTheBusbarSectionsAfterTheReferenceBusbarSection() {
        return createTheBusbarSectionsAfterTheReferenceBusbarSection;
    }

    public boolean isCreateOnAllParallelBusbars() {
        return createOnAllParallelBusbars;
    }

    public SwitchKind getLeftSwitchKind() {
        return leftSwitchKind;
    }

    public SwitchKind getRightSwitchKind() {
        return rightSwitchKind;
    }

    public boolean isLeftSwitchFictitious() {
        return leftSwitchFictitious;
    }

    public boolean isRightSwitchFictitious() {
        return rightSwitchFictitious;
    }

    @Override
    public void apply(Network network, NamingStrategy namingStrategy, boolean throwException, ComputationManager computationManager, ReportNode reportNode) {
        BusbarSection referenceBusbarSection = network.getBusbarSection(getReferenceBusbarSectionId());
        if (referenceBusbarSection == null) {
            busbarSectionDoesNotExist(getReferenceBusbarSectionId(), reportNode, throwException);
            return;
        }

        VoltageLevel voltageLevel = referenceBusbarSection.getTerminal().getVoltageLevel();
        if (voltageLevel == null) {
            return;
        }

        // Check that all busbar sections have the extension BusbarSectionPosition
        boolean allBusbarSectionsWithExtension = voltageLevel.getNodeBreakerView().getBusbarSectionStream().allMatch(busbarSection ->
            Objects.nonNull(busbarSection.getExtension(BusbarSectionPosition.class)));
        if (!allBusbarSectionsWithExtension) {
            busbarSectionsWithoutPositionReport(reportNode, voltageLevel.getId());
            logOrThrow(throwException, String.format("Some busbar sections have no position in voltage level (%s)", voltageLevel.getId()));
            return;
        }

        BusbarSectionPosition referenceBusbarSectionPosition = referenceBusbarSection.getExtension(BusbarSectionPosition.class);
        List<BusbarSection> busbarSections = !isCreateOnAllParallelBusbars() ? List.of(referenceBusbarSection) : getParallelBusbarSections(voltageLevel, referenceBusbarSectionPosition);

        int nextSectionIndex = findNextSectionIndex(voltageLevel, referenceBusbarSectionPosition);
        final SwitchKind switchKind1 = isCreateTheBusbarSectionsAfterTheReferenceBusbarSection() ? getLeftSwitchKind() : getRightSwitchKind();
        final boolean switchFictitious1 = isCreateTheBusbarSectionsAfterTheReferenceBusbarSection() ? isLeftSwitchFictitious() : isRightSwitchFictitious();
        final SwitchKind switchKind2 = isCreateTheBusbarSectionsAfterTheReferenceBusbarSection() ? getRightSwitchKind() : getLeftSwitchKind();
        final boolean switchFictitious2 = isCreateTheBusbarSectionsAfterTheReferenceBusbarSection() ? isRightSwitchFictitious() : isLeftSwitchFictitious();

        busbarSections.forEach(busbarSection ->
            createSection(new CreateSectionParameters(
                    busbarSection,
                    voltageLevel,
                    nextSectionIndex,
                    switchKind1,
                    switchFictitious1,
                    switchKind2,
                    switchFictitious2,
                    namingStrategy,
                    reportNode,
                    throwException))
        );
    }

    private record CreateSectionParameters(BusbarSection busbarSection,
                                           VoltageLevel voltageLevel,
                                           int nextSectionIndex,
                                           SwitchKind switchKind1,
                                           boolean switchFictitious1,
                                           SwitchKind switchKind2,
                                           boolean switchFictitious2,
                                           NamingStrategy namingStrategy,
                                           ReportNode reportNode,
                                           boolean throwException) { }

    private void createSection(CreateSectionParameters createSectionParameters) {
        BusbarSection busbarSection = createSectionParameters.busbarSection;
        VoltageLevel voltageLevel = createSectionParameters.voltageLevel;
        int nextSectionIndex = createSectionParameters.nextSectionIndex;
        SwitchKind switchKind1 = createSectionParameters.switchKind1;
        boolean switchFictitious1 = createSectionParameters.switchFictitious1;
        SwitchKind switchKind2 = createSectionParameters.switchKind2;
        boolean switchFictitious2 = createSectionParameters.switchFictitious2;
        NamingStrategy namingStrategy = createSectionParameters.namingStrategy;
        ReportNode reportNode = createSectionParameters.reportNode;
        boolean throwException = createSectionParameters.throwException;

        BusbarSectionPosition busbarSectionPosition = busbarSection.getExtension(BusbarSectionPosition.class);

        if (nextSectionIndex == -1) {
            // Insert the busbar section before the first section or after the last

            // Create a new busbar section
            BusbarSection newBusbarSection = createBusbarSection(voltageLevel, namingStrategy, busbarSectionPosition);

            // Create new switches between busbarSection and newBusbarSection
            createSwitchesBetweenBusbarSections(voltageLevel, busbarSection, newBusbarSection, namingStrategy, switchKind1, switchFictitious1);
        } else {
            // Insert a new busbar section and new switches between two existing busbar sections.
            // Existing switches between the busbarSection and the neighbor busbar section must be removed.
            // Therefore, the graph is traversed, starting from the referenceBusbarSection terminal with a
            // customized Traverser to get these switches and this neighbor busbar section
            BusbarSectionFinderTraverser traverser = new BusbarSectionFinderTraverser(busbarSection.getId(), busbarSectionPosition.getBusbarIndex(), nextSectionIndex);
            busbarSection.getTerminal().traverse(traverser);
            BusbarSection neighbourBusbarSection = traverser.getFoundBusbarSection();
            if (neighbourBusbarSection == null) {
                failToInsertBusbarSectionReport(reportNode, voltageLevel.getId(), busbarSection.getId());
                String message = String.format("Can't insert a busbar section in voltage level (%s) before or after busbar section (%s) : no neighbour busbar section found to do the operation",
                        voltageLevel.getId(), busbarSection.getId());
                logOrThrow(throwException, message);
                return;
            }
            List<Switch> switchesEncountered = traverser.getSwitchesEncountered();

            // Remove the switches encountered
            switchesEncountered.forEach(s -> voltageLevel.getNodeBreakerView().removeSwitch(s.getId()));

            // Create a new busbar section
            BusbarSection newBusbarSection = createBusbarSection(voltageLevel, namingStrategy, busbarSectionPosition);

            // Create new switches between busbarSection and newBusbarSection
            createSwitchesBetweenBusbarSections(voltageLevel, busbarSection, newBusbarSection, namingStrategy, switchKind1, switchFictitious1);

            // Create new switches between newBusbarSection and neighbourBusbarSection
            createSwitchesBetweenBusbarSections(voltageLevel, newBusbarSection, neighbourBusbarSection, namingStrategy, switchKind2, switchFictitious2);
        }
    }

    private int findNextSectionIndex(VoltageLevel vl, BusbarSectionPosition referenceBusbarSectionPosition) {
        int nextSectionIndex = isCreateTheBusbarSectionsAfterTheReferenceBusbarSection()
            ? getMinimalPositionAfter(vl, referenceBusbarSectionPosition.getSectionIndex())
            : getMaximalPositionBefore(vl, referenceBusbarSectionPosition.getSectionIndex());

        // If no position is available before or after referenceBusbarSectionPosition.sectionIndex,
        // give space by incrementing the section index of all busbar sections after referenceBusbarSection.
        // Same when inserting before the first busbar section with index=1, which could lead to diagram issues
        boolean indexesShouldBeIncremented = nextSectionIndex == -1 ?
            !isCreateTheBusbarSectionsAfterTheReferenceBusbarSection() && referenceBusbarSectionPosition.getSectionIndex() == 1 :
            abs(referenceBusbarSectionPosition.getSectionIndex() - nextSectionIndex) == 1;
        if (indexesShouldBeIncremented) {
            incrementSectionIndexes(vl, referenceBusbarSectionPosition.getSectionIndex());
            if (isCreateTheBusbarSectionsAfterTheReferenceBusbarSection()) {
                nextSectionIndex += 1;
            }
        }
        return nextSectionIndex;
    }

    private static class BusbarSectionFinderTraverser implements Terminal.TopologyTraverser {
        private final String startingBusBarSectionId;
        private final int busbarIndex;
        private final int sectionIndex;
        private BusbarSection foundBusbarSection;
        private final List<Switch> switchesEncountered = new ArrayList<>();

        public BusbarSectionFinderTraverser(String startingBusBarSectionId, int busbarIndex, int sectionIndex) {
            this.startingBusBarSectionId = startingBusBarSectionId;
            this.busbarIndex = busbarIndex;
            this.sectionIndex = sectionIndex;
        }

        @Override
        public TraverseResult traverse(Terminal terminal, boolean connected) {
            if (terminal.getConnectable().getType() == IdentifiableType.BUSBAR_SECTION) {
                BusbarSection busbarSection = (BusbarSection) terminal.getConnectable();
                if (busbarSection.getId().equals(startingBusBarSectionId)) {
                    return TraverseResult.CONTINUE;
                }
                BusbarSectionPosition busbarSectionPosition = busbarSection.getExtension(BusbarSectionPosition.class);
                if (busbarSectionPosition != null) {
                    if (busbarSectionPosition.getBusbarIndex() == busbarIndex &&
                        busbarSectionPosition.getSectionIndex() == sectionIndex) {
                        // We found the desired busbar section
                        foundBusbarSection = busbarSection;
                        return TraverseResult.TERMINATE_TRAVERSER;
                    } else {
                        switchesEncountered.clear();
                        return TraverseResult.TERMINATE_PATH;
                    }
                } else {
                    switchesEncountered.clear();
                    return TraverseResult.TERMINATE_PATH;
                }
            } else {
                switchesEncountered.clear();
                return TraverseResult.TERMINATE_PATH;
            }
        }

        @Override
        public TraverseResult traverse(Switch aSwitch) {
            switchesEncountered.add(aSwitch);
            return TraverseResult.CONTINUE;
        }

        public BusbarSection getFoundBusbarSection() {
            return foundBusbarSection;
        }

        public List<Switch> getSwitchesEncountered() {
            return switchesEncountered;
        }
    }

    @Override
    public NetworkModificationImpact hasImpactOnNetwork(Network network) {
        impact = DEFAULT_IMPACT;
        BusbarSection busbarSection = network.getBusbarSection(getReferenceBusbarSectionId());
        if (!checkVoltageLevel(busbarSection)) {
            impact = NetworkModificationImpact.CANNOT_BE_APPLIED;
        }
        return impact;
    }

    private BusbarSection createBusbarSection(VoltageLevel vl,
                                              NamingStrategy namingStrategy,
                                              BusbarSectionPosition busbarSectionPosition) {
        int busbarSectionNode = vl.getNodeBreakerView().getMaximumNodeIndex() + 1;
        int sectionNum = isCreateTheBusbarSectionsAfterTheReferenceBusbarSection() ? busbarSectionPosition.getSectionIndex() + 1 : busbarSectionPosition.getSectionIndex() - 1;
        int busbarNum = busbarSectionPosition.getBusbarIndex();
        BusbarSection busbarSection = vl.getNodeBreakerView()
            .newBusbarSection()
            .setId(namingStrategy.getBusbarId(busbarSectionPrefixId, busbarNum, sectionNum))
            .setName(Integer.toString(busbarSectionNode))
            .setNode(busbarSectionNode)
            .add();
        busbarSection.newExtension(BusbarSectionPositionAdder.class)
            .withBusbarIndex(busbarNum)
            .withSectionIndex(sectionNum)
            .add();
        return busbarSection;
    }

    private int getMinimalPositionAfter(VoltageLevel vl, int referenceSectionIndex) {
        return vl.getNodeBreakerView().getBusbarSectionStream()
            .filter(busbarSection -> busbarSection.getExtension(BusbarSectionPosition.class).getSectionIndex() > referenceSectionIndex)
            .min(Comparator.comparing(busbarSection -> busbarSection.getExtension(BusbarSectionPosition.class).getSectionIndex()))
            .map(busbarSection -> busbarSection.getExtension(BusbarSectionPosition.class).getSectionIndex())
            .orElse(-1);
    }

    private int getMaximalPositionBefore(VoltageLevel vl, int referenceSectionIndex) {
        return vl.getNodeBreakerView().getBusbarSectionStream()
            .filter(busbarSection -> busbarSection.getExtension(BusbarSectionPosition.class).getSectionIndex() < referenceSectionIndex)
            .max(Comparator.comparing(busbarSection -> busbarSection.getExtension(BusbarSectionPosition.class).getSectionIndex()))
            .map(busbarSection -> busbarSection.getExtension(BusbarSectionPosition.class).getSectionIndex())
            .orElse(-1);
    }

    private void incrementSectionIndexes(VoltageLevel vl, int referenceSectionIndex) {
        vl.getNodeBreakerView().getBusbarSectionStream().forEach(busbarSection -> {
            int sIndex = busbarSection.getExtension(BusbarSectionPosition.class).getSectionIndex();
            int sIndexToCompare = isCreateTheBusbarSectionsAfterTheReferenceBusbarSection() ? referenceSectionIndex + 1 : referenceSectionIndex;
            if (sIndex >= sIndexToCompare) {
                busbarSection.getExtension(BusbarSectionPosition.class).setSectionIndex(sIndex + 1);
            }
        });
    }

    private void createSwitchesBetweenBusbarSections(VoltageLevel vl, BusbarSection busbarSection1, BusbarSection busbarSection2,
                                                     NamingStrategy namingStrategy,
                                                     SwitchKind switchKind,
                                                     boolean fictitious) {
        // Create new switches between busbarSection1 and busbarSection2
        int busbarSection1Node = busbarSection1.getTerminal().getNodeBreakerView().getNode();
        int busbarSection2Node = busbarSection2.getTerminal().getNodeBreakerView().getNode();
        int firstDisconnectorNode2 = switchKind == SwitchKind.BREAKER ? vl.getNodeBreakerView().getMaximumNodeIndex() + 1 : busbarSection2Node;
        int busbarNum = busbarSection1.getExtension(BusbarSectionPosition.class).getBusbarIndex();
        int busbarSection1Num = busbarSection1.getExtension(BusbarSectionPosition.class).getSectionIndex();
        int busbarSection2Num = busbarSection2.getExtension(BusbarSectionPosition.class).getSectionIndex();

        // Prefix
        String chunkingPrefixId = namingStrategy.getChunkPrefix(switchPrefixId, List.of(switchKind), busbarNum, busbarSection1Num, busbarSection2Num);

        // Add the first disconnector
        createNBDisconnector(busbarSection1Node, firstDisconnectorNode2, namingStrategy.getDisconnectorBetweenChunksId(busbarSection1, chunkingPrefixId, busbarSection1Node, firstDisconnectorNode2), vl.getNodeBreakerView(), false, fictitious);

        if (switchKind == SwitchKind.BREAKER) {
            // Add a breaker
            int breakerNode2 = vl.getNodeBreakerView().getMaximumNodeIndex() + 1;
            createNBBreaker(firstDisconnectorNode2, breakerNode2, namingStrategy.getBreakerId(chunkingPrefixId, firstDisconnectorNode2, breakerNode2), vl.getNodeBreakerView(), false, fictitious);

            // Add a second disconnector
            createNBDisconnector(breakerNode2, busbarSection2Node, namingStrategy.getDisconnectorBetweenChunksId(busbarSection2, chunkingPrefixId, breakerNode2, busbarSection2Node), vl.getNodeBreakerView(), false, fictitious);
        }
    }
}