PositionByClustering.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.clustering;

import com.powsybl.sld.layout.position.*;
import com.powsybl.sld.model.cells.BusCell;
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.cells.ShuntCell;
import com.powsybl.sld.model.coordinate.Direction;
import com.powsybl.sld.model.coordinate.Side;
import com.powsybl.sld.model.graphs.VoltageLevelGraph;
import com.powsybl.sld.model.nodes.BusNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

import static com.powsybl.sld.model.coordinate.Direction.BOTTOM;
import static com.powsybl.sld.model.coordinate.Direction.TOP;
import static com.powsybl.sld.model.coordinate.Side.LEFT;
import static com.powsybl.sld.model.coordinate.Side.RIGHT;

/**
 * PositionByClustering finds adequate positions for the busBars with the following principles:
 * All the connections to the BusBar of the leg of an ExternCell, or each leg of an InternCell shall be stackable
 * (ie could be displayed with disconnectors to busbar vertically aligned).
 * This implies that the busBars of a leg shall be spread in different vertical structuralPosition,
 * (and in many case having the same horizontal structuralPosition).
 * The first step consists in building LegBusSets that contains busBars that shall be vertically aligned (considering
 * they have legs of cell that impose it).
 * Then BSClusters are initiated by building one BSCluster per LegBusSet.
 * The BSClusters are then merged 2 by 2 starting by BSClusters that have the strongest Link.
 * We differentiate the side of a BSCluster using BSClusterSide is considered. This is similar
 * to what si done with LegBusSet but the assessment of the strength of the link considers both sides of the
 * cluster.
 * Therefore, with cluster A and B, there are 4 BSClusterSide A-Right A-Left B-Right and B-Left. The links that
 * are considered are (A-Right, B-Left), (A-Right, B-Right), (B-Right, B-Left), (B-Right, B-Right). When merging,
 * alignment is required (meaning that clusters could be reversed to ensure the connection sides between the
 * 2 clusters are respected : 1st cluster-Right is merged with 2nd cluster-left).
 * </li>
 * </ul>
 *
 * @author Benoit Jeanson {@literal <benoit.jeanson at rte-france.com>}
 */

// WE ASSUME THAT IT IS POSSIBLE TO STACK ALL CELLS AND BE ABLE TO ORGANIZE THE VOLTAGELEVEL ACCORDINGLY

public class PositionByClustering extends AbstractPositionFinder {

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

    @Override
    public Map<BusNode, Integer> indexBusPosition(List<BusNode> busNodes, List<BusCell> busCells) {
        Map<BusNode, Integer> busToNb = new LinkedHashMap<>();
        int i = 1;
        for (BusNode n : busNodes.stream()
                .sorted(Comparator.comparing(BusNode::getId))
                .collect(Collectors.toList())) {
            busToNb.put(n, i);
            n.setBusBarIndexSectionIndex(0, 0);
            i++;
        }
        return busToNb;
    }

    public BSCluster organizeBusSets(VoltageLevelGraph graph, List<VerticalBusSet> verticalBusSets) {
        List<BSCluster> bsClusters = BSCluster.createBSClusters(verticalBusSets);
        Links links = new Links(bsClusters);
        while (!links.isEmpty()) {
            links.mergeLink(links.getStrongestLink());
        }
        BSCluster bsCluster = links.getFinalBsCluster();

        tetrisHorizontalBusLists(bsCluster);
        bsCluster.getHorizontalBusLists().forEach(hl -> LOGGER.info(hl.toString()));
        bsCluster.establishBusNodePosition();
        establishFeederPositions(bsCluster);

        return bsCluster;
    }

