Repository: powsybl/powsybl-open-loadflow
Pull request #1213: Remove network results and violation detection on disabled branches on one side in fast DC Security Analysis
Author: login: p-arvy
Labels: name: bug
description: Something isn't working
Created: 2025-03-27T17:14:48Z | Merged: 2025-04-04T08:04:48Z

**Please check if the PR fulfills these requirements**
<!-- please use `'[x]'` to check the checkboxes, or submit the PR and then click the checkboxes -->
- [x] The commit message follows our guidelines
- [x] Tests for the changes have been added (for bug fixes / features)
- [x] Docs have been added / updated (for bug fixes / features)
- [ ] A PR or issue has been opened in all impacted repositories (if any)


**Does this PR already have an issue describing the problem?**
<!-- If so, link to this issue using `'Fixes #XXX'` and skip the rest -->
No.


**What kind of change does this PR introduce?**
<!-- Bug fix, feature, docs update, ... -->
This PR aims to remove network results and limit violation detection in the fast DC SA, for branches that have been disabled on one side due to a contingency.


**What is the current behavior?**
<!-- You can also link to an open issue here -->
Currently, if branches are disabled only on one side (for example, in the case of a contingency on a busbar section), the fast DC SA will compute network results and may detect limit violations on these branches.

This behavior differs from the slow DC SA, as the latter uses a listener to replace `ClosedBranchSide1/2DcFlowEquationTerm` with `EvaluableConstants.ZERO` when one side of the line is disabled. As a result, branch results are set to 0 on both sides, and no limits are violated (**although they are still analyzed**).


**What is the new behavior (if this is a feature change)?**
In fast DC SA, a customized filter is now applied before computing the network results or checking for limit violations. This filter is inspired by the work done in PR #1169.

A branch where one side is disabled by a contingency is considered disabled, meaning no computation or verification is performed on these branches. This differs from the slow DC SA (see the previous paragraph), where the filter simply checks whether the branches have been disabled on both sides by the contingency.

**Does this PR introduce a breaking change or deprecate an API?**
- [ ] Yes
- [x] No

**If yes, please check if the following requirements are fulfilled**
<!-- If no breaking changes or API deprecations were introduced, delete this section -->
- [ ] The *Breaking Change* or *Deprecated* label has been added
- [ ] The migration steps are described in the following section

**What changes might users need to make in their application due to this PR? (migration steps)**
<!-- If this PR introduces a breaking change, describe the migration steps -->
<!-- The content of this section will be copied in the migration guide -->

**Other information**:
<!-- if any of the questions/checkboxes don't apply, please delete them entirely -->


Diff:
diff --git a/docs/security/parameters.md b/docs/security/parameters.md
index 89a9738b06..18b80b2c24 100644
--- a/docs/security/parameters.md
+++ b/docs/security/parameters.md
@@ -55,7 +55,8 @@ The `dcFastMode` property allows to use fast DC security analysis, based on Wood
 when DC mode is activated.
 
 Please note that fast mode has a few limitations:
