CellBlockDecomposer.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;

import com.powsybl.commons.PowsyblException;
import com.powsybl.sld.model.blocks.*;
import com.powsybl.sld.model.cells.*;
import com.powsybl.sld.model.graphs.VoltageLevelGraph;
import com.powsybl.sld.model.nodes.BusNode;
import com.powsybl.sld.model.nodes.FeederNode;
import com.powsybl.sld.model.nodes.Node;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * Contain function to dispose components of cells based on Hierarchical Layout
 *
 * @author Benoit Jeanson {@literal <benoit.jeanson at rte-france.com>}
 * @author Nicolas Duchene
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
final class CellBlockDecomposer {

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

    private CellBlockDecomposer() {
    }

    static void determineShuntCellBlocks(ShuntCell shuntCell) {
        shuntCell.setRootBlock(BodyPrimaryBlock.createBodyPrimaryBlockForShuntCell(shuntCell.getNodes()));
    }

    /**
     * Search BlockPrimary and build Block hierarchy by merging blocks together; also
     * list blocks connected to busbar
     */
    static void determineComplexCell(VoltageLevelGraph vlGraph, BusCell busCell, boolean exceptionIfPatternNotHandled) {
        List<Block> blocks = createPrimaryBlocks(vlGraph, busCell);
        mergeBlocks(vlGraph, busCell, blocks, exceptionIfPatternNotHandled);
    }

    private static List<Block> createPrimaryBlocks(VoltageLevelGraph voltageLevelGraph, BusCell busCell) {
        // Search all primary blocks
        List<Block> blocks = new ArrayList<>();
        Map<Node, Integer> nodeRemainingSlots = new HashMap<>();
        busCell.getNodes().stream()
                .filter(n -> n.getType() != Node.NodeType.BUS && n.getType() != Node.NodeType.FEEDER)
                .forEach(n -> nodeRemainingSlots.put(n, n.getCardinality(voltageLevelGraph)));
        elaborateLegPrimaryBlock(busCell, nodeRemainingSlots, blocks);
        elaborateFeederPrimaryBlock(busCell, nodeRemainingSlots, blocks);
        rElaborateBodyPrimaryBlocks(busCell, blocks.get(0).getEndingNode(), nodeRemainingSlots, blocks); // the first block is a LegPrimaryBlock, the endingNode is a good start

        return blocks;
    }

    private static void mergeBlocks(VoltageLevelGraph vlGraph, BusCell busCell, List<Block> blocks, boolean exceptionIfPatternNotHandled) {
        List<LegPrimaryBlock> legPrimaryBlocks = blocks.stream().filter(LegPrimaryBlock.class::isInstance).map(LegPrimaryBlock.class::cast).collect(Collectors.toList());
        List<FeederPrimaryBlock> feederPrimaryBlocks = blocks.stream().filter(FeederPrimaryBlock.class::isInstance).map(FeederPrimaryBlock.class::cast).collect(Collectors.toList());

        // Merge blocks to obtain a hierarchy of blocks
        while (blocks.size() != 1) {
            boolean merged = searchParallelMerge(blocks);
            merged |= searchSerialMerge(vlGraph, blocks);
            if (!merged) {
                if (exceptionIfPatternNotHandled) {
                    throw new PowsyblException("Blocks detection impossible for cell " + busCell);
                } else {
                    LOGGER.error("{} busCell, cannot merge any additional blocks, {} blocks remains", busCell.getType(), blocks.size());
                    Block undefinedBlock = new UndefinedBlock(new ArrayList<>(blocks));
                    blocks.clear();
                    blocks.add(undefinedBlock);
                    break;
                }
            }
        }
        busCell.blocksSetting(blocks.get(0), legPrimaryBlocks, feederPrimaryBlocks);
    }

