diff --git a/src/main/java/com/powsybl/openloadflow/ac/outerloop/DistributedSlackOuterLoop.java b/src/main/java/com/powsybl/openloadflow/ac/outerloop/DistributedSlackOuterLoop.java index 855f9842b2..27de989342 100644 --- a/src/main/java/com/powsybl/openloadflow/ac/outerloop/DistributedSlackOuterLoop.java +++ b/src/main/java/com/powsybl/openloadflow/ac/outerloop/DistributedSlackOuterLoop.java @@ -80,7 +80,7 @@ public OuterLoopResult check(AcOuterLoopContext context, ReportNode reportNode) if (Math.abs(remainingMismatch) > ActivePowerDistribution.P_RESIDUE_EPS) { Reports.reportMismatchDistributionFailure(iterationReportNode, remainingMismatch * PerUnit.SB); } else { - reportAndLogSuccess(iterationReportNode, slackBusActivePowerMismatch, resultWbh); + ActivePowerDistribution.reportAndLogSuccess(iterationReportNode, slackBusActivePowerMismatch, resultWbh); } DistributedSlackContextData contextData = (DistributedSlackContextData) context.getData(); contextData.addDistributedActivePower(distributedActivePower); @@ -97,10 +97,4 @@ public double getSlackBusActivePowerMismatch(AcOuterLoopContext context) { return context.getLastSolverResult().getSlackBusActivePowerMismatch(); } - private static void reportAndLogSuccess(ReportNode reportNode, double slackBusActivePowerMismatch, ActivePowerDistribution.ResultWithFailureBehaviorHandling result) { - Reports.reportMismatchDistributionSuccess(reportNode, slackBusActivePowerMismatch * PerUnit.SB, result.iteration()); - - LOGGER.info("Slack bus active power ({} MW) distributed in {} distribution iteration(s)", - slackBusActivePowerMismatch * PerUnit.SB, result.iteration()); - } } diff --git a/src/main/java/com/powsybl/openloadflow/dc/DcLoadFlowEngine.java b/src/main/java/com/powsybl/openloadflow/dc/DcLoadFlowEngine.java index b4cc51d6a3..da3c9fc7b4 100644 --- a/src/main/java/com/powsybl/openloadflow/dc/DcLoadFlowEngine.java +++ b/src/main/java/com/powsybl/openloadflow/dc/DcLoadFlowEngine.java @@ -11,6 +11,7 @@ import com.powsybl.commons.report.ReportNode; import com.powsybl.loadflow.LoadFlowParameters; import com.powsybl.math.matrix.MatrixException; +import com.powsybl.openloadflow.OpenLoadFlowParameters; import com.powsybl.openloadflow.dc.equations.DcEquationType; import com.powsybl.openloadflow.dc.equations.DcVariableType; import com.powsybl.openloadflow.equations.*; @@ -18,11 +19,13 @@ import com.powsybl.openloadflow.lf.outerloop.OuterLoopResult; import com.powsybl.openloadflow.lf.outerloop.OuterLoopStatus; import com.powsybl.openloadflow.network.LfBus; +import com.powsybl.openloadflow.network.LfGenerator; import com.powsybl.openloadflow.network.LfNetwork; import com.powsybl.openloadflow.network.LfNetworkLoader; import com.powsybl.openloadflow.network.util.ActivePowerDistribution; import com.powsybl.openloadflow.network.util.UniformValueVoltageInitializer; import com.powsybl.openloadflow.network.util.VoltageInitializer; +import com.powsybl.openloadflow.util.PerUnit; import com.powsybl.openloadflow.util.Reports; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; @@ -205,8 +208,41 @@ public DcLoadFlowResult run() { double initialSlackBusActivePowerMismatch = getActivePowerMismatch(network.getBuses()); double distributedActivePower = 0.0; if (parameters.isDistributedSlack() || parameters.isAreaInterchangeControl()) { - // FIXME handle distribution failure - distributedActivePower = distributeSlack(network, network.getBuses(), parameters.getBalanceType(), parameters.getNetworkParameters().isUseActiveLimits()); + LoadFlowParameters.BalanceType balanceType = parameters.getBalanceType(); + boolean useActiveLimits = parameters.getNetworkParameters().isUseActiveLimits(); + ActivePowerDistribution activePowerDistribution = ActivePowerDistribution.create(balanceType, false, useActiveLimits); + var result = activePowerDistribution.run(network, initialSlackBusActivePowerMismatch); + final LfGenerator referenceGenerator; + final OpenLoadFlowParameters.SlackDistributionFailureBehavior behavior; + if (parameters.isAreaInterchangeControl()) { + // actual behavior will be handled by the outerloop itself, just leave on slack bus here + behavior = OpenLoadFlowParameters.SlackDistributionFailureBehavior.LEAVE_ON_SLACK_BUS; + referenceGenerator = null; + } else { + behavior = parameters.getSlackDistributionFailureBehavior(); + referenceGenerator = context.getNetwork().getReferenceGenerator(); + } + ActivePowerDistribution.ResultWithFailureBehaviorHandling resultWbh = ActivePowerDistribution.handleDistributionFailureBehavior( + behavior, + referenceGenerator, + initialSlackBusActivePowerMismatch, + result, + "Failed to distribute slack bus active power mismatch, %.2f MW remains" + ); + double remainingMismatch = resultWbh.remainingMismatch(); + distributedActivePower = initialSlackBusActivePowerMismatch - remainingMismatch; + if (Math.abs(remainingMismatch) > ActivePowerDistribution.P_RESIDUE_EPS) { + Reports.reportMismatchDistributionFailure(reportNode, remainingMismatch * PerUnit.SB); + } else { + ActivePowerDistribution.reportAndLogSuccess(reportNode, initialSlackBusActivePowerMismatch, resultWbh); + } + if (resultWbh.failed()) { + distributedActivePower -= resultWbh.failedDistributedActivePower(); + runningContext.lastSolverSuccess = false; + runningContext.lastOuterLoopResult = new OuterLoopResult("DistributedSlack", OuterLoopStatus.FAILED, resultWbh.failedMessage()); + Reports.reportDcLfComplete(reportNode, runningContext.lastSolverSuccess, runningContext.lastOuterLoopResult.status().name()); + return buildDcLoadFlowResult(network, runningContext, initialSlackBusActivePowerMismatch, distributedActivePower); + } } // we need to copy the target array because JacobianMatrix.solveTransposed take as an input the second member diff --git a/src/main/java/com/powsybl/openloadflow/network/util/ActivePowerDistribution.java b/src/main/java/com/powsybl/openloadflow/network/util/ActivePowerDistribution.java index bdf1b061e4..6fb9758a5e 100644 --- a/src/main/java/com/powsybl/openloadflow/network/util/ActivePowerDistribution.java +++ b/src/main/java/com/powsybl/openloadflow/network/util/ActivePowerDistribution.java @@ -8,12 +8,14 @@ package com.powsybl.openloadflow.network.util; import com.powsybl.commons.PowsyblException; +import com.powsybl.commons.report.ReportNode; import com.powsybl.loadflow.LoadFlowParameters; import com.powsybl.openloadflow.OpenLoadFlowParameters; import com.powsybl.openloadflow.network.LfBus; import com.powsybl.openloadflow.network.LfGenerator; import com.powsybl.openloadflow.network.LfNetwork; import com.powsybl.openloadflow.util.PerUnit; +import com.powsybl.openloadflow.util.Reports; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -165,4 +167,11 @@ public static ResultWithFailureBehaviorHandling handleDistributionFailureBehavio return resultWithFailureBehaviorHandling; } + + public static void reportAndLogSuccess(ReportNode reportNode, double slackBusActivePowerMismatch, ResultWithFailureBehaviorHandling result) { + Reports.reportMismatchDistributionSuccess(reportNode, slackBusActivePowerMismatch * PerUnit.SB, result.iteration()); + + LOGGER.info("Slack bus active power ({} MW) distributed in {} distribution iteration(s)", + slackBusActivePowerMismatch * PerUnit.SB, result.iteration()); + } } diff --git a/src/test/java/com/powsybl/openloadflow/dc/DcLoadFlowTest.java b/src/test/java/com/powsybl/openloadflow/dc/DcLoadFlowTest.java index 35356c13b4..6f4cc679b6 100644 --- a/src/test/java/com/powsybl/openloadflow/dc/DcLoadFlowTest.java +++ b/src/test/java/com/powsybl/openloadflow/dc/DcLoadFlowTest.java @@ -8,6 +8,7 @@ package com.powsybl.openloadflow.dc; import com.powsybl.commons.report.ReportNode; +import com.powsybl.computation.local.LocalComputationManager; import com.powsybl.ieeecdf.converter.IeeeCdfNetworkFactory; import com.powsybl.iidm.network.*; import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory; @@ -26,13 +27,16 @@ import com.powsybl.openloadflow.util.PerUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.util.List; +import java.util.concurrent.CompletionException; import static com.powsybl.openloadflow.util.LoadFlowAssert.assertActivePowerEquals; +import static com.powsybl.openloadflow.util.LoadFlowAssert.assertReportContains; +import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; /** * @author Sylvain Leclerc {@literal } @@ -116,6 +120,21 @@ void tuto1Test() { assertEquals(-450, line2.getTerminal2().getP(), 0.01); } + @ParameterizedTest(name = "distributedSlack={0}") + @ValueSource(booleans = {true, false}) + void testSlackDistributionEnabledDisabledResults(boolean distributedSlack) { + Network network = EurostagFactory.fix(EurostagTutorialExample1Factory.create()); + + parameters.setDistributedSlack(distributedSlack); + LoadFlowResult result = loadFlowRunner.run(network, parameters); + assertTrue(result.isFullyConverged()); + var componentResults = result.getComponentResults(); + assertEquals(1, componentResults.size()); + assertEquals(1, componentResults.get(0).getSlackBusResults().size()); + assertEquals(distributedSlack ? -7.0 : 0.0, componentResults.get(0).getDistributedActivePower(), 1e-3); + assertEquals(distributedSlack ? 0.0 : -7.0, componentResults.get(0).getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3); + } + @Test void fourBusesTest() { Network network = FourBusNetworkFactory.create(); @@ -259,8 +278,8 @@ void multiCcTest() { .setVoltageRegulatorOn(false) .add(); for (Line l : List.of(network.getLine("L13-14-1"), - network.getLine("L6-13-1"), - network.getLine("L6-12-1"))) { + network.getLine("L6-13-1"), + network.getLine("L6-12-1"))) { l.getTerminal1().disconnect(); l.getTerminal2().disconnect(); } @@ -320,8 +339,8 @@ void shuntCompensatorActivePowerZero() { .setBus("NLOAD") .setSectionCount(1) .newLinearModel() - .setBPerSection(0.111) - .setMaximumSectionCount(1) + .setBPerSection(0.111) + .setMaximumSectionCount(1) .add() .add(); loadFlowRunner.run(network, parameters); @@ -473,7 +492,7 @@ void outerLoopMaxTotalIterationTest() { // For this case, AIC outer loop needs 3 iterations to be stable, phase control needs 1. parametersExt.setAreaInterchangePMaxMismatch(1) - .setMaxOuterLoopIterations(1); + .setMaxOuterLoopIterations(1); var result = loadFlowRunner.run(network, parameters); assertFalse(result.isFullyConverged()); assertEquals(LoadFlowResult.ComponentResult.Status.MAX_ITERATION_REACHED, result.getComponentResults().get(0).getStatus()); @@ -510,4 +529,45 @@ void testDcApproxIgnoreG() { assertEquals(307.436, line2.getTerminal1().getP(), 0.01); assertEquals(-307.436, line2.getTerminal2().getP(), 0.01); } + + @Test + void testDcSlackDistributionFailureBehavior() { + Network network = IeeeCdfNetworkFactory.create57(); + parameters.setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_P); + Generator referenceGenerator = network.getGenerator("B1-G"); + + parametersExt.setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.LEAVE_ON_SLACK_BUS); + ReportNode reportNode = ReportNode.newRootReportNode().withMessageTemplate("test", "test").build(); + var result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, reportNode); + assertTrue(result.isFullyConverged()); + assertEquals(1, result.getComponentResults().size()); + assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus()); + assertEquals(321.9, result.getComponentResults().get(0).getSlackBusResults().get(0).getActivePowerMismatch(), 0.01); + assertEquals(0, result.getComponentResults().get(0).getDistributedActivePower(), 0.01); + assertReportContains("Failed to distribute slack bus active power mismatch, [-+]?321\\.\\d* MW remains", reportNode); + + parametersExt.setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.FAIL); + reportNode = ReportNode.newRootReportNode().withMessageTemplate("test", "test").build(); + result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, reportNode); + assertFalse(result.isFullyConverged()); + assertEquals(1, result.getComponentResults().size()); + assertEquals(LoadFlowResult.ComponentResult.Status.FAILED, result.getComponentResults().get(0).getStatus()); + assertEquals("Outer loop failed: Failed to distribute slack bus active power mismatch, 321.90 MW remains", result.getComponentResults().get(0).getStatusText()); + assertEquals(321.9, result.getComponentResults().get(0).getSlackBusResults().get(0).getActivePowerMismatch(), 0.01); + assertEquals(0, result.getComponentResults().get(0).getDistributedActivePower(), 0.01); + assertReportContains("Failed to distribute slack bus active power mismatch, [-+]?321\\.\\d* MW remains", reportNode); + + parametersExt.setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.THROW); + CompletionException e = assertThrows(CompletionException.class, () -> loadFlowRunner.run(network, parameters)); + assertEquals("Failed to distribute slack bus active power mismatch, 321.90 MW remains", e.getCause().getMessage()); + + parametersExt.setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.DISTRIBUTE_ON_REFERENCE_GENERATOR); + parametersExt.setReferenceBusSelectionMode(ReferenceBusSelectionMode.GENERATOR_REFERENCE_PRIORITY); + reportNode = ReportNode.newRootReportNode().withMessageTemplate("test", "test").build(); + result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, reportNode); + assertEquals(0, result.getComponentResults().get(0).getSlackBusResults().get(0).getActivePowerMismatch(), 0.01); + assertEquals(321.9, result.getComponentResults().get(0).getDistributedActivePower(), 0.01); + assertActivePowerEquals(-450.8, referenceGenerator.getTerminal()); // -128.9 - 321.9 = -450.8 + assertReportContains("Slack bus active power \\([-+]?321\\.\\d* MW\\) distributed in 1 distribution iteration\\(s\\)", reportNode); + } } diff --git a/src/test/resources/multipleConnectedComponentsDcReport.txt b/src/test/resources/multipleConnectedComponentsDcReport.txt index 4096a91606..c75d627731 100644 --- a/src/test/resources/multipleConnectedComponentsDcReport.txt +++ b/src/test/resources/multipleConnectedComponentsDcReport.txt @@ -6,6 +6,7 @@ Network balance: active generation=3.0 MW, active load=2.0 MW, reactive generation=0.0 MVar, reactive load=0.0 MVar Angle reference bus: b1_vl_0 Slack bus: b1_vl_0 + Slack bus active power (-0.9999999999999997 MW) distributed in 1 distribution iteration(s) DC load flow completed (solverSuccess=true, outerloopStatus=STABLE) + Network CC1 SC1 + Network info @@ -13,5 +14,6 @@ Network balance: active generation=2.0 MW, active load=4.0 MW, reactive generation=0.0 MVar, reactive load=0.0 MVar Angle reference bus: b5_vl_0 Slack bus: b5_vl_0 + Slack bus active power (2.0 MW) distributed in 1 distribution iteration(s) DC load flow completed (solverSuccess=true, outerloopStatus=STABLE) No calculation will be done on 1 network(s) that have no generators