Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
bdbf0a6
First commit from previous outerloop implementation
SylvestreSakti Dec 16, 2025
fcc3efe
WIP
SylvestreSakti Dec 17, 2025
fffe70d
Merge branch 'main' into hvdc_ac_emulation_outerloop
SylvestreSakti Dec 22, 2025
8a62eb3
Left FreezingHvdcACEmulationOuterloop as it was (reinitializing angle…
SylvestreSakti Dec 22, 2025
e63d523
Clean code
SylvestreSakti Dec 22, 2025
0b658be
Fix incorrect HvdcState saving and restoring
SylvestreSakti Dec 22, 2025
8221457
Better handling of disable change
SylvestreSakti Dec 23, 2025
702d732
Clean and add reports and add unit tests
SylvestreSakti Dec 23, 2025
26b8799
Merge branch 'main' into hvdc_ac_emulation_outerloop
SylvestreSakti Jan 5, 2026
27b365f
Add coverage
SylvestreSakti Jan 7, 2026
37d033c
Fix minor issues
SylvestreSakti Jan 7, 2026
b7c00e9
Merge branch 'main' into hvdc_ac_emulation_outerloop
SylvestreSakti Jan 15, 2026
643faee
Store values in per unit
SylvestreSakti Feb 6, 2026
29c540e
Remove unused method
SylvestreSakti Feb 6, 2026
46e6875
Missing Bus2 in NaN condition
SylvestreSakti Feb 6, 2026
5578cbe
Remove freezingHvdc outer loop back to initial angles
SylvestreSakti Feb 6, 2026
3977abb
Merge branch 'main' into hvdc_ac_emulation_outerloop
SylvestreSakti Feb 6, 2026
36d47a6
Reports FR
SylvestreSakti Feb 6, 2026
1102065
Update authors
SylvestreSakti Feb 6, 2026
2eef767
Checkstyle
SylvestreSakti Feb 6, 2026
0d2ac94
Merge branch 'main' into hvdc_ac_emulation_outerloop
SylvestreSakti Feb 26, 2026
7a1dd65
Merge branch 'main' into hvdc_ac_emulation_outerloop
SylvestreSakti Mar 5, 2026
baf5a92
Add docs and comments
SylvestreSakti Mar 6, 2026
e72d1ad
Merge branch 'main' into hvdc_ac_emulation_outerloop
SylvestreSakti Mar 23, 2026
97a0498
Merge branch 'main' into hvdc_ac_emulation_outerloop
SylvestreSakti Mar 23, 2026
2761d23
Merge branch 'main' into hvdc_ac_emulation_outerloop
SylvestreSakti Mar 24, 2026
339b9f0
Merge branch 'main' into hvdc_ac_emulation_outerloop
SylvestreSakti Mar 24, 2026
a5c18e9
Merge remote-tracking branch 'origin/main' into hvdc_ac_emulation_out…
Hadrien-Godard Mar 26, 2026
2e6a626
Refacto
Hadrien-Godard Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/loadflow/loadflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ The HVDC line losses are described in a dedicated section further below.
In both control modes (active power setpoint mode or in AC emulation mode), the target value $P$ is bounded by a maximum active power $P_{max}$ that can be either:
- the `maxP` configured for the HVDC line,
- or alternatively separate limit values for both directions using the [HVDC operator active power range iIDM extension](inv:powsyblcore:*:*:#hvdc-operator-active-power-range-extension)
In AC Emulation, these boundaries are handled through the HVDC AC emulation limit outer loop. After solving the equation system, if the computed active power flow through the HVDC overpasses the limits, saturation is applied by the outer loop.
Note that this HVDC AC emulation outer loop is only available in AC calculation, and normal DC calculation (not available in DC Woodbury Security analysis and DC Woodbury Sensitivity Analysis).

The reactive power flow on each side of the line depends on whether voltage regulation of the converters is enabled. If the voltage regulation is enabled, then the VSC converter behaves like a generator regulating the voltage.
Otherwise, reactive power of the converter at AC side is given by its reactive power setpoint.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,37 +38,23 @@ public abstract class AbstractHvdcAcEmulationFlowEquationTerm extends AbstractEl

protected final double lossFactor2;

protected final double pMaxFromCS1toCS2;

protected final double pMaxFromCS2toCS1;

protected AbstractHvdcAcEmulationFlowEquationTerm(LfHvdc hvdc, LfBus bus1, LfBus bus2, VariableSet<AcVariableType> variableSet) {
super(hvdc);
ph1Var = variableSet.getVariable(bus1.getNum(), AcVariableType.BUS_PHI);
Comment thread
vidaldid-rte marked this conversation as resolved.
ph2Var = variableSet.getVariable(bus2.getNum(), AcVariableType.BUS_PHI);
variables = List.of(ph1Var, ph2Var);
k = hvdc.getDroop() * 180 / Math.PI;
p0 = hvdc.getP0();
k = hvdc.getAcEmulationControl().getDroop() * 180 / Math.PI;
p0 = hvdc.getAcEmulationControl().getP0();
r = hvdc.getR();
lossFactor1 = hvdc.getConverterStation1().getLossFactor() / 100;
lossFactor2 = hvdc.getConverterStation2().getLossFactor() / 100;
pMaxFromCS1toCS2 = hvdc.getPMaxFromCS1toCS2();
pMaxFromCS2toCS1 = hvdc.getPMaxFromCS2toCS1();
}

protected static double rawP(double p0, double k, double ph1, double ph2) {
return p0 + k * (ph1 - ph2);
}

protected static double boundedP(double rawP, double pMaxFromCS1toCS2, double pMaxFromCS2toCS1) {
// If there is a maximal active power
// it is applied at the entry of the controller VSC station
// on the AC side of the network.
if (rawP >= 0) {
return Math.min(rawP, pMaxFromCS1toCS2);
} else {
return Math.max(rawP, -pMaxFromCS2toCS1);
}
// Since open-loadflow v2.2.0, saturation support is removed from this equation term (it is now managed by the HVDC AC emulation limits outer loop)
// This change enables the possibility to have a very high active power flow through the HVDC in the Newton Raphson (before the outer loop applies saturation)
// The risk of having this high active power flow causing Newton Raphson divergence is negligible
}

protected double ph1() {
Expand All @@ -83,8 +69,8 @@ protected static double getVscLossMultiplier(double lossFactor1, double lossFact
return (1 - lossFactor1) * (1 - lossFactor2);
}

protected static double getAbsActivePowerWithLosses(double boundedP, double lossController, double lossNonController, double r) {
double lineInputPower = (1 - lossController) * Math.abs(boundedP);
public static double getAbsActivePowerWithLosses(double pController, double lossController, double lossNonController, double r) {
double lineInputPower = (1 - lossController) * Math.abs(pController);
return (1 - lossNonController) * (lineInputPower - getHvdcLineLosses(lineInputPower, r));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,29 @@ private static void createHvdcAcEmulationEquations(LfHvdc hvdc, EquationSystem<A
.addTerm(p2);
hvdc.setP2(p2);
}
updateHvdcAcEmulationEquations(hvdc);
}

public static void updateHvdcAcEmulationEquations(LfHvdc hvdc) {
if (hvdc.getBus1() != null && !hvdc.getBus1().isDisabled()
&& hvdc.getBus2() != null && !hvdc.getBus2().isDisabled()
&& !hvdc.isDisabled() && hvdc.isAcEmulation()) {
switch (hvdc.getAcEmulationControl().getAcEmulationStatus()) {
case LINEAR_MODE -> {
setActive(hvdc.getP1(), true);
setActive(hvdc.getP2(), true);
}
case SATURATION_MODE_FROM_CS1_TO_CS2,
SATURATION_MODE_FROM_CS2_TO_CS1,
FROZEN -> {
setActive(hvdc.getP1(), false);
setActive(hvdc.getP2(), false);
}
}
} else {
setActive(hvdc.getP1(), false);
setActive(hvdc.getP2(), false);
}
}

private void createImpedantBranchEquations(LfBranch branch,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,21 @@ public void onDisableChange(LfElement element, boolean disabled) {
shunt.getVoltageControl().ifPresent(vc -> updateVoltageControls(vc.getControlledBus()));
break;
case HVDC:
// nothing to do
LfHvdc hvdc = (LfHvdc) element;
if (hvdc.isAcEmulation()) {
AcEquationSystemCreator.updateHvdcAcEmulationEquations(hvdc);
}
break;
default:
throw new IllegalStateException("Unknown element type: " + element.getType());
}
}

@Override
public void onHvdcAcEmulationStatusChange(LfHvdc hvdc, LfHvdc.AcEmulationControl.AcEmulationStatus acEmulationStatus) {
AcEquationSystemCreator.updateHvdcAcEmulationEquations(hvdc);
}

private void recreateDistributionEquations(LfZeroImpedanceNetwork network) {
for (LfBus bus : network.getGraph().vertexSet()) {
bus.getGeneratorVoltageControl()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,49 +24,37 @@ public HvdcAcEmulationSide1ActiveFlowEquationTerm(LfHvdc hvdc, LfBus bus1, LfBus
super(hvdc, bus1, bus2, variableSet);
}

public static double p1(double p0, double k, double pMaxFromCS1toCS2, double pMaxFromCS2toCS1, double lossFactor1, double lossFactor2, double r, double ph1, double ph2) {
public static double p1(double p0, double k, double lossFactor1, double lossFactor2, double r, double ph1, double ph2) {
double rawP = rawP(p0, k, ph1, ph2);
// if converterStation1 is controller, then p1 is positive, otherwise it is negative
return isController(rawP) ? boundedP(rawP, pMaxFromCS1toCS2, pMaxFromCS2toCS1) : -getAbsActivePowerWithLosses(boundedP(rawP, pMaxFromCS1toCS2, pMaxFromCS2toCS1), lossFactor1, lossFactor2, r);
return isController(rawP) ? rawP : -getAbsActivePowerWithLosses(rawP, lossFactor1, lossFactor2, r);
}

private static boolean isController(double rawP) {
return rawP >= 0;
}

private static boolean isInOperatingRange(double rawP, double pMaxFromCS1toCS2, double pMaxFromCS2toCS1) {
return rawP < pMaxFromCS1toCS2 && rawP > -pMaxFromCS2toCS1;
}

public static double dp1dph1(double p0, double k, double pMaxFromCS1toCS2, double pMaxFromCS2toCS1, double lossFactor1, double lossFactor2, double ph1, double ph2) {
public static double dp1dph1(double p0, double k, double lossFactor1, double lossFactor2, double ph1, double ph2) {
double rawP = rawP(p0, k, ph1, ph2);
if (isInOperatingRange(rawP, pMaxFromCS1toCS2, pMaxFromCS2toCS1)) {
return (isController(rawP) ? 1 : getVscLossMultiplier(lossFactor1, lossFactor2)) * k; // derivative of cable loss is neglected
} else {
return 0;
}
return isController(rawP) ? k : k * getVscLossMultiplier(lossFactor1, lossFactor2); // derivative of cable loss is neglected
}

public static double dp1dph2(double p0, double k, double pMaxFromCS1toCS2, double pMaxFromCS2toCS1, double lossFactor1, double lossFactor2, double ph1, double ph2) {
return -dp1dph1(p0, k, pMaxFromCS1toCS2, pMaxFromCS2toCS1, lossFactor1, lossFactor2, ph1, ph2);
public static double dp1dph2(double p0, double k, double lossFactor1, double lossFactor2, double ph1, double ph2) {
return -dp1dph1(p0, k, lossFactor1, lossFactor2, ph1, ph2);
}

@Override
public double eval() {
return element.isAcEmulationFrozen() ? p1(p0, k, pMaxFromCS1toCS2, pMaxFromCS2toCS1, lossFactor1, lossFactor2, r, element.getAngleDifferenceToFreeze(), 0)
: p1(p0, k, pMaxFromCS1toCS2, pMaxFromCS2toCS1, lossFactor1, lossFactor2, r, ph1(), ph2());
return p1(p0, k, lossFactor1, lossFactor2, r, ph1(), ph2());
}

@Override
public double der(Variable<AcVariableType> variable) {
Objects.requireNonNull(variable);
if (element.isAcEmulationFrozen()) {
return 0;
}
if (variable.equals(ph1Var)) {
return dp1dph1(p0, k, pMaxFromCS1toCS2, pMaxFromCS2toCS1, lossFactor1, lossFactor2, ph1(), ph2());
return dp1dph1(p0, k, lossFactor1, lossFactor2, ph1(), ph2());
} else if (variable.equals(ph2Var)) {
return dp1dph2(p0, k, pMaxFromCS1toCS2, pMaxFromCS2toCS1, lossFactor1, lossFactor2, ph1(), ph2());
return dp1dph2(p0, k, lossFactor1, lossFactor2, ph1(), ph2());
} else {
throw new IllegalStateException("Unknown variable: " + variable);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,49 +24,37 @@ public HvdcAcEmulationSide2ActiveFlowEquationTerm(LfHvdc hvdc, LfBus bus1, LfBus
super(hvdc, bus1, bus2, variableSet);
}

public static double p2(double p0, double k, double pMaxFromCS1toCS2, double pMaxFromCS2toCS1, double lossFactor1, double lossFactor2, double r, double ph1, double ph2) {
public static double p2(double p0, double k, double lossFactor1, double lossFactor2, double r, double ph1, double ph2) {
double rawP = rawP(p0, k, ph1, ph2);
// if converterStation2 is controller, then p2 is positive, otherwise it is negative
return isController(rawP) ? -boundedP(rawP, pMaxFromCS1toCS2, pMaxFromCS2toCS1) : -getAbsActivePowerWithLosses(boundedP(rawP, pMaxFromCS1toCS2, pMaxFromCS2toCS1), lossFactor2, lossFactor1, r);
return isController(rawP) ? -rawP : -getAbsActivePowerWithLosses(rawP, lossFactor2, lossFactor1, r);
}

private static boolean isController(double rawP) {
return rawP < 0;
}

private static boolean isInOperatingRange(double rawP, double pMaxFromCS1toCS2, double pMaxFromCS2toCS1) {
return rawP < pMaxFromCS2toCS1 && rawP > -pMaxFromCS1toCS2;
}

public static double dp2dph1(double p0, double k, double pMaxFromCS1toCS2, double pMaxFromCS2toCS1, double lossFactor1, double lossFactor2, double ph1, double ph2) {
public static double dp2dph1(double p0, double k, double lossFactor1, double lossFactor2, double ph1, double ph2) {
double rawP = rawP(p0, k, ph1, ph2);
if (isInOperatingRange(rawP, pMaxFromCS1toCS2, pMaxFromCS2toCS1)) {
return -(isController(rawP) ? 1 : getVscLossMultiplier(lossFactor1, lossFactor2)) * k; // derivative of cable loss is neglected
} else {
return 0;
}
return isController(rawP) ? -k : -k * getVscLossMultiplier(lossFactor1, lossFactor2); // derivative of cable loss is neglected
}

public static double dp2dph2(double p0, double k, double pMaxFromCS1toCS2, double pMaxFromCS2toCS1, double lossFactor1, double lossFactor2, double ph1, double ph2) {
return -dp2dph1(p0, k, pMaxFromCS1toCS2, pMaxFromCS2toCS1, lossFactor1, lossFactor2, ph1, ph2);
public static double dp2dph2(double p0, double k, double lossFactor1, double lossFactor2, double ph1, double ph2) {
return -dp2dph1(p0, k, lossFactor1, lossFactor2, ph1, ph2);
}

@Override
public double eval() {
return element.isAcEmulationFrozen() ? p2(p0, k, pMaxFromCS1toCS2, pMaxFromCS2toCS1, lossFactor1, lossFactor2, r, element.getAngleDifferenceToFreeze(), 0)
: p2(p0, k, pMaxFromCS1toCS2, pMaxFromCS2toCS1, lossFactor1, lossFactor2, r, ph1(), ph2());
return p2(p0, k, lossFactor1, lossFactor2, r, ph1(), ph2());
}

@Override
public double der(Variable<AcVariableType> variable) {
Objects.requireNonNull(variable);
if (element.isAcEmulationFrozen()) {
return 0;
}
if (variable.equals(ph1Var)) {
return dp2dph1(p0, k, pMaxFromCS1toCS2, pMaxFromCS2toCS1, lossFactor1, lossFactor2, ph1(), ph2());
return dp2dph1(p0, k, lossFactor1, lossFactor2, ph1(), ph2());
} else if (variable.equals(ph2Var)) {
return dp2dph2(p0, k, pMaxFromCS1toCS2, pMaxFromCS2toCS1, lossFactor1, lossFactor2, ph1(), ph2());
return dp2dph2(p0, k, lossFactor1, lossFactor2, ph1(), ph2());
} else {
throw new IllegalStateException("Unknown variable: " + variable);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* 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.openloadflow.ac.outerloop;

import com.powsybl.commons.report.ReportNode;
import com.powsybl.openloadflow.ac.AcLoadFlowContext;
import com.powsybl.openloadflow.ac.AcLoadFlowParameters;
import com.powsybl.openloadflow.ac.AcOuterLoopContext;
import com.powsybl.openloadflow.ac.equations.AcEquationType;
import com.powsybl.openloadflow.ac.equations.AcVariableType;
import com.powsybl.openloadflow.lf.outerloop.AbstractHvdcAcEmulationLimitsOuterLoop;
import com.powsybl.openloadflow.lf.outerloop.OuterLoopResult;
import com.powsybl.openloadflow.lf.outerloop.OuterLoopStatus;
import com.powsybl.openloadflow.network.LfHvdc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* @author Sylvestre Prabakaran {@literal <sylvestre.prabakaran at rte-france.com>}
* @author Hadrien Godard {@literal <hadrien.godard at artelys.com>}
Comment thread
vidaldid-rte marked this conversation as resolved.
*/
public class AcHvdcAcEmulationLimitsOuterLoop
extends AbstractHvdcAcEmulationLimitsOuterLoop<AcVariableType, AcEquationType, AcLoadFlowParameters, AcLoadFlowContext, AcOuterLoopContext>
implements AcOuterLoop {

private static final Logger LOGGER = LoggerFactory.getLogger(AcHvdcAcEmulationLimitsOuterLoop.class);
public static final String NAME = "AcHvdcAcEmulationLimits";

@Override
public String getName() {
return NAME;
}

@Override
public OuterLoopResult check(AcOuterLoopContext context, ReportNode reportNode) {
OuterLoopStatus status = OuterLoopStatus.STABLE;

for (LfHvdc hvdc : context.getNetwork().getHvdcs()) {
if (!hvdc.isAcEmulation() || hvdc.getBus1().isDisabled() || hvdc.getBus2().isDisabled() || hvdc.isDisabled()) {
continue;
}
if (checkAcEmulationMode(hvdc, true, LOGGER, reportNode)) {
LOGGER.trace("Hvdc '{}' AC emulation state is changed to {}", hvdc.getId(), hvdc.getAcEmulationControl().getAcEmulationStatus());
status = OuterLoopStatus.UNSTABLE;
}
}
return new OuterLoopResult(this, status);
}

@Override
public boolean isNeeded(AcLoadFlowContext context) {
// Needed if the network contains an lfHVDC in AC Emulation mode
return context.getNetwork().getHvdcs().stream().anyMatch(LfHvdc::isAcEmulation);
}
}
Loading
Loading