Repository: powsybl/powsybl-open-loadflow
Pull request #1228: Sensitivity Analysis : Select tie line factor with its dangling line id
Author: login: SylvestreSakti
Created: 2025-04-25T13:22:31Z | Merged: 2025-05-21T10:45:47Z

**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?**
No


**What kind of change does this PR introduce?**
When computing sensitivity factor of a tie line, we want to be able to select it by its dangling line id

**What is the current behavior?**
When a dangling line id is specified, the corresponding LfDanglingLineBranch is selected (only if the dangling line is not paired)

**What is the new behavior (if this is a feature change)?**
When a dangling line id is specified, if the dangling line is paired (and its studied side is the side 1 corresponding to the network side), the corresponding LfTieLineBranch is selected


**Does this PR introduce a breaking change or deprecate an API?**
- [ ] Yes
- [x] No



Diff:
diff --git a/docs/sensitivity/getting_started.md b/docs/sensitivity/getting_started.md
index 45e31c0aaf..ce96ddbb1b 100644
--- a/docs/sensitivity/getting_started.md
+++ b/docs/sensitivity/getting_started.md
@@ -30,14 +30,27 @@ Here is a table to summarize supported use cases:
 |-------------------------|------------------------------------------|-------------------------------------|------------------------------------------|-------------------------------------|------------------------------------------|------------------------------------------|------------------------------------------|------------------------------------------|
 | BRANCH_ACTIVE_POWER_1   | <span style="color:green">AC + DC</span> | <span style="color:red">N/A</span>  | <span style="color:green">AC + DC</span> | <span style="color:red">N/A</span>  | <span style="color:green">AC + DC</span> | <span style="color:green">AC + DC</span> | <span style="color:green">AC + DC</span> | <span style="color:green">AC + DC</span> |
 | BRANCH_CURRENT_1        | <span style="color:green">AC</span>      | <span style="color:green">AC</span> | <span style="color:green">AC</span>      | <span style="color:green">AC</span> | <span style="color:green">AC</span>      | <span style="color:green">AC</span>      | <span style="color:green">AC</span>      | <span style="color:green">AC</span>      |
-| BRANCH_REACTIVE_POWER_1 | <span style="color:red">N/A</span>       | <span style="color:green">AC</span> | <span style="color:red">N/A</span>       | <span style="color:green">AC</span> | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       |
+| BRANCH_REACTIVE_POWER_1 | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>  | <span style="color:red">N/A</span>       | <span style="color:green">AC</span> | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       |
 | BRANCH_ACTIVE_POWER_2   | <span style="color:green">AC + DC</span> | <span style="color:red">N/A</span>  | <span style="color:green">AC + DC</span> | <span style="color:red">N/A</span>  | <span style="color:green">AC + DC</span> | <span style="color:green">AC + DC</span> | <span style="color:green">AC + DC</span> | <span style="color:green">AC + DC</span> |
 | BRANCH_CURRENT_2        | <span style="color:green">AC</span>      | <span style="color:green">AC</span> | <span style="color:green">AC</span>      | <span style="color:green">AC</span> | <span style="color:green">AC</span>      | <span style="color:green">AC</span>      | <span style="color:green">AC</span>      | <span style="color:green">AC</span>      |
-| BRANCH_REACTIVE_POWER_2 | <span style="color:red">N/A</span>       | <span style="color:green">AC</span> | <span style="color:red">N/A</span>       | <span style="color:green">AC</span> | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       |
+| BRANCH_REACTIVE_POWER_2 | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>  | <span style="color:red">N/A</span>       | <span style="color:green">AC</span> | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       |
 | BRANCH_ACTIVE_POWER_3   | <span style="color:green">AC + DC</span> | <span style="color:red">N/A</span>  | <span style="color:green">AC + DC</span> | <span style="color:red">N/A</span>  | <span style="color:green">AC + DC</span> | <span style="color:green">AC + DC</span> | <span style="color:green">AC + DC</span> | <span style="color:green">AC + DC</span> |
 | BRANCH_CURRENT_3        | <span style="color:green">AC</span>      | <span style="color:green">AC</span> | <span style="color:green">AC</span>      | <span style="color:green">AC</span> | <span style="color:green">AC</span>      | <span style="color:green">AC</span>      | <span style="color:green">AC</span>      | <span style="color:green">AC</span>      |
