diff --git a/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/builders/NetworkGraphBuilder.java b/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/builders/NetworkGraphBuilder.java index bf35ff117..138bef591 100644 --- a/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/builders/NetworkGraphBuilder.java +++ b/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/builders/NetworkGraphBuilder.java @@ -15,7 +15,6 @@ import com.powsybl.sld.model.graphs.*; import com.powsybl.sld.model.nodes.*; import com.powsybl.sld.postprocessor.GraphBuildPostProcessor; -import org.jgrapht.alg.connectivity.ConnectivityInspector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -109,8 +108,6 @@ private void buildGraph(VoltageLevelGraph graph, VoltageLevel vl) { LOGGER.info("{} nodes, {} edges", graph.getNodes().size(), graph.getEdges().size()); handleGraphPostProcessors(graph); - - handleConnectedComponents(graph); } private void addBranchEdges(VoltageLevelGraph graph, VoltageLevel vl) { @@ -262,10 +259,6 @@ private FeederNode createFeederLccNode(VoltageLevelGraph graph, HvdcConverterSta .orElseGet(() -> NodeFactory.createLccConverterStationInjection(graph, hvdcStation.getId(), hvdcStation.getNameOrId())); } - private Node createInternal2wtSideNode(VoltageLevelGraph graph, TwoWindingsTransformer branch, TwoSides side) { - return NodeFactory.createConnectivityNode(graph, branch.getId() + "_" + side.name(), NODE); - } - private FeederNode createFeeder2wtNode(VoltageLevelGraph graph, TwoWindingsTransformer branch, TwoSides side) { String id = branch.getId() + "_" + side.name(); String name = branch.getNameOrId(); @@ -410,9 +403,7 @@ public void visitStaticVarCompensator(StaticVarCompensator svc) { @Override public void visitTwoWindingsTransformer(TwoWindingsTransformer transformer, TwoSides side) { - Node transformerNode = isInternalToVoltageLevel(transformer) - ? createInternal2wtSideNode(graph, transformer, side) - : createFeeder2wtNode(graph, transformer, side); + Node transformerNode = createFeeder2wtNode(graph, transformer, side); addTerminalNode(transformerNode, transformer.getTerminal(side)); } @@ -624,34 +615,6 @@ private void ensureNodeExists(VoltageLevelGraph graph, int n, Map nodesByNumber.computeIfAbsent(n, k -> NodeFactory.createConnectivityNode(graph, String.valueOf(k))); } - /** - * Check if the graph is connected or not - */ - private void handleConnectedComponents(VoltageLevelGraph graph) { - List> 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)); - } - connectedSets.forEach(s -> ensureOneBusInConnectedComponent(graph, s)); - } - - private void ensureOneBusInConnectedComponent(VoltageLevelGraph graph, Set nodes) { - if (nodes.stream().anyMatch(node -> node.getType() == Node.NodeType.BUS)) { - return; - } - Node biggestFn = nodes.stream() - .filter(node -> node.getType() == Node.NodeType.INTERNAL) - .min(Comparator.comparingInt(node -> node.getAdjacentEdges().size()) - .reversed() - .thenComparing(Node::getId)) // for stable fictitious node selection, also sort on id - .orElseThrow(() -> new PowsyblException("Empty node set")); - graph.substituteNode(biggestFn, NodeFactory.createFictitiousBusNode(graph, biggestFn.getId() + "FictitiousBus")); - } - /** * Discover and apply postprocessor plugins to add custom nodes **/ @@ -705,16 +668,7 @@ private boolean addLineEdge(Graph graph, String lineId, Terminal t1, Terminal t2 return isNotNull; } - private void add2wtEdges(VoltageLevelGraph graph, List twoWindingsTransformers) { - for (TwoWindingsTransformer transfo : twoWindingsTransformers) { - Node n1 = graph.getNode(transfo.getId() + "_" + TwoSides.ONE); - Node n2 = graph.getNode(transfo.getId() + "_" + TwoSides.TWO); - NodeFactory.createInternal2WTNode(graph, transfo.getId(), transfo.getNameOrId(), - n1, n2, transfo.hasPhaseTapChanger()); - } - } - - private void add2wtEdges(SubstationGraph graph, List twoWindingsTransformers) { + private void add2wtEdges(BaseGraph graph, List twoWindingsTransformers) { for (TwoWindingsTransformer transfo : twoWindingsTransformers) { Terminal t1 = transfo.getTerminal1(); Terminal t2 = transfo.getTerminal2(); diff --git a/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/layout/GraphRefiner.java b/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/layout/GraphRefiner.java index c8cf103ba..dbadf1aac 100644 --- a/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/layout/GraphRefiner.java +++ b/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/layout/GraphRefiner.java @@ -6,9 +6,16 @@ */ package com.powsybl.sld.layout; +import com.powsybl.commons.PowsyblException; +import com.powsybl.sld.model.graphs.NodeFactory; import com.powsybl.sld.model.graphs.VoltageLevelGraph; -import com.powsybl.sld.model.nodes.*; +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; @@ -21,15 +28,23 @@ * @author Florian Dupuy {@literal } */ 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) { + 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(); @@ -51,6 +66,34 @@ void run(VoltageLevelGraph graph, LayoutParameters layoutParameters) { graph.substituteNodesMirroringGroundDisconnectionComponent(); } + /** + * Check if the graph is connected or not + */ + private void handleConnectedComponents(VoltageLevelGraph graph) { + List> 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)); + } + connectedSets.forEach(s -> ensureOneBusInConnectedComponent(graph, s)); + } + + private void ensureOneBusInConnectedComponent(VoltageLevelGraph graph, Set nodes) { + if (nodes.stream().anyMatch(node -> node.getType() == Node.NodeType.BUS)) { + return; + } + Node biggestFn = nodes.stream() + .filter(node -> node.getType() == Node.NodeType.INTERNAL) + .min(Comparator.comparingInt(node -> node.getAdjacentEdges().size()) + .reversed() + .thenComparing(Node::getId)) // for stable fictitious node selection, also sort on id + .orElseThrow(() -> new PowsyblException("Empty node set")); + graph.substituteNode(biggestFn, NodeFactory.createFictitiousBusNode(graph, biggestFn.getId() + "FictitiousBus")); + } + private Predicate getNodesOnBusPredicate(VoltageLevelGraph graph, List componentsOnBusbars) { Set nodesOnBusBetweenBuses = getNodesOnBusBetweenBuses(graph, componentsOnBusbars); return node -> componentsOnBusbars.contains(node.getComponentType()) && !nodesOnBusBetweenBuses.contains(node); diff --git a/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/layout/PositionVoltageLevelLayoutFactory.java b/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/layout/PositionVoltageLevelLayoutFactory.java index 4b4d49c0b..95c3eb6e1 100644 --- a/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/layout/PositionVoltageLevelLayoutFactory.java +++ b/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/layout/PositionVoltageLevelLayoutFactory.java @@ -43,7 +43,10 @@ public PositionVoltageLevelLayoutFactory(PositionFinder positionFinder, Position @Override public Layout create(VoltageLevelGraph graph) { // For adapting the graph to the diagram layout - GraphRefiner graphRefiner = new GraphRefiner(positionVoltageLevelLayoutFactoryParameters.isRemoveUnnecessaryFictitiousNodes(), positionVoltageLevelLayoutFactoryParameters.isSubstituteSingularFictitiousByFeederNode()); + GraphRefiner graphRefiner = new GraphRefiner( + positionVoltageLevelLayoutFactoryParameters.isRemoveUnnecessaryFictitiousNodes(), + positionVoltageLevelLayoutFactoryParameters.isSubstituteSingularFictitiousByFeederNode(), + positionVoltageLevelLayoutFactoryParameters.isSubstituteInternalMiddle2wtByEquipmentNodes()); // For cell detection ImplicitCellDetector cellDetector = new ImplicitCellDetector(positionVoltageLevelLayoutFactoryParameters.isExceptionIfPatternNotHandled()); diff --git a/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/layout/PositionVoltageLevelLayoutFactoryParameters.java b/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/layout/PositionVoltageLevelLayoutFactoryParameters.java index f977bd653..d81885262 100644 --- a/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/layout/PositionVoltageLevelLayoutFactoryParameters.java +++ b/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/layout/PositionVoltageLevelLayoutFactoryParameters.java @@ -25,6 +25,7 @@ public class PositionVoltageLevelLayoutFactoryParameters { private boolean exceptionIfPatternNotHandled = false; private boolean handleShunts = false; private Map busInfoMap = new HashMap<>(); + private boolean substituteInternalMiddle2wtByEquipmentNodes = true; public boolean isFeederStacked() { return feederStacked; @@ -79,4 +80,13 @@ public PositionVoltageLevelLayoutFactoryParameters setBusInfoMap(Map getFeederNodeStream() { .map(FeederNode.class::cast); } + private Stream getFeederNodeStream(FeederType feederType) { + return getFeederNodeStream().filter(feederNode -> feederNode.getFeeder().getFeederType() == feederType); + } + public List getNodes() { return new ArrayList<>(nodes); } @@ -661,7 +666,7 @@ public double getInnerHeight(double verticalSpaceBus) { } public void substituteNodesMirroringGroundDisconnectionComponent() { - List groundDisconnections = getFeederNodeStream().filter(feederNode -> feederNode.getFeeder().getFeederType() == FeederType.GROUND) + List groundDisconnections = getFeederNodeStream(FeederType.GROUND) .map(this::getGroundDisconnection) .filter(Objects::nonNull) .toList(); @@ -687,6 +692,44 @@ private GroundDisconnection getGroundDisconnection(FeederNode groundFeederNode) return null; } + public void substituteInternalMiddle2wtByEquipmentNodes() { + for (FeederNode feederNode : getFeederNodeStream(FeederType.TWO_WINDINGS_TRANSFORMER_LEG).toList()) { + if (nodesById.get(feederNode.getId()) == feederNode) { // check if not already removed + substituteInternalMiddle2wtByEquipmentNode(feederNode); + } + } + } + + private void substituteInternalMiddle2wtByEquipmentNode(FeederNode feederNode) { + MiddleTwtNode middleNode = getMultiTermNodes().stream() + .filter(m2wtn -> m2wtn.getAdjacentNodes().stream().anyMatch(n -> n == feederNode)) + .findFirst().orElse(null); + Node otherSideNode = Optional.ofNullable(middleNode) + .flatMap(m2wtn -> m2wtn.getAdjacentNodes().stream().filter(n -> n != feederNode).findFirst()) + .orElse(null); + if (middleNode instanceof Middle2WTNode middle2WTNode + && otherSideNode instanceof FeederNode otherSideFeederNode + && otherSideFeederNode.getFeeder() instanceof FeederTwLeg otherSideFeederTwLeg + && otherSideFeederTwLeg.getOwnVoltageLevelInfos().getId().equals(voltageLevelInfos.getId())) { + + ConnectivityNode connectivityNodeA = NodeFactory.createConnectivityNode(this, feederNode.getId(), ComponentTypeName.NODE); + ConnectivityNode connectivityNodeB = NodeFactory.createConnectivityNode(this, otherSideNode.getId(), ComponentTypeName.NODE); + + // substitute nodes in voltage level + substituteNode(feederNode, connectivityNodeA); + substituteNode(otherSideNode, connectivityNodeB); + + // substitute the node which was "outside" the voltage level (multiTermNode / snake line) by a node inside the voltage level + ConnectivityNode connectivityNode1 = otherSideFeederTwLeg.getSide() == NodeSide.ONE ? connectivityNodeB : connectivityNodeA; + ConnectivityNode connectivityNode2 = otherSideFeederTwLeg.getSide() == NodeSide.ONE ? connectivityNodeA : connectivityNodeB; + NodeFactory.createInternal2WTNode(this, + middle2WTNode.getId(), middle2WTNode.getName(), middle2WTNode.getEquipmentId(), + connectivityNode1, connectivityNode2, middle2WTNode.getComponentType()); + multiTermNodes.remove(middleNode); + twtEdges.removeAll(middleNode.getAdjacentEdges()); + } + } + private record GroundDisconnection(List nodes, FeederNode ground, SwitchNode disconnector, Node forkNode) { } } diff --git a/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/model/nodes/Internal2WTNode.java b/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/model/nodes/Internal2WTNode.java index 992fefded..fb10c1d10 100644 --- a/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/model/nodes/Internal2WTNode.java +++ b/single-line-diagram/single-line-diagram-core/src/main/java/com/powsybl/sld/model/nodes/Internal2WTNode.java @@ -15,8 +15,8 @@ * @author Florian Dupuy {@literal } */ public class Internal2WTNode extends EquipmentNode { - public Internal2WTNode(String id, String nameOrId, String componentType) { - super(NodeType.INTERNAL, id, nameOrId, id, componentType, false); + public Internal2WTNode(String id, String nameOrId, String equipmentId, String componentType) { + super(NodeType.INTERNAL, id, nameOrId, equipmentId, componentType, false); } @Override diff --git a/single-line-diagram/single-line-diagram-core/src/test/java/com/powsybl/sld/iidm/TestInternalBranchesNodeBreaker.java b/single-line-diagram/single-line-diagram-core/src/test/java/com/powsybl/sld/iidm/TestInternalBranchesNodeBreaker.java index 6da527714..405a73304 100644 --- a/single-line-diagram/single-line-diagram-core/src/test/java/com/powsybl/sld/iidm/TestInternalBranchesNodeBreaker.java +++ b/single-line-diagram/single-line-diagram-core/src/test/java/com/powsybl/sld/iidm/TestInternalBranchesNodeBreaker.java @@ -10,10 +10,11 @@ import com.powsybl.diagram.test.Networks; import com.powsybl.sld.builders.NetworkGraphBuilder; import com.powsybl.sld.layout.PositionVoltageLevelLayoutFactory; +import com.powsybl.sld.layout.PositionVoltageLevelLayoutFactoryParameters; import com.powsybl.sld.layout.VerticalSubstationLayoutFactory; +import com.powsybl.sld.layout.positionfromextension.PositionFromExtension; import com.powsybl.sld.model.graphs.SubstationGraph; import com.powsybl.sld.model.graphs.VoltageLevelGraph; -import com.powsybl.sld.svg.DefaultSVGWriter; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -36,14 +37,23 @@ void testVLGraph() { // build graph VoltageLevelGraph g = graphBuilder.buildVoltageLevelGraph(network.getVoltageLevel("VL1").getId()); - // Run layout + // Run layout with default parameters and compare subsequent SVG with reference voltageLevelGraphLayout(g); - - // write SVG and compare to reference - DefaultSVGWriter defaultSVGWriter = new DefaultSVGWriter(componentLibrary, layoutParameters, svgParameters); assertEquals(toString("/InternalBranchesNodeBreaker.svg"), toSVG(g, "/InternalBranchesNodeBreaker.svg", componentLibrary, layoutParameters, svgParameters, getDefaultDiagramLabelProvider(), getDefaultDiagramStyleProvider())); + } + + @Test + void testVLGraphExternal2WT() { + // build graph + VoltageLevelGraph g = graphBuilder.buildVoltageLevelGraph(network.getVoltageLevel("VL1").getId()); + // Run layout with specific parameters and compare subsequent SVG with reference + PositionVoltageLevelLayoutFactoryParameters pvllfParameters = new PositionVoltageLevelLayoutFactoryParameters() + .setSubstituteInternalMiddle2wtByEquipmentNodes(false); + new PositionVoltageLevelLayoutFactory(new PositionFromExtension(), pvllfParameters).create(g).run(this.layoutParameters); + assertEquals(toString("/InternalBranchesNodeBreaker_externalPst.svg"), + toSVG(g, "/InternalBranchesNodeBreaker_externalPst.svg", componentLibrary, layoutParameters, svgParameters, getDefaultDiagramLabelProvider(), getDefaultDiagramStyleProvider())); } @Test diff --git a/single-line-diagram/single-line-diagram-core/src/test/java/com/powsybl/sld/layout/PositionVoltageLevelLayoutFactoryParametersTest.java b/single-line-diagram/single-line-diagram-core/src/test/java/com/powsybl/sld/layout/PositionVoltageLevelLayoutFactoryParametersTest.java index eb56fb261..f6296ad31 100644 --- a/single-line-diagram/single-line-diagram-core/src/test/java/com/powsybl/sld/layout/PositionVoltageLevelLayoutFactoryParametersTest.java +++ b/single-line-diagram/single-line-diagram-core/src/test/java/com/powsybl/sld/layout/PositionVoltageLevelLayoutFactoryParametersTest.java @@ -36,6 +36,10 @@ void test() { parameters.setSubstituteSingularFictitiousByFeederNode(false); assertFalse(parameters.isSubstituteSingularFictitiousByFeederNode()); + assertTrue(parameters.isSubstituteInternalMiddle2wtByEquipmentNodes()); + parameters.setSubstituteInternalMiddle2wtByEquipmentNodes(false); + assertFalse(parameters.isSubstituteInternalMiddle2wtByEquipmentNodes()); + assertFalse(parameters.isExceptionIfPatternNotHandled()); parameters.setExceptionIfPatternNotHandled(true); assertTrue(parameters.isExceptionIfPatternNotHandled()); diff --git a/single-line-diagram/single-line-diagram-core/src/test/resources/InternalBranchesNodeBreaker_externalPst.svg b/single-line-diagram/single-line-diagram-core/src/test/resources/InternalBranchesNodeBreaker_externalPst.svg new file mode 100644 index 000000000..486bacb6c --- /dev/null +++ b/single-line-diagram/single-line-diagram-core/src/test/resources/InternalBranchesNodeBreaker_externalPst.svg @@ -0,0 +1,662 @@ + + + + + + + + BBS11 + + + + BBS12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + L1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + L11 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + L12 + + + + + + + + + + + + + + + + + + + 375 + + + + + 48 + + + + + + + + + + + + + + + + + T11 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T3_12 + + + T3_12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + G + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + L11 + + + + + + + + + + + + + + + + + + + 375 + + + + + 48 + + + + + + + + + + + + + + + + + T11 + + + + + + + + + + + + + + + + + + + 375 + + + + + 48 + + + + + + + + + + + + + + + + + + + T12 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + T3_12 + + + T3_12 + + + + + + + + + + + + + + + + +