Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LfAction refactoring and area interchange target action support #1172

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions docs/security/inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ With Open Load Flow only the following remedial actions are currently implemente
- `ShuntCompensatorPositionAction`
- `GeneratorAction`
- `HvdcAction`
- `AreaInterchangeTargetAction`

Note: Some limitations in the use of these actions exist, please read the documentation about the [security analysis specific parameters](parameters).
3 changes: 2 additions & 1 deletion src/main/java/com/powsybl/openloadflow/NetworkCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.powsybl.openloadflow.ac.AcLoadFlowResult;
import com.powsybl.openloadflow.ac.solver.AcSolverStatus;
import com.powsybl.openloadflow.network.*;
import com.powsybl.openloadflow.network.action.AbstractLfBranchAction;
import com.powsybl.openloadflow.network.impl.AbstractLfGenerator;
import com.powsybl.openloadflow.network.impl.LfLegBranch;
import com.powsybl.openloadflow.network.util.PreviousValueVoltageInitializer;
Expand Down Expand Up @@ -239,7 +240,7 @@ private static void updateSwitch(boolean open, LfNetwork lfNetwork, LfBranch lfB
} else {
connectivity.addEdge(lfBranch.getBus1(), lfBranch.getBus2(), lfBranch);
}
LfAction.updateBusesAndBranchStatus(connectivity);
AbstractLfBranchAction.updateBusesAndBranchStatus(connectivity);
} finally {
connectivity.undoTemporaryChanges();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.powsybl.openloadflow.lf.outerloop.OuterLoopResult;
import com.powsybl.openloadflow.lf.outerloop.OuterLoopStatus;
import com.powsybl.openloadflow.network.*;
import com.powsybl.openloadflow.network.action.AbstractLfBranchAction;
import com.powsybl.openloadflow.util.PerUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -81,7 +82,7 @@ public OuterLoopResult check(AcOuterLoopContext context, ReportNode reportNode)
GraphConnectivity<LfBus, LfBranch> connectivity = network.getConnectivity();
branchesToOpen.forEach(connectivity::removeEdge);
branchesToClose.forEach(branch -> connectivity.addEdge(branch.getBus1(), branch.getBus2(), branch));
LfAction.updateBusesAndBranchStatus(connectivity);
AbstractLfBranchAction.updateBusesAndBranchStatus(connectivity);
network.getConnectivity().undoTemporaryChanges();
// we have now to really change the network connectivity.
branchesToOpen.forEach(connectivity::removeEdge);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import com.powsybl.openloadflow.dc.equations.DcEquationType;
import com.powsybl.openloadflow.equations.Equation;
import com.powsybl.openloadflow.network.*;
import com.powsybl.openloadflow.network.action.AbstractLfTapChangerAction;
import com.powsybl.openloadflow.network.action.LfAction;

import java.util.*;

Expand Down Expand Up @@ -112,7 +114,8 @@ public static double[] runDcLoadFlowWithModifiedTargetVector(DcLoadFlowContext l
if (!pstActions.isEmpty()) {
// set transformer phase shift to new shifting value
pstActions.stream()
.map(LfAction::getTapPositionChange)
.filter(AbstractLfTapChangerAction.class::isInstance)
.map(action -> ((AbstractLfTapChangerAction<?>) action).getChange())
.filter(Objects::nonNull)
.forEach(tapPositionChange -> {
LfBranch lfBranch = tapPositionChange.getBranch();
Expand Down
363 changes: 0 additions & 363 deletions src/main/java/com/powsybl/openloadflow/network/LfAction.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2025, Coreso SA (https://www.coreso.eu/) and TSCNET Services GmbH (https://www.tscnet.eu/)
* 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.action;

import com.powsybl.action.Action;

/**
* @author Bertrand Rix {@literal <bertrand.rix at artelys.com>}
*/
public abstract class AbstractLfAction<A extends Action> implements LfAction {

protected final String id;

protected final A action;

AbstractLfAction(String id, A action) {
this.id = id;
this.action = action;
}

@Override
public String getId() {
return id;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Copyright (c) 2025, Coreso SA (https://www.coreso.eu/) and TSCNET Services GmbH (https://www.tscnet.eu/)
* Copyright (c) 2022, 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.network.action;

import com.powsybl.action.Action;
import com.powsybl.openloadflow.graph.GraphConnectivity;
import com.powsybl.openloadflow.network.*;

import java.util.HashSet;
import java.util.Set;

/**
* @author Bertrand Rix {@literal <bertrand.rix at artelys.com>}
* @author Anne Tilloy {@literal <anne.tilloy at rte-france.com>}
* @author Jean-Luc Bouchot {@literal <jlbouchot at gmail.com>}
*/
public abstract class AbstractLfBranchAction<A extends Action> extends AbstractLfAction<A> {

private LfBranch disabledBranch = null; // switch to open

private LfBranch enabledBranch = null; // switch to close

AbstractLfBranchAction(String id, A action) {
super(id, action);
}

protected void setDisabledBranch(LfBranch disabledBranch) {
this.disabledBranch = disabledBranch;
}

protected void setEnabledBranch(LfBranch enabledBranch) {
this.enabledBranch = enabledBranch;
}

public LfBranch getDisabledBranch() {
return this.disabledBranch;
}

public LfBranch getEnabledBranch() {
return this.enabledBranch;
}

abstract boolean findEnabledDisabledBranches(LfNetwork lfNetwork);

/**
* Standalone apply
*/
@Override
public boolean apply(LfNetwork network, LfContingency contingency, LfNetworkParameters networkParameters) {
boolean found = findEnabledDisabledBranches(network);
GraphConnectivity<LfBus, LfBranch> connectivity = network.getConnectivity();

// re-update connectivity according to post contingency state (revert after LfContingency apply)
connectivity.startTemporaryChanges();
if (contingency != null) {
contingency.getDisabledNetwork().getBranches().forEach(connectivity::removeEdge);
}

// update connectivity according to post action state
connectivity.startTemporaryChanges();

applyOnConnectivity(network, connectivity);
updateBusesAndBranchStatus(connectivity);

// reset connectivity to discard post contingency connectivity and post action connectivity
connectivity.undoTemporaryChanges();
connectivity.undoTemporaryChanges();
return found;
}

/**
* Optimized apply on an existing connectivity (to apply several branch actions at the same time)
*/
public boolean applyOnConnectivity(LfNetwork network, GraphConnectivity<LfBus, LfBranch> connectivity) {
boolean found = findEnabledDisabledBranches(network);
updateConnectivity(connectivity);
return found;
}

private void updateConnectivity(GraphConnectivity<LfBus, LfBranch> connectivity) {
if (disabledBranch != null && disabledBranch.getBus1() != null && disabledBranch.getBus2() != null) {
connectivity.removeEdge(disabledBranch);
}
if (enabledBranch != null) {
connectivity.addEdge(enabledBranch.getBus1(), enabledBranch.getBus2(), enabledBranch);
}
}

public static void updateBusesAndBranchStatus(GraphConnectivity<LfBus, LfBranch> connectivity) {
// disable buses and branches that won't be part of the main connected component
Set<LfBus> removedBuses = connectivity.getVerticesRemovedFromMainComponent();
removedBuses.forEach(bus -> bus.setDisabled(true));
Set<LfBranch> removedBranches = new HashSet<>(connectivity.getEdgesRemovedFromMainComponent());
// we should manage branches open at one side.
for (LfBus bus : removedBuses) {
bus.getBranches().stream().filter(b -> !b.isConnectedAtBothSides()).forEach(removedBranches::add);
}
removedBranches.forEach(branch -> branch.setDisabled(true));

// enable buses and branches that will be part of the main connected component
Set<LfBus> addedBuses = connectivity.getVerticesAddedToMainComponent();
addedBuses.forEach(bus -> bus.setDisabled(false));
Set<LfBranch> addedBranches = new HashSet<>(connectivity.getEdgesAddedToMainComponent());
// we should manage branches open at one side.
for (LfBus bus : addedBuses) {
bus.getBranches().stream().filter(b -> !b.isConnectedAtBothSides()).forEach(addedBranches::add);
}
addedBranches.forEach(branch -> branch.setDisabled(false));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright (c) 2025, Coreso SA (https://www.coreso.eu/) and TSCNET Services GmbH (https://www.tscnet.eu/)
* Copyright (c) 2022, 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.network.action;

import com.powsybl.action.AbstractTapChangerTapPositionAction;
import com.powsybl.openloadflow.network.*;
import com.powsybl.openloadflow.network.impl.LfLegBranch;

/**
* @author Bertrand Rix {@literal <bertrand.rix at artelys.com>}
* @author Anne Tilloy {@literal <anne.tilloy at rte-france.com>}
* @author Jean-Luc Bouchot {@literal <jlbouchot at gmail.com>}
*/
public abstract class AbstractLfTapChangerAction<A extends AbstractTapChangerTapPositionAction> extends AbstractLfAction<A> {

protected TapPositionChange change;

protected LfBranch branch;

AbstractLfTapChangerAction(String id, A action, LfNetwork network) {
super(id, action);
String branchId = action.getSide().map(side -> LfLegBranch.getId(side, action.getTransformerId())).orElseGet(action::getTransformerId);
this.branch = network.getBranchById(branchId);
if (this.branch != null) {
if (branch.getPiModel() instanceof SimplePiModel) {
throw new UnsupportedOperationException("Tap position action: only one tap in branch " + branch.getId());
}
this.change = new TapPositionChange(branch, action.getTapPosition(), action.isRelativeValue());
}
}

public TapPositionChange getChange() {
return change;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Copyright (c) 2025, Coreso SA (https://www.coreso.eu/) and TSCNET Services GmbH (https://www.tscnet.eu/)
* 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.action;

import com.powsybl.openloadflow.network.*;

/**
* @author Bertrand Rix {@literal <bertrand.rix at artelys.com>}
*/
public interface LfAction {

String getId();

boolean apply(LfNetwork network, LfContingency contingency, LfNetworkParameters networkParameters);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* Copyright (c) 2025, Coreso SA (https://www.coreso.eu/) and TSCNET Services GmbH (https://www.tscnet.eu/)
* Copyright (c) 2022, 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.network.action;

import com.powsybl.action.*;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.iidm.network.Network;
import com.powsybl.openloadflow.graph.GraphConnectivity;
import com.powsybl.openloadflow.network.*;

import java.util.*;

import static com.powsybl.openloadflow.network.action.AbstractLfBranchAction.updateBusesAndBranchStatus;

/**
* @author Bertrand Rix {@literal <bertrand.rix at artelys.com>}
* @author Anne Tilloy {@literal <anne.tilloy at rte-france.com>}
* @author Jean-Luc Bouchot {@literal <jlbouchot at gmail.com>}
*/
public final class LfActionUtils {

private static final String ACTION_ID = "actionId";

private static final String CONTINGENCY_ID = "contingencyId";

private LfActionUtils() {
}

public static LfAction createLfAction(Action action, Network network, boolean breakers, LfNetwork lfNetwork) {
obrix marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why don(t you return an optional ? The previous code could help detect NOOP actions for the network by returning an empty value if the element did not exist.
You now loose the ability to detect actions that do nothing on a given LfNetwork.

Copy link
Member Author

@obrix obrix Jan 24, 2025

Choose a reason for hiding this comment

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

The old code was returning an optional but actually did not do anything with the "empty" information and just silently ignored the action.
Same checks are done in the apply method with a boolean return and a warning is logged (to be discussed maybe).
Ultimately I think the operator strategy result API should hold the info of what action was applied successfully, this is also why I designed it like this, but this a change of API to be seen in the futur..

Copy link
Collaborator

@vidaldid-rte vidaldid-rte Jan 24, 2025

Choose a reason for hiding this comment

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

Thanks for the explanation.
But isn't there a functional change ?
In current code, I suppose there is no LF run if the list of LF actions is empty for a strategy. Is it the same here .

The point of reporting the missing action is a good point but:
Should be done in a report (users won't look at the log)
May not be relevant in the (rare ?) case of contingency and actions covering different network component.

Copy link
Member Author

Choose a reason for hiding this comment

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

In the current code, even if the list of lfAction converted from iidm actions is empty there is always a load flow done and an operator strategy result is produced (see AbstractSecurityAnalysis.runSimulations usage of lfActionById).
Whether that is a good thing is probably a good question but there is no functionnal change here as far as I can see.

Furthermore we still have the checkActions method that do some additional checks and raise an exception if there is some inconsistency. This one is still there.

Ok for putting the missing action in the report, good idea!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh... You a right ! So let-s consider the fact that the LF is run even when there is no action something not ideal, but independant from this work....

I'll close that thread when the report is there.

Objects.requireNonNull(action);
Objects.requireNonNull(network);
return switch (action.getType()) {
case SwitchAction.NAME -> new LfSwitchAction(action.getId(), (SwitchAction) action);
case TerminalsConnectionAction.NAME ->
new LfTerminalsConnectionAction(action.getId(), (TerminalsConnectionAction) action);
case PhaseTapChangerTapPositionAction.NAME ->
new LfPhaseTapChangerAction(action.getId(), (PhaseTapChangerTapPositionAction) action, lfNetwork);
case RatioTapChangerTapPositionAction.NAME ->
new LfRatioTapChangerAction(action.getId(), (RatioTapChangerTapPositionAction) action, lfNetwork);
case LoadAction.NAME ->
new LfLoadAction(action.getId(), (LoadAction) action, network, breakers);
case GeneratorAction.NAME -> new LfGeneratorAction(action.getId(), (GeneratorAction) action);
case HvdcAction.NAME -> new LfHvdcAction(action.getId(), (HvdcAction) action);
case ShuntCompensatorPositionAction.NAME ->
new LfShuntCompensatorPositionAction(action.getId(), (ShuntCompensatorPositionAction) action);
case AreaInterchangeTargetAction.NAME ->
new LfAreaInterchangeTargetAction(action.getId(), (AreaInterchangeTargetAction) action);
default -> throw new UnsupportedOperationException("Unsupported action type: " + action.getType());
};
}

public static void applyListOfActions(List<LfAction> actions, LfNetwork network, LfContingency contingency, LfNetworkParameters networkParameters, ReportNode node) {
Objects.requireNonNull(actions);
Objects.requireNonNull(network);

// first apply action modifying connectivity
List<LfAction> branchActions = actions.stream()
.filter(action -> action instanceof AbstractLfBranchAction<?>)
.toList();
updateConnectivity(branchActions, network, contingency, node);

// then process remaining changes of actions
actions.stream()
.filter(action -> !(action instanceof AbstractLfBranchAction<?>))
.forEach(action -> {
if (!action.apply(network, contingency, networkParameters)) {
reportActionApplicationFailure(action.getId(), contingency.getId(), node);
}
});
}

private static void updateConnectivity(List<LfAction> branchActions, LfNetwork network, LfContingency contingency, ReportNode node) {
GraphConnectivity<LfBus, LfBranch> connectivity = network.getConnectivity();

// re-update connectivity according to post contingency state (revert after LfContingency apply)
connectivity.startTemporaryChanges();
contingency.getDisabledNetwork().getBranches().forEach(connectivity::removeEdge);

// update connectivity according to post action state
connectivity.startTemporaryChanges();

branchActions.forEach(action -> {
if (!((AbstractLfBranchAction<?>) action).applyOnConnectivity(network, connectivity)) {
reportActionApplicationFailure(action.getId(), contingency.getId(), node);
}
});

updateBusesAndBranchStatus(connectivity);

// reset connectivity to discard post contingency connectivity and post action connectivity
connectivity.undoTemporaryChanges();
connectivity.undoTemporaryChanges();
}

private static void reportActionApplicationFailure(String actionId, String contingencyId, ReportNode node) {
node.newReportNode()
.withMessageTemplate("LfActionUtils", "Action '${actionId}': may not have been applied successfully on contingency '${contingencyId}'")
.withUntypedValue(ACTION_ID, actionId)
.withUntypedValue(CONTINGENCY_ID, contingencyId)
.add();
}

}
Loading
Loading