-| BRANCH_REACTIVE_POWER_3 | <span style="color:red">N/A</span>       | <span style="color:green">AC</span> | <span style="color:red">N/A</span>       | <span style="color:green">AC</span> | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       |
+| BRANCH_REACTIVE_POWER_3 | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>  | <span style="color:red">N/A</span>       | <span style="color:green">AC</span> | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       |
 | BUS_VOLTAGE             | <span style="color:red">N/A</span>       | <span style="color:green">AC</span> | <span style="color:red">N/A</span>       | <span style="color:green">AC</span> | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       |
 | BUS_REACTIVE_POWER      | <span style="color:red">N/A</span>       | <span style="color:green">AC</span> | <span style="color:red">N/A</span>       | <span style="color:green">AC</span> | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       | <span style="color:red">N/A</span>       |
 
-</div>
\ No newline at end of file
+</div>
+
+## Input function types
+
+### Branch sensitivity functions : 
+BRANCH_ACTIVE_POWER_#, BRANCH_REACTIVE_POWER_# or BRANCH_CURRENT_# are associated to branch objects (lines, transformers, dangling lines, tie lines). The # index corresponding to the side of the studied branch. 
+- If it is a line, a tie line, or a two windings transformer : The side is 1 or 2.
+- If it is a three windings transformer : The side is 1, 2 or 3 corresponding to the studied leg of the transformer.
+- If it is a dangling line : The side is 1 (network side) or 2 (boundary side). Note that if the dangling line is paired, only side 1 (network side) can be specified, and the sensitivity function is computed at the corresponding tie line side.
+
+### Bus sensitivity functions :
+BUS_VOLTAGE or BUS_REACTIVE_POWER are associated to the bus of the given network element.
+
+
diff --git a/src/main/java/com/powsybl/openloadflow/sensi/AbstractSensitivityAnalysis.java b/src/main/java/com/powsybl/openloadflow/sensi/AbstractSensitivityAnalysis.java
index e7e22e7e6b..a11565cf85 100644
--- a/src/main/java/com/powsybl/openloadflow/sensi/AbstractSensitivityAnalysis.java
+++ b/src/main/java/com/powsybl/openloadflow/sensi/AbstractSensitivityAnalysis.java
@@ -775,6 +775,24 @@ protected void checkLoadFlowParameters(LoadFlowParameters lfParameters) {
         }
     }
 