    /**
     * Search possibility to merge two blocks into a chain layout.block and do the merging
     *
     * @param vlGraph VoltageLevelGraph
     * @param blocks list of blocks we can merge
     */
    private static boolean searchSerialMerge(VoltageLevelGraph vlGraph, List<Block> blocks) {
        int i = 0;
        boolean identifiedMerge = false;

        while (i < blocks.size() - 1) {
            List<Block> blockToRemove = new ArrayList<>();
            boolean chainIdentified = false;
            Block b1 = blocks.get(i);
            SerialBlock serialBlock = new SerialBlock(b1);
            for (int j = i + 1; j < blocks.size(); j++) {
                Block b2 = blocks.get(j);
                if (serialBlock.addSubBlock(vlGraph, b2)) {
                    chainIdentified = true;
                    blockToRemove.add(b2);
                }
            }
            if (chainIdentified) {
                blockToRemove.add(b1);
                identifiedMerge = true;
                blocks.removeAll(blockToRemove);
                blocks.add(i, serialBlock);
                i = -1; // start again from the beginning
            }
            i++;
        }
        return identifiedMerge;
    }

    /**
     * Search possibility to merge some blocks into a parallel layout.block and do the merging
     *
     * @param blocks list of blocks we can merge
     */
    private static boolean searchParallelMerge(List<Block> blocks) {
        List<List<Block>> blocksBundlesToMerge = new ArrayList<>();
        Node commonNode;
        int i = 0;
        while (i < blocks.size()) {
            List<Block> blocksBundle = new ArrayList<>();
            for (int j = i + 1; j < blocks.size(); j++) {
                commonNode = checkParallelCriteria(blocks.get(i), blocks.get(j));
                if (commonNode != null) {
                    blocksBundle.add(blocks.get(j));
                }
            }
            if (blocksBundle.isEmpty()) {
                i++;
            } else {
                blocksBundle.add(0, blocks.get(i));
                blocks.removeAll(blocksBundle);
                blocksBundlesToMerge.add(blocksBundle);
            }
        }
        for (List<Block> blocksBundle : blocksBundlesToMerge) {
            blocks.add(blocksBundle.stream().allMatch(LegPrimaryBlock.class::isInstance)
                    ? new LegParallelBlock(blocksBundle.stream().filter(LegPrimaryBlock.class::isInstance).map(LegPrimaryBlock.class::cast).toList(), true)
                    : new BodyParallelBlock(blocksBundle, false));
        }
        return !blocksBundlesToMerge.isEmpty();
    }

    /**
     * Compare two blocks to see if they are parallel
     *
     * @param block1 layout.block
     * @param block2 layout.block
     * @return true if the two blocks are similar : same start and end
     */
    private static Node checkParallelCriteria(Block block1, Block block2) {
        Node s1 = block1.getExtremityNode(Block.Extremity.START);
        Node e1 = block1.getExtremityNode(Block.Extremity.END);
        Node s2 = block2.getExtremityNode(Block.Extremity.START);
        Node e2 = block2.getExtremityNode(Block.Extremity.END);

        if (s1.checkNodeSimilarity(s2) && e1.checkNodeSimilarity(e2)
                || s1.checkNodeSimilarity(e2) && e1.checkNodeSimilarity(s2)) {
            if (s1.equals(s2) || s1.equals(e2)) {
                return s1;
            } else {
                return e1;
            }
        } else {
            return null;
        }
    }

    private static void addNodeInBlockNodes(Map<Node, Integer> nodeRemainingSlots, List<Node> nodes, Node node, int weight) {
        nodes.add(node);
        nodeRemainingSlots.computeIfPresent(node, (n, remainingSlots) -> remainingSlots - weight);
    }

    private static void elaborateLegPrimaryBlock(BusCell busCell, Map<Node, Integer> nodeRemainingSlots, List<Block> blocks) {
        for (BusNode busNode : busCell.getBusNodes()) {
            for (Node busConnection : busCell.getInternalAdjacentNodes(busNode)) {
                List<Node> legPrimaryBlockNodes = new ArrayList<>();
                addNodeInBlockNodes(nodeRemainingSlots, legPrimaryBlockNodes, busNode, 1);
                addNodeInBlockNodes(nodeRemainingSlots, legPrimaryBlockNodes, busConnection, 2);
                addNodeInBlockNodes(nodeRemainingSlots, legPrimaryBlockNodes, getNextNode(busConnection, busNode), 1);
                blocks.add(new LegPrimaryBlock(legPrimaryBlockNodes));
            }
        }
    }