-- Contingencies applied on branches opened on one side are ignored.
+- Contingencies applied on branches opened on one side are ignored. Also, if a contingency causes the loss of one side of a branch, 
+it is considered completely disabled, and no results are reported for this branch.
 - AC emulation of HVDC lines is disabled, as it is not yet supported.
 Instead, the [active power setpoint](../loadflow/loadflow.md#computing-hvdc-power-flow) mode is used to control the active power flow through these lines. 
 - Only PST remedial actions are supported for now.
diff --git a/src/main/java/com/powsybl/openloadflow/dc/fastdc/ConnectivityBreakAnalysis.java b/src/main/java/com/powsybl/openloadflow/dc/fastdc/ConnectivityBreakAnalysis.java
index 92141f933d..7960d40070 100644
--- a/src/main/java/com/powsybl/openloadflow/dc/fastdc/ConnectivityBreakAnalysis.java
+++ b/src/main/java/com/powsybl/openloadflow/dc/fastdc/ConnectivityBreakAnalysis.java
@@ -142,10 +142,12 @@ private static List<ConnectivityAnalysisResult> computeConnectivityData(LfNetwor
 
         GraphConnectivity<LfBus, LfBranch> connectivity = lfNetwork.getConnectivity();
         for (PropagatedContingency contingency : potentiallyBreakingConnectivityContingencies) {
-            List<ComputedContingencyElement> breakingConnectivityCandidates = contingency.getBranchIdsToOpen().keySet().stream().map(contingencyElementByBranch::get).collect(Collectors.toList());
+            List<ComputedContingencyElement> breakingConnectivityCandidates = contingency.getBranchIdsToOpen().keySet().stream().sorted().map(contingencyElementByBranch::get).collect(Collectors.toList());
 
             // we confirm the breaking of connectivity by network connectivity
-            Set<ComputedContingencyElement> breakingConnectivityElements;
+            // the traversal order of the set must be deterministic to ensure consistent element selection when multiple elements can restore connectivity
+            // without this, fast DC post contingency states may vary for buses disconnected from main connected component, depending on which elements were selected
+            LinkedHashSet<ComputedContingencyElement> breakingConnectivityElements;
             connectivity.startTemporaryChanges();
             try {
                 ComputedContingencyElement.applyToConnectivity(lfNetwork, connectivity, breakingConnectivityCandidates);
diff --git a/src/main/java/com/powsybl/openloadflow/sa/AbstractNetworkResult.java b/src/main/java/com/powsybl/openloadflow/sa/AbstractNetworkResult.java
index dffea7c59d..d8624bdf68 100644
--- a/src/main/java/com/powsybl/openloadflow/sa/AbstractNetworkResult.java
+++ b/src/main/java/com/powsybl/openloadflow/sa/AbstractNetworkResult.java
@@ -21,6 +21,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /**
  * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
@@ -43,11 +44,11 @@ protected AbstractNetworkResult(LfNetwork network, StateMonitorIndex monitorInde
         this.createResultExtension = createResultExtension;
     }
 
-    protected void addResults(StateMonitor monitor, Consumer<LfBranch> branchConsumer) {
+    protected void addResults(StateMonitor monitor, Consumer<LfBranch> branchConsumer, Predicate<LfBranch> isBranchDisabled) {
         Objects.requireNonNull(monitor);
         if (!monitor.getBranchIds().isEmpty()) {
             network.getBranches().stream()
-                    .filter(lfBranch -> !lfBranch.isDisabled())
+                    .filter(lfBranch -> !isBranchDisabled.test(lfBranch))
                     .forEach(lfBranch -> {
                         for (String originalId : lfBranch.getOriginalIds()) {
                             if (monitor.getBranchIds().contains(originalId)) {
diff --git a/src/main/java/com/powsybl/openloadflow/sa/LimitViolationManager.java b/src/main/java/com/powsybl/openloadflow/sa/LimitViolationManager.java
index 72774ea1b5..ab5c19183a 100644
--- a/src/main/java/com/powsybl/openloadflow/sa/LimitViolationManager.java
+++ b/src/main/java/com/powsybl/openloadflow/sa/LimitViolationManager.java
@@ -10,6 +10,7 @@
 import com.powsybl.iidm.network.*;
 import com.powsybl.openloadflow.network.LfBranch;
 import com.powsybl.openloadflow.network.LfBus;
+import com.powsybl.openloadflow.network.LfElement;
 import com.powsybl.openloadflow.network.LfNetwork;
 import com.powsybl.openloadflow.util.Evaluable;
 import com.powsybl.openloadflow.util.PerUnit;
@@ -20,6 +21,7 @@
 
 import java.util.*;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.function.ToDoubleFunction;
 
 /**
@@ -60,10 +62,19 @@ public List<LimitViolation> getLimitViolations() {
      * @param network network on which the violation limits are checked
      */
     public void detectViolations(LfNetwork network) {
+        detectViolations(network, LfElement::isDisabled);
+    }
+
+    /**
+     * Detect violations on branches and on buses
+     * @param network network on which the violation limits are checked
+     * @param isBranchDisabled predicate to evaluate if a branch of the network is disabled or not
+     */
+    public void detectViolations(LfNetwork network, Predicate<LfBranch> isBranchDisabled) {
         Objects.requireNonNull(network);
 
         // Detect violation limits on branches
-        network.getBranches().stream().filter(b -> !b.isDisabled()).forEach(this::detectBranchViolations);
+        network.getBranches().stream().filter(b -> !isBranchDisabled.test(b)).forEach(this::detectBranchViolations);
 
         // Detect violation limits on buses
         network.getBuses().stream().filter(b -> !b.isDisabled()).forEach(this::detectBusViolations);
diff --git a/src/main/java/com/powsybl/openloadflow/sa/PostContingencyNetworkResult.java b/src/main/java/com/powsybl/openloadflow/sa/PostContingencyNetworkResult.java
index c703777155..b0b7f398ae 100644
--- a/src/main/java/com/powsybl/openloadflow/sa/PostContingencyNetworkResult.java
+++ b/src/main/java/com/powsybl/openloadflow/sa/PostContingencyNetworkResult.java
@@ -10,6 +10,7 @@
 import com.powsybl.contingency.Contingency;
 import com.powsybl.contingency.ContingencyElement;
 import com.powsybl.contingency.ContingencyElementType;
+import com.powsybl.openloadflow.network.LfBranch;
 import com.powsybl.openloadflow.network.LfNetwork;
 import com.powsybl.security.monitor.StateMonitor;
 import com.powsybl.security.monitor.StateMonitorIndex;
@@ -18,6 +19,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.function.Predicate;
 
 /**
  * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
@@ -43,7 +45,7 @@ protected void clear() {
         branchResults.clear();
     }
 
-    public void addResults(StateMonitor monitor) {
+    public void addResults(StateMonitor monitor, Predicate<LfBranch> isBranchDisabled) {
         addResults(monitor, branch -> {
             var preContingencyBranchResult = preContingencyMonitorInfos.getBranchResult(branch.getId());
             double preContingencyBranchP1 = preContingencyBranchResult != null ? preContingencyBranchResult.getP1() : Double.NaN;
@@ -61,17 +63,21 @@ public void addResults(StateMonitor monitor) {
                 }
             }
             branchResults.addAll(branch.createBranchResult(preContingencyBranchP1, preContingencyBranchOfContingencyP1, createResultExtension));
-        });
+        }, isBranchDisabled);
     }
 
     @Override
     public void update() {
+        update(LfBranch::isDisabled);
+    }
+
+    public void update(Predicate<LfBranch> isBranchDisabled) {
         clear();
         StateMonitor stateMonitor = monitorIndex.getSpecificStateMonitors().get(contingency.getId());
         if (stateMonitor != null) {
-            addResults(stateMonitor);
+            addResults(stateMonitor, isBranchDisabled);
         } else {
-            addResults(monitorIndex.getAllStateMonitor());
+            addResults(monitorIndex.getAllStateMonitor(), isBranchDisabled);
         }
     }
 
diff --git a/src/main/java/com/powsybl/openloadflow/sa/PreContingencyNetworkResult.java b/src/main/java/com/powsybl/openloadflow/sa/PreContingencyNetworkResult.java
index 294bf6e04b..0ff2eb7f67 100644
--- a/src/main/java/com/powsybl/openloadflow/sa/PreContingencyNetworkResult.java
+++ b/src/main/java/com/powsybl/openloadflow/sa/PreContingencyNetworkResult.java
@@ -7,12 +7,14 @@
  */
 package com.powsybl.openloadflow.sa;
 
+import com.powsybl.openloadflow.network.LfBranch;
 import com.powsybl.openloadflow.network.LfNetwork;
 import com.powsybl.security.monitor.StateMonitor;
 import com.powsybl.security.monitor.StateMonitorIndex;
 import com.powsybl.security.results.BranchResult;
 
 import java.util.*;
+import java.util.function.Predicate;
 
 /**
  * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
@@ -31,18 +33,22 @@ protected void clear() {
         branchResults.clear();
     }
 
-    private void addResults(StateMonitor monitor) {
+    private void addResults(StateMonitor monitor, Predicate<LfBranch> isBranchDisabled) {
         addResults(monitor, branch -> {
             branch.createBranchResult(Double.NaN, Double.NaN, createResultExtension)
                     .forEach(branchResult -> branchResults.put(branchResult.getBranchId(), branchResult));
-        });
+        }, isBranchDisabled);
     }
 
     @Override
     public void update() {
+        update(LfBranch::isDisabled);
+    }
+
+    public void update(Predicate<LfBranch> isBranchDisabled) {
         clear();
-        addResults(monitorIndex.getNoneStateMonitor());
-        addResults(monitorIndex.getAllStateMonitor());
+        addResults(monitorIndex.getNoneStateMonitor(), isBranchDisabled);
+        addResults(monitorIndex.getAllStateMonitor(), isBranchDisabled);
     }
 
     public BranchResult getBranchResult(String branchId) {
diff --git a/src/main/java/com/powsybl/openloadflow/sa/WoodburyDcSecurityAnalysis.java b/src/main/java/com/powsybl/openloadflow/sa/WoodburyDcSecurityAnalysis.java
index de95668d22..afb0a2277d 100644
--- a/src/main/java/com/powsybl/openloadflow/sa/WoodburyDcSecurityAnalysis.java
+++ b/src/main/java/com/powsybl/openloadflow/sa/WoodburyDcSecurityAnalysis.java
@@ -45,6 +45,7 @@
 
 import java.util.*;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
@@ -206,7 +207,8 @@ private void filterActions(List<Action> actions) {
     private PostContingencyResult computePostContingencyResultFromPostContingencyStates(DcLoadFlowContext loadFlowContext, Contingency contingency, LfContingency lfContingency,
                                                                                         LimitViolationManager preContingencyLimitViolationManager, PreContingencyNetworkResult preContingencyNetworkResult,
                                                                                         boolean createResultExtension, SecurityAnalysisParameters.IncreasedViolationsParameters violationsParameters,
-                                                                                        double[] postContingencyStates, List<LimitReduction> limitReductions) {
+                                                                                        double[] postContingencyStates, Predicate<LfBranch> isBranchDisabledDueToContingency,
+                                                                                        List<LimitReduction> limitReductions) {
 
         // update network state with post contingency states
         LfNetwork lfNetwork = loadFlowContext.getNetwork();
@@ -217,11 +219,11 @@ private PostContingencyResult computePostContingencyResultFromPostContingencySta
 
         // update post contingency network result
         var postContingencyNetworkResult = new PostContingencyNetworkResult(lfNetwork, monitorIndex, createResultExtension, preContingencyNetworkResult, contingency);
-        postContingencyNetworkResult.update();
+        postContingencyNetworkResult.update(isBranchDisabledDueToContingency);
 
         // detect violations
         var postContingencyLimitViolationManager = new LimitViolationManager(preContingencyLimitViolationManager, limitReductions, violationsParameters);
-        postContingencyLimitViolationManager.detectViolations(lfNetwork);
+        postContingencyLimitViolationManager.detectViolations(lfNetwork, isBranchDisabledDueToContingency);
 
         // connectivity result due to the application of the lf contingency
         var connectivityResult = new ConnectivityResult(
@@ -244,7 +246,8 @@ private PostContingencyResult computePostContingencyResultFromPostContingencySta
      */
     private PostContingencyResult processPostContingencyResult(DcLoadFlowContext context, PropagatedContingency contingency, LfContingency lfContingency, Supplier<double[]> postContingencyStatesSupplier,
                                                                LimitViolationManager preContingencyLimitViolationManager, PreContingencyNetworkResult preContingencyNetworkResult,
-                                                               boolean createResultExtension, SecurityAnalysisParameters.IncreasedViolationsParameters violationsParameters,
+                                                               Predicate<LfBranch> isBranchDisabledDueToContingency, boolean createResultExtension,
+                                                               SecurityAnalysisParameters.IncreasedViolationsParameters violationsParameters,
                                                                List<LimitReduction> limitReductions) {
         logPostContingencyStart(context.getNetwork(), lfContingency);
         Stopwatch stopwatch = Stopwatch.createStarted();
@@ -252,7 +255,7 @@ private PostContingencyResult processPostContingencyResult(DcLoadFlowContext con
         double[] postContingencyStates = postContingencyStatesSupplier.get();
         PostContingencyResult postContingencyResult = computePostContingencyResultFromPostContingencyStates(context, contingency.getContingency(),
                 lfContingency, preContingencyLimitViolationManager, preContingencyNetworkResult, createResultExtension,
-                violationsParameters, postContingencyStates, limitReductions);
+                violationsParameters, postContingencyStates, isBranchDisabledDueToContingency, limitReductions);
 
         stopwatch.stop();
         logPostContingencyEnd(context.getNetwork(), lfContingency, stopwatch);
@@ -266,7 +269,7 @@ private PostContingencyResult processPostContingencyResult(DcLoadFlowContext con
     private OperatorStrategyResult computeOperatorStrategyResultFromPostContingencyAndOperatorStrategyStates(DcLoadFlowContext loadFlowContext, LfContingency lfContingency, OperatorStrategy operatorStrategy,
                                                                                                              List<LfAction> operatorStrategyLfActions, LimitViolationManager preContingencyLimitViolationManager,
                                                                                                              boolean createResultExtension, SecurityAnalysisParameters.IncreasedViolationsParameters violationsParameters,
-                                                                                                             double[] postContingencyAndOperatorStrategyStates, List<LimitReduction> limitReductions) {
+                                                                                                             double[] postContingencyAndOperatorStrategyStates, Predicate<LfBranch> isBranchDisabledDueToContingency, List<LimitReduction> limitReductions) {
         // update network state with post contingency and post operator strategy states
         LfNetwork lfNetwork = loadFlowContext.getNetwork();
         loadFlowContext.getEquationSystem().getStateVector().set(postContingencyAndOperatorStrategyStates);
@@ -278,11 +281,11 @@ private OperatorStrategyResult computeOperatorStrategyResultFromPostContingencyA
 
         // update network result
         var postActionsNetworkResult = new PreContingencyNetworkResult(lfNetwork, monitorIndex, createResultExtension);
-        postActionsNetworkResult.update();
+        postActionsNetworkResult.update(isBranchDisabledDueToContingency);
 
         // detect violations
         var postActionsViolationManager = new LimitViolationManager(preContingencyLimitViolationManager, limitReductions, violationsParameters);
-        postActionsViolationManager.detectViolations(lfNetwork);
+        postActionsViolationManager.detectViolations(lfNetwork, isBranchDisabledDueToContingency);
 
         return new OperatorStrategyResult(operatorStrategy, PostContingencyComputationStatus.CONVERGED,
                 new LimitViolationsResult(postActionsViolationManager.getLimitViolations()),
@@ -297,8 +300,8 @@ private OperatorStrategyResult computeOperatorStrategyResultFromPostContingencyA
     private OperatorStrategyResult processOperatorStrategyResult(DcLoadFlowContext context, LfContingency contingency, OperatorStrategy operatorStrategy,
                                                                  Function<List<LfAction>, double[]> postContingencyAndOperatorStrategyStatesSupplier,
                                                                  LimitViolationManager preContingencyLimitViolationManager, PostContingencyResult postContingencyResult,
-                                                                 Map<String, LfAction> lfActionById, boolean createResultExtension, SecurityAnalysisParameters.IncreasedViolationsParameters violationsParameters,
-                                                                 List<LimitReduction> limitReductions) {
+                                                                 Map<String, LfAction> lfActionById, Predicate<LfBranch> isBranchDisabledDueToContingency, boolean createResultExtension,
+                                                                 SecurityAnalysisParameters.IncreasedViolationsParameters violationsParameters, List<LimitReduction> limitReductions) {
         // get the actions associated to the operator strategy
         List<String> actionIds = checkCondition(operatorStrategy, postContingencyResult.getLimitViolationsResult());
         List<LfAction> operatorStrategyLfActions = actionIds.stream()
@@ -311,7 +314,7 @@ private OperatorStrategyResult processOperatorStrategyResult(DcLoadFlowContext c
 
         double[] postContingencyAndOperatorStrategyStates = postContingencyAndOperatorStrategyStatesSupplier.apply(operatorStrategyLfActions);
         OperatorStrategyResult operatorStrategyResult = computeOperatorStrategyResultFromPostContingencyAndOperatorStrategyStates(context, contingency, operatorStrategy, operatorStrategyLfActions,
-                preContingencyLimitViolationManager, createResultExtension, violationsParameters, postContingencyAndOperatorStrategyStates, limitReductions);
+                preContingencyLimitViolationManager, createResultExtension, violationsParameters, postContingencyAndOperatorStrategyStates, isBranchDisabledDueToContingency, limitReductions);
 
         stopwatch.stop();
         logActionEnd(context.getNetwork(), operatorStrategy, stopwatch);
@@ -333,9 +336,13 @@ private void addPostContingencyAndOperatorStrategyResults(DcLoadFlowContext cont
             ReportNode postContSimReportNode = Reports.createPostContingencySimulation(lfNetwork.getReportNode(), contingency.getContingency().getId());
             lfNetwork.setReportNode(postContSimReportNode);
 
+            // predicate to determine if a branch is disabled or not due to the contingency
+            // note that branches with one side opened due to the contingency are considered are disabled
+            Predicate<LfBranch> isBranchDisabled = branch -> lfContingency.getDisabledNetwork().getBranchesStatus().containsKey(branch);
+
             // process post contingency result with supplier giving post contingency states
             PostContingencyResult postContingencyResult = processPostContingencyResult(context, contingency, lfContingency, toPostContingencyStates, preContingencyLimitViolationManager,
-                    preContingencyNetworkResult, createResultExtension, violationsParameters, limitReductions);
+                    preContingencyNetworkResult, isBranchDisabled, createResultExtension, violationsParameters, limitReductions);
             postContingencyResults.add(postContingencyResult);
 
             // restore pre contingency states for next calculation
@@ -349,7 +356,7 @@ private void addPostContingencyAndOperatorStrategyResults(DcLoadFlowContext cont
 
                     // process operator strategy result with supplier giving post contingency and post operator strategy states
                     OperatorStrategyResult operatorStrategyResult = processOperatorStrategyResult(context, lfContingency, operatorStrategy, toPostContingencyAndOperatorStrategyStates, preContingencyLimitViolationManager,
-                            postContingencyResult, lfActionById, createResultExtension, violationsParameters, limitReductions);
+                            postContingencyResult, lfActionById, isBranchDisabled, createResultExtension, violationsParameters, limitReductions);
                     operatorStrategyResults.add(operatorStrategyResult);
 
                     // restore pre contingency states for next calculation
diff --git a/src/test/java/com/powsybl/openloadflow/network/NodeBreakerNetworkFactory.java b/src/test/java/com/powsybl/openloadflow/network/NodeBreakerNetworkFactory.java
index 8ce15679ad..27881a2122 100644
--- a/src/test/java/com/powsybl/openloadflow/network/NodeBreakerNetworkFactory.java
+++ b/src/test/java/com/powsybl/openloadflow/network/NodeBreakerNetworkFactory.java
@@ -7,10 +7,7 @@
  */
 package com.powsybl.openloadflow.network;
 
-import com.powsybl.iidm.network.Network;
-import com.powsybl.iidm.network.Substation;
-import com.powsybl.iidm.network.TopologyKind;
-import com.powsybl.iidm.network.VoltageLevel;
+import com.powsybl.iidm.network.*;
 
 public final class NodeBreakerNetworkFactory {
 
@@ -174,6 +171,142 @@ public static Network create() {
         return network;
     }
 
+    /**
+     * <pre>
+     *           G
+     *           |     C
+     * BBS1 ----------[+]------------------ BBS2     VL1
+     *         |          [+] B1    B5 [+]
+     *         |           |            |
+     *         |           |        PS1 |
+     *         |           |            |      LD2
+     *         |           |        B8 [+]      |
+     *      L1 |           | L2        ------------ BBS4     VL3
+     *         |           |         B9 [+]  [+] B10
+     *         |           |             |    |
+     *         |           |          L3 |    | L4
+     *         |           |             |    |
+     *     B3 [+]         [+] B4     B6 [+]  [+] B7
+     * BBS3 -------------------------------------     VL2
+     *                        |
+     *                        LD
+     * </pre>
+     *
+     * @author Pierre Arvy {@literal <pierre.arvy at artelys.com>}
+     */
+    public static Network createWith4Bars() {
+        Network network = create();
+        network.getVoltageLevel("VL1").getNodeBreakerView().newBreaker()
+                .setId("B5")
+                .setNode1(1)
+                .setNode2(7)
+                .add();
+        network.getVoltageLevel("VL2").getNodeBreakerView().newBreaker()
+                .setId("B6")
+                .setNode1(0)
+                .setNode2(4)
+                .add();
+        network.getVoltageLevel("VL2").getNodeBreakerView().newBreaker()
+                .setId("B7")
+                .setNode1(0)
+                .setNode2(5)
+                .add();
+        VoltageLevel vl3 = network.getSubstation("S").newVoltageLevel()
+                .setId("VL3")
+                .setNominalV(400)
+                .setLowVoltageLimit(370.)
+                .setHighVoltageLimit(420.)
+                .setTopologyKind(TopologyKind.NODE_BREAKER)
+                .add();
+        vl3.getNodeBreakerView().newBusbarSection()
+                .setId("BBS4")
+                .setNode(0)
+                .add();
+        vl3.getNodeBreakerView().newBreaker()
+                .setId("B8")
+                .setNode1(0)
+                .setNode2(1)
+                .add();
+        vl3.getNodeBreakerView().newBreaker()
+                .setId("B9")
+                .setNode1(0)
+                .setNode2(2)
+                .add();
+        vl3.getNodeBreakerView().newBreaker()
+                .setId("B10")
+                .setNode1(0)
+                .setNode2(3)
+                .add();
+        vl3.getNodeBreakerView().newInternalConnection()
+                .setNode1(0)
+                .setNode2(4)
+                .add();
+        vl3.newLoad()
+                .setId("LD2")
+                .setNode(4)
+                .setP0(200.0)
+                .setQ0(50.0)
+                .add();
+        TwoWindingsTransformer ps1 = network.getSubstation("S").newTwoWindingsTransformer()
+                .setId("PS1")
+                .setVoltageLevel1("VL1")
+                .setNode1(7)
+                .setVoltageLevel2("VL3")
+                .setNode2(1)
+                .setRatedU1(400)
+                .setRatedU2(400)
+                .setR(0)
+                .setX(100.0)
+                .setG(0.0)
+                .setB(0.0)
+                .add();
+        ps1.newPhaseTapChanger()
+                .setTapPosition(1)
+                .setRegulationTerminal(ps1.getTerminal2())
+                .setRegulationMode(PhaseTapChanger.RegulationMode.FIXED_TAP)
+                .setRegulationValue(200)
+                .beginStep()
+                .setAlpha(-5)
+                .setRho(1.0)
+                .setR(0.0)
+                .setX(50)
+                .setG(0.0)
+                .setB(0.0)
+                .endStep()
+                .beginStep()
+                .setAlpha(-5)
+                .setRho(1.0)
+                .setR(0.0)
+                .setX(100)
+                .setG(0.0)
+                .setB(0.0)
+                .endStep()
+                .add();
+        network.newLine()
+                .setId("L3")
+                .setVoltageLevel1("VL2")
+                .setNode1(4)
+                .setVoltageLevel2("VL3")
+                .setNode2(2)
+                .setR(3.0)
+                .setX(33.0)
+                .setB1(386E-6 / 2)
+                .setB2(386E-6 / 2)
+                .add();
+        network.newLine()
+                .setId("L4")
+                .setVoltageLevel1("VL3")
+                .setNode1(3)
+                .setVoltageLevel2("VL2")
+                .setNode2(5)
+                .setR(3.0)
+                .setX(33.0)
+                .setB1(386E-6 / 2)
+                .setB2(386E-6 / 2)
+                .add();
+        return network;
+    }
+
     private static void createBar(VoltageLevel vl, String id, int node) {
         vl.getNodeBreakerView().newBusbarSection().setId(id).setNode(node).add();
     }
diff --git a/src/test/java/com/powsybl/openloadflow/sa/OpenSecurityAnalysisTest.java b/src/test/java/com/powsybl/openloadflow/sa/OpenSecurityAnalysisTest.java
index 208e8e4cb9..07472c61d2 100644
--- a/src/test/java/com/powsybl/openloadflow/sa/OpenSecurityAnalysisTest.java
+++ b/src/test/java/com/powsybl/openloadflow/sa/OpenSecurityAnalysisTest.java
@@ -4388,4 +4388,48 @@ void testContingencyParametersOuterLoopNamesDc() {
         PostContingencyResult postContingencyResult1 = getPostContingencyResult(result1, "load3_outerLoopNames");
         assertEquals(36.667, postContingencyResult1.getNetworkResult().getBranchResult("tl1").getP1(), LoadFlowAssert.DELTA_POWER);
     }
+
+    @Test
+    void testDcSaNoViolationDetectedOnRemovedBranchOnOneSide() {
+        Network network = NodeBreakerNetworkFactory.createWith4Bars();
+
+        // add small limits on disabled lines to verify there is no violation detected
+        network.getLine("L3").newCurrentLimits1().setPermanentLimit(0.1).add();
+        network.getLine("L3").newCurrentLimits2().setPermanentLimit(0.1).add();
+        network.getLine("L4").newCurrentLimits1().setPermanentLimit(0.1).add();
+        network.getLine("L4").newCurrentLimits2().setPermanentLimit(0.1).add();
+
+        LoadFlowParameters lfParameters = new LoadFlowParameters();
+        lfParameters.setDc(true);
+        setSlackBusId(lfParameters, "VL1_0");
+        SecurityAnalysisParameters securityAnalysisParameters = new SecurityAnalysisParameters();
+        securityAnalysisParameters.setLoadFlowParameters(lfParameters);
+
+        // this contingency will disable L4 on side 1 and L5 on side 2
+        List<Contingency> contingencies = Stream.of("BBS3")
+                .map(id -> new Contingency(id, new BusbarSectionContingency(id)))
+                .collect(Collectors.toList());
+        List<StateMonitor> monitors = createAllBranchesMonitors(network);
+
+        SecurityAnalysisResult resultDefaultDcSa = runSecurityAnalysis(network, contingencies, monitors, securityAnalysisParameters);
+        PostContingencyResult postContingencyResult = getPostContingencyResult(resultDefaultDcSa, "BBS3");
+        assertEquals(200.0, postContingencyResult.getNetworkResult().getBranchResult("PS1").getP1(), DELTA_POWER);
+        assertEquals(-200.0, postContingencyResult.getNetworkResult().getBranchResult("PS1").getP2(), DELTA_POWER);
+        assertEquals(0, postContingencyResult.getLimitViolationsResult().getLimitViolations().size());
+        // in default dc mode, branch results with 0 flow are created for disabled branches on one side
+        assertEquals(5, postContingencyResult.getNetworkResult().getBranchResults().size());
+
+        // set dc sa mode
+        OpenSecurityAnalysisParameters openSecurityAnalysisParameters = new OpenSecurityAnalysisParameters();
+        openSecurityAnalysisParameters.setDcFastMode(true);
+        securityAnalysisParameters.addExtension(OpenSecurityAnalysisParameters.class, openSecurityAnalysisParameters);
+
+        SecurityAnalysisResult resultFastDcSa = runSecurityAnalysis(network, contingencies, monitors, securityAnalysisParameters);
+        postContingencyResult = getPostContingencyResult(resultFastDcSa, "BBS3");
+        assertEquals(200.0, postContingencyResult.getNetworkResult().getBranchResult("PS1").getP1(), DELTA_POWER);
+        assertEquals(-200.0, postContingencyResult.getNetworkResult().getBranchResult("PS1").getP2(), DELTA_POWER);
+        assertEquals(0, postContingencyResult.getLimitViolationsResult().getLimitViolations().size());
+        // in fast dc mode, no branch result is created for disabled branches on one side
+        assertEquals(1, postContingencyResult.getNetworkResult().getBranchResults().size());
+    }
 }
diff --git a/src/test/java/com/powsybl/openloadflow/sa/WoodburyDcSecurityAnalysisWithActionsTest.java b/src/test/java/com/powsybl/openloadflow/sa/WoodburyDcSecurityAnalysisWithActionsTest.java
index 8b434a088d..7b9c065a14 100644
--- a/src/test/java/com/powsybl/openloadflow/sa/WoodburyDcSecurityAnalysisWithActionsTest.java
+++ b/src/test/java/com/powsybl/openloadflow/sa/WoodburyDcSecurityAnalysisWithActionsTest.java
@@ -11,13 +11,11 @@
 import com.powsybl.action.GeneratorActionBuilder;
 import com.powsybl.action.PhaseTapChangerTapPositionAction;
 import com.powsybl.commons.report.ReportNode;
-import com.powsybl.contingency.BranchContingency;
-import com.powsybl.contingency.Contingency;
-import com.powsybl.contingency.ContingencyContext;
-import com.powsybl.contingency.LoadContingency;
+import com.powsybl.contingency.*;
 import com.powsybl.iidm.network.Network;
 import com.powsybl.loadflow.LoadFlowParameters;
 import com.powsybl.openloadflow.OpenLoadFlowParameters;
+import com.powsybl.openloadflow.network.NodeBreakerNetworkFactory;
 import com.powsybl.openloadflow.network.PhaseControlFactory;
 import com.powsybl.openloadflow.network.SlackBusSelectionMode;
 import com.powsybl.openloadflow.util.LoadFlowAssert;
@@ -35,7 +33,10 @@
 
 import java.util.List;
 import java.util.concurrent.CompletionException;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
+import static com.powsybl.openloadflow.util.LoadFlowAssert.DELTA_POWER;
 import static org.junit.jupiter.api.Assertions.*;
 
 /**
@@ -451,4 +452,51 @@ void testFastDcSaWithActionNotOnPst() {
                 () -> runSecurityAnalysis(network, contingencies, monitors, securityAnalysisParameters, operatorStrategies, actions, ReportNode.NO_OP));
         assertTrue(thrown.getCause().getMessage().contains("For now, only PhaseTapChangerTapPositionAction is allowed in WoodburyDcSecurityAnalysis"));
     }
+
+    @Test
+    void testDcSaPhaseTapChangerTapPositionChangeNoViolationDetectedOnRemovedBranchOnOneSide() {
+        Network network = NodeBreakerNetworkFactory.createWith4Bars();
+
+        // add small limits on disabled lines to verify there is no violation detected
+        network.getLine("L3").newCurrentLimits1().setPermanentLimit(0.1).add();
+        network.getLine("L3").newCurrentLimits2().setPermanentLimit(0.1).add();
+        network.getLine("L4").newCurrentLimits1().setPermanentLimit(0.1).add();
+        network.getLine("L4").newCurrentLimits2().setPermanentLimit(0.1).add();
+
+        setSlackBusId(parameters, "VL1_0");
+        SecurityAnalysisParameters securityParameters = new SecurityAnalysisParameters();
+        securityParameters.setLoadFlowParameters(parameters);
+
+        // this contingency will disable L4 on side 1 and L5 on side 2
+        List<Contingency> contingencies = Stream.of("BBS3")
+                .map(id -> new Contingency(id, new BusbarSectionContingency(id)))
+                .collect(Collectors.toList());
+        List<StateMonitor> monitors = createAllBranchesMonitors(network);
+        // the action has no real effect on the flow, we add it to verify operator strategy result
+        List<Action> actions = List.of(new PhaseTapChangerTapPositionAction("pstChange", "PS1", false, 0));
+        List<OperatorStrategy> operatorStrategies = List.of(new OperatorStrategy("strategyPstChange", ContingencyContext.specificContingency("BBS3"), new TrueCondition(), List.of("pstChange")));
+
+        SecurityAnalysisResult resultDefaultDcSa = runSecurityAnalysis(network, contingencies, monitors, securityParameters,
+                operatorStrategies, actions, ReportNode.NO_OP);
+        OperatorStrategyResult operatorStrategyResult = getOperatorStrategyResult(resultDefaultDcSa, "strategyPstChange");
+        assertEquals(200.0, operatorStrategyResult.getNetworkResult().getBranchResult("PS1").getP1(), DELTA_POWER);
+        assertEquals(-200.0, operatorStrategyResult.getNetworkResult().getBranchResult("PS1").getP2(), DELTA_POWER);
+        assertEquals(0, operatorStrategyResult.getLimitViolationsResult().getLimitViolations().size());
+        // in default dc mode, branch results with 0 flow are created for disabled branches on one side
+        assertEquals(5, operatorStrategyResult.getNetworkResult().getBranchResults().size());
+
+        // set dc sa mode
+        OpenSecurityAnalysisParameters openSecurityAnalysisParameters = new OpenSecurityAnalysisParameters();
+        openSecurityAnalysisParameters.setDcFastMode(true);
+        securityParameters.addExtension(OpenSecurityAnalysisParameters.class, openSecurityAnalysisParameters);
+
+        SecurityAnalysisResult resultFastDcSa = runSecurityAnalysis(network, contingencies, monitors, securityParameters,
+                operatorStrategies, actions, ReportNode.NO_OP);
+        operatorStrategyResult = getOperatorStrategyResult(resultFastDcSa, "strategyPstChange");
+        assertEquals(200.0, operatorStrategyResult.getNetworkResult().getBranchResult("PS1").getP1(), DELTA_POWER);
+        assertEquals(-200.0, operatorStrategyResult.getNetworkResult().getBranchResult("PS1").getP2(), DELTA_POWER);
+        assertEquals(0, operatorStrategyResult.getLimitViolationsResult().getLimitViolations().size());
+        // in fast dc mode, no branch result is created for disabled branches on one side
+        assertEquals(1, operatorStrategyResult.getNetworkResult().getBranchResults().size());
+    }
 }


Reviews:
user: login: p-arvy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T14:59:30Z
user: login: Hadrien-Godard
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-02T14:02:21Z
user: login: vidaldid-rte
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T14:23:43Z
user: login: vidaldid-rte
body: Just one minor remark about a comment (about sorting) that should be in the code to explain the purpose and importance of sorting.
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-04-03T14:28:41Z
user: login: p-arvy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T16:44:08Z
user: login: vidaldid-rte
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-03T16:47:37Z
user: login: Hadrien-Godard
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-04T07:35:48Z

Review comments:
diff_hunk: @@ -142,10 +142,10 @@ private static List<ConnectivityAnalysisResult> computeConnectivityData(LfNetwor
 
         GraphConnectivity<LfBus, LfBranch> connectivity = lfNetwork.getConnectivity();
         for (PropagatedContingency contingency : potentiallyBreakingConnectivityContingencies) {
-            List<ComputedContingencyElement> breakingConnectivityCandidates = contingency.getBranchIdsToOpen().keySet().stream().map(contingencyElementByBranch::get).collect(Collectors.toList());
+            List<ComputedContingencyElement> breakingConnectivityCandidates = contingency.getBranchIdsToOpen().keySet().stream().sorted().map(contingencyElementByBranch::get).collect(Collectors.toList());
path: src/main/java/com/powsybl/openloadflow/dc/fastdc/ConnectivityBreakAnalysis.java
user: login: p-arvy
body: When multiple elements can restore connectivity, the traversal order of the `Set`, which may vary between runs, causes the `elementsToReconnect` to change.

Depending on the selected elements, the post-contingent angles of nodes disconnected from the main connected component (which should therefore not be used—this is what this PR aims to address) may vary. As a result, they are not deterministic. Applying a sort and changing the `Set` to a `LinkedHashSet` resolves this "issue".
created_at: 2025-03-28T14:59:29Z
author_association: MEMBER
diff_hunk: @@ -142,10 +142,10 @@ private static List<ConnectivityAnalysisResult> computeConnectivityData(LfNetwor
 
         GraphConnectivity<LfBus, LfBranch> connectivity = lfNetwork.getConnectivity();
         for (PropagatedContingency contingency : potentiallyBreakingConnectivityContingencies) {
-            List<ComputedContingencyElement> breakingConnectivityCandidates = contingency.getBranchIdsToOpen().keySet().stream().map(contingencyElementByBranch::get).collect(Collectors.toList());
+            List<ComputedContingencyElement> breakingConnectivityCandidates = contingency.getBranchIdsToOpen().keySet().stream().sorted().map(contingencyElementByBranch::get).collect(Collectors.toList());
path: src/main/java/com/powsybl/openloadflow/dc/fastdc/ConnectivityBreakAnalysis.java
user: login: vidaldid-rte
body: This info is valuable as a comment in the code.
created_at: 2025-04-03T14:23:43Z
author_association: COLLABORATOR
diff_hunk: @@ -142,10 +142,10 @@ private static List<ConnectivityAnalysisResult> computeConnectivityData(LfNetwor
 
         GraphConnectivity<LfBus, LfBranch> connectivity = lfNetwork.getConnectivity();
         for (PropagatedContingency contingency : potentiallyBreakingConnectivityContingencies) {
-            List<ComputedContingencyElement> breakingConnectivityCandidates = contingency.getBranchIdsToOpen().keySet().stream().map(contingencyElementByBranch::get).collect(Collectors.toList());
+            List<ComputedContingencyElement> breakingConnectivityCandidates = contingency.getBranchIdsToOpen().keySet().stream().sorted().map(contingencyElementByBranch::get).collect(Collectors.toList());
path: src/main/java/com/powsybl/openloadflow/dc/fastdc/ConnectivityBreakAnalysis.java
user: login: p-arvy
body: Done. Thanks.
created_at: 2025-04-03T16:44:08Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-03T16:41:56Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=com.powsybl%3Apowsybl-open-loadflow&pullRequest=1213) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=com.powsybl%3Apowsybl-open-loadflow&pullRequest=1213&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=com.powsybl%3Apowsybl-open-loadflow&pullRequest=1213&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=com.powsybl%3Apowsybl-open-loadflow&pullRequest=1213&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=com.powsybl%3Apowsybl-open-loadflow&pullRequest=1213&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=com.powsybl%3Apowsybl-open-loadflow&pullRequest=1213&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=com.powsybl%3Apowsybl-open-loadflow&pullRequest=1213)


author_association: NONE