VerticalBusSet.java

/**
 * Copyright (c) 2019, 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/.
 */
package com.powsybl.sld.layout.position;

import com.powsybl.sld.model.blocks.*;
import com.powsybl.sld.model.cells.ArchCell;
import com.powsybl.sld.model.cells.Cell;
import com.powsybl.sld.model.cells.ExternCell;
import com.powsybl.sld.model.cells.InternCell;
import com.powsybl.sld.model.coordinate.Direction;
import com.powsybl.sld.model.coordinate.Side;
import com.powsybl.sld.model.graphs.NodeFactory;
import com.powsybl.sld.model.graphs.VoltageLevelGraph;
import com.powsybl.sld.model.nodes.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.stream.Collectors;

/**
 * A VerticalBusSet contains the set of BusNodes that shall be vertically presented, and the cells that have a pattern of
 * connection included in the busNodeSet. It is embedded into a BSCluster.
 *
 * @author Benoit Jeanson {@literal <benoit.jeanson at rte-france.com>}
 */
public final class VerticalBusSet {

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

    private final Set<BusNode> busNodeSet;
    private final Set<ExternCell> externCells = new LinkedHashSet<>();
    private final List<ArchCell> archCells = new ArrayList<>();
    private final Set<InternCellSide> internCellSides = new LinkedHashSet<>();

    private VerticalBusSet(Map<BusNode, Integer> busToNb, List<BusNode> busNodes) {
        busNodeSet = new TreeSet<>(Comparator.comparingInt(busToNb::get));
        busNodeSet.addAll(busNodes);
    }

    private VerticalBusSet(Map<BusNode, Integer> busToNb, ExternCell cell) {
        this(busToNb, cell.getBusNodes());
        externCells.add(cell);
    }

    private VerticalBusSet(Map<BusNode, Integer> nodeToNb, ArchCell cell) {
        this(nodeToNb, cell.getBusNodes());
        archCells.add(cell);
    }

    private VerticalBusSet(Map<BusNode, Integer> busToNb, InternCell internCell, Side side) {
        this(busToNb, internCell.getSideBusNodes(side));
        addInternCell(internCell, side);
    }

    private VerticalBusSet(Map<BusNode, Integer> busToNb, BusNode busNode) {
        this(busToNb, Collections.singletonList(busNode));
    }

    void addInternCell(InternCell internCell, Side side) {
        internCellSides.add(new InternCellSide(internCell, side));
    }

    private boolean contains(Collection<BusNode> busNodeCollection) {
        return busNodeSet.containsAll(busNodeCollection);
    }

    private boolean contains(VerticalBusSet vbs) {
        return contains(vbs.getBusNodeSet());
    }

    private void absorbs(VerticalBusSet vbsToAbsorb) {
        busNodeSet.addAll(vbsToAbsorb.busNodeSet);
        externCells.addAll(vbsToAbsorb.externCells);
        archCells.addAll(vbsToAbsorb.archCells);
        internCellSides.addAll(vbsToAbsorb.internCellSides);
    }

    List<InternCell> getInternCellsFromShape(InternCell.Shape shape) {
        return internCellSides.stream().map(InternCellSide::getCell)
                .filter(cell -> cell.checkIsShape(shape))
                .distinct()
                .collect(Collectors.toList());
    }

    public Set<BusNode> getBusNodeSet() {
        return busNodeSet;
    }

    public Set<ExternCell> getExternCells() {
        return externCells;
    }

    public List<ArchCell> getArchCells() {
        return archCells;
    }

    Set<InternCellSide> getInternCellSides() {
        return internCellSides;
    }

