GraphRefiner.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/.
 */
package com.powsybl.sld.layout;

import com.powsybl.sld.model.graphs.NodeFactory;
import com.powsybl.sld.model.graphs.VoltageLevelGraph;
import com.powsybl.sld.model.nodes.BusNode;
import com.powsybl.sld.model.nodes.Node;
import org.jgrapht.alg.connectivity.ConnectivityInspector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * Refines the graph so that it becomes consistent with the diagram layout.
 * In particular for cell detection: it inserts the bus connection nodes and the hook nodes needed for it.
 *
 * @author Florian Dupuy {@literal <florian.dupuy at rte-france.com>}
 */
public class GraphRefiner {
    private static final Logger LOGGER = LoggerFactory.getLogger(GraphRefiner.class);
    private final boolean removeUnnecessaryFictitiousNodes;
    private final boolean substituteSingularFictitiousByFeederNode;
    private final boolean substituteInternalMiddle2wtByEquipmentNodes;

    public GraphRefiner(boolean removeUnnecessaryFictitiousNodes, boolean substituteSingularFictitiousByFeederNode,
                        boolean substituteInternalMiddle2wtByEquipmentNodes) {
        this.removeUnnecessaryFictitiousNodes = removeUnnecessaryFictitiousNodes;
        this.substituteSingularFictitiousByFeederNode = substituteSingularFictitiousByFeederNode;
        this.substituteInternalMiddle2wtByEquipmentNodes = substituteInternalMiddle2wtByEquipmentNodes;
    }

    void run(VoltageLevelGraph graph, LayoutParameters layoutParameters) {
        if (substituteInternalMiddle2wtByEquipmentNodes) {
            graph.substituteInternalMiddle2wtByEquipmentNodes();
        }
        handleConnectedComponents(graph);
        graph.substituteFictitiousNodesMirroringBusNodes();
        if (removeUnnecessaryFictitiousNodes) {
            graph.removeUnnecessaryConnectivityNodes();
        }
        if (substituteSingularFictitiousByFeederNode) {
            graph.substituteSingularFictitiousByFeederNode();
        }
        if (layoutParameters.isRemoveFictitiousSwitchNodes()) {
            graph.removeFictitiousSwitchNode();
        }

        graph.extendBusesConnectedToBuses();

        Predicate<Node> nodesOnBus = getNodesOnBusPredicate(graph, layoutParameters.getComponentsOnBusbars());
        graph.insertBusConnections(nodesOnBus);
        graph.insertHookNodesAtBuses();
        graph.insertHookNodesAtFeeders();

        graph.substituteNodesMirroringGroundDisconnectionComponent();
    }

    /**
     * Check if the graph is connected or not
     */
    private void handleConnectedComponents(VoltageLevelGraph graph) {
        List<Set<Node>> connectedSets = new ConnectivityInspector<>(graph.toJgrapht()).connectedSets();
        if (connectedSets.size() != 1) {
            LOGGER.warn("{} connected components found", connectedSets.size());
            connectedSets.stream()
                    .sorted(Comparator.comparingInt(Set::size))
                    .map(setNodes -> setNodes.stream().map(Node::getId).collect(Collectors.toSet()))
                    .forEach(strings -> LOGGER.warn("   - {}", strings));
        }
        // Add a fictitious bus for all connected components without any bus
        connectedSets.stream()
                .filter(s -> s.stream().noneMatch(node -> node.getType() == Node.NodeType.BUS))
                .forEach(s -> addFictitiousBusInConnectedComponent(graph, s));
    }

    private void addFictitiousBusInConnectedComponent(VoltageLevelGraph graph, Set<Node> nodes) {
        // Replace the most meshed fictitious node by a fictitious BusNode.
        // If no fictitious node, insert/add a fictitious BusNode at the most meshed node of the set.
        Comparator<Node> mostMeshedComparator = Comparator.<Node>comparingInt(node -> node.getAdjacentEdges().size()).reversed().thenComparing(Node::getId); // for stable fictitious node selection, also sort on id
        int sectionIndex = 1 + graph.getNodeBuses().stream().mapToInt(BusNode::getSectionIndex).max().orElse(0);
        nodes.stream().filter(node -> node.getType() == Node.NodeType.INTERNAL)
                .min(mostMeshedComparator)
                .ifPresentOrElse(
                        mostMeshedFictitiousNode -> {
                            BusNode busNode = NodeFactory.createFictitiousBusNode(graph, mostMeshedFictitiousNode.getId() + "_FictitiousBus", 1, sectionIndex);
                            graph.substituteNode(mostMeshedFictitiousNode, busNode);
                        },
                        () -> {
                            Node mostMeshedNode = nodes.stream().min(mostMeshedComparator).orElseThrow(); // always non-empty set
                            BusNode busNode = NodeFactory.createFictitiousBusNode(graph, mostMeshedNode.getId() + "_FictitiousBus", 1, sectionIndex);
                            graph.insertNodeNextTo(busNode, mostMeshedNode);
                        });
    }

    private Predicate<Node> getNodesOnBusPredicate(VoltageLevelGraph graph, List<String> componentsOnBusbars) {
        Set<Node> nodesOnBusBetweenBuses = getNodesOnBusBetweenBuses(graph, componentsOnBusbars);
        return node -> componentsOnBusbars.contains(node.getComponentType()) && !nodesOnBusBetweenBuses.contains(node);
    }

    private Set<Node> getNodesOnBusBetweenBuses(VoltageLevelGraph graph, List<String> componentsOnBusbars) {
        return graph.getNodeBuses().stream()
                .flatMap(nodeBus -> nodeBus.getAdjacentNodes().stream())
                .filter(nodeConnectedToBus -> componentsOnBusbars.contains(nodeConnectedToBus.getComponentType()))
                .filter(n -> n.getAdjacentNodes().stream().allMatch(BusNode.class::isInstance))
                .collect(Collectors.toSet());
    }
}