Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e0f9372
Support multiple limits group using new object LfLimitsGroup
SylvestreSakti Feb 5, 2026
ebff9f2
Bump powsybl-core version to 7.2.0-SNAPSHOT
SylvestreSakti Feb 5, 2026
66cbc70
Merge branch 'main' into multiple_limits_group
SylvestreSakti Feb 5, 2026
fbfbd3d
Merge branch 'ci/core-7.2.0-SNAPSHOT' into multiple_limits_group
SylvestreSakti Feb 5, 2026
e243ab4
Bump IIDM version to 1.16 in unit test files (#1343)
SylvestreSakti Feb 5, 2026
771a51e
rename methods
SylvestreSakti Feb 5, 2026
aba1950
Fix
SylvestreSakti Feb 5, 2026
b7d7bd7
Merge branch 'ci/core-7.2.0-SNAPSHOT' into multiple_limits_group
SylvestreSakti Feb 5, 2026
f02b50f
Merge branch 'main' into ci/core-7.2.0-SNAPSHOT
SylvestreSakti Feb 26, 2026
d8eb80e
Merge branch 'main' into ci/core-7.2.0-SNAPSHOT
SylvestreSakti Mar 9, 2026
c0f10a1
Rename DanglingLine to BoundaryLine (#1364)
SylvestreSakti Mar 10, 2026
973de5e
Adapt to core 3669 - Properties on MinMaxReactiveLimits (#1376)
olperr1 Mar 20, 2026
21f5fd3
Minimal adaptation for sensitivity analysis with operator strategies …
olperr1 Mar 20, 2026
6240535
Adaptation for new security analysis API (powsybl-core 3789) (#1377)
jeandemanged Mar 20, 2026
236d4fe
Merge branch 'main' into ci/core-7.2.0-SNAPSHOT
SylvestreSakti Mar 20, 2026
9ef955e
Bump to powsybl-core 7.2.0-RC1
SylvestreSakti Mar 20, 2026
237b172
Fix doc issue : Dangling Line to Boundary Line
SylvestreSakti Mar 20, 2026
c9ad568
Merge branch 'ci/core-7.2.0-SNAPSHOT' into multiple_limits_group
SylvestreSakti Mar 20, 2026
4ee095b
Return operationalLimitsGroupId with LimitViolation
SylvestreSakti Mar 20, 2026
6837305
Modify unit tests to compare also operationalLimtisGroupId
SylvestreSakti Mar 20, 2026
b56a6c2
Checkstyle
SylvestreSakti Mar 20, 2026
38b42b4
Checkstyle
SylvestreSakti Mar 20, 2026
a5c94ab
Support multiple limits and add test
SylvestreSakti Mar 20, 2026
e2367c1
Merge branch 'main' into multiple_limits_group
SylvestreSakti Mar 23, 2026
a4cfb72
Merge branch 'main' into multiple_limits_group
SylvestreSakti Mar 24, 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
72 changes: 70 additions & 2 deletions src/main/java/com/powsybl/openloadflow/network/LfBranch.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.powsybl.iidm.network.util.LimitViolationUtils;
import com.powsybl.openloadflow.sa.LimitReductionManager;
import com.powsybl.openloadflow.util.Evaluable;
import com.powsybl.openloadflow.util.PerUnit;
import com.powsybl.security.results.BranchResult;

import java.util.*;
Expand All @@ -38,6 +39,73 @@ enum BranchType {
record LfBranchResults(double p1, double p2, double q1, double q2, double i1, double i2) {
}

class LfLimitsGroup {

private final List<LfLimit> sortedLimits;

Comment thread
NathanDissoubray marked this conversation as resolved.
private final String operationalLimitsGroupId;

public LfLimitsGroup(List<LfLimit> sortedLimits, String operationalLimitsGroupId) {
this.sortedLimits = sortedLimits;
this.operationalLimitsGroupId = operationalLimitsGroupId;
}

public String getOperationalLimitsGroupId() {
return operationalLimitsGroupId;
}

public List<LfLimit> getSortedLimits() {
return sortedLimits;
}

private static double getScaleForLimitType(LimitType type, LfBus bus) {
return switch (type) {
case ACTIVE_POWER, APPARENT_POWER -> 1.0 / PerUnit.SB;
case CURRENT -> 1.0 / PerUnit.ib(bus.getNominalV());
default ->
throw new UnsupportedOperationException(String.format("Getting scale for limit type %s is not supported.", type));
};
}

/**
* Create the list of LfLimits from a LoadingLimits and a list of reductions.
* The resulting list will contain the permanent limit
* This list is returned in a LfLimitsGroup object
*/
public static LfLimitsGroup createSortedLimitsList(LoadingLimits loadingLimits, String operationalLimitsGroupId, LfBus bus, double[] limitReductions) {
List<LfLimit> sortedLimits = new ArrayList<>(3);
if (loadingLimits != null) {
double toPerUnit = getScaleForLimitType(loadingLimits.getLimitType(), bus);

int i = 0;
for (LoadingLimits.TemporaryLimit temporaryLimit : loadingLimits.getTemporaryLimits()) {
if (temporaryLimit.getAcceptableDuration() != 0) {
// it is not useful to add a limit with acceptable duration equal to zero as the only value plausible
// for this limit is infinity.
// https://javadoc.io/doc/com.powsybl/powsybl-core/latest/com/powsybl/iidm/network/CurrentLimits.html
double reduction = limitReductions.length == 0 ? 1d : limitReductions[i + 1]; // Temporary limit's reductions are stored starting from index 1 in `limitReductions`
double originalValuePerUnit = temporaryLimit.getValue() * toPerUnit;
sortedLimits.add(0, LfLimit.createTemporaryLimit(temporaryLimit.getName(), temporaryLimit.getAcceptableDuration(),
originalValuePerUnit, reduction));
}
i++;
}
double reduction = limitReductions.length == 0 ? 1d : limitReductions[0];
sortedLimits.add(LfLimit.createPermanentLimit(loadingLimits.getPermanentLimit() * toPerUnit, reduction));
Comment thread
NathanDissoubray marked this conversation as resolved.
}
if (sortedLimits.size() > 1) {
// we only make that fix if there is more than a permanent limit attached to the branch.
for (int i = sortedLimits.size() - 1; i > 0; i--) {
// From the permanent limit to the most serious temporary limit.
sortedLimits.get(i).setAcceptableDuration(sortedLimits.get(i - 1).getAcceptableDuration());
}
sortedLimits.get(0).setAcceptableDuration(0);
}
return new LfLimitsGroup(sortedLimits, operationalLimitsGroupId);
}

}

class LfLimit {

private final String name;
Expand Down Expand Up @@ -211,9 +279,9 @@ static int[] createIndex(LfNetwork network, List<LfBranch> branches) {

List<Evaluable> getAdditionalClosedQ2();

List<LfLimit> getLimits1(LimitType type, LimitReductionManager limitReductionManager);
List<LfLimitsGroup> getLimits1(LimitType type, LimitReductionManager limitReductionManager);

default List<LfLimit> getLimits2(LimitType type, LimitReductionManager limitReductionManager) {
default List<LfLimitsGroup> getLimits2(LimitType type, LimitReductionManager limitReductionManager) {
return Collections.emptyList();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import com.powsybl.openloadflow.network.*;
import com.powsybl.openloadflow.sa.LimitReductionManager;
import com.powsybl.openloadflow.util.Evaluable;
import com.powsybl.openloadflow.util.PerUnit;
import net.jafama.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -30,17 +29,17 @@ public abstract class AbstractLfBranch extends AbstractElement implements LfBran

protected final LfBus bus2;

private List<LfLimit> currentLimits1;
private List<LfLimitsGroup> currentLimits1;

private List<LfLimit> activePowerLimits1;
private List<LfLimitsGroup> activePowerLimits1;

private List<LfLimit> apparentPowerLimits1;
private List<LfLimitsGroup> apparentPowerLimits1;

private List<LfLimit> currentLimits2;
private List<LfLimitsGroup> currentLimits2;

private List<LfLimit> activePowerLimits2;
private List<LfLimitsGroup> activePowerLimits2;

private List<LfLimit> apparentPowerLimits2;
private List<LfLimitsGroup> apparentPowerLimits2;

protected final PiModel piModel;

Expand Down Expand Up @@ -92,42 +91,6 @@ public Optional<ThreeSides> getOriginalSide() {
return Optional.empty();
}

/**
* Create the list of LfLimits from a LoadingLimits and a list of reductions.
* The resulting list will contain the permanent limit
*/
protected static List<LfLimit> createSortedLimitsList(LoadingLimits loadingLimits, LfBus bus, double[] limitReductions) {
List<LfLimit> sortedLimits = new ArrayList<>(3);
if (loadingLimits != null) {
double toPerUnit = getScaleForLimitType(loadingLimits.getLimitType(), bus);

int i = 0;
for (LoadingLimits.TemporaryLimit temporaryLimit : loadingLimits.getTemporaryLimits()) {
if (temporaryLimit.getAcceptableDuration() != 0) {
// it is not useful to add a limit with acceptable duration equal to zero as the only value plausible
// for this limit is infinity.
// https://javadoc.io/doc/com.powsybl/powsybl-core/latest/com/powsybl/iidm/network/CurrentLimits.html
double reduction = limitReductions.length == 0 ? 1d : limitReductions[i + 1]; // Temporary limit's reductions are stored starting from index 1 in `limitReductions`
double originalValuePerUnit = temporaryLimit.getValue() * toPerUnit;
sortedLimits.add(0, LfLimit.createTemporaryLimit(temporaryLimit.getName(), temporaryLimit.getAcceptableDuration(),
originalValuePerUnit, reduction));
}
i++;
}
double reduction = limitReductions.length == 0 ? 1d : limitReductions[0];
sortedLimits.add(LfLimit.createPermanentLimit(loadingLimits.getPermanentLimit() * toPerUnit, reduction));
}
if (sortedLimits.size() > 1) {
// we only make that fix if there is more than a permanent limit attached to the branch.
for (int i = sortedLimits.size() - 1; i > 0; i--) {
// From the permanent limit to the most serious temporary limit.
sortedLimits.get(i).setAcceptableDuration(sortedLimits.get(i - 1).getAcceptableDuration());
}
sortedLimits.get(0).setAcceptableDuration(0);
}
return sortedLimits;
}

@Override
public ElementType getType() {
return ElementType.BRANCH;
Expand All @@ -143,7 +106,7 @@ public LfBus getBus2() {
return bus2;
}

private List<LfLimit> getLimits1(LimitType type) {
private List<LfLimitsGroup> getLimits1(LimitType type) {
switch (type) {
case ACTIVE_POWER -> {
return activePowerLimits1;
Expand All @@ -158,7 +121,7 @@ private List<LfLimit> getLimits1(LimitType type) {
}
}

private void setLimits1(LimitType type, List<LfLimit> limits) {
private void setLimits1(LimitType type, List<LfLimitsGroup> limits) {
switch (type) {
case ACTIVE_POWER -> activePowerLimits1 = limits;
case APPARENT_POWER -> apparentPowerLimits1 = limits;
Expand All @@ -167,19 +130,31 @@ private void setLimits1(LimitType type, List<LfLimit> limits) {
}
}

public <T extends LoadingLimits> List<LfLimit> getLimits1(LimitType type, Supplier<Optional<T>> loadingLimitsSupplier, LimitReductionManager limitReductionManager) {
var limits = getLimits1(type);
private <T extends LoadingLimits> List<LfLimitsGroup> createLimits(Supplier<Map<String, T>> loadingLimitsSupplier, LimitReductionManager limitReductionManager, TwoSides side) {
// It is possible to apply the reductions here since the only supported ContingencyContext for LimitReduction is ALL.
Map<String, T> allSelectedLoadingLimits = loadingLimitsSupplier.get(); // Map of all selected loading limits indexed by their operational limits group id
List<LfLimitsGroup> limits = new ArrayList<>();
for (Map.Entry<String, T> loadingLimitsEntry : allSelectedLoadingLimits.entrySet()) {
T loadingLimits = loadingLimitsEntry.getValue();
String operationalLimitsGroupId = loadingLimitsEntry.getKey();
limits.add(LfLimitsGroup.createSortedLimitsList(loadingLimits,
operationalLimitsGroupId,
side == TwoSides.ONE ? bus1 : bus2,
getLimitReductions(side, limitReductionManager, loadingLimits)));
}
return limits;
}

public <T extends LoadingLimits> List<LfLimitsGroup> getLimits1(LimitType type, Supplier<Map<String, T>> loadingLimitsSupplier, LimitReductionManager limitReductionManager) {
List<LfLimitsGroup> limits = getLimits1(type);
if (limits == null) {
// It is possible to apply the reductions here since the only supported ContingencyContext for LimitReduction is ALL.
var loadingLimits = loadingLimitsSupplier.get().orElse(null);
limits = createSortedLimitsList(loadingLimits, bus1,
getLimitReductions(TwoSides.ONE, limitReductionManager, loadingLimits));
limits = createLimits(loadingLimitsSupplier, limitReductionManager, TwoSides.ONE);
setLimits1(type, limits);
}
return limits;
}

private List<LfLimit> getLimits2(LimitType type) {
private List<LfLimitsGroup> getLimits2(LimitType type) {
switch (type) {
case ACTIVE_POWER -> {
return activePowerLimits2;
Expand All @@ -194,7 +169,7 @@ private List<LfLimit> getLimits2(LimitType type) {
}
}

private void setLimits2(LimitType type, List<LfLimit> limits) {
private void setLimits2(LimitType type, List<LfLimitsGroup> limits) {
switch (type) {
case ACTIVE_POWER -> activePowerLimits2 = limits;
case APPARENT_POWER -> apparentPowerLimits2 = limits;
Expand All @@ -203,13 +178,10 @@ private void setLimits2(LimitType type, List<LfLimit> limits) {
}
}

public <T extends LoadingLimits> List<LfLimit> getLimits2(LimitType type, Supplier<Optional<T>> loadingLimitsSupplier, LimitReductionManager limitReductionManager) {
public <T extends LoadingLimits> List<LfLimitsGroup> getLimits2(LimitType type, Supplier<Map<String, T>> loadingLimitsSupplier, LimitReductionManager limitReductionManager) {
var limits = getLimits2(type);
if (limits == null) {
// It is possible to apply the reductions here since the only supported ContingencyContext for LimitReduction is ALL.
var loadingLimits = loadingLimitsSupplier.get().orElse(null);
limits = createSortedLimitsList(loadingLimits, bus2,
getLimitReductions(TwoSides.TWO, limitReductionManager, loadingLimits));
limits = createLimits(loadingLimitsSupplier, limitReductionManager, TwoSides.TWO);
setLimits2(type, limits);
}
return limits;
Expand Down Expand Up @@ -264,19 +236,6 @@ protected void updateSolvedTapPosition(RatioTapChanger rtc, double ptcRho, doubl
Transformers.findTapPosition(rtc, ptcRho, rho).ifPresent(rtc::setSolvedTapPosition);
}

protected static double getScaleForLimitType(LimitType type, LfBus bus) {
switch (type) {
case ACTIVE_POWER,
APPARENT_POWER:
return 1.0 / PerUnit.SB;
case CURRENT:
return 1.0 / PerUnit.ib(bus.getNominalV());
case VOLTAGE:
default:
throw new UnsupportedOperationException(String.format("Getting scale for limit type %s is not supported.", type));
}
}

@Override
public Optional<TransformerVoltageControl> getVoltageControl() {
return Optional.ofNullable(voltageControl);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@
*/
package com.powsybl.openloadflow.network.impl;

import com.powsybl.iidm.network.BoundaryLine;
import com.powsybl.iidm.network.LimitType;
import com.powsybl.iidm.network.LoadingLimits;
import com.powsybl.iidm.network.TwoSides;
import com.powsybl.iidm.network.*;
import com.powsybl.openloadflow.network.*;
import com.powsybl.openloadflow.sa.LimitReductionManager;
import com.powsybl.openloadflow.util.PerUnit;
Expand All @@ -19,6 +16,10 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
* @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
Expand Down Expand Up @@ -80,15 +81,23 @@ public List<BranchResult> createBranchResult(double preContingencyBranchP1, doub
return List.of(buildBranchResult(loadFlowModel, zeroImpedanceFlows, currentScale, currentScale, Double.NaN, Double.NaN));
}

private <T extends LoadingLimits> Supplier<Map<String, T>> toMapIndexedByOperationalLimitsGroupId(Function<OperationalLimitsGroup, Optional<T>> limitsGetter) {
return () -> getBoundaryLine()
.getAllSelectedOperationalLimitsGroups()
.stream()
.filter(o -> limitsGetter.apply(o).isPresent())
.collect(Collectors.toMap(OperationalLimitsGroup::getId, o -> limitsGetter.apply(o).orElseThrow()));
}

@Override
public List<LfLimit> getLimits1(final LimitType type, LimitReductionManager limitReductionManager) {
public List<LfLimitsGroup> getLimits1(final LimitType type, LimitReductionManager limitReductionManager) {
switch (type) {
case ACTIVE_POWER:
return getLimits1(type, () -> getBoundaryLine().getActivePowerLimits(), limitReductionManager);
return getLimits1(type, toMapIndexedByOperationalLimitsGroupId(OperationalLimitsGroup::getActivePowerLimits), limitReductionManager);
case APPARENT_POWER:
return getLimits1(type, () -> getBoundaryLine().getApparentPowerLimits(), limitReductionManager);
return getLimits1(type, toMapIndexedByOperationalLimitsGroupId(OperationalLimitsGroup::getApparentPowerLimits), limitReductionManager);
case CURRENT:
return getLimits1(type, () -> getBoundaryLine().getCurrentLimits(), limitReductionManager);
return getLimits1(type, toMapIndexedByOperationalLimitsGroupId(OperationalLimitsGroup::getCurrentLimits), limitReductionManager);
case VOLTAGE:
default:
throw new UnsupportedOperationException(String.format("Getting %s limits is not supported.", type.name()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
* @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
Expand Down Expand Up @@ -228,30 +231,38 @@ public List<BranchResult> createBranchResult(double preContingencyBranchP1, doub
return List.of(branchResult);
}

private <T extends LoadingLimits> Supplier<Map<String, T>> toMapIndexedByOperationalLimitsGroupId(Function<OperationalLimitsGroup, Optional<T>> limitsGetter, TwoSides side) {
return () -> getBranch()
.getAllSelectedOperationalLimitsGroups(side)
.stream()
.filter(o -> limitsGetter.apply(o).isPresent())
.collect(Collectors.toMap(OperationalLimitsGroup::getId, o -> limitsGetter.apply(o).orElseThrow()));
}

@Override
public List<LfLimit> getLimits1(final LimitType type, LimitReductionManager limitReductionManager) {
public List<LfLimitsGroup> getLimits1(final LimitType type, LimitReductionManager limitReductionManager) {
switch (type) {
case ACTIVE_POWER:
return getLimits1(type, () -> getBranch().getActivePowerLimits1(), limitReductionManager);
return getLimits1(type, toMapIndexedByOperationalLimitsGroupId(OperationalLimitsGroup::getActivePowerLimits, TwoSides.ONE), limitReductionManager);
case APPARENT_POWER:
return getLimits1(type, () -> getBranch().getApparentPowerLimits1(), limitReductionManager);
return getLimits1(type, toMapIndexedByOperationalLimitsGroupId(OperationalLimitsGroup::getApparentPowerLimits, TwoSides.ONE), limitReductionManager);
case CURRENT:
return getLimits1(type, () -> getBranch().getCurrentLimits1(), limitReductionManager);
return getLimits1(type, toMapIndexedByOperationalLimitsGroupId(OperationalLimitsGroup::getCurrentLimits, TwoSides.ONE), limitReductionManager);
case VOLTAGE:
default:
throw new UnsupportedOperationException(String.format("Getting %s limits is not supported.", type.name()));
}
}

@Override
public List<LfLimit> getLimits2(final LimitType type, LimitReductionManager limitReductionManager) {
public List<LfLimitsGroup> getLimits2(final LimitType type, LimitReductionManager limitReductionManager) {
switch (type) {
case ACTIVE_POWER:
return getLimits2(type, () -> getBranch().getActivePowerLimits2(), limitReductionManager);
return getLimits2(type, toMapIndexedByOperationalLimitsGroupId(OperationalLimitsGroup::getActivePowerLimits, TwoSides.TWO), limitReductionManager);
case APPARENT_POWER:
return getLimits2(type, () -> getBranch().getApparentPowerLimits2(), limitReductionManager);
return getLimits2(type, toMapIndexedByOperationalLimitsGroupId(OperationalLimitsGroup::getApparentPowerLimits, TwoSides.TWO), limitReductionManager);
case CURRENT:
return getLimits2(type, () -> getBranch().getCurrentLimits2(), limitReductionManager);
return getLimits2(type, toMapIndexedByOperationalLimitsGroupId(OperationalLimitsGroup::getCurrentLimits, TwoSides.TWO), limitReductionManager);
case VOLTAGE:
default:
throw new UnsupportedOperationException(String.format("Getting %s limits is not supported.", type.name()));
Expand Down
Loading
Loading