Skip to content
3 changes: 3 additions & 0 deletions docs/loadflow/loadflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ $\sum_{i} I_i + \frac{V_1 - V_2}{R}= 0$ for dcBus1

$\sum_{i} I_i - \frac{V_1 - V_2}{R}= 0$ for dcBus2

If a DC line is disconnected on any side, no current can pass through it.

### Line Commutated Converter

Expand All @@ -377,6 +378,8 @@ The voltage source converter is the link between AC and DC networks, it is linke
DC buses at the other side.<br>
Please note that converters with a second optional AC terminal are not supported by Open Load Flow.

If a terminal of the converter is disconnected, the converter is not included in the load flow, and none of the equations below is added.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will probably require a bit more details, e.g. whether an equation imposing I=0 is added, or what happens with losses and on the AC side.


The converter can control either the power received by the AC network (`P_PCC` control mode)
or the voltage between its two DC buses (`V_DC` control mode).
At least one of the voltage source converters of the DC network must be in `V_DC` mode. Otherwise, an exception will be thrown.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -805,11 +805,16 @@ protected void createDcLineEquations(LfDcLine dcLine, LfDcBus dcBus1, LfDcBus dc
SingleEquationTerm<AcVariableType, AcEquationType> closedP2 = null;
SingleEquationTerm<AcVariableType, AcEquationType> closedI2 = null;

// open equations, could be null because only necessary if already open and never closed, or open during simulation
SingleEquationTerm<AcVariableType, AcEquationType> openP1 = null;
SingleEquationTerm<AcVariableType, AcEquationType> openI1 = null;
SingleEquationTerm<AcVariableType, AcEquationType> openP2 = null;
SingleEquationTerm<AcVariableType, AcEquationType> openI2 = null;

if (dcBus1 != null && dcBus2 != null) {
if (!dcBus1.isGrounded()) {
closedP1 = new ClosedDcLineSide1PowerEquationTerm(dcLine, dcBus1, dcBus2, equationSystem.getVariableSet());
closedI1 = new ClosedDcLineSide1CurrentEquationTerm(dcLine, dcBus1, dcBus2, equationSystem.getVariableSet());

}
if (!dcBus2.isGrounded()) {
closedP2 = new ClosedDcLineSide2PowerEquationTerm(dcLine, dcBus1, dcBus2, equationSystem.getVariableSet());
Expand All @@ -819,13 +824,27 @@ protected void createDcLineEquations(LfDcLine dcLine, LfDcBus dcBus1, LfDcBus dc
i1 = closedI1;
p2 = closedP2;
i2 = closedI2;
} else if (dcBus1 != null) {
openP1 = new OpenDcLineEquationTerm(dcLine);
openI1 = new OpenDcLineEquationTerm(dcLine);
p1 = openP1;
i1 = openI1;
p2 = EvaluableConstants.ZERO;
i2 = EvaluableConstants.ZERO;
} else if (dcBus2 != null) {
openP2 = new OpenDcLineEquationTerm(dcLine);
openI2 = new OpenDcLineEquationTerm(dcLine);
p1 = EvaluableConstants.ZERO;
i1 = EvaluableConstants.ZERO;
p2 = openP2;
i2 = openI2;
}

createDcLineEquations(dcLine, dcBus1, dcBus2, equationSystem,
p1, i1,
p2, i2,
closedP1, closedI1,
closedP2, closedI2);
p1, i1, p2, i2,
closedP1, closedI1, closedP2, closedI2,
openP1, openI1, openP2, openI2
);
}

protected EquationTerm<AcVariableType, AcEquationType> createClosedBranchSide1ActiveFlowEquationTerm(LfBranch branch, LfBus bus1, LfBus bus2,
Expand Down Expand Up @@ -957,33 +976,48 @@ protected static void createDcLineEquations(LfDcLine dcLine, LfDcBus dcBus1, LfD
Evaluable p1, Evaluable i1,
Evaluable p2, Evaluable i2,
SingleEquationTerm<AcVariableType, AcEquationType> closedP1, SingleEquationTerm<AcVariableType, AcEquationType> closedI1,
SingleEquationTerm<AcVariableType, AcEquationType> closedP2, SingleEquationTerm<AcVariableType, AcEquationType> closedI2) {

SingleEquationTerm<AcVariableType, AcEquationType> closedP2, SingleEquationTerm<AcVariableType, AcEquationType> closedI2,
SingleEquationTerm<AcVariableType, AcEquationType> openP1, SingleEquationTerm<AcVariableType, AcEquationType> openI1,
SingleEquationTerm<AcVariableType, AcEquationType> openP2, SingleEquationTerm<AcVariableType, AcEquationType> openI2) {
if (closedI1 != null) {
equationSystem.getEquation(dcBus1.getNum(), AcEquationType.DC_BUS_TARGET_I).orElseThrow()
.addTerm(closedI1);
}
if (openI1 != null) {
equationSystem.getEquation(dcBus1.getNum(), AcEquationType.DC_BUS_TARGET_I).orElseThrow()
.addTerm(openI1);
}
if (i1 != null) {
dcLine.setI1(i1);
}
if (closedI2 != null) {
equationSystem.getEquation(dcBus2.getNum(), AcEquationType.DC_BUS_TARGET_I).orElseThrow()
.addTerm(closedI2);
}
if (openI2 != null) {
equationSystem.getEquation(dcBus2.getNum(), AcEquationType.DC_BUS_TARGET_I).orElseThrow()
.addTerm(openI2);
}
if (i2 != null) {
dcLine.setI2(i2);
}

if (closedP1 != null) {
equationSystem.attach(closedP1);
}
if (openP1 != null) {
equationSystem.attach(openP1);
}
if (p1 != null) {
dcLine.setP1(p1);
}

if (closedP2 != null) {
equationSystem.attach(closedP2);
}
if (openP2 != null) {
equationSystem.attach(openP2);
}
if (p2 != null) {
dcLine.setP2(p2);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* Copyright (c) 2026, SuperGrid Institute (http://www.supergrid-institute.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.openloadflow.ac.equations.dcnetwork;

import com.powsybl.openloadflow.ac.equations.AcEquationType;
import com.powsybl.openloadflow.ac.equations.AcVariableType;
import com.powsybl.openloadflow.equations.AbstractElementEquationTerm;
import com.powsybl.openloadflow.equations.Variable;
import com.powsybl.openloadflow.network.LfDcLine;

import java.util.List;
import java.util.Objects;

/**
* Equation term returning 0 for the current in an open DC line.
*
* @author Baptiste Perreyon {@literal <baptiste.perreyon at supergrid-institute.com>}
*/
public class OpenDcLineEquationTerm extends AbstractElementEquationTerm<LfDcLine, AcVariableType, AcEquationType> {

public OpenDcLineEquationTerm(LfDcLine dcLine) {
super(dcLine);
}

@Override
public double eval() {
return 0.0;
}

@Override
public double der(Variable<AcVariableType> variable) {
Objects.requireNonNull(variable);
throw new IllegalStateException("Unknown variable: " + variable);
}

@Override
public String getName() {
return "dc_open";
Comment thread
bperr marked this conversation as resolved.
Outdated
}

@Override
public List<Variable<AcVariableType>> getVariables() {
return List.of();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* Copyright (c) 2026, SuperGrid Institute (http://www.supergrid-institute.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.openloadflow.network.impl;

import com.powsybl.commons.PowsyblException;
import com.powsybl.iidm.network.*;

import java.util.*;
import java.util.function.Predicate;

/**
* Helper methods to validate a DcComponent configuration
*
* @author Baptiste Perreyon {@literal <baptiste.perreyon at supergrid-institute.com>}
*/
final class DcNetworkValidationHelpers {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call to factor out these static functions. I am wondering if it is possible to simplify this code a bit. Maybe extract the topology visitor, and replace the predicate on converters by a boolean stating whether the test is needed or not?


private DcNetworkValidationHelpers() {
}

/**
* Check that at least one of the DC terminal of each AC-DC converter is indirectly connected (i.e. through DC
* lines) to a DC ground.
Comment thread
bperr marked this conversation as resolved.
Outdated
*
* @param acDcConverters A list of AC-DC converters to check.
* @throws PowsyblException If at least one AC-DC converter is not indirectly connected to a DC ground
*/
public static void checkAllConvertersAreIndirectlyConnectedToADcGround(List<AcDcConverter<?>> acDcConverters) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to leave it package-private? I think these static methods are only used in LfNetworkLoaderImpl anyway.

for (AcDcConverter<?> converter : acDcConverters) {
boolean isTerminal1IndirectlyConnectedToGround = isConnectedToGround(converter.getDcTerminal1());
boolean isTerminal2IndirectlyConnectedToGround = isConnectedToGround(converter.getDcTerminal2());
if (!isTerminal1IndirectlyConnectedToGround && !isTerminal2IndirectlyConnectedToGround) {
throw new PowsyblException(String.format("Converter %s is not indirectly connected to a DC ground", converter.getId()));
}
}
}

/**
* Check that at least one AC-DC converter controls the DC voltage.
*
* @param acDcConverters A list of AC-DC converters to check.
* @throws PowsyblException If no AC-DC converter control the DC voltage.
*/
public static void checkAtLeastOneConverterControlsVdc(List<AcDcConverter<?>> acDcConverters, int numDcc) {
// Ensure at least one converter controls the DC voltage. V_DC controls it directly; P_PCC_DROOP also counts
// as DC-voltage control since the droop law settles the DC voltage.
boolean isVdcControlled = false;
for (AcDcConverter<?> converter : acDcConverters) {
if (converter.getControlMode() == AcDcConverter.ControlMode.V_DC || converter.getControlMode() == AcDcConverter.ControlMode.P_PCC_DROOP) {
isVdcControlled = true;
break;
}
}
if (!isVdcControlled) {
throw new PowsyblException("At least one AC/DC converter control mode must be V_DC or P_PCC_DROOP in each DC component, but DC component " + numDcc + " does not have any");
}
}

/**
* Check that both DC terminals of each AC-DC converter in P_PCC mode are indirectly connected (i.e. through DC
* lines) to an element imposing voltage (a non P_PCC AC-DC converter or a DC ground).
*
* @param acDcConverters A list of AC-DC converters to check.
* @throws PowsyblException If at least one terminal of one AC-DC converter is not indirectly connected to an
* element imposing voltage
*/
public static void checkPccConverterAreIndirectlyConnectedToElementImposingVoltage(List<AcDcConverter<?>> acDcConverters) {
for (AcDcConverter<?> converter : acDcConverters) {
if (converter.getControlMode() == AcDcConverter.ControlMode.P_PCC) {
boolean isConverterTerminal1Fine = isConnectedToNonPccConverterOrGround(converter.getDcTerminal1());
if (!isConverterTerminal1Fine) {
throw new PowsyblException(String.format("Converter %s is in P_PCC control mode but its first DC bus is not connected to an element imposing voltage", converter.getId()));
}

boolean isConverterTerminal2Fine = isConnectedToNonPccConverterOrGround(converter.getDcTerminal2());
if (!isConverterTerminal2Fine) {
throw new PowsyblException(String.format("Converter %s is in P_PCC control mode but its second DC bus is not connected to an element imposing voltage", converter.getId()));
}
}
}
}

/**
* Perform topological search to find out if the terminal of a AC-DC converter in P_PCC mode is indirectly connected
* (i.e. through DC lines) to an element imposing voltage (a DC ground or a AC-DC converter not in P_PCC mode)
*
* @param startTerminal The terminal of the AC-DC converter used as a starting point.
* @return true if the terminal is indirectly connected to an element imposing voltage, false otherwise.
*/
private static boolean isConnectedToNonPccConverterOrGround(DcTerminal startTerminal) {
return isIndirectlyConnectedToVoltageImposingElement(startTerminal, converter ->
converter.getControlMode() != AcDcConverter.ControlMode.P_PCC
&& converter.getDcTerminal1().isConnected() && converter.getDcTerminal2().isConnected()
&& converter.getTerminal1().isConnected());
}

/**
* Perform topological search to find out if the terminal of a AC-DC converter is indirectly connected (i.e.
* through DC lines) to a DC ground.
*
* @param startTerminal The terminal of the AC-DC converter used as a starting point.
* @return true if the terminal is indirectly connected to a DC ground, false otherwise.
*/
private static boolean isConnectedToGround(DcTerminal startTerminal) {
return isIndirectlyConnectedToVoltageImposingElement(startTerminal, converter -> false);
}

/**
* Perform a breadth-first topological search, following only DC lines, to find out if the terminal of a AC-DC
* converter is indirectly connected to an element imposing voltage: a connected DC ground, or an AC-DC converter
* satisfying {@code isVoltageImposingConverter}.
*
* @param startTerminal The terminal of the AC-DC converter used as a starting point.
* @param isVoltageImposingConverter Tells whether an encountered AC-DC converter should, on its own, be
* considered as imposing voltage. Pass a predicate always returning false to
* only look for a DC ground.
* @return true if the terminal is indirectly connected to an element imposing voltage, false otherwise.
*/
private static boolean isIndirectlyConnectedToVoltageImposingElement(DcTerminal startTerminal,

Check failure on line 124 in src/main/java/com/powsybl/openloadflow/network/impl/DcNetworkValidationHelpers.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 20 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=com.powsybl%3Apowsybl-open-loadflow&issues=AZ-TCYQi4hBPh09VQHyL&open=AZ-TCYQi4hBPh09VQHyL&pullRequest=1471
Predicate<AcDcConverter<?>> isVoltageImposingConverter) {
DcBus startBus = startTerminal.getDcBus();
if (startBus == null) {
return false; // startTerminal not connected
}

Set<String> visitedBusIds = new HashSet<>();
Deque<DcBus> queue = new ArrayDeque<>();
visitedBusIds.add(startBus.getId());
queue.add(startBus);

while (!queue.isEmpty()) {
DcBus currentBus = queue.poll();
boolean[] found = {false};

currentBus.visitConnectedEquipments(new DcTopologyVisitor() {
@Override
public void visitAcDcConverter(AcDcConverter<?> converter, TerminalNumber terminalNumber) {
if (isVoltageImposingConverter.test(converter)) {
found[0] = true;
}
}

@Override
public void visitDcLine(DcLine dcLine, TwoSides side) {
// side is connected to currentBus; check the other side
TwoSides otherSide = (side == TwoSides.ONE) ? TwoSides.TWO : TwoSides.ONE;
DcTerminal otherTerminal = dcLine.getDcTerminal(otherSide);
if (otherTerminal.isConnected()) {
DcBus otherBus = otherTerminal.getDcBus();
if (visitedBusIds.add(otherBus.getId())) {
queue.add(otherBus);
}
}
}

@Override
public void visitDcGround(DcGround dcGround) {
if (dcGround.getDcTerminal().isConnected()) {
found[0] = true;
}
}
});

if (found[0]) {
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ public void updateState(LfNetworkStateUpdateParameters parameters, LfNetworkUpda
public void updateFlows(double i1, double i2, double p1, double p2) {
var dcLine = getDcLine();

if (dcBus1 == null || dcBus2 == null) {
// Current and power should be zero for both sides
if (i1 != 0 || i2 != 0 || p1 != 0 || p2 != 0) {
throw new IllegalArgumentException("Current and power should be zero");
}
dcLine.getDcTerminal1().setI(0);
dcLine.getDcTerminal2().setI(0);
dcLine.getDcTerminal1().setP(0);
dcLine.getDcTerminal2().setP(0);
return;
}
// If a DC bus is grounded, its current and power variable are NaN.
// However, we can infer them from the other DC bus (power should be zero)
dcLine.getDcTerminal1().setI((dcBus1.isGrounded() ? -i2 : i1) * PerUnit.ibDc(dcBus1.getNominalV()));
Expand Down
Loading
Loading