diff --git a/docs/grid_model/extensions.md b/docs/grid_model/extensions.md index 0a7b36da64c..a87ad6beeda 100644 --- a/docs/grid_model/extensions.md +++ b/docs/grid_model/extensions.md @@ -328,6 +328,44 @@ Side 1 and side 2 correspond respectively to terminal1 and terminal2 of the line This extension is provided in the `com.powsybl:powsybl-iidm-extensions` module. +(line-couplings-extension)= +## Line Couplings + +This extension models the mutual coupling between two lines. It is attached to the `Network` object and contains a list of mutual couplings. + +The attributes of a `MutualCoupling` are: + +| Attribute | Type | Unit | Required | Default value | Description | +|--------------|-------------|------|----------|---------------|---------------------------------------------------------------| +| line1 | Line | - | yes | - | The first coupled line | +| line2 | Line | - | yes | - | The second coupled line | +| r | double | Ω | yes | - | The mutual coupling resistance | +| x | double | Ω | yes | - | The mutual coupling reactance | +| line1Segment | LineSegment | - | no | 0..1 | The starting and ending positions on the first line (0 to 1) | +| line2Segment | LineSegment | - | no | 0..1 | The starting and ending positions on the second line (0 to 1) | + +The position of the mutual coupling is expressed as a ratio between 0 and 1, through a `LineSegment` object. +A `LineSegment` has a start and an end position, with the constraint that: 0 ≤ start ≤ end ≤ 1 +Mutual coupling is symmetric: a coupling between line1 and line2 is equivalent to a coupling between line2 and line1. +Mutual couplings are considered symmetric. Therefore, the extension cannot contain both (line1, line2) and (line2, line1). + +Example of code to add a mutual coupling: + +```java +Line line1 = network.getLine("L1"); +Line line2 = network.getLine("L2"); +network.newExtension(LineCouplingsAdder.class).add(); +network.getExtension(LineCouplings.class) + .newMutualCoupling() + .withLine1(line1) + .withLine2(line2) + .withR(0.1) + .withX(0.4) + .add(); +``` + +This extension is provided in the `com.powsybl:powsybl-iidm-extensions` module. + (two-winding-transformer-fortescue)= ## Two-winding Transformer Fortescue diff --git a/docs/grid_model/network_subnetwork.md b/docs/grid_model/network_subnetwork.md index e3893de9ae9..8e98d050687 100644 --- a/docs/grid_model/network_subnetwork.md +++ b/docs/grid_model/network_subnetwork.md @@ -40,6 +40,7 @@ In the PowSyBl grid model, the Network contains [substations](#substation), whic The `SourceFormat` attribute is a required attribute that indicates the origin of the network model automatically set by the [importers](../grid_exchange_formats/index.md). If the case date and the forecast distance cannot be found in the case file, the network is considered as a snapshot: the case date is set to the current date, and the forecast distance is set to `0`. **Available extensions** +- [Line Couplings](extensions.md#line-couplings) (substation)= ## Substation diff --git a/iidm/iidm-extensions/src/main/java/com/powsybl/iidm/network/extensions/LineCouplings.java b/iidm/iidm-extensions/src/main/java/com/powsybl/iidm/network/extensions/LineCouplings.java new file mode 100644 index 00000000000..0e7fb8ae5be --- /dev/null +++ b/iidm/iidm-extensions/src/main/java/com/powsybl/iidm/network/extensions/LineCouplings.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.iidm.network.extensions; + +import com.powsybl.commons.PowsyblException; +import com.powsybl.commons.extensions.Extension; +import com.powsybl.iidm.network.Line; +import com.powsybl.iidm.network.Network; + +import java.util.List; +import java.util.Optional; + +/** + * This extension stores the mutual couplings on the network. + * + * @author Coline Piloquet {@literal } + */ +public interface LineCouplings extends Extension { + + String NAME = "lineCouplings"; + + @Override + default String getName() { + return NAME; + } + + /** + * Gets all mutual couplings in the network. + * @return a list of mutual couplings + */ + List getMutualCouplings(); + + /** + * Adds a mutual coupling. + * @param mutualCoupling the mutual coupling to add + * @return the current line couplings extension + * @throws PowsyblException if the coupling is invalid or already exists + */ + LineCouplings add(MutualCoupling mutualCoupling); + + /** + * Creates a new mutual coupling adder. + * @return a mutual coupling adder + */ + MutualCouplingAdder newMutualCoupling(); + + /** + * Finds a mutual coupling between two lines. + * The order of the lines does not matter. + * @param line1 the first line + * @param line2 the second line + * @return the mutual coupling if found, or empty if not + */ + Optional findMutualCoupling(Line line1, Line line2); + + /** + * Removes a mutual coupling. + * @param mutualCoupling the mutual coupling to remove + * @return true if the mutual coupling was removed, false otherwise + */ + boolean removeMutualCoupling(MutualCoupling mutualCoupling); + + /** + * Removes a mutual coupling between two lines. + * @param line1 the first line + * @param line2 the second line + * @return true if the mutual coupling was removed, false otherwise + */ + boolean removeMutualCoupling(Line line1, Line line2); +} diff --git a/iidm/iidm-extensions/src/main/java/com/powsybl/iidm/network/extensions/LineCouplingsAdder.java b/iidm/iidm-extensions/src/main/java/com/powsybl/iidm/network/extensions/LineCouplingsAdder.java new file mode 100644 index 00000000000..a3c5dc14f56 --- /dev/null +++ b/iidm/iidm-extensions/src/main/java/com/powsybl/iidm/network/extensions/LineCouplingsAdder.java @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.iidm.network.extensions; + +import com.powsybl.commons.extensions.ExtensionAdder; +import com.powsybl.iidm.network.Network; + +/** + * @author Coline Piloquet {@literal } + */ +public interface LineCouplingsAdder extends ExtensionAdder { + + @Override + default Class getExtensionClass() { + return LineCouplings.class; + } +} diff --git a/iidm/iidm-extensions/src/main/java/com/powsybl/iidm/network/extensions/LineSegment.java b/iidm/iidm-extensions/src/main/java/com/powsybl/iidm/network/extensions/LineSegment.java new file mode 100644 index 00000000000..2a287f0c1d1 --- /dev/null +++ b/iidm/iidm-extensions/src/main/java/com/powsybl/iidm/network/extensions/LineSegment.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.iidm.network.extensions; + +import com.powsybl.commons.PowsyblException; + +/** + * Represents a line segment defined by its start and end positions. + * The positions must satisfy the following conditions: + * - The start and end positions must be valid double values. + * - The start position must be greater than or equal to 0. + * - The end position must be less than or equal to 1. + * - The start position must be less than or equal to the end position. + * + * @author Coline Piloquet {@literal } + */ +public record LineSegment(double start, double end) { + + public static final LineSegment FULL_LINE = new LineSegment(0, 1); + + /** + * Constructs a LineSegment using the specified start and end positions. + * The line segment is valid only if the following conditions are met: + * - The start and end positions are valid double values. + * - The start position is greater than or equal to 0. + * - The end position is less than or equal to 1. + * - The start position is less than or equal to the end position. + */ + public LineSegment { + if (Double.isNaN(start) || Double.isNaN(end) || start < 0 || end > 1 || start > end) { + throw new PowsyblException("Invalid line segment: start: " + start + ", end: " + end + "."); + } + } +} diff --git a/iidm/iidm-extensions/src/main/java/com/powsybl/iidm/network/extensions/MutualCoupling.java b/iidm/iidm-extensions/src/main/java/com/powsybl/iidm/network/extensions/MutualCoupling.java new file mode 100644 index 00000000000..421a1dc42c1 --- /dev/null +++ b/iidm/iidm-extensions/src/main/java/com/powsybl/iidm/network/extensions/MutualCoupling.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.iidm.network.extensions; + +import com.powsybl.iidm.network.Line; + +/** + * This extension models electrical coupling between pairs of lines. + * + * @author Coline Piloquet {@literal } + */ +public interface MutualCoupling { + + /** + * Gets the first line. + * @return the first line + */ + Line getLine1(); + + /** + * Gets the second line. + * @return the second line + */ + Line getLine2(); + + /** + * Gets the mutual coupling resistance. + * @return the resistance in ohms + */ + double getR(); + + /** + * Sets the mutual coupling resistance. + * @param r the resistance in ohms + */ + void setR(double r); + + /** + * Gets the mutual coupling reactance. + * @return the reactance in ohms + */ + double getX(); + + /** + * Sets the mutual coupling reactance. + * @param x the reactance in ohms + */ + void setX(double x); + + /** + * Gets the starting and ending position of the mutual coupling on the first line. + * The positions are a proportion of the line length and are between 0 and 1. + * @return the starting position + */ + LineSegment getLine1Segment(); + + /** + * Gets the starting and ending position of the mutual coupling on the second line. + * The positions are a proportion of the line length and are between 0 and 1. + * @return the starting position + */ + LineSegment getLine2Segment(); + + /** + * Sets the starting and ending position of the mutual coupling on the second line. + * The positions are a proportion of the line length and are between 0 and 1. + * @param line1Segment the segment on line 1 + */ + void setLine1Segment(LineSegment line1Segment); + + /** + * Sets the starting and ending position of the mutual coupling on the second line. + * The positions are a proportion of the line length and are between 0 and 1. + * @param line2Segment the segment on line 2 + */ + void setLine2Segment(LineSegment line2Segment); + +} diff --git a/iidm/iidm-extensions/src/main/java/com/powsybl/iidm/network/extensions/MutualCouplingAdder.java b/iidm/iidm-extensions/src/main/java/com/powsybl/iidm/network/extensions/MutualCouplingAdder.java new file mode 100644 index 00000000000..56f4e77c0f2 --- /dev/null +++ b/iidm/iidm-extensions/src/main/java/com/powsybl/iidm/network/extensions/MutualCouplingAdder.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.iidm.network.extensions; + +import com.powsybl.iidm.network.Line; + +/** + * This interface is a builder to add a mutual coupling between two lines. + * + * @author Coline Piloquet {@literal } + */ +public interface MutualCouplingAdder { + + /** + * Sets the first line. + * @param line1 the first line + * @return the current mutual coupling adder + */ + MutualCouplingAdder withLine1(Line line1); + + /** + * Sets the second line. + * @param line2 the second line + * @return the current mutual coupling adder + */ + MutualCouplingAdder withLine2(Line line2); + + /** + * Sets the mutual coupling resistance. + * @param r the resistance in ohms + * @return the current mutual coupling adder + */ + MutualCouplingAdder withR(double r); + + /** + * Sets the mutual coupling reactance. + * @param x the reactance in ohms + * @return the current mutual coupling adder + */ + MutualCouplingAdder withX(double x); + + /** + * Sets the position of the mutual coupling on the first line. + * The positions are a proportion of the line length and are between 0 and 1. + * @param segment the segment of the line + * @return the current mutual coupling adder + */ + MutualCouplingAdder withLine1Segment(LineSegment segment); + + /** + * Sets the position of the mutual coupling on the second line. + * The positions are a proportion of the line length and are between 0 and 1. + * @param segment the segment of the line + * @return the current mutual coupling adder + */ + MutualCouplingAdder withLine2Segment(LineSegment segment); + + /** + * Adds the mutual coupling. + * @return the added mutual coupling + */ + MutualCoupling add(); +} diff --git a/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/extensions/LineCouplingsAdderImpl.java b/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/extensions/LineCouplingsAdderImpl.java new file mode 100644 index 00000000000..35aeba7dc1a --- /dev/null +++ b/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/extensions/LineCouplingsAdderImpl.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.iidm.network.impl.extensions; + +import com.powsybl.commons.extensions.AbstractExtensionAdder; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.extensions.LineCouplingsAdder; +import com.powsybl.iidm.network.extensions.LineCouplings; + +/** + * @author Coline Piloquet {@literal } + */ +public class LineCouplingsAdderImpl extends AbstractExtensionAdder implements LineCouplingsAdder { + + public LineCouplingsAdderImpl(Network network) { + super(network); + } + + @Override + protected LineCouplingsImpl createExtension(Network network) { + return new LineCouplingsImpl(); + } +} diff --git a/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/extensions/LineCouplingsAdderImplProvider.java b/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/extensions/LineCouplingsAdderImplProvider.java new file mode 100644 index 00000000000..e8d3e62b279 --- /dev/null +++ b/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/extensions/LineCouplingsAdderImplProvider.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.iidm.network.impl.extensions; + +import com.google.auto.service.AutoService; +import com.powsybl.commons.extensions.ExtensionAdderProvider; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.extensions.LineCouplingsAdder; +import com.powsybl.iidm.network.extensions.LineCouplings; + +/** + * @author Coline Piloquet {@literal } + */ +@AutoService(ExtensionAdderProvider.class) +public class LineCouplingsAdderImplProvider implements ExtensionAdderProvider { + + @Override + public String getImplementationName() { + return "Default"; + } + + @Override + public String getExtensionName() { + return LineCouplings.NAME; + } + + @Override + public Class getAdderClass() { + return LineCouplingsAdder.class; + } + + @Override + public LineCouplingsAdder newAdder(Network extendable) { + return new LineCouplingsAdderImpl(extendable); + } + +} diff --git a/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/extensions/LineCouplingsImpl.java b/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/extensions/LineCouplingsImpl.java new file mode 100644 index 00000000000..ffcc2fe641d --- /dev/null +++ b/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/extensions/LineCouplingsImpl.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.iidm.network.impl.extensions; + +import com.powsybl.commons.PowsyblException; +import com.powsybl.commons.extensions.AbstractExtension; +import com.powsybl.iidm.network.*; +import com.powsybl.iidm.network.extensions.LineCouplings; +import com.powsybl.iidm.network.extensions.MutualCoupling; +import com.powsybl.iidm.network.extensions.MutualCouplingAdder; + +import java.util.*; + +/** + * @author Coline Piloquet {@literal } + */ +public class LineCouplingsImpl extends AbstractExtension implements LineCouplings { + + private final List mutualCouplings = new LinkedList<>(); + private NetworkListener listener; + + @Override + public List getMutualCouplings() { + return Collections.unmodifiableList(mutualCouplings); + } + + @Override + public LineCouplings add(MutualCoupling mutualCoupling) { + Objects.requireNonNull(mutualCoupling); + + // Prevent duplicates and symmetrical mutual couplings + boolean alreadyExists = mutualCouplings.stream() + .anyMatch(mc -> matches(mc, mutualCoupling.getLine1(), mutualCoupling.getLine2())); + + if (alreadyExists) { + throw new PowsyblException("Mutual coupling already exists between lines " + + mutualCoupling.getLine1().getId() + " and " + + mutualCoupling.getLine2().getId()); + } + + mutualCouplings.add(mutualCoupling); + return this; + } + + @Override + public MutualCouplingAdder newMutualCoupling() { + return new MutualCouplingAdderImpl(this); + } + + @Override + public Optional findMutualCoupling(Line line1, Line line2) { + return mutualCouplings.stream() + .filter(mc -> matches(mc, line1, line2)) + .findFirst(); + } + + @Override + public boolean removeMutualCoupling(MutualCoupling mutualCoupling) { + return mutualCouplings.remove(mutualCoupling); + } + + @Override + public boolean removeMutualCoupling(Line line1, Line line2) { + return mutualCouplings.removeIf(mc -> matches(mc, line1, line2)); + } + + private boolean matches(MutualCoupling mutualCoupling, Line line1, Line line2) { + return mutualCoupling.getLine1() == line1 && mutualCoupling.getLine2() == line2 + || mutualCoupling.getLine1() == line2 && mutualCoupling.getLine2() == line1; + } + + @Override + public void setExtendable(Network network) { + // Clean potential previous subscription + unsubscribeListener(); + super.setExtendable(network); + + listener = new NetworkListener() { + @Override + public void beforeRemoval(Identifiable identifiable) { + if (identifiable instanceof Line line) { + mutualCouplings.removeIf(mc -> mc.getLine1() == line || mc.getLine2() == line); + } + } + }; + network.addListener(listener); + } + + @Override + public void cleanup() { + unsubscribeListener(); + } + + private void unsubscribeListener() { + if (this.getExtendable() != null && listener != null) { + this.getExtendable().removeListener(listener); + } + } +} diff --git a/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/extensions/MutualCouplingAdderImpl.java b/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/extensions/MutualCouplingAdderImpl.java new file mode 100644 index 00000000000..7f2eb91f0dc --- /dev/null +++ b/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/extensions/MutualCouplingAdderImpl.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.iidm.network.impl.extensions; + +import com.powsybl.commons.PowsyblException; +import com.powsybl.iidm.network.Line; +import com.powsybl.iidm.network.extensions.LineCouplings; +import com.powsybl.iidm.network.extensions.LineSegment; +import com.powsybl.iidm.network.extensions.MutualCoupling; +import com.powsybl.iidm.network.extensions.MutualCouplingAdder; + +import java.util.Objects; + +/** + * @author Coline Piloquet {@literal } + */ +public class MutualCouplingAdderImpl implements MutualCouplingAdder { + + LineCouplings mutualCouplings; + + private Line line1; + private Line line2; + + private double r; + private double x; + + private LineSegment line1Segment = LineSegment.FULL_LINE; + private LineSegment line2Segment = LineSegment.FULL_LINE; + + MutualCouplingAdderImpl(LineCouplings mutualCouplings) { + this.mutualCouplings = Objects.requireNonNull(mutualCouplings); + } + + @Override + public MutualCouplingAdder withLine1(Line line1) { + this.line1 = line1; + return this; + } + + @Override + public MutualCouplingAdder withLine2(Line line2) { + this.line2 = line2; + return this; + } + + @Override + public MutualCouplingAdder withR(double r) { + this.r = r; + return this; + } + + @Override + public MutualCouplingAdder withX(double x) { + this.x = x; + return this; + } + + @Override + public MutualCouplingAdder withLine1Segment(LineSegment segment) { + this.line1Segment = Objects.requireNonNull(segment); + return this; + } + + @Override + public MutualCouplingAdder withLine2Segment(LineSegment segment) { + this.line2Segment = Objects.requireNonNull(segment); + return this; + } + + @Override + public MutualCoupling add() { + validate(); + MutualCouplingImpl mutualCoupling = new MutualCouplingImpl(line1, line2, r, x, line1Segment, line2Segment); + mutualCouplings.add(mutualCoupling); + return mutualCoupling; + } + + private void validate() { + if (line1 == null || line2 == null) { + throw new PowsyblException("Lines cannot be null."); + } + if (line1.equals(line2)) { + throw new PowsyblException("Lines must be different."); + } + if (Double.isNaN(r)) { + throw new PowsyblException("r must be defined."); + } + if (Double.isNaN(x)) { + throw new PowsyblException("x must be defined."); + } + } +} diff --git a/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/extensions/MutualCouplingImpl.java b/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/extensions/MutualCouplingImpl.java new file mode 100644 index 00000000000..6e29b2c9f04 --- /dev/null +++ b/iidm/iidm-impl/src/main/java/com/powsybl/iidm/network/impl/extensions/MutualCouplingImpl.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.iidm.network.impl.extensions; + +import com.powsybl.iidm.network.Line; +import com.powsybl.iidm.network.extensions.LineSegment; +import com.powsybl.iidm.network.extensions.MutualCoupling; + +import java.util.Objects; + +/** + * @author Coline Piloquet {@literal } + */ +public class MutualCouplingImpl implements MutualCoupling { + + private final Line line1; + private final Line line2; + + private double r; + private double x; + + private LineSegment line1Segment; + private LineSegment line2Segment; + + MutualCouplingImpl(Line line1, Line line2, double r, double x, LineSegment line1Segment, LineSegment line2Segment) { + this.line1 = Objects.requireNonNull(line1); + this.line2 = Objects.requireNonNull(line2); + this.r = r; + this.x = x; + this.line1Segment = Objects.requireNonNull(line1Segment); + this.line2Segment = Objects.requireNonNull(line2Segment); + } + + @Override + public Line getLine1() { + return line1; + } + + @Override + public Line getLine2() { + return line2; + } + + @Override + public double getR() { + return r; + } + + @Override + public void setR(double r) { + this.r = r; + } + + @Override + public double getX() { + return x; + } + + @Override + public void setX(double x) { + this.x = x; + } + + @Override + public LineSegment getLine1Segment() { + return line1Segment; + } + + @Override + public LineSegment getLine2Segment() { + return line2Segment; + } + + @Override + public void setLine1Segment(LineSegment lineSegment) { + this.line1Segment = Objects.requireNonNull(lineSegment); + } + + @Override + public void setLine2Segment(LineSegment lineSegment) { + this.line2Segment = Objects.requireNonNull(lineSegment); + } +} diff --git a/iidm/iidm-impl/src/test/java/com/powsybl/iidm/network/impl/tck/extensions/LineCouplingsTest.java b/iidm/iidm-impl/src/test/java/com/powsybl/iidm/network/impl/tck/extensions/LineCouplingsTest.java new file mode 100644 index 00000000000..8f493cc4eeb --- /dev/null +++ b/iidm/iidm-impl/src/test/java/com/powsybl/iidm/network/impl/tck/extensions/LineCouplingsTest.java @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.iidm.network.impl.tck.extensions; + +import com.powsybl.iidm.network.tck.extensions.AbstractLineCouplingsTest; + +/** + * @author Coline Piloquet {@literal } + */ +class LineCouplingsTest extends AbstractLineCouplingsTest { +} diff --git a/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/LineCouplingsSerDe.java b/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/LineCouplingsSerDe.java new file mode 100644 index 00000000000..78831702c54 --- /dev/null +++ b/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/LineCouplingsSerDe.java @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.iidm.serde.extensions; + +import com.google.auto.service.AutoService; +import com.powsybl.commons.PowsyblException; +import com.powsybl.commons.extensions.ExtensionSerDe; +import com.powsybl.commons.io.DeserializerContext; +import com.powsybl.commons.io.SerializerContext; +import com.powsybl.commons.io.TreeDataReader; +import com.powsybl.commons.io.TreeDataWriter; +import com.powsybl.iidm.network.Line; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.extensions.*; +import com.powsybl.iidm.serde.IidmVersion; + +import java.util.Map; + +/** + * @author Coline Piloquet {@literal } + */ +@AutoService(ExtensionSerDe.class) +public class LineCouplingsSerDe extends AbstractVersionableNetworkExtensionSerDe { + + public static final String MUTUAL_COUPLING_ROOT_ELEMENT_NAME = "mutualCoupling"; + + public enum Version implements SerDeVersion { + V_1_0("/xsd/lineCouplings_V1_0.xsd", "http://www.powsybl.org/schema/iidm/ext/line_couplings/1_0", + new VersionNumbers(1, 0), IidmVersion.V_1_17, null); + + private final VersionInfo versionInfo; + + Version(String xsdResourcePath, String namespaceUri, VersionNumbers versionNumbers, IidmVersion minIidmVersionIncluded, IidmVersion maxIidmVersionExcluded) { + this.versionInfo = new VersionInfo(xsdResourcePath, namespaceUri, "lmc", versionNumbers, + minIidmVersionIncluded, maxIidmVersionExcluded, LineCouplings.NAME); + } + + @Override + public VersionInfo getVersionInfo() { + return versionInfo; + } + } + + public LineCouplingsSerDe() { + super(LineCouplings.NAME, LineCouplings.class, Version.values()); + } + + @Override + public Map getArrayNameToSingleNameMap() { + return Map.of(LineCouplings.NAME, MUTUAL_COUPLING_ROOT_ELEMENT_NAME); + } + + @Override + public void write(LineCouplings extension, SerializerContext context) { + TreeDataWriter writer = context.getWriter(); + writer.writeStartNodes(); + for (MutualCoupling mutualCoupling : extension.getMutualCouplings()) { + writer.writeStartNode(getNamespaceUri(), MUTUAL_COUPLING_ROOT_ELEMENT_NAME); + writer.writeStringAttribute("line1", mutualCoupling.getLine1().getId()); + writer.writeStringAttribute("line2", mutualCoupling.getLine2().getId()); + writer.writeDoubleAttribute("r", mutualCoupling.getR()); + writer.writeDoubleAttribute("x", mutualCoupling.getX()); + writer.writeDoubleAttribute("line1Start", mutualCoupling.getLine1Segment().start(), 0); + writer.writeDoubleAttribute("line1End", mutualCoupling.getLine1Segment().end(), 1); + writer.writeDoubleAttribute("line2Start", mutualCoupling.getLine2Segment().start(), 0); + writer.writeDoubleAttribute("line2End", mutualCoupling.getLine2Segment().end(), 1); + writer.writeEndNode(); + } + writer.writeEndNodes(); + } + + @Override + public LineCouplings read(Network network, DeserializerContext context) { + TreeDataReader reader = context.getReader(); + LineCouplings extension = network.newExtension(LineCouplingsAdder.class).add(); + reader.readChildNodes(elementName -> { + if (elementName.equals(MUTUAL_COUPLING_ROOT_ELEMENT_NAME)) { + String line1Id = reader.readStringAttribute("line1"); + String line2Id = reader.readStringAttribute("line2"); + double r = reader.readDoubleAttribute("r"); + double x = reader.readDoubleAttribute("x"); + LineSegment segment1 = readSegment(reader, "line1Start", "line1End"); + LineSegment segment2 = readSegment(reader, "line2Start", "line2End"); + + Line line1 = network.getLine(line1Id); + Line line2 = network.getLine(line2Id); + + if (line1 == null) { + throw new PowsyblException("Line '" + line1Id + "' not found in network."); + } + if (line2 == null) { + throw new PowsyblException("Line '" + line2Id + "' not found in network."); + } + + MutualCouplingAdder adder = extension.newMutualCoupling(); + adder.withLine1(line1) + .withLine2(line2) + .withR(r) + .withX(x) + .withLine1Segment(segment1) + .withLine2Segment(segment2) + .add(); + + reader.readEndNode(); + } else { + throw new PowsyblException("Unknown element name '" + elementName + "' in 'lineCouplings'"); + } + }); + return extension; + } + + private static LineSegment readSegment(TreeDataReader reader, + String startAttr, + String endAttr) { + + return new LineSegment( + reader.readDoubleAttribute(startAttr, 0), + reader.readDoubleAttribute(endAttr, 1) + ); + } + + @Override + public boolean isSerializable(LineCouplings extension) { + return !extension.getMutualCouplings().isEmpty(); + } +} diff --git a/iidm/iidm-serde/src/main/resources/xsd/lineCouplings_V1_0.xsd b/iidm/iidm-serde/src/main/resources/xsd/lineCouplings_V1_0.xsd new file mode 100644 index 00000000000..a54f4ad3aa9 --- /dev/null +++ b/iidm/iidm-serde/src/main/resources/xsd/lineCouplings_V1_0.xsd @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/extensions/LineCouplingsXmlTest.java b/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/extensions/LineCouplingsXmlTest.java new file mode 100644 index 00000000000..ffc29ae8d54 --- /dev/null +++ b/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/extensions/LineCouplingsXmlTest.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.iidm.serde.extensions; + +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.extensions.LineCouplings; +import com.powsybl.iidm.network.extensions.LineCouplingsAdder; +import com.powsybl.iidm.network.extensions.LineSegment; +import com.powsybl.iidm.network.extensions.MutualCoupling; +import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory; +import com.powsybl.iidm.serde.AbstractIidmSerDeTest; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Coline Piloquet {@literal } + */ +class LineCouplingsXmlTest extends AbstractIidmSerDeTest { + + @Test + void test() throws IOException { + Network network = EurostagTutorialExample1Factory.create(); + network.setCaseDate(ZonedDateTime.parse("2016-12-07T11:18:52.881+01:00")); + network.newExtension(LineCouplingsAdder.class).add(); + network.getExtension(LineCouplings.class) + .newMutualCoupling() + .withLine1(network.getLine("NHV1_NHV2_1")) + .withLine2(network.getLine("NHV1_NHV2_2")) + .withR(0.5) + .withX(1.0) + .add(); + + Network network2 = allFormatsRoundTripTest(network, "/lineCouplingsRef_V1_0.xml"); + + LineCouplings mutualCouplings = network2.getExtension(LineCouplings.class); + assertEquals(1, mutualCouplings.getMutualCouplings().size()); + MutualCoupling mutualCoupling = mutualCouplings.getMutualCouplings().getFirst(); + assertEquals("NHV1_NHV2_1", mutualCoupling.getLine1().getId()); + assertEquals("NHV1_NHV2_2", mutualCoupling.getLine2().getId()); + assertEquals(0.5, mutualCoupling.getR()); + assertEquals(1, mutualCoupling.getX()); + + assertEquals(LineSegment.FULL_LINE, mutualCoupling.getLine1Segment()); + assertEquals(LineSegment.FULL_LINE, mutualCoupling.getLine2Segment()); + } + + @Test + void testWithLineSegments() throws IOException { + Network network = EurostagTutorialExample1Factory.create(); + network.setCaseDate(ZonedDateTime.parse("2016-12-07T11:18:52.881+01:00")); + network.newExtension(LineCouplingsAdder.class).add(); + network.getExtension(LineCouplings.class) + .newMutualCoupling() + .withLine1(network.getLine("NHV1_NHV2_1")) + .withLine2(network.getLine("NHV1_NHV2_2")) + .withLine1Segment(new LineSegment(0.4, 0.8)) + .withLine2Segment(new LineSegment(0.2, 0.6)) + .withR(0.5) + .withX(1.0) + .add(); + + Network network2 = allFormatsRoundTripTest(network, "/lineCouplingsRef-V1_0_with_lineSegments.xml"); + + LineCouplings mutualCouplings = network2.getExtension(LineCouplings.class); + assertEquals(1, mutualCouplings.getMutualCouplings().size()); + MutualCoupling mutualCoupling = mutualCouplings.getMutualCouplings().getFirst(); + assertEquals("NHV1_NHV2_1", mutualCoupling.getLine1().getId()); + assertEquals("NHV1_NHV2_2", mutualCoupling.getLine2().getId()); + assertEquals(0.5, mutualCoupling.getR()); + assertEquals(1, mutualCoupling.getX()); + assertEquals(new LineSegment(0.4, 0.8), mutualCoupling.getLine1Segment()); + assertEquals(new LineSegment(0.2, 0.6), mutualCoupling.getLine2Segment()); + } +} diff --git a/iidm/iidm-serde/src/test/resources/lineCouplingsRef-V1_0_with_lineSegments.xml b/iidm/iidm-serde/src/test/resources/lineCouplingsRef-V1_0_with_lineSegments.xml new file mode 100644 index 00000000000..8d9c7a8d86f --- /dev/null +++ b/iidm/iidm-serde/src/test/resources/lineCouplingsRef-V1_0_with_lineSegments.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/iidm/iidm-serde/src/test/resources/lineCouplingsRef_V1_0.xml b/iidm/iidm-serde/src/test/resources/lineCouplingsRef_V1_0.xml new file mode 100644 index 00000000000..090ac7aee70 --- /dev/null +++ b/iidm/iidm-serde/src/test/resources/lineCouplingsRef_V1_0.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/iidm/iidm-tck/src/test/java/com/powsybl/iidm/network/tck/extensions/AbstractLineCouplingsTest.java b/iidm/iidm-tck/src/test/java/com/powsybl/iidm/network/tck/extensions/AbstractLineCouplingsTest.java new file mode 100644 index 00000000000..ec5905151be --- /dev/null +++ b/iidm/iidm-tck/src/test/java/com/powsybl/iidm/network/tck/extensions/AbstractLineCouplingsTest.java @@ -0,0 +1,297 @@ +/** + * Copyright (c) 2026, 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/. + * SPDX-License-Identifier: MPL-2.0 + */ +package com.powsybl.iidm.network.tck.extensions; + +import com.powsybl.commons.PowsyblException; +import com.powsybl.iidm.network.Line; +import com.powsybl.iidm.network.Network; +import com.powsybl.iidm.network.extensions.*; +import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory; +import org.junit.jupiter.api.Test; + +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author Coline Piloquet {@literal } + */ +public abstract class AbstractLineCouplingsTest { + + @Test + public void test() { + Network network = EurostagTutorialExample1Factory.create(); + network.setCaseDate(ZonedDateTime.parse("2016-12-07T11:18:52.881+01:00")); + network.newExtension(LineCouplingsAdder.class).add(); + network.getExtension(LineCouplings.class) + .newMutualCoupling() + .withLine1(network.getLine("NHV1_NHV2_1")) + .withLine2(network.getLine("NHV1_NHV2_2")) + .withR(0.5) + .withX(1.0) + .add(); + + LineCouplings mutualCouplings = network.getExtension(LineCouplings.class); + assertEquals(1, mutualCouplings.getMutualCouplings().size()); + MutualCoupling mutualCoupling = mutualCouplings.getMutualCouplings().getFirst(); + assertEquals("NHV1_NHV2_1", mutualCoupling.getLine1().getId()); + assertEquals("NHV1_NHV2_2", mutualCoupling.getLine2().getId()); + assertEquals(0.5, mutualCoupling.getR()); + assertEquals(1.0, mutualCoupling.getX()); + assertEquals(LineSegment.FULL_LINE, mutualCoupling.getLine1Segment()); + assertEquals(LineSegment.FULL_LINE, mutualCoupling.getLine2Segment()); + } + + @Test + void testSameLine() { + Network network = EurostagTutorialExample1Factory.create(); + network.newExtension(LineCouplingsAdder.class).add(); + + Line line = network.getLine("NHV1_NHV2_1"); + MutualCouplingAdder adder = network.getExtension(LineCouplings.class) + .newMutualCoupling() + .withLine1(line) + .withLine2(line) + .withR(0.1) + .withX(0.2); + + PowsyblException exception = assertThrows(PowsyblException.class, adder::add); + assertEquals("Lines must be different.", exception.getMessage()); + } + + @Test + void testNullLine() { + Network network = EurostagTutorialExample1Factory.create(); + network.newExtension(LineCouplingsAdder.class).add(); + + Line line = network.getLine("NHV1_NHV2_1"); + MutualCouplingAdder adder = network.getExtension(LineCouplings.class) + .newMutualCoupling() + .withLine1(line) + .withLine2(null) + .withR(0.1) + .withX(0.2); + + PowsyblException exception = assertThrows(PowsyblException.class, adder::add); + assertEquals("Lines cannot be null.", exception.getMessage()); + + adder.withLine1(null) + .withLine2(line); + + PowsyblException exception2 = assertThrows(PowsyblException.class, adder::add); + assertEquals("Lines cannot be null.", exception2.getMessage()); + + } + + @Test + void testDuplicateCoupling() { + Network network = EurostagTutorialExample1Factory.create(); + network.newExtension(LineCouplingsAdder.class).add(); + + LineCouplings lc = network.getExtension(LineCouplings.class); + + lc.newMutualCoupling() + .withLine1(network.getLine("NHV1_NHV2_1")) + .withLine2(network.getLine("NHV1_NHV2_2")) + .withR(0.1) + .withX(0.2) + .add(); + + MutualCouplingAdder adder = lc.newMutualCoupling() + .withLine1(network.getLine("NHV1_NHV2_2")) + .withLine2(network.getLine("NHV1_NHV2_1")) + .withR(0.3) + .withX(0.4); + + PowsyblException exception = assertThrows(PowsyblException.class, adder::add); + assertEquals("Mutual coupling already exists between lines NHV1_NHV2_2 and NHV1_NHV2_1", exception.getMessage()); + } + + @Test + void testFindSymmetric() { + Network network = EurostagTutorialExample1Factory.create(); + network.newExtension(LineCouplingsAdder.class).add(); + + LineCouplings lc = network.getExtension(LineCouplings.class); + + Line l1 = network.getLine("NHV1_NHV2_1"); + Line l2 = network.getLine("NHV1_NHV2_2"); + + lc.newMutualCoupling() + .withLine1(l1) + .withLine2(l2) + .withR(0.1) + .withX(0.2) + .add(); + + assertTrue(lc.findMutualCoupling(l1, l2).isPresent()); + assertTrue(lc.findMutualCoupling(l2, l1).isPresent()); + } + + @Test + void testRemoveByLines() { + Network network = EurostagTutorialExample1Factory.create(); + network.newExtension(LineCouplingsAdder.class).add(); + + LineCouplings lc = network.getExtension(LineCouplings.class); + + Line l1 = network.getLine("NHV1_NHV2_1"); + Line l2 = network.getLine("NHV1_NHV2_2"); + + lc.newMutualCoupling() + .withLine1(l1) + .withLine2(l2) + .withR(0.1) + .withX(0.2) + .add(); + + // Test with mutual coupling that does not exist + assertFalse(lc.removeMutualCoupling(l1, l1)); + + assertTrue(lc.removeMutualCoupling(l2, l1)); + assertEquals(0, lc.getMutualCouplings().size()); + } + + @Test + void testRemoveByMutualCoupling() { + Network network = EurostagTutorialExample1Factory.create(); + network.newExtension(LineCouplingsAdder.class).add(); + + LineCouplings lc = network.getExtension(LineCouplings.class); + + Line l1 = network.getLine("NHV1_NHV2_1"); + Line l2 = network.getLine("NHV1_NHV2_2"); + + lc.newMutualCoupling() + .withLine1(l1) + .withLine2(l2) + .withR(0.1) + .withX(0.2) + .add(); + + assertTrue(lc.removeMutualCoupling(lc.getMutualCouplings().getFirst())); + assertEquals(0, lc.getMutualCouplings().size()); + } + + @Test + void testSetters() { + Network network = EurostagTutorialExample1Factory.create(); + network.newExtension(LineCouplingsAdder.class).add(); + + Line l1 = network.getLine("NHV1_NHV2_1"); + Line l2 = network.getLine("NHV1_NHV2_2"); + + MutualCoupling mc = network.getExtension(LineCouplings.class) + .newMutualCoupling() + .withLine1(l1) + .withLine2(l2) + .withR(0.1) + .withX(0.2) + .add(); + + assertEquals(0.1, mc.getR()); + assertEquals(0.2, mc.getX()); + assertEquals(LineSegment.FULL_LINE, mc.getLine1Segment()); + assertEquals(LineSegment.FULL_LINE, mc.getLine2Segment()); + + mc.setR(0.5); + mc.setX(1.2); + mc.setLine1Segment(new LineSegment(0.2, 0.8)); + mc.setLine2Segment(new LineSegment(0.1, 0.9)); + + assertEquals(0.5, mc.getR()); + assertEquals(1.2, mc.getX()); + assertEquals(new LineSegment(0.2, 0.8), mc.getLine1Segment()); + assertEquals(new LineSegment(0.1, 0.9), mc.getLine2Segment()); + } + + @Test + void testInvalidLineSegment() { + PowsyblException exception = assertThrows(PowsyblException.class, () -> new LineSegment(0.6, 0.4)); + assertEquals("Invalid line segment: start: 0.6, end: 0.4.", exception.getMessage()); + + PowsyblException exception2 = assertThrows(PowsyblException.class, () -> new LineSegment(-0.5, 1.1)); + assertEquals("Invalid line segment: start: -0.5, end: 1.1.", exception2.getMessage()); + } + + @Test + void testInvalidRAndX() { + Network network = EurostagTutorialExample1Factory.create(); + Line l1 = network.getLine("NHV1_NHV2_1"); + Line l2 = network.getLine("NHV1_NHV2_2"); + network.newExtension(LineCouplingsAdder.class).add(); + LineCouplings lc = network.getExtension(LineCouplings.class); + // R is NaN + MutualCouplingAdder mutualCouplingAdder = lc.newMutualCoupling().withLine1(l1).withLine2(l2).withR(Double.NaN).withX(0.1); + PowsyblException exception = assertThrows(PowsyblException.class, mutualCouplingAdder::add); + assertEquals("r must be defined.", exception.getMessage()); + + // X is NaN + mutualCouplingAdder = lc.newMutualCoupling().withLine1(l1).withLine2(l2).withR(0.1).withX(Double.NaN); + exception = assertThrows(PowsyblException.class, mutualCouplingAdder::add); + assertEquals("x must be defined.", exception.getMessage()); + } + + @Test + void testListener() { + Network network = EurostagTutorialExample1Factory.create(); + Line l1 = network.getLine("NHV1_NHV2_1"); + Line l2 = network.getLine("NHV1_NHV2_2"); + Line l1Copy = network.newLine(l1) + .setId("NHV1_NHV2_1_COPY") + .setBus1(EurostagTutorialExample1Factory.NHV1) + .setConnectableBus1(EurostagTutorialExample1Factory.NHV1) + .setBus2(EurostagTutorialExample1Factory.NHV2) + .setConnectableBus2(EurostagTutorialExample1Factory.NHV2) + .add(); + Line l2Copy = network.newLine(l2) + .setId("NHV1_NHV2_2_COPY") + .setBus1(EurostagTutorialExample1Factory.NHV1) + .setConnectableBus1(EurostagTutorialExample1Factory.NHV1) + .setBus2(EurostagTutorialExample1Factory.NHV2) + .setConnectableBus2(EurostagTutorialExample1Factory.NHV2) + .add(); + + network.newExtension(LineCouplingsAdder.class).add(); + LineCouplings lc = network.getExtension(LineCouplings.class); + lc.newMutualCoupling() + .withLine1(l1) + .withLine2(l2) + .withR(0.1) + .withX(0.2) + .add(); + lc.newMutualCoupling() + .withLine1(l1Copy) + .withLine2(l2) + .withR(0.1) + .withX(0.2) + .add(); + lc.newMutualCoupling() + .withLine1(l1) + .withLine2(l2Copy) + .withR(0.1) + .withX(0.2) + .add(); + lc.newMutualCoupling() + .withLine1(l2) + .withLine2(l2Copy) + .withR(0.1) + .withX(0.2) + .add(); + assertEquals(4, lc.getMutualCouplings().size()); + + l1Copy.remove(); + assertEquals(3, lc.getMutualCouplings().size()); + + l2Copy.remove(); + assertEquals(1, lc.getMutualCouplings().size()); + } +}