    private static void elaborateFeederPrimaryBlock(BusCell busCell, Map<Node, Integer> nodeRemainingSlots, List<Block> blocks) {
        for (FeederNode feederNode : busCell.getFeederNodes()) {
            for (Node feederConnection : busCell.getInternalAdjacentNodes(feederNode)) {
                List<Node> feederPrimaryBlockNodes = new ArrayList<>();
                addNodeInBlockNodes(nodeRemainingSlots, feederPrimaryBlockNodes, feederNode, 1);
                addNodeInBlockNodes(nodeRemainingSlots, feederPrimaryBlockNodes, feederConnection, 1);
                blocks.add(new FeederPrimaryBlock(feederPrimaryBlockNodes));
            }
        }
    }

    private static boolean checkRemainingSlots(Map<Node, Integer> nodeRemainingSlots, Node node, int greaterOrEqVal) {
        return nodeRemainingSlots.containsKey(node) && nodeRemainingSlots.get(node) >= greaterOrEqVal;
    }

    /**
     * Search for primary blocks. A {@link PrimaryBlock} is identified when the the following pattern is detected:
     * BUS|FICTITIOUS|FEEDER|SHUNT - n * SWITCH - BUS|FICTITIOUS|FEEDER|SHUNT (with n >= 0).
     * When there is one BUS, it is instantiated as LegPrimaryBlock with this pattern (only allowed pattern with a bus):
     * BUS - SWITCH - FICTITIOUS.
     * When there is one FEEDER, it is instantiated as FeederPrimaryBlock with this pattern (only allowed pattern with a
     * feeder): FICTITIOUS - FEEDER.
     * Otherwise it is instantiated as BodyPrimaryBlock.
     * @param busCell         the busCell on which we elaborate primary blocks
     * @param entryNode       first node for the primary block (non-switch node)
     * @param nodeRemainingSlots map of the nodes to organize with their number of available belongings to PrimaryBlocks
     * @param blocks          the list of elaborated primary blocks
     */
    private static void rElaborateBodyPrimaryBlocks(BusCell busCell, Node entryNode,
                                                    Map<Node, Integer> nodeRemainingSlots, List<Block> blocks) {

        if (checkRemainingSlots(nodeRemainingSlots, entryNode, 1)) {
            for (Node node : busCell.getInternalAdjacentNodes(entryNode)) {
                if (checkRemainingSlots(nodeRemainingSlots, node, 1)) {
                    List<Node> primaryPattern = pileUp2adjNodes(entryNode, node, nodeRemainingSlots);
                    blocks.add(BodyPrimaryBlock.createBodyPrimaryBlockInBusCell(primaryPattern));
                    Node lastNode = primaryPattern.get(primaryPattern.size() - 1);
                    rElaborateBodyPrimaryBlocks(busCell, lastNode, nodeRemainingSlots, blocks);
                }
            }
        }
    }

    /**
     * Pile up switches encountered in given array, starting from node in the direction opposite to parentNode, stopping
     * when encountering a node which is not a switch and returning that node.
     * @param parentNode Parent node for direction
     * @param node Node on which to start
     * @param nodeRemainingSlots map of the nodes to organize with their number of available belongings to PrimaryBlocks
     * @return list of nodes that constitute a BodyPrimaryBlockPattern
     */
    private static List<Node> pileUp2adjNodes(Node parentNode, Node node, Map<Node, Integer> nodeRemainingSlots) {
        List<Node> nodes = new ArrayList<>();
        addNodeInBlockNodes(nodeRemainingSlots, nodes, parentNode, 1);
        Node parentCurrentNode = parentNode;
        Node currentNode = node;
        while (currentNode.getAdjacentNodes().size() == 2 && checkRemainingSlots(nodeRemainingSlots, currentNode, 2)) {
            addNodeInBlockNodes(nodeRemainingSlots, nodes, currentNode, 2); // the node is not an extremity of the PrimaryBlock -> 2 slots are taken
            Node nextNode = getNextNode(currentNode, parentCurrentNode);
            parentCurrentNode = currentNode;
            currentNode = nextNode;
        }
        addNodeInBlockNodes(nodeRemainingSlots, nodes, currentNode, 1);
        return nodes;
    }

    private static Node getNextNode(Node currentNode, Node parentCurrentNode) {
        List<Node> adjacentNodes = currentNode.getAdjacentNodes();
        return adjacentNodes.get(adjacentNodes.get(0).equals(parentCurrentNode) ? 1 : 0);
    }
}