    static List<VerticalBusSet> createVerticalBusSets(VoltageLevelGraph graph, Map<BusNode, Integer> busToNb) {

        graph.getExternCellStream().toList().forEach(cell -> fixMultisectionExternCells(cell, graph));
        List<ExternCell> externCells = graph.getExternCellStream()
                .sorted(Comparator.comparing(Cell::getFullId)) // if order is not yet defined & avoid randomness
                .collect(Collectors.toList());

        List<VerticalBusSet> verticalBusSets = new ArrayList<>();

        externCells.forEach(cell -> pushVbs(verticalBusSets, new VerticalBusSet(busToNb, cell)));

        graph.getArchCellStream().forEach(cell -> pushVbs(verticalBusSets, new VerticalBusSet(busToNb, cell)));

        graph.getInternCellStream()
                .filter(cell -> cell.checkIsNotShape(InternCell.Shape.MAYBE_ONE_LEG, InternCell.Shape.UNHANDLED_PATTERN))
                .sorted(Comparator.comparing(cell -> -((InternCell) cell).getBusNodes().size())         // bigger first to identify encompassed InternCell at the end with the smaller one
                        .thenComparing(cell -> ((InternCell) cell).getFullId()))                        // avoid randomness
                .forEachOrdered(cell -> pushInternCell(verticalBusSets, busToNb, cell));

        graph.getInternCellStream()
                .filter(cell -> cell.checkIsShape(InternCell.Shape.MAYBE_ONE_LEG))
                .sorted(Comparator.comparing(Cell::getFullId)) // if order is not yet defined & avoid randomness
                .forEachOrdered(cell -> pushInternCell(verticalBusSets, busToNb, cell));

        // find orphan busNodes and build their VBS
        List<BusNode> allBusNodes = new ArrayList<>(graph.getNodeBuses());
        allBusNodes.removeAll(verticalBusSets.stream().
                flatMap(legBusSet -> legBusSet.getBusNodeSet().stream()).collect(Collectors.toList()));
        allBusNodes.stream()
                .sorted(Comparator.comparing(Node::getId))              //avoid randomness
                .forEach(busNode -> verticalBusSets.add(new VerticalBusSet(busToNb, busNode)));
        return verticalBusSets;
    }

    private static void fixMultisectionExternCells(ExternCell externCell, VoltageLevelGraph graph) {
        List<LegPrimaryBlock> legPrimaryBlocks = externCell.getLegPrimaryBlocks();
        List<BusNode> busNodes = legPrimaryBlocks.stream().map(LegPrimaryBlock::getBusNode).distinct().toList();

        List<Integer> busbarIndices = busNodes.stream().map(BusNode::getBusbarIndex).distinct().toList();
        // Detecting incoherent bus positions set from the user (from extension or directly when creating the
        // busNode, WHEN PositionFromExtension is used instead of PositionByClustering).
        // We rule out PositionByClustering where all busbar indices are set to zero and still are at this point
        // (note that zero means no value in the code so far) by dismissing the detection if there is a zero busbar
        // index. There cannot be any zero busbar index with PositionFromExtension, they are replaced in the call
        // PositionFromExtension::setMissingPositionIndices.
        if (busbarIndices.size() < busNodes.size() && busbarIndices.get(0) != 0
                && externCell.getRootBlock() instanceof SerialBlock rootSerialBlock) {
            List<LegPrimaryBlock> sortedLegs = legPrimaryBlocks.stream().sorted(Comparator.comparing(lpb -> lpb.getBusNode().getBusbarIndex())).toList();
            LegPrimaryBlock legKept = sortedLegs.get(0);
            int order = externCell.getFeederNodes().stream().map(FeederNode::getOrder).flatMap(Optional::stream).findFirst().orElse(-1);
            var direction = externCell.getFeederNodes().stream().map(FeederNode::getDirection).findFirst().orElse(Direction.UNDEFINED);
            if (rootSerialBlock.getLowerBlock() instanceof LegParallelBlock) {
                fixLegParallelExternCell(externCell, graph, sortedLegs, legKept, order, direction);
            } else if (rootSerialBlock.getLowerBlock() instanceof BodyParallelBlock legBodyParallelBlock) {
                fixLegBodyParallelExternCell(externCell, graph, legBodyParallelBlock, sortedLegs, legKept, order, direction);
            } else {
                externCell.setRootBlock(new UndefinedBlock(List.of(externCell.getRootBlock())));
                LOGGER.error("ExternCell pattern not handled");
            }
        }
    }