+    private Pair<SensitivityFunctionType, String> checkAndUpdateFunctionTypeDanglingLine(Network network, SensitivityFunctionType functionType, String functionId) {
+        if (isFlowFunction(functionType) || isReactivePowerFunctionType(functionType)) {
+            DanglingLine danglingLine = network.getDanglingLine(functionId);
+            if (danglingLine != null && danglingLine.isPaired()) {
+                if (functionType.getSide().orElseThrow() != 1) { // Check that user wants side 1 of the dangling line (i.e. network side value and not boundary side)
+                    throw new PowsyblException("Dangling line " + functionId + " is paired. Sensitivity function can only be computed on its side 1 (given type " + functionType + ")");
+                }
+                TieLine tieLine = danglingLine.getTieLine().orElseThrow();
+                TwoSides danglingLineSide = tieLine.getDanglingLine(TwoSides.ONE) == danglingLine ? TwoSides.ONE : TwoSides.TWO; // Search side of the tie line corresponding to the studied dangling line
+                if (LOGGER.isInfoEnabled()) {
+                    LOGGER.info("Dangling line {} is paired. Computing sensitivity function of its tie line {} on side {}", functionId, tieLine.getId(), danglingLineSide.getNum());
+                }
+                return Pair.of(updateFunctionTypeSide(functionType, danglingLineSide), tieLine.getId()); // Conversion to the corresponding tie line sensitivity function
+            }
+        }
+        return Pair.of(functionType, functionId); // Returning input as it is
+    }
+
     private static LfBranch checkAndGetBranchOrLeg(Network network, String branchId, SensitivityFunctionType fType, LfNetwork lfNetwork) {
         Branch<?> branch = network.getBranch(branchId);
         if (branch != null) {
@@ -972,7 +990,15 @@ protected SensitivityFactorHolder<V, E> readAndCheckFactors(Network network, Map
         final Map<String, Bus> busCache = new HashMap<>();
         InjectionVariableIdToBusIdCache injectionVariableIdToBusIdCache = new InjectionVariableIdToBusIdCache();
         int[] factorIndex = new int[1];
-        factorReader.read((functionType, functionId, variableType, variableId, variableSet, contingencyContext) -> {
+        factorReader.read((functionTypeToCheck, functionIdToCheck, variableType, variableId, variableSet, contingencyContext) -> {
+            SensitivityFunctionType functionType = functionTypeToCheck;
+            String functionId = functionIdToCheck;
+            if (network.getDanglingLine(functionIdToCheck) != null && network.getDanglingLine(functionIdToCheck).isPaired()) {
+                //In case of dangling line associated to a tie line, we have to update the sensitivity function
+                Pair<SensitivityFunctionType, String> updatedFunction = checkAndUpdateFunctionTypeDanglingLine(network, functionTypeToCheck, functionIdToCheck);
+                functionType = updatedFunction.getLeft();
+                functionId = updatedFunction.getRight();
+            }
             if (variableSet) {
                 if (isActivePowerFunctionType(functionType)) {
                     if (variableType == SensitivityVariableType.INJECTION_ACTIVE_POWER) {
@@ -1144,6 +1170,19 @@ private static boolean isCurrentFunctionType(SensitivityFunctionType functionTyp
                 || functionType == SensitivityFunctionType.BRANCH_CURRENT_3;
     }
 
+    private static SensitivityFunctionType updateFunctionTypeSide(SensitivityFunctionType functionType, TwoSides side) {
+        if (side != null) {
+            if (isActivePowerFunctionType(functionType)) {
+                return side == TwoSides.ONE ? SensitivityFunctionType.BRANCH_ACTIVE_POWER_1 : SensitivityFunctionType.BRANCH_ACTIVE_POWER_2;
+            } else if (isReactivePowerFunctionType(functionType)) {
+                return side == TwoSides.ONE ? SensitivityFunctionType.BRANCH_REACTIVE_POWER_1 : SensitivityFunctionType.BRANCH_REACTIVE_POWER_2;
+            } else if (isCurrentFunctionType(functionType)) {
+                return side == TwoSides.ONE ? SensitivityFunctionType.BRANCH_CURRENT_1 : SensitivityFunctionType.BRANCH_CURRENT_2;
+            }
+        }
+        throw new IllegalArgumentException("Illegal function type side switching");
+    }
+
     protected Pair<Boolean, Boolean> hasBusTargetVoltage(SensitivityFactorReader factorReader, Network network) {
         // Left value if we find a BUS_TARGET_VOLTAGE factor and right value if it is linked to a transformer.
         AtomicBoolean hasBusTargetVoltage = new AtomicBoolean(false);
diff --git a/src/test/java/com/powsybl/openloadflow/sensi/AbstractSensitivityAnalysisTest.java b/src/test/java/com/powsybl/openloadflow/sensi/AbstractSensitivityAnalysisTest.java
index 3842bde532..37982d4795 100644
--- a/src/test/java/com/powsybl/openloadflow/sensi/AbstractSensitivityAnalysisTest.java
+++ b/src/test/java/com/powsybl/openloadflow/sensi/AbstractSensitivityAnalysisTest.java
@@ -119,6 +119,19 @@ protected static SensitivityFactor createBranchFlowPerLinearGlsk(String function
         return createBranchFlowPerLinearGlsk(functionId, variableId, Objects.isNull(contingencyId) ? ContingencyContext.all() : ContingencyContext.specificContingency(contingencyId), side);
     }
 
+    protected static SensitivityFactor createBranchReactivePowerPerTargetV(String functionId, String variableId, String contingencyId, TwoSides side) {
+        SensitivityFunctionType ftype = side.equals(TwoSides.ONE) ? SensitivityFunctionType.BRANCH_REACTIVE_POWER_1 : SensitivityFunctionType.BRANCH_REACTIVE_POWER_2;
+        return new SensitivityFactor(ftype, functionId, SensitivityVariableType.BUS_TARGET_VOLTAGE, variableId, false, Objects.isNull(contingencyId) ? ContingencyContext.all() : ContingencyContext.specificContingency(contingencyId));
+    }
+
+    protected static SensitivityFactor createBranchReactivePowerPerTargetV(String functionId, String variableId, TwoSides side) {
+        return createBranchReactivePowerPerTargetV(functionId, variableId, null, side);
+    }
+
+    protected static SensitivityFactor createBranchReactivePowerPerTargetV(String functionId, String variableId) {
+        return createBranchReactivePowerPerTargetV(functionId, variableId, TwoSides.ONE);
+    }
+
     protected static SensitivityFactor createBranchFlowPerLinearGlsk(String functionId, String variableId, ContingencyContext contingencyContext) {
         return createBranchFlowPerLinearGlsk(functionId, variableId, contingencyContext, TwoSides.ONE);
     }
diff --git a/src/test/java/com/powsybl/openloadflow/sensi/AcSensitivityAnalysisTest.java b/src/test/java/com/powsybl/openloadflow/sensi/AcSensitivityAnalysisTest.java
index 64ca591077..2e34547655 100644
--- a/src/test/java/com/powsybl/openloadflow/sensi/AcSensitivityAnalysisTest.java
+++ b/src/test/java/com/powsybl/openloadflow/sensi/AcSensitivityAnalysisTest.java
@@ -1516,6 +1516,58 @@ void testUnsupportedVariablesSensiV() {
         assertEquals("Variable type INJECTION_ACTIVE_POWER not supported with function type BUS_VOLTAGE", e.getCause().getMessage());
     }
 
+    @Test
+    void testWithTieLinesSpecifiedByDanglingLines() {
+        SensitivityAnalysisParameters sensiParameters = createParameters(false, "b1_vl_0", true);
+        sensiParameters.getLoadFlowParameters().setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_P_MAX);
+        Network network = BoundaryFactory.createWithTieLine();
+        //BRANCH_ACTIVE_POWER
+        List<SensitivityFactor> factors = network.getDanglingLineStream().map(line -> createBranchFlowPerInjectionIncrease(line.getId(), "g1")).collect(Collectors.toList());
+        factors.add(createBranchFlowPerInjectionIncrease("t12", "g1", TwoSides.ONE)); // Adding tie line BRANCH_ACTIVE_POWER_1
+        factors.add(createBranchFlowPerInjectionIncrease("t12", "g1", null, TwoSides.TWO)); // Adding tie line BRANCH_ACTIVE_POWER_2
+        //BRANCH_CURRENT
+        factors.addAll(network.getDanglingLineStream().map(line -> createBranchIntensityPerInjectionIncrease(line.getId(), "g1")).toList());
+        factors.add(createBranchIntensityPerInjectionIncrease("t12", "g1", TwoSides.ONE)); // Adding tie line BRANCH_CURRENT_1
+        factors.add(createBranchIntensityPerInjectionIncrease("t12", "g1", TwoSides.TWO)); // Adding tie line BRANCH_CURRENT_2
+        //BRANCH_REACTIVE_POWER
+        factors.addAll(network.getDanglingLineStream().map(line -> createBranchReactivePowerPerTargetV(line.getId(), "g1")).toList());
+        factors.add(createBranchReactivePowerPerTargetV("t12", "g1", TwoSides.ONE)); // Adding tie line BRANCH_CURRENT_1
+        factors.add(createBranchReactivePowerPerTargetV("t12", "g1", TwoSides.TWO)); // Adding tie line BRANCH_CURRENT_2
+
+        List<Contingency> contingencies = Collections.emptyList();
+        List<SensitivityVariableSet> variableSets = Collections.emptyList();
+        SensitivityAnalysisResult result = sensiRunner.run(network, factors, contingencies, variableSets, sensiParameters);
+        assertEquals(12, result.getValues().size());
+
+        // Dangling line h1 side 1 and Tie line t12 side 1 should represent the same sensitivity values
+        assertEquals(35.0, result.getBranchFlow1FunctionReferenceValue("h1"), LoadFlowAssert.DELTA_POWER);
+        assertEquals(35.0, result.getBranchFlow1FunctionReferenceValue("t12"), LoadFlowAssert.DELTA_POWER);
+        assertEquals(0.5, result.getBranchFlow1SensitivityValue("g1", "h1", SensitivityVariableType.INJECTION_ACTIVE_POWER), LoadFlowAssert.DELTA_POWER);
+        assertEquals(0.5, result.getBranchFlow1SensitivityValue("g1", "t12", SensitivityVariableType.INJECTION_ACTIVE_POWER), LoadFlowAssert.DELTA_POWER);
+        assertEquals(50.518, result.getBranchCurrent1FunctionReferenceValue("h1"), LoadFlowAssert.DELTA_I);
+        assertEquals(50.518, result.getBranchCurrent1FunctionReferenceValue("t12"), LoadFlowAssert.DELTA_I);
+        assertEquals(0.5, result.getBranchCurrent1SensitivityValue("g1", "h1", SensitivityVariableType.INJECTION_ACTIVE_POWER), LoadFlowAssert.DELTA_I);
+        assertEquals(0.5, result.getBranchCurrent1SensitivityValue("g1", "t12", SensitivityVariableType.INJECTION_ACTIVE_POWER), LoadFlowAssert.DELTA_I);
+        assertEquals(0.0038, result.getFunctionReferenceValue("h1", SensitivityFunctionType.BRANCH_REACTIVE_POWER_1), LoadFlowAssert.DELTA_POWER);
+        assertEquals(0.0038, result.getFunctionReferenceValue("t12", SensitivityFunctionType.BRANCH_REACTIVE_POWER_1), LoadFlowAssert.DELTA_POWER);
+        assertEquals(338.983, result.getSensitivityValue("g1", "h1", SensitivityFunctionType.BRANCH_REACTIVE_POWER_1, SensitivityVariableType.BUS_TARGET_VOLTAGE), LoadFlowAssert.DELTA_POWER);
+        assertEquals(338.983, result.getSensitivityValue("g1", "t12", SensitivityFunctionType.BRANCH_REACTIVE_POWER_1, SensitivityVariableType.BUS_TARGET_VOLTAGE), LoadFlowAssert.DELTA_POWER);
+
+        // Dangling line h2 side 1 and Tie line t12 side 2 should represent the same sensitivity values
+        assertEquals(-35.0, result.getBranchFlow1FunctionReferenceValue("h2"), LoadFlowAssert.DELTA_POWER);
+        assertEquals(-35.0, result.getBranchFlow2FunctionReferenceValue("t12"), LoadFlowAssert.DELTA_POWER);
+        assertEquals(-0.5, result.getBranchFlow1SensitivityValue("g1", "h2", SensitivityVariableType.INJECTION_ACTIVE_POWER), LoadFlowAssert.DELTA_POWER);
+        assertEquals(-0.5, result.getBranchFlow2SensitivityValue("g1", "t12", SensitivityVariableType.INJECTION_ACTIVE_POWER), LoadFlowAssert.DELTA_POWER);
+        assertEquals(50.518, result.getBranchCurrent1FunctionReferenceValue("h2"), LoadFlowAssert.DELTA_I);
+        assertEquals(50.518, result.getBranchCurrent2FunctionReferenceValue("t12"), LoadFlowAssert.DELTA_I);
+        assertEquals(0.5, result.getBranchCurrent1SensitivityValue("g1", "h2", SensitivityVariableType.INJECTION_ACTIVE_POWER), LoadFlowAssert.DELTA_I);
+        assertEquals(0.5, result.getBranchCurrent2SensitivityValue("g1", "t12", SensitivityVariableType.INJECTION_ACTIVE_POWER), LoadFlowAssert.DELTA_I);
+        assertEquals(-0.0024, result.getFunctionReferenceValue("h2", SensitivityFunctionType.BRANCH_REACTIVE_POWER_1), LoadFlowAssert.DELTA_POWER);
+        assertEquals(-0.0024, result.getFunctionReferenceValue("t12", SensitivityFunctionType.BRANCH_REACTIVE_POWER_2), LoadFlowAssert.DELTA_POWER);
+        assertEquals(-338.983, result.getSensitivityValue("g1", "h2", SensitivityFunctionType.BRANCH_REACTIVE_POWER_1, SensitivityVariableType.BUS_TARGET_VOLTAGE), LoadFlowAssert.DELTA_POWER);
+        assertEquals(-338.983, result.getSensitivityValue("g1", "t12", SensitivityFunctionType.BRANCH_REACTIVE_POWER_2, SensitivityVariableType.BUS_TARGET_VOLTAGE), LoadFlowAssert.DELTA_POWER);
+    }
+
     @Test
     void testFailingLf() {
         Network network = TwoBusNetworkFactory.create();
diff --git a/src/test/java/com/powsybl/openloadflow/sensi/DcSensitivityAnalysisTest.java b/src/test/java/com/powsybl/openloadflow/sensi/DcSensitivityAnalysisTest.java
index 1c08a14c0d..d52d1188f0 100644
--- a/src/test/java/com/powsybl/openloadflow/sensi/DcSensitivityAnalysisTest.java
+++ b/src/test/java/com/powsybl/openloadflow/sensi/DcSensitivityAnalysisTest.java
@@ -1027,16 +1027,43 @@ void testWithTieLines() {
     }
 
     @Test
-    void testWithTieLines2() {
+    void testWithTieLinesWrongDanglingLine() {
         SensitivityAnalysisParameters sensiParameters = createParameters(true, "b1_vl_0", true);
         sensiParameters.getLoadFlowParameters().setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_P_MAX);
         Network network = BoundaryFactory.createWithTieLine();
-        List<SensitivityFactor> factors = network.getDanglingLineStream().map(line -> createBranchFlowPerInjectionIncrease(line.getId(), "g1")).collect(Collectors.toList());
+        // Specifying side 2 of dangling line as sensitivity function, which is not possible because the dangling line is paired (boundary side is not accessible)
+        List<SensitivityFactor> factors = network.getDanglingLineStream().map(line -> createBranchFlowPerInjectionIncrease(line.getId(), "g1", null, TwoSides.TWO)).collect(Collectors.toList());
         List<Contingency> contingencies = Collections.emptyList();
         List<SensitivityVariableSet> variableSets = Collections.emptyList();
         CompletionException e = assertThrows(CompletionException.class, () -> sensiRunner.run(network, factors, contingencies, variableSets, sensiParameters));
         assertTrue(e.getCause() instanceof PowsyblException);
-        assertEquals("Branch, tie line, dangling line or leg of 'h1' not found", e.getCause().getMessage());
+        assertEquals("Dangling line h1 is paired. Sensitivity function can only be computed on its side 1 (given type BRANCH_ACTIVE_POWER_2)", e.getCause().getMessage());
+    }
+
+    @Test
+    void testWithTieLinesSpecifiedByDanglingLines() {
+        SensitivityAnalysisParameters sensiParameters = createParameters(true, "b1_vl_0", true);
+        sensiParameters.getLoadFlowParameters().setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_P_MAX);
+        Network network = BoundaryFactory.createWithTieLine();
+        List<SensitivityFactor> factors = network.getDanglingLineStream().map(line -> createBranchFlowPerInjectionIncrease(line.getId(), "g1")).collect(Collectors.toList());
+        factors.add(createBranchFlowPerInjectionIncrease("t12", "g1", TwoSides.ONE)); // Adding tie line BRANCH_ACTIVE_POWER_1
+        factors.add(createBranchFlowPerInjectionIncrease("t12", "g1", TwoSides.TWO)); // Adding tie line BRANCH_ACTIVE_POWER_2
+        List<Contingency> contingencies = Collections.emptyList();
+        List<SensitivityVariableSet> variableSets = Collections.emptyList();
+        SensitivityAnalysisResult result = sensiRunner.run(network, factors, contingencies, variableSets, sensiParameters);
+        assertEquals(4, result.getValues().size());
+
+        // Dangling line h1 side 1 and Tie line t12 side 1 should represent the same sensitivity values
+        assertEquals(35.0, result.getBranchFlow1FunctionReferenceValue("h1"), LoadFlowAssert.DELTA_POWER);
+        assertEquals(35.0, result.getBranchFlow1FunctionReferenceValue("t12"), LoadFlowAssert.DELTA_POWER);
+        assertEquals(0.5, result.getBranchFlow1SensitivityValue("g1", "h1", SensitivityVariableType.INJECTION_ACTIVE_POWER), LoadFlowAssert.DELTA_POWER);
+        assertEquals(0.5, result.getBranchFlow1SensitivityValue("g1", "t12", SensitivityVariableType.INJECTION_ACTIVE_POWER), LoadFlowAssert.DELTA_POWER);
+
+        // Dangling line h2 side 1 and Tie line t12 side 2 should represent the same sensitivity values
+        assertEquals(-35.0, result.getBranchFlow1FunctionReferenceValue("h2"), LoadFlowAssert.DELTA_POWER);
+        assertEquals(-35.0, result.getBranchFlow2FunctionReferenceValue("t12"), LoadFlowAssert.DELTA_POWER);
+        assertEquals(-0.5, result.getBranchFlow1SensitivityValue("g1", "h2", SensitivityVariableType.INJECTION_ACTIVE_POWER), LoadFlowAssert.DELTA_POWER);
+        assertEquals(-0.5, result.getBranchFlow2SensitivityValue("g1", "t12", SensitivityVariableType.INJECTION_ACTIVE_POWER), LoadFlowAssert.DELTA_POWER);
     }
 
     @Test


Reviews:
user: login: vidaldid-rte
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T14:36:39Z
user: login: vidaldid-rte
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-15T13:15:01Z
user: login: Hadrien-Godard
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T15:51:51Z
user: login: Hadrien-Godard
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T15:54:56Z
user: login: vidaldid-rte
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T07:46:24Z
user: login: SylvestreSakti
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T08:49:47Z
user: login: Hadrien-Godard
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-20T09:19:19Z

Review comments:
diff_hunk: @@ -1144,6 +1170,19 @@ private static boolean isCurrentFunctionType(SensitivityFunctionType functionTyp
                 || functionType == SensitivityFunctionType.BRANCH_CURRENT_3;
     }
 
+    private static SensitivityFunctionType switchFunctionTypeSide(SensitivityFunctionType functionType, TwoSides side) {
path: src/main/java/com/powsybl/openloadflow/sensi/AbstractSensitivityAnalysis.java
user: login: vidaldid-rte
body: I am confused by the method name. switch means systematically modify. Here you want to update the function type given the new side for the target object.
Maybe  updateFunctionStypeSide(functionType, newSide)
created_at: 2025-05-09T14:36:39Z
author_association: COLLABORATOR
diff_hunk: @@ -775,6 +775,24 @@ protected void checkLoadFlowParameters(LoadFlowParameters lfParameters) {
         }
     }
 
+    private Pair<SensitivityFunctionType, String> checkAndUpdateFunctionTypeDanglingLine(Network network, SensitivityFunctionType functionType, String functionId) {
+        if (isFlowFunction(functionType) || isReactivePowerFunctionType(functionType)) {
path: src/main/java/com/powsybl/openloadflow/sensi/AbstractSensitivityAnalysis.java
user: login: Hadrien-Godard
body: If I am right, for this test to be false, you should request for a sensitivity on a bus voltage, with the same Id as a dangling line.
However this test is significant because it prevents the unwanted side effect that you can have in this "weird" case.
So, I suggest to add a unit test.
This will also cover the last line of this method.
created_at: 2025-05-19T15:51:51Z
author_association: MEMBER
diff_hunk: @@ -775,6 +775,24 @@ protected void checkLoadFlowParameters(LoadFlowParameters lfParameters) {
         }
     }
 
+    private Pair<SensitivityFunctionType, String> checkAndUpdateFunctionTypeDanglingLine(Network network, SensitivityFunctionType functionType, String functionId) {
+        if (isFlowFunction(functionType) || isReactivePowerFunctionType(functionType)) {
+            DanglingLine danglingLine = network.getDanglingLine(functionId);
+            if (danglingLine != null && danglingLine.isPaired()) {
path: src/main/java/com/powsybl/openloadflow/sensi/AbstractSensitivityAnalysis.java
user: login: Hadrien-Godard
body: This one cannot be false, as it is tested before calling this method.
created_at: 2025-05-19T15:54:56Z
author_association: MEMBER
diff_hunk: @@ -775,6 +775,24 @@ protected void checkLoadFlowParameters(LoadFlowParameters lfParameters) {
         }
     }
 
+    private Pair<SensitivityFunctionType, String> checkAndUpdateFunctionTypeDanglingLine(Network network, SensitivityFunctionType functionType, String functionId) {
+        if (isFlowFunction(functionType) || isReactivePowerFunctionType(functionType)) {
+            DanglingLine danglingLine = network.getDanglingLine(functionId);
+            if (danglingLine != null && danglingLine.isPaired()) {
path: src/main/java/com/powsybl/openloadflow/sensi/AbstractSensitivityAnalysis.java
user: login: vidaldid-rte
body: @Hadrien-Godard  this is true. But only in the context of the current call graph. Robust and maintainable  API design should be indeependant of the call graph at a moment in time, and it is good practice to check assumptions in the function.
So although the Sonar robot complains this is not tested, I am for keeping this. It improves code quality.
created_at: 2025-05-20T07:46:24Z
author_association: COLLABORATOR
diff_hunk: @@ -775,6 +775,24 @@ protected void checkLoadFlowParameters(LoadFlowParameters lfParameters) {
         }
     }
 
+    private Pair<SensitivityFunctionType, String> checkAndUpdateFunctionTypeDanglingLine(Network network, SensitivityFunctionType functionType, String functionId) {
+        if (isFlowFunction(functionType) || isReactivePowerFunctionType(functionType)) {
path: src/main/java/com/powsybl/openloadflow/sensi/AbstractSensitivityAnalysis.java
user: login: SylvestreSakti
body: I did not think about this case, and indeed, it could have been a weird one ! But when trying to implement the unit test, actually it is impossible (because when trying to add a sensitivity with a bus voltage function, the reader automatically selects its bus as the functionId, and it is impossible to have a network that has a dangling line and a bus with the same id).

I wanted to keep these conditions in case other function types are implemented in the future, and that would be unsupported in this case. Again, it is for maintanibility but it has the drawback of reducing sonar's coverage.
created_at: 2025-05-20T08:49:46Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-20T09:21:03Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=com.powsybl%3Apowsybl-open-loadflow&pullRequest=1228) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [86.2% Coverage on New Code](https://sonarcloud.io/component_measures?id=com.powsybl%3Apowsybl-open-loadflow&pullRequest=1228&metric=new_coverage&view=list) (required ≥ 90%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=com.powsybl%3Apowsybl-open-loadflow&pullRequest=1228)


author_association: NONE