    private void tetrisHorizontalBusLists(BSCluster bsCluster) {
        List<HorizontalBusList> horizontalBusLists = bsCluster.getHorizontalBusLists();
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("{}", horizontalBusLists);
        }
        List<HorizontalBusList> sortedHbl = horizontalBusLists.stream()
                .sorted(Comparator.comparingInt(HorizontalBusList::getStartingIndex)
                        .thenComparing(hl -> hl.getBusNodes().get(0).getId())) // cope with randomness
                .collect(Collectors.toList());
        int clusterLength = sortedHbl.stream()
                .mapToInt(HorizontalBusList::getEndingIndex)
                .max().orElse(0);
        int i = 0;
        while (i < sortedHbl.size()) {
            HorizontalBusList lane = sortedHbl.get(i);
            int actualMaxIndex = lane.getEndingIndex();
            while (actualMaxIndex < clusterLength) {
                int finalActualMax = actualMaxIndex;
                HorizontalBusList hblToAdd = sortedHbl.stream()
                        .filter(l -> l.getStartingIndex() >= finalActualMax)
                        .findFirst().orElse(null);
                if (hblToAdd != null) {
                    lane.merge(hblToAdd);
                    sortedHbl.remove(hblToAdd);
                    horizontalBusLists.remove(hblToAdd);
                    actualMaxIndex = lane.getEndingIndex();
                } else {
                    i++;
                    break;
                }
            }
            i++;
        }
    }

    private void establishFeederPositions(BSCluster bsCluster) {
        int cellOrder = 0;
        for (VerticalBusSet vbs : bsCluster.getVerticalBusSets()) {
            for (ExternCell cell : vbs.getExternCells()) {
                cell.setOrder(cellOrder++);
            }
        }
    }

    @Override
    public void organizeDirections(VoltageLevelGraph graph, List<Subsection> subsections) {

        int cellPos = 0;
        int cellShuntShift = 0;

        for (Subsection ss : subsections) {
            for (ExternCell externCell : ss.getExternCells()) {
                if (externCell.getShuntCells().stream().anyMatch(sc -> sc.getSideCell(RIGHT) == externCell)) {
                    cellShuntShift++; // an ExternCell on the right of a Shunt does not take its turn for flipping direction
                } else {
                    Direction direction = (cellPos + cellShuntShift) % 2 == 0 ? TOP : BOTTOM;
                    externCell.setDirection(direction);
                }
                cellPos++;
            }
        }

        Set<ShuntCell> visitedShuntCells = new HashSet<>();
        graph.getShuntCellStream().forEach(shuntCell -> {
            // starting from each shunt, find the shunt-connected set of extern cells to set the same direction for all of them
            List<ExternCell> externCells = new ArrayList<>();
            shuntTraversal(shuntCell, visitedShuntCells, externCells);
            externCells.stream().map(Cell::getDirection).filter(d -> d != Direction.UNDEFINED).findFirst()
                    .ifPresent(d -> externCells.forEach(externCell -> externCell.setDirection(d)));
        });
    }

    private void shuntTraversal(ShuntCell shuntCell, Set<ShuntCell> visitedShuntCells, List<ExternCell> externCells) {
        if (visitedShuntCells.contains(shuntCell)) {
            return;
        }

        visitedShuntCells.add(shuntCell);

        ExternCell leftCell = shuntCell.getSideCell(LEFT);
        ExternCell rightCell = shuntCell.getSideCell(RIGHT);
        externCells.add(leftCell);
        externCells.add(rightCell);

        Stream.of(leftCell, rightCell)
                .map(ExternCell::getShuntCells)
                .flatMap(Collection::stream)
                .filter(sc -> sc != shuntCell)
                .forEach(sc -> shuntTraversal(sc, visitedShuntCells, externCells));
    }

    public static void mergeHorizontalBusLists(BSCluster leftCluster, BSCluster rightCluster) {
        List<HorizontalBusList> availableHblToMerge = new ArrayList<>(leftCluster.getHorizontalBusLists());
        mergeHblWithCommonBusNode(leftCluster, rightCluster, availableHblToMerge);
        mergeHblWithFlatCell(leftCluster, rightCluster, availableHblToMerge);
        mergeHblWithNoLink(leftCluster, rightCluster);
    }

    private static void mergeHblWithCommonBusNode(BSCluster leftCluster, BSCluster rightCluster, List<HorizontalBusList> availableHblToMerge) {
        List<BusNode> commonNodes = new ArrayList<>(leftCluster.hblSideBuses(Side.RIGHT));
        commonNodes.retainAll(rightCluster.hblSideBuses(Side.LEFT));
        commonNodes.forEach(busNode ->
                finalizeHblBuilding(leftCluster, rightCluster, busNode, busNode, availableHblToMerge));
    }

    private static void mergeHblWithFlatCell(BSCluster leftCluster, BSCluster rightCluster,
                                        List<HorizontalBusList> availableHblToMerge) {
        List<BusNode> myAvailableRightBuses = BSCluster.hblSideBuses(Side.RIGHT, availableHblToMerge);
        List<InternCell> myConcernedFlatCells = leftCluster.getSideCandidateFlatCell(Side.RIGHT)
                .stream().filter(internCell -> {
                    List<BusNode> nodes = internCell.getBusNodes();
                    nodes.retainAll(myAvailableRightBuses);
                    return !myAvailableRightBuses.isEmpty();
                }).sorted(Comparator.comparing(InternCell::getFullId)) //avoid randomness
                .collect(Collectors.toList());
        List<InternCell> otherConcernedFlatCells = rightCluster.getSideCandidateFlatCell(Side.LEFT);
        myConcernedFlatCells.retainAll(otherConcernedFlatCells);

        myConcernedFlatCells.stream()
                .sorted(Comparator
                        .<InternCell>comparingInt(cell -> Link.flatCellDistanceToEdges(cell,
                                new BSClusterSide(leftCluster, Side.RIGHT),
                                new BSClusterSide(rightCluster, Side.LEFT)))
                        .thenComparing(InternCell::getFullId))
                .forEachOrdered(internCell -> {
                    Optional<BusNode> myNode = internCellNodeInHblSide(leftCluster, Side.RIGHT, internCell);
                    Optional<BusNode> otherNode = internCellNodeInHblSide(rightCluster, Side.LEFT, internCell);
                    if (myNode.isPresent() && otherNode.isPresent()) {
                        internCell.setFlat();
                        finalizeHblBuilding(leftCluster, rightCluster, myNode.get(), otherNode.get(), availableHblToMerge);
                    }
                });
    }

    private static Optional<BusNode> internCellNodeInHblSide(BSCluster bsCluster, Side side, InternCell cell) {
        List<BusNode> hblBuses = bsCluster.hblSideBuses(side);
        hblBuses.retainAll(cell.getBusNodes());
        return hblBuses.stream().findFirst();
    }

    private static void finalizeHblBuilding(BSCluster leftCluster, BSCluster rightCluster,
                                      BusNode myNode, BusNode otherBus, List<HorizontalBusList> availableHblToMerge) {
        Optional<HorizontalBusList> myHbl = leftCluster.getHblFromSideBus(myNode, Side.RIGHT);
        Optional<HorizontalBusList> otherHbl = rightCluster.getHblFromSideBus(otherBus, Side.LEFT);
        if (otherHbl.isPresent() && myHbl.isPresent()) {
            myHbl.get().merge(otherHbl.get());
            rightCluster.removeHbl(otherHbl.get());
            availableHblToMerge.remove(myHbl.get());
        }
    }
}