    private static void fixLegBodyParallelExternCell(ExternCell externCell, VoltageLevelGraph graph,
                                                     BodyParallelBlock legBodyParallelBlock, List<LegPrimaryBlock> sortedLegs,
                                                     LegPrimaryBlock legKept, int orderMin, Direction direction) {
        Map<LegPrimaryBlock, Block> legToSubBlock = new HashMap<>();
        Map<Block, List<LegPrimaryBlock>> subBlockToLegs = new HashMap<>();
        for (LegPrimaryBlock legPrimaryBlock : sortedLegs) {
            Block ancestor = legPrimaryBlock;
            while (ancestor.getParentBlock() != legBodyParallelBlock) {
                ancestor = ancestor.getParentBlock();
            }
            legToSubBlock.put(legPrimaryBlock, ancestor);
            subBlockToLegs.computeIfAbsent(ancestor, b -> new ArrayList<>()).add(legPrimaryBlock);
        }

        Block subBlockKept = legToSubBlock.get(legKept);
        List<Block> subBlocksRemoved = legBodyParallelBlock.getSubBlocks().stream().filter(b -> b != subBlockKept).toList();

        externCell.removeOtherLegs(subBlockKept, legKept);

        Node fork = subBlockKept.getEndingNode();
        int order = orderMin;

        for (int i = 0; i < subBlocksRemoved.size(); i++) {

            Block sBlock = subBlocksRemoved.get(i);
            ConnectivityNode archNode = NodeFactory.createConnectivityNode(graph, "Arch" + i + "_" + fork.getId());
            substituteForkNode(graph, sBlock, archNode, fork);

            if (sBlock instanceof LegPrimaryBlock lpb) {
                // If one of the detached subBlocks is a LegPrimaryBlock, we need to replace it by a SerialBlock so that
                // it gets properly displayed. On extra node needs to be added for the stack line
                ConnectivityNode hookNode = graph.insertConnectivityNode(archNode, fork, archNode.getId() + "_hook");
                BodyPrimaryBlock body = BodyPrimaryBlock.createBodyPrimaryBlockInBusCell(List.of(hookNode, archNode));
                sBlock = new SerialBlock(List.of(lpb, body));
                subBlockToLegs.put(sBlock, subBlockToLegs.get(lpb));
            }

            ArchCell archCell = ArchCell.create(graph, sBlock.getNodeStream().toList(), subBlockKept);
            archCell.setOrder(++order);
            archCell.setDirection(direction);

            archCell.blocksSetting(sBlock, subBlockToLegs.get(sBlock), List.of());
        }
    }

    private static void substituteForkNode(VoltageLevelGraph graph, Block block, ConnectivityNode substitute, Node fork) {
        block.replaceEndingNode(substitute);
        List<Edge> edgesToTransfer = new ArrayList<>(fork.getAdjacentEdges()).stream()
                .filter(edge -> block.contains(edge.getOppositeNode(fork)))
                .toList();
        graph.transferEdges(fork, substitute, edgesToTransfer);
        graph.addEdge(fork, substitute);
    }

    private static void fixLegParallelExternCell(ExternCell externCell, VoltageLevelGraph graph,
                                                 List<LegPrimaryBlock> sortedLegs, LegPrimaryBlock legKept,
                                                 int orderMin, Direction direction) {
        List<LegPrimaryBlock> legsRemoved = sortedLegs.subList(1, sortedLegs.size());

        externCell.removeOtherLegs(legKept);

        int order = orderMin;
        for (LegPrimaryBlock legPrimaryBlock : legsRemoved) {
            List<Node> legNodes = legPrimaryBlock.getNodes();
            Node fork = legNodes.get(legNodes.size() - 1);
            ConnectivityNode archNode = graph.insertConnectivityNode(legNodes.get(legNodes.size() - 2), fork, "Arch_" + legNodes.get(1).getId());

            List<Node> fakeCellNodes = new ArrayList<>(legNodes.subList(0, legNodes.size() - 1));
            fakeCellNodes.add(archNode);
            ArchCell archCell = ArchCell.create(graph, fakeCellNodes, legKept);
            archCell.setOrder(++order);
            archCell.setDirection(direction);

            archCell.blocksSetting(new LegPrimaryBlock(fakeCellNodes), List.of(legPrimaryBlock), List.of());
        }
    }

    public static void pushVbs(List<VerticalBusSet> verticalBusSets, VerticalBusSet verticalBusSet) {
        for (VerticalBusSet vbs : verticalBusSets) {
            if (vbs.contains(verticalBusSet)) {
                vbs.absorbs(verticalBusSet);
                return;
            }
        }
        List<VerticalBusSet> absorbedVbs = new ArrayList<>();
        for (VerticalBusSet vbs : verticalBusSets) {
            if (verticalBusSet.contains(vbs)) {
                absorbedVbs.add(vbs);
                verticalBusSet.absorbs(vbs);
            }
        }
        verticalBusSets.removeAll(absorbedVbs);
        verticalBusSets.add(verticalBusSet);
    }

