Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a1ee8a6
first refacto
Mathieu-Deharbe Nov 21, 2025
f31fdc2
corrects TU
Mathieu-Deharbe Nov 24, 2025
47b4492
permanent limit
Mathieu-Deharbe Nov 24, 2025
8d6e732
applicabilityToString
Mathieu-Deharbe Nov 24, 2025
2f4df9d
applicabilityToString in the UTs
Mathieu-Deharbe Nov 24, 2025
fc9d514
correct handling of ambiguous ADD or MODIFY
Mathieu-Deharbe Nov 24, 2025
6ce5c99
code style
Mathieu-Deharbe Nov 24, 2025
6bc3cc7
sub report node for each side
Mathieu-Deharbe Nov 24, 2025
05eab30
remove olgUtils
Mathieu-Deharbe Nov 24, 2025
617b04c
corrects logSideNode
Mathieu-Deharbe Nov 24, 2025
dda8e3d
corrects logSideNode
Mathieu-Deharbe Nov 24, 2025
c0e89d2
log limits only if there is at least one actual modification
Mathieu-Deharbe Nov 25, 2025
a722970
Merge branch 'main' into limits-modification-logs
Mathieu-Deharbe Nov 25, 2025
cc3401c
corrections post merge conflicts
Mathieu-Deharbe Nov 25, 2025
9df943c
if (!limitsReportsSide1.isEmpty() || !limitsReportsSide2.isEmpty()) {
Mathieu-Deharbe Nov 25, 2025
0d9870d
createLogNodeForSide
Mathieu-Deharbe Nov 25, 2025
fd828df
Merge remote-tracking branch 'origin/limits-modification-logs' into l…
Mathieu-Deharbe Nov 25, 2025
777057f
if (!limitsReportsSide1.isEmpty() || !limitsReportsSide2.isEmpty()) {
Mathieu-Deharbe Nov 25, 2025
803482c
check on createLogNodeForSide parameter
Mathieu-Deharbe Nov 25, 2025
a3df970
corrections post review
Mathieu-Deharbe Nov 25, 2025
7b57038
corrects name modification
Mathieu-Deharbe Nov 26, 2025
77930de
logs in case of line ceration and transfo creation
Mathieu-Deharbe Nov 26, 2025
188eed4
rename classes
Mathieu-Deharbe Nov 26, 2025
6155133
TU testRename
Mathieu-Deharbe Nov 26, 2025
1a1aa0b
Merge branch 'main' into limits-modification-logs
basseche Nov 27, 2025
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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/**
* Copyright (c) 2025, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.gridsuite.modification.modifications.olg;

import com.powsybl.commons.report.ReportNode;
import com.powsybl.commons.report.TypedValue;
import com.powsybl.iidm.network.*;
import org.gridsuite.modification.dto.*;

import java.util.*;

import static org.gridsuite.modification.dto.OperationalLimitsGroupInfos.Applicability.*;
import static org.gridsuite.modification.dto.OperationalLimitsGroupInfos.Applicability.EQUIPMENT;
import static org.gridsuite.modification.dto.OperationalLimitsGroupModificationType.DELETE;
import static org.gridsuite.modification.modifications.AbstractBranchModification.*;

/**
* handles the modification of a list of operational limits groups from AbstractBranchModification
*
* @author Mathieu DEHARBE <mathieu.deharbe at rte-france.com>
*/
public class OlgsModification {
private final Branch<?> modifiedBranch; // branch modified by the network modification
private final List<OperationalLimitsGroupModificationInfos> olgModificationInfos;
private final ReportNode olgsReportNode;

public OlgsModification(
Branch<?> branch,
List<OperationalLimitsGroupModificationInfos> operationalLimitsInfos,
ReportNode limitSetsReportNode) {
modifiedBranch = branch;
olgModificationInfos = operationalLimitsInfos != null ? operationalLimitsInfos : new ArrayList<>();
olgsReportNode = limitSetsReportNode;
}

public void modifyOperationalLimitsGroups(OperationalLimitsGroupsModificationType operationalLimitsGroupsModificationType) {

if (operationalLimitsGroupsModificationType == OperationalLimitsGroupsModificationType.REPLACE) {
// because we are replacing all the limit sets we remove all the limit sets that are not specified in the network modification
// the others may be modified instead of recreated so it is better to not delete them in order to have more precise logs
deleteOlgsUnspecifiedInTheModification(SIDE1);
deleteOlgsUnspecifiedInTheModification(SIDE2);
}

for (OperationalLimitsGroupModificationInfos opLGModifInfos : olgModificationInfos) {
if (opLGModifInfos.getModificationType() == null) {
continue;
}

ArrayList<ReportNode> olgSetReports = new ArrayList<>();

if (!opLGModifInfos.getModificationType().equals(DELETE)) {
detectApplicabilityChange(opLGModifInfos, olgSetReports);
}

new OlgModification(
modifiedBranch,
opLGModifInfos,
olgsReportNode
).applyModificationToOperationalLimitsGroup();
}
}

private void deleteOlgsUnspecifiedInTheModification(OperationalLimitsGroupInfos.Applicability applicability) {
List<String> olgToBeDeleted = new ArrayList<>();
Collection<OperationalLimitsGroup> operationalLimitsGroups = applicability == SIDE1 ?
modifiedBranch.getOperationalLimitsGroups1() :
modifiedBranch.getOperationalLimitsGroups2();
operationalLimitsGroups.stream().filter(
operationalLimitsGroup ->
olgModificationInfos.stream().noneMatch(
operationalLimitsGroupModificationInfos ->
// we don't want to remove the limit sets specified in the network modification (operationalLimitsGroups) :
Objects.equals(operationalLimitsGroupModificationInfos.getId(), operationalLimitsGroup.getId())
&& (operationalLimitsGroupModificationInfos.getApplicability() == applicability
|| operationalLimitsGroupModificationInfos.getApplicability() == EQUIPMENT)
)
).forEach(operationalLimitsGroup -> olgToBeDeleted.add(operationalLimitsGroup.getId()));

Iterator<String> i = olgToBeDeleted.iterator();
while (i.hasNext()) {
String s = i.next();
new OlgModification(
modifiedBranch,
OperationalLimitsGroupModificationInfos.builder()
.id(s)
.applicability(applicability)
.build(),
olgsReportNode
).removeOlg();
}
}

private void logApplicabilityChange(List<ReportNode> olgReports, String groupId, OperationalLimitsGroupInfos.Applicability applicability) {
olgReports.add(ReportNode.newRootReportNode().withMessageTemplate("network.modification.applicabilityChanged")
.withUntypedValue(OPERATIONAL_LIMITS_GROUP_NAME, groupId)
.withUntypedValue(APPLICABILITY, applicability.toString())
.withSeverity(TypedValue.INFO_SEVERITY)
.build());
}

private boolean shouldDeletedOtherSide(Branch<?> branch, OperationalLimitsGroupModificationInfos limitsModifInfos) {
boolean hasModificationOnSideOne = !olgModificationInfos.stream().filter(opLimitModifInfo ->
opLimitModifInfo.getId().equals(limitsModifInfos.getId()) && opLimitModifInfo.getApplicability().equals(SIDE1))
.toList().isEmpty();

boolean hasModificationOnSideTwo = !olgModificationInfos.stream().filter(opLimitModifInfo ->
opLimitModifInfo.getId().equals(limitsModifInfos.getId()) && opLimitModifInfo.getApplicability().equals(SIDE2))
.toList().isEmpty();

switch (limitsModifInfos.getApplicability()) {
case SIDE1 -> {
return !hasModificationOnSideTwo && branch.getOperationalLimitsGroup2(limitsModifInfos.getId()).isPresent();
}
case SIDE2 -> {
return !hasModificationOnSideOne && branch.getOperationalLimitsGroup1(limitsModifInfos.getId()).isPresent();
}
default -> {
return false;
}
}
}

// If we are changing applicability we may not find operational limits group where we should so check both sides
private void detectApplicabilityChange(OperationalLimitsGroupModificationInfos modifiedLimitSetInfos, List<ReportNode> olgReports) {

OperationalLimitsGroup limitsGroup1 = modifiedBranch.getOperationalLimitsGroup1(modifiedLimitSetInfos.getId()).orElse(null);
OperationalLimitsGroup limitsGroup2 = modifiedBranch.getOperationalLimitsGroup2(modifiedLimitSetInfos.getId()).orElse(null);
if (limitsGroup1 == null && modifiedLimitSetInfos.getApplicability().equals(SIDE2)
|| limitsGroup2 == null && modifiedLimitSetInfos.getApplicability().equals(SIDE1)) {
return;
} else if (limitsGroup1 != null && limitsGroup2 != null && !modifiedLimitSetInfos.getApplicability().equals(EQUIPMENT)) {
// applicability change from EQUIPMENT to one side
if (shouldDeletedOtherSide(modifiedBranch, modifiedLimitSetInfos)) {
if (modifiedLimitSetInfos.getApplicability().equals(SIDE1)) {
modifiedBranch.removeOperationalLimitsGroup2(modifiedLimitSetInfos.getId());
logApplicabilityChange(olgReports, limitsGroup1.getId(), SIDE1);
} else if (modifiedLimitSetInfos.getApplicability().equals(SIDE2)) {
modifiedBranch.removeOperationalLimitsGroup1(modifiedLimitSetInfos.getId());
logApplicabilityChange(olgReports, limitsGroup2.getId(), SIDE2);
}
}
return;
}

switch (modifiedLimitSetInfos.getApplicability()) {
case SIDE1 -> moveLimitSetToTheOtherSide(modifiedBranch, limitsGroup2, modifiedLimitSetInfos.getId(), true, olgReports);
case SIDE2 -> moveLimitSetToTheOtherSide(modifiedBranch, limitsGroup1, modifiedLimitSetInfos.getId(), false, olgReports);
case EQUIPMENT -> {
boolean applicabilityChanged = false;
if (limitsGroup1 == null && limitsGroup2 != null) {
limitsGroup1 = modifiedBranch.newOperationalLimitsGroup1(limitsGroup2.getId());
copyOperationalLimitsGroup(limitsGroup1.newCurrentLimits(), limitsGroup2);
applicabilityChanged = true;
}
if (limitsGroup2 == null && limitsGroup1 != null) {
limitsGroup2 = modifiedBranch.newOperationalLimitsGroup2(limitsGroup1.getId());
copyOperationalLimitsGroup(limitsGroup2.newCurrentLimits(), limitsGroup1);
applicabilityChanged = true;
}
if (applicabilityChanged) {
logApplicabilityChange(olgReports, limitsGroup1.getId(), EQUIPMENT);
}
}
}
}

private void moveLimitSetToTheOtherSide(Branch<?> branch,
OperationalLimitsGroup limitsGroupToCopy, String modifiedLimitSet,
boolean isSide1,
List<ReportNode> olgReports) {
// if we have only one limit set with the same name but applicability is not good
// we should copy existing limit set on the right side and removed it from the other side
if (olgModificationInfos.stream().filter(limitSet -> limitSet.getId().equals(modifiedLimitSet)).toList().size() == 1) {
// Copy operational limits group to the other side
OperationalLimitsGroup limitsGroup = isSide1 ? branch.newOperationalLimitsGroup1(limitsGroupToCopy.getId())
: branch.newOperationalLimitsGroup2(limitsGroupToCopy.getId());
copyOperationalLimitsGroup(limitsGroup.newCurrentLimits(), limitsGroupToCopy);

olgReports.add(ReportNode.newRootReportNode().withMessageTemplate("network.modification.applicabilityChanged")
.withUntypedValue(OPERATIONAL_LIMITS_GROUP_NAME, limitsGroupToCopy.getId())
.withUntypedValue(APPLICABILITY, isSide1 ? SIDE1.toString() : SIDE2.toString())
.withSeverity(TypedValue.INFO_SEVERITY)
.build());
// Remove copied operational limits group
if (isSide1) {
branch.removeOperationalLimitsGroup2(modifiedLimitSet);
} else {
branch.removeOperationalLimitsGroup1(modifiedLimitSet);
}
}
}

private void copyOperationalLimitsGroup(CurrentLimitsAdder limitsAdder, OperationalLimitsGroup opLimitGroupToCopy) {
// Copy all limits of the other side
opLimitGroupToCopy.getCurrentLimits().ifPresent(currentLimits -> {
limitsAdder.setPermanentLimit(currentLimits.getPermanentLimit());

for (LoadingLimits.TemporaryLimit tempLimit : currentLimits.getTemporaryLimits()) {
addTemporaryLimit(limitsAdder, tempLimit.getName(), tempLimit.getValue(), tempLimit.getAcceptableDuration());
}
limitsAdder.add();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,10 @@ network.modification.lccCreated = New lcc with id=${id} created
network.modification.limitSetAdded = Limit set ${name} added
network.modification.operationalLimitsGroupDeleted = Limit set ${operationalLimitsGroupName} has been deleted on ${side}
network.modification.operationalLimitsGroupAdded = Limit set ${operationalLimitsGroupName} added on ${side}
network.modification.operationalLimitsGroupAdded.detail = Added to operational limits group on ${side}
network.modification.operationalLimitsGroupReplaced = Limit set ${operationalLimitsGroupName} has been replaced on ${side}
network.modification.operationalLimitsGroupModified = Limit set ${operationalLimitsGroupName} has been modified on ${side}
network.modification.operationalLimitsGroupModified.detail = Modified in operational limits group on ${side}
network.modification.limitSetSelectedOnSide1 = limit set selected on side 1 : ${selectedOperationalLimitsGroup1}
network.modification.limitSetSelectedOnSide2 = limit set selected on side 2 : ${selectedOperationalLimitsGroup2}
network.modification.noLimitSetSelectedOnSide1 = No limit set selected on side 1
Expand Down Expand Up @@ -298,8 +300,8 @@ network.modification.tapChangerStepsModification = Taps were replaced by new one
network.modification.tapsModification = Taps
network.modification.temporaryLimitAdded.name = ${name} (${duration}) added with ${value}
network.modification.temporaryLimitDeleted.name = ${name} (${duration}) deleted
network.modification.temporaryLimitModified.name = ${name} (${duration}) : ${oldValue} -> ${value}
network.modification.temporaryLimitsModification = Temporary current limits :
network.modification.temporaryLimitValueModified.name = Temporary limit ${name} (${duration}) : ${oldValue} -> ${value}
network.modification.temporaryLimitModified.name = Temporary limit ${name}; value: ${value}; acceptable duration: ${duration}
network.modification.temporaryLimitsReplaced = Previous temporary limits were removed
network.modification.temporaryLimitsNoMatch = No existing temporary limit found with acceptableDuration = ${limitAcceptableDuration} matching is based on acceptableDuration if that helps
network.modification.terminal1Disconnected = Equipment with id=${id} disconnected on side 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ void testDelete() {
Network network = getNetwork();
AbstractModification modification = lineModificationInfos4.toModification();
String errorMessage = assertThrows(PowsyblException.class, () -> modification.apply(network)).getMessage();
assertEquals("Cannot delete operational limit group doesNotExist which has not been found in equipment on side SIDE2", errorMessage);
assertEquals("Cannot delete operational limit group doesNotExist which has not been found in equipment on side 2", errorMessage);
}

private void changeLineConnectionState(Line existingEquipment, boolean expectedState) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,13 +238,13 @@ protected void assertAfterNetworkModificationApplication() {

private void assertAfterNetworkModificationApplication(ReportNode reportNode) {
assertAfterNetworkModificationApplication();
assertLogMessageWithoutRank("Limit set DEFAULT has been modified on SIDE1", "network.modification.operationalLimitsGroupModified", reportNode);
assertLogMessageWithoutRank("Limit set DEFAULT has been modified on side 1", "network.modification.operationalLimitsGroupModified", reportNode);
assertLogMessageWithoutRank("Previous temporary limits were removed", "network.modification.temporaryLimitsReplaced", reportNode);
assertLogMessageWithoutRank("Cannot add DEFAULT operational limit group, one with the given name already exists", "network.modification.tabular.modification.exception", reportNode);
assertLogMessageWithoutRank("No existing temporary limit found with acceptableDuration = 3 matching is based on acceptableDuration if that helps", "network.modification.temporaryLimitsNoMatch", reportNode);
assertLogMessageWithoutRank("limit set selected on side 2 : group0", "network.modification.limitSetSelectedOnSide2", reportNode);
assertLogMessageWithoutRank("Limit set group0 has been replaced on SIDE2", "network.modification.operationalLimitsGroupReplaced", reportNode);
assertLogMessageWithoutRank("Limit set DEFAULT added on SIDE1", "network.modification.operationalLimitsGroupAdded", reportNode);
assertLogMessageWithoutRank("Limit set group0 has been replaced on side 2", "network.modification.operationalLimitsGroupReplaced", reportNode);
assertLogMessageWithoutRank("Limit set DEFAULT added on side 1", "network.modification.operationalLimitsGroupAdded", reportNode);

}

Expand Down