diff --git a/src/main/java/com/powsybl/forcelayout/ForceLayout.java b/src/main/java/com/powsybl/forcelayout/ForceLayout.java index 49d7da30..65f48899 100644 --- a/src/main/java/com/powsybl/forcelayout/ForceLayout.java +++ b/src/main/java/com/powsybl/forcelayout/ForceLayout.java @@ -61,6 +61,7 @@ public class ForceLayout { private static final double DEFAULT_REPULSION = 800.0; private static final double DEFAULT_FRICTION = 500; private static final double DEFAULT_MAX_SPEED = 100; + private static final double DEFAULT_SPRING_REPULSION_FACTOR = 0.0; // Disabled by default private int maxSteps; private double minEnergyThreshold; @@ -68,6 +69,7 @@ public class ForceLayout { private double repulsion; private double friction; private double maxSpeed; + private double springRepulsionFactor; private final Graph graph; private final Map points = new LinkedHashMap<>(); @@ -82,6 +84,7 @@ public ForceLayout(Graph graph) { this.repulsion = DEFAULT_REPULSION; this.friction = DEFAULT_FRICTION; this.maxSpeed = DEFAULT_MAX_SPEED; + this.springRepulsionFactor = DEFAULT_SPRING_REPULSION_FACTOR; this.graph = Objects.requireNonNull(graph); } @@ -116,6 +119,11 @@ public ForceLayout setMaxSpeed(double maxSpeed) { return this; } + public ForceLayout setSpringRepulsionFactor(double springRepulsionFactor) { + this.springRepulsionFactor = springRepulsionFactor; + return this; + } + private void initializePoints() { for (V vertex : graph.vertexSet()) { points.put(vertex, new Point(random.nextDouble(), random.nextDouble())); @@ -140,7 +148,10 @@ public void execute() { int i; for (i = 0; i < maxSteps; i++) { - applyCoulombsLaw(); + applyCoulombsLawToPoints(); + if (springRepulsionFactor != 0.0) { + applyCoulombsLawToSprings(); + } applyHookesLaw(); attractToCenter(); updateVelocity(); @@ -159,15 +170,57 @@ public void execute() { LOGGER.info("Elapsed time: {}", elapsedTime / 1e9); } - private void applyCoulombsLaw() { + private Vector coulombsForce(Vector p1, Vector p2, double repulsion) { + Vector distance = p1.subtract(p2); + Vector direction = distance.normalize(); + return direction.multiply(repulsion).divide(distance.magnitudeSquare() * 0.5 + 0.1); + } + + private void applyCoulombsLawToPoints() { for (Point point : points.values()) { + Vector p = point.getPosition(); for (Point otherPoint : points.values()) { if (!point.equals(otherPoint)) { - Vector distance = point.getPosition().subtract(otherPoint.getPosition()); - Vector direction = distance.normalize(); + point.applyForce(coulombsForce(p, otherPoint.getPosition(), repulsion)); + } + } + } + } - Vector force = direction.multiply(repulsion).divide(distance.magnitudeSquare() * 0.5 + 0.1); + private void applyCoulombsLawToSprings() { + for (Point point : points.values()) { + Vector p = point.getPosition(); + for (Spring spring : springs) { + Point n1 = spring.getNode1(); + Point n2 = spring.getNode2(); + if (!n1.equals(point) && !n2.equals(point)) { + Vector q1 = spring.getNode1().getPosition(); + Vector q2 = spring.getNode2().getPosition(); + Vector center = q1.add(q2.subtract(q1).multiply(0.5)); + Vector force = coulombsForce(p, center, repulsion * springRepulsionFactor); point.applyForce(force); + n1.applyForce(force.multiply(-0.5)); + n2.applyForce(force.multiply(-0.5)); + } + } + } + for (Spring spring : springs) { + Point n1 = spring.getNode1(); + Point n2 = spring.getNode2(); + Vector p1 = spring.getNode1().getPosition(); + Vector p2 = spring.getNode2().getPosition(); + Vector center = p1.add(p2.subtract(p1).multiply(0.5)); + for (Spring otherSpring : springs) { + if (!spring.equals(otherSpring)) { + // Compute the repulsion force between centers of the springs + Vector op1 = otherSpring.getNode1().getPosition(); + Vector op2 = otherSpring.getNode2().getPosition(); + Vector otherCenter = op1.add(op2.subtract(op1).multiply(0.5)); + Vector force = coulombsForce(center, otherCenter, repulsion * springRepulsionFactor); + + // And apply it to both points of the spring + n1.applyForce(force); + n2.applyForce(force); } } } diff --git a/src/main/java/com/powsybl/nad/layout/BasicForceLayout.java b/src/main/java/com/powsybl/nad/layout/BasicForceLayout.java index 49638826..c6277e3a 100644 --- a/src/main/java/com/powsybl/nad/layout/BasicForceLayout.java +++ b/src/main/java/com/powsybl/nad/layout/BasicForceLayout.java @@ -23,6 +23,7 @@ public class BasicForceLayout extends AbstractLayout { protected void nodesLayout(Graph graph, LayoutParameters layoutParameters) { org.jgrapht.Graph jgraphtGraph = graph.getJgraphtGraph(layoutParameters.isTextNodesForceLayout()); ForceLayout forceLayout = new ForceLayout<>(jgraphtGraph); + forceLayout.setSpringRepulsionFactor(layoutParameters.getSpringRepulsionFactorForceLayout()); forceLayout.execute(); jgraphtGraph.vertexSet().forEach(node -> { diff --git a/src/main/java/com/powsybl/nad/layout/LayoutParameters.java b/src/main/java/com/powsybl/nad/layout/LayoutParameters.java index ae22b780..55dfa7ab 100644 --- a/src/main/java/com/powsybl/nad/layout/LayoutParameters.java +++ b/src/main/java/com/powsybl/nad/layout/LayoutParameters.java @@ -11,12 +11,14 @@ */ public class LayoutParameters { private boolean textNodesForceLayout = false; + private double springRepulsionFactorForceLayout = 0.0; public LayoutParameters() { } public LayoutParameters(LayoutParameters other) { this.textNodesForceLayout = other.textNodesForceLayout; + this.springRepulsionFactorForceLayout = other.springRepulsionFactorForceLayout; } public boolean isTextNodesForceLayout() { @@ -27,4 +29,13 @@ public LayoutParameters setTextNodesForceLayout(boolean textNodesForceLayout) { this.textNodesForceLayout = textNodesForceLayout; return this; } + + public LayoutParameters setSpringRepulsionFactorForceLayout(double springRepulsionFactorForceLayout) { + this.springRepulsionFactorForceLayout = springRepulsionFactorForceLayout; + return this; + } + + public double getSpringRepulsionFactorForceLayout() { + return springRepulsionFactorForceLayout; + } } diff --git a/src/test/java/com/powsybl/nad/layout/ForceLayoutTest.java b/src/test/java/com/powsybl/nad/layout/ForceLayoutTest.java new file mode 100644 index 00000000..2054a592 --- /dev/null +++ b/src/test/java/com/powsybl/nad/layout/ForceLayoutTest.java @@ -0,0 +1,170 @@ +/** + * 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.nad.layout; + +import com.powsybl.iidm.network.*; +import com.powsybl.nad.AbstractTest; +import com.powsybl.nad.svg.LabelProvider; +import com.powsybl.nad.svg.StyleProvider; +import com.powsybl.nad.svg.SvgParameters; +import com.powsybl.nad.svg.iidm.DefaultLabelProvider; +import com.powsybl.nad.svg.iidm.NominalVoltageStyleProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Luma Zamarreno + */ +class ForceLayoutTest extends AbstractTest { + + @BeforeEach + public void setup() { + setLayoutParameters(new LayoutParameters().setTextNodesForceLayout(false)); + setSvgParameters(new SvgParameters() + .setInsertNameDesc(false) + .setSvgWidthAndHeightAdded(false)); + } + + @Override + protected StyleProvider getStyleProvider(Network network) { + return new NominalVoltageStyleProvider(network); + } + + @Override + protected LabelProvider getLabelProvider(Network network) { + return new DefaultLabelProvider(network, getSvgParameters()); + } + + @Test + void testDiamondNoSpringRepulsionFactor() { + assertEquals( + toString("/diamond-spring-repulsion-factor-0.0.svg"), + generateSvgString(createDiamondNetwork(), "/diamond-spring-repulsion-factor-0.0.svg")); + } + + @Test + void testDiamondSmallSpringRepulsionFactor() { + getLayoutParameters().setSpringRepulsionFactorForceLayout(0.2); + assertEquals( + toString("/diamond-spring-repulsion-factor-0.2.svg"), + generateSvgString(createDiamondNetwork(), "/diamond-spring-repulsion-factor-0.2.svg")); + } + + static Network createDiamondNetwork() { + Network network = NetworkFactory.findDefault().createNetwork("diamond", "manual"); + network.setName("diamond"); + + Substation subA = network.newSubstation().setId("A").add(); + Bus subA400 = createBus(subA, 400); + Bus subA230 = createBus(subA, 230); + createTransformer(subA400, subA230); + + Substation subB = network.newSubstation().setId("B").add(); + Bus subB230 = createBus(subB, 230); + createLine(subA230, subB230); + + Substation subC = network.newSubstation().setId("C").add(); + Bus subC230 = createBus(subC, 230); + Bus subC66 = createBus(subC, 66); + Bus subC20 = createBus(subC, 20); + createTransformer(subC230, subC66); + createTransformer(subC66, subC20); + createLine(subB230, subC230); + + Substation subD = network.newSubstation().setId("D").add(); + Bus subD66 = createBus(subD, 66); + Bus subD10 = createBus(subD, 10); + createTransformer(subD66, subD10); + createLine(subC66, subD66); + + Substation subE = network.newSubstation().setId("E").add(); + Bus subE10 = createBus(subE, 10); + createLine(subD10, subE10); + + Bus subF10 = createBus(network, "F", 10); + Bus subG10 = createBus(network, "G", 10); + Bus subH10 = createBus(network, "H", 10); + Bus subI10 = createBus(network, "I", 10); + Bus subJ10 = createBus(network, "J", 10); + Bus subK10 = createBus(network, "K", 10); + + createLine(subE10, subF10); + createLine(subF10, subG10); + createLine(subG10, subH10); + createLine(subH10, subD10); + + createLine(subF10, subI10); + createLine(subI10, subJ10); + createLine(subJ10, subK10); + createLine(subK10, subD10); + + return network; + } + + private static Bus createBus(Network network, String substationId, double nominalVoltage) { + Substation substation = network.newSubstation().setId(substationId).add(); + return createBus(substation, nominalVoltage); + } + + private static Bus createBus(Substation substation, double nominalVoltage) { + String vlId = String.format("%s %.0f", substation.getId(), nominalVoltage); + String busId = String.format("%s %s", vlId, "Bus"); + return substation.newVoltageLevel() + .setId(vlId) + .setNominalV(nominalVoltage) + .setTopologyKind(TopologyKind.BUS_BREAKER) + .add() + .getBusBreakerView() + .newBus() + .setId(busId) + .add(); + } + + private static void createTransformer(Bus bus1, Bus bus2) { + Substation substation = bus1.getVoltageLevel().getSubstation().orElseThrow(); + String id = String.format("%s %.0f %.0f", + substation.getId(), + bus1.getVoltageLevel().getNominalV(), + bus2.getVoltageLevel().getNominalV()); + substation.newTwoWindingsTransformer().setId(id) + .setR(0.0) + .setX(1.0) + .setG(0.0) + .setB(0.0) + .setVoltageLevel1(bus1.getVoltageLevel().getId()) + .setVoltageLevel2(bus2.getVoltageLevel().getId()) + .setConnectableBus1(bus1.getId()) + .setConnectableBus2(bus2.getId()) + .setRatedU1(bus1.getVoltageLevel().getNominalV()) + .setRatedU2(bus2.getVoltageLevel().getNominalV()) + .setBus1(bus1.getId()) + .setBus2(bus2.getId()) + .add(); + } + + private static void createLine(Bus bus1, Bus bus2) { + String id = String.format("%s - %s", + bus1.getVoltageLevel().getSubstation().orElseThrow().getId(), + bus2.getVoltageLevel().getSubstation().orElseThrow().getId()); + bus1.getNetwork().newLine().setId(id) + .setR(0.0) + .setX(1.0) + .setG1(0.0) + .setB1(0.0) + .setG2(0.0) + .setB2(0.0) + .setVoltageLevel1(bus1.getVoltageLevel().getId()) + .setVoltageLevel2(bus2.getVoltageLevel().getId()) + .setConnectableBus1(bus1.getId()) + .setConnectableBus2(bus2.getId()) + .setBus1(bus1.getId()) + .setBus2(bus2.getId()) + .add(); + } +} diff --git a/src/test/java/com/powsybl/nad/layout/LayoutParametersTest.java b/src/test/java/com/powsybl/nad/layout/LayoutParametersTest.java index 372609fb..ca236123 100644 --- a/src/test/java/com/powsybl/nad/layout/LayoutParametersTest.java +++ b/src/test/java/com/powsybl/nad/layout/LayoutParametersTest.java @@ -18,10 +18,12 @@ class LayoutParametersTest { @Test void test() { LayoutParameters layoutParameters0 = new LayoutParameters() - .setTextNodesForceLayout(true); + .setTextNodesForceLayout(true) + .setSpringRepulsionFactorForceLayout(1.0); LayoutParameters layoutParameters1 = new LayoutParameters(layoutParameters0); assertEquals(layoutParameters0.isTextNodesForceLayout(), layoutParameters1.isTextNodesForceLayout()); + assertEquals(layoutParameters0.getSpringRepulsionFactorForceLayout(), layoutParameters1.getSpringRepulsionFactorForceLayout()); } } diff --git a/src/test/resources/diamond-spring-repulsion-factor-0.0.svg b/src/test/resources/diamond-spring-repulsion-factor-0.0.svg new file mode 100644 index 00000000..dbc02c60 --- /dev/null +++ b/src/test/resources/diamond-spring-repulsion-factor-0.0.svg @@ -0,0 +1,839 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + A 230 + A 400 + B 230 + C 20 + C 230 + C 66 + D 10 + D 66 + E 10 + F 10 + G 10 + H 10 + I 10 + J 10 + K 10 + + diff --git a/src/test/resources/diamond-spring-repulsion-factor-0.2.svg b/src/test/resources/diamond-spring-repulsion-factor-0.2.svg new file mode 100644 index 00000000..5d7cbd7a --- /dev/null +++ b/src/test/resources/diamond-spring-repulsion-factor-0.2.svg @@ -0,0 +1,839 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + 0 + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + A 230 + A 400 + B 230 + C 20 + C 230 + C 66 + D 10 + D 66 + E 10 + F 10 + G 10 + H 10 + I 10 + J 10 + K 10 + +