    private static void pushInternCell(List<VerticalBusSet> verticalBusSets, Map<BusNode, Integer> nodeToNb, InternCell internCell) {
        List<VerticalBusSet> attachedLegBusSets = new ArrayList<>();
        for (VerticalBusSet vbs : verticalBusSets) {
            boolean attachedToLbs = internCell.getBusNodes().stream().anyMatch(vbs.busNodeSet::contains);
            if (attachedToLbs) {
                attachedLegBusSets.add(vbs);
                if (vbs.busNodeSet.containsAll(internCell.getBusNodes())) {
                    vbs.addInternCell(internCell, Side.UNDEFINED);
                    if (internCell.getShape() == InternCell.Shape.MAYBE_ONE_LEG) {
                        internCell.setShape(InternCell.Shape.ONE_LEG);
                    } else {
                        internCell.setShape(InternCell.Shape.VERTICAL);
                    }
                    return;
                }
            }
        }

        // We didn't find any legBusSet which absorbs the intern cell
        if (internCell.getShape() == InternCell.Shape.MAYBE_ONE_LEG) {
            replaceByMultilegOrSetOneLeg(internCell, attachedLegBusSets);
        }
        if (internCell.getShape() != InternCell.Shape.ONE_LEG) {
            pushVbs(verticalBusSets, new VerticalBusSet(nodeToNb, internCell, Side.LEFT));
            pushVbs(verticalBusSets, new VerticalBusSet(nodeToNb, internCell, Side.RIGHT));
        } else {
            pushVbs(verticalBusSets, new VerticalBusSet(nodeToNb, internCell, Side.UNDEFINED));
        }
    }

    private static void replaceByMultilegOrSetOneLeg(InternCell internCell, List<VerticalBusSet> attachedVerticalBusSets) {
        // We consider that a one leg intern cell should not force the corresponding busNodes to be in the same LegBusSet
        // (forcing them to be parallel), hence we try to replace that one leg by a multileg
        // The goal here is to split the corresponding LegParallelBlock into 2 stacked parts
        LegParallelBlock oneLeg = (LegParallelBlock) internCell.getSideToLeg(Side.UNDEFINED);
        List<LegPrimaryBlock> subBlocks = oneLeg.getSubBlocks();
        if (subBlocks.size() == 2) {
            internCell.replaceOneLegByMultiLeg(subBlocks.get(0), subBlocks.get(1));
        } else {
            // Each subBlock has one BusNode which might be in the existing LegBusSets.
            // The LegBusSets which contain at least one BusNode from current internCell are given as attachedVerticalBusSets parameter.
            // We first try to split the subBlocks based on the LegBusSets
            Collection<List<LegPrimaryBlock>> groupSubBlocksLbs = subBlocks.stream().collect(Collectors.groupingBy(
                    sb -> attachedVerticalBusSets.stream().filter(lbs -> lbs.busNodeSet.contains(sb.getBusNode())).findFirst())).values();
            if (groupSubBlocksLbs.size() == 2) {
                replaceByMultiLeg(internCell, groupSubBlocksLbs);
            } else {
                // We then try to split the subBlocks using the sectionIndex of their busNode
                Collection<List<LegPrimaryBlock>> groupSubBlocksSi = subBlocks.stream().collect(Collectors.groupingBy(
                        sb -> sb.getBusNode().getSectionIndex())).values();
                if (groupSubBlocksSi.size() == 2) {
                    replaceByMultiLeg(internCell, groupSubBlocksSi);
                } else {
                    // Failed to replace it by a multileg -> marks it one leg
                    internCell.setOneLeg();
                }
            }
        }
    }

    private static void replaceByMultiLeg(InternCell internCell, Collection<List<LegPrimaryBlock>> groupSubBlocks) {
        var it = groupSubBlocks.iterator();
        List<LegPrimaryBlock> left = it.next();
        List<LegPrimaryBlock> right = it.next();
        internCell.replaceOneLegByMultiLeg(
                left.size() == 1 ? left.get(0) : new LegParallelBlock(left, true),
                right.size() == 1 ? right.get(0) : new LegParallelBlock(right, true));
    }
}