From 2813242524583e6481a6d80db0ca97e54ce3ebd0 Mon Sep 17 00:00:00 2001 From: Valentin Mouradian <144696437+vmouradian@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:25:07 +0100 Subject: [PATCH 1/6] Fix DC Load Flow bus imbalance (#1138) Signed-off-by: vmouradian Co-authored-by: Didier Vidal --- ...bstractClosedBranchDcFlowEquationTerm.java | 12 ++--- .../ClosedBranchSide1DcFlowEquationTerm.java | 2 +- .../ClosedBranchSide2DcFlowEquationTerm.java | 2 +- .../openloadflow/equations/Equation.java | 13 +++-- .../openloadflow/equations/EquationTerm.java | 6 +-- .../equations/EquationVector.java | 6 +-- .../equations/InjectionDerivable.java | 8 ---- .../powsybl/openloadflow/util/Derivable.java | 2 - .../openloadflow/dc/DcLoadFlowTest.java | 47 +++++++++++++++++++ 9 files changed, 69 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/powsybl/openloadflow/dc/equations/AbstractClosedBranchDcFlowEquationTerm.java b/src/main/java/com/powsybl/openloadflow/dc/equations/AbstractClosedBranchDcFlowEquationTerm.java index 8544b5a44a..ec494563f6 100644 --- a/src/main/java/com/powsybl/openloadflow/dc/equations/AbstractClosedBranchDcFlowEquationTerm.java +++ b/src/main/java/com/powsybl/openloadflow/dc/equations/AbstractClosedBranchDcFlowEquationTerm.java @@ -97,7 +97,8 @@ public double calculateSensi(DenseMatrix dx, int column) { double dph1 = dx.get(ph1Var.getRow(), column); double dph2 = dx.get(ph2Var.getRow(), column); double da1 = a1Var != null ? dx.get(a1Var.getRow(), column) : 0; - return calculateSensi(dph1, dph2, da1); + // - eval(0,0,0) to have an exact epression and remove the constant term of the affine function (wich is 0 in practe because A2 = 0) + return eval(dph1, dph2, da1) - eval(0, 0, 0); } protected double ph1(StateVector sv) { @@ -116,16 +117,11 @@ protected double ph2() { return ph2(sv); } - protected abstract double calculateSensi(double ph1, double ph2, double a1); + protected abstract double eval(double ph1, double ph2, double a1); @Override public double eval() { - return calculateSensi(ph1(), ph2(), a1()); - } - - @Override - public double eval(StateVector sv) { - return calculateSensi(ph1(sv), ph2(sv), a1(sv)); + return eval(ph1(), ph2(), a1()); } protected double a1(StateVector sv) { diff --git a/src/main/java/com/powsybl/openloadflow/dc/equations/ClosedBranchSide1DcFlowEquationTerm.java b/src/main/java/com/powsybl/openloadflow/dc/equations/ClosedBranchSide1DcFlowEquationTerm.java index 95cab9902a..d9971898e3 100644 --- a/src/main/java/com/powsybl/openloadflow/dc/equations/ClosedBranchSide1DcFlowEquationTerm.java +++ b/src/main/java/com/powsybl/openloadflow/dc/equations/ClosedBranchSide1DcFlowEquationTerm.java @@ -36,7 +36,7 @@ public static ClosedBranchSide1DcFlowEquationTerm create(LfBranch branch, LfBus } @Override - protected double calculateSensi(double ph1, double ph2, double a1) { + protected double eval(double ph1, double ph2, double a1) { double deltaPhase = ph2 - ph1 + A2 - a1; return -getPower() * deltaPhase; } diff --git a/src/main/java/com/powsybl/openloadflow/dc/equations/ClosedBranchSide2DcFlowEquationTerm.java b/src/main/java/com/powsybl/openloadflow/dc/equations/ClosedBranchSide2DcFlowEquationTerm.java index 4a72481156..236d708239 100644 --- a/src/main/java/com/powsybl/openloadflow/dc/equations/ClosedBranchSide2DcFlowEquationTerm.java +++ b/src/main/java/com/powsybl/openloadflow/dc/equations/ClosedBranchSide2DcFlowEquationTerm.java @@ -36,7 +36,7 @@ public static ClosedBranchSide2DcFlowEquationTerm create(LfBranch branch, LfBus } @Override - protected double calculateSensi(double ph1, double ph2, double a1) { + protected double eval(double ph1, double ph2, double a1) { double deltaPhase = ph2 - ph1 + A2 - a1; return getPower() * deltaPhase; } diff --git a/src/main/java/com/powsybl/openloadflow/equations/Equation.java b/src/main/java/com/powsybl/openloadflow/equations/Equation.java index b8c11a024d..b0a75ecad2 100644 --- a/src/main/java/com/powsybl/openloadflow/equations/Equation.java +++ b/src/main/java/com/powsybl/openloadflow/equations/Equation.java @@ -156,9 +156,16 @@ public double eval() { for (EquationTerm term : terms) { if (term.isActive()) { value += term.eval(); - if (term.hasRhs()) { - value -= term.rhs(); - } + } + } + return value; + } + + public double evalLhs() { + double value = 0; + for (EquationTerm term : terms) { + if (term.isActive()) { + value += term.evalLhs(); } } return value; diff --git a/src/main/java/com/powsybl/openloadflow/equations/EquationTerm.java b/src/main/java/com/powsybl/openloadflow/equations/EquationTerm.java index d882c67e38..3f280fd95f 100644 --- a/src/main/java/com/powsybl/openloadflow/equations/EquationTerm.java +++ b/src/main/java/com/powsybl/openloadflow/equations/EquationTerm.java @@ -178,11 +178,11 @@ static & Quantity, E extends Enum & Quantity> EquationTerm double eval(); /** - * Evaluate the equation term with an alternative state vector. + * Evaluate equation lhs of the equation term * @return value of the equation term */ - default double eval(StateVector sv) { - throw new UnsupportedOperationException("Not implemented"); + default double evalLhs() { + return eval() - (hasRhs() ? rhs() : 0); } /** diff --git a/src/main/java/com/powsybl/openloadflow/equations/EquationVector.java b/src/main/java/com/powsybl/openloadflow/equations/EquationVector.java index ee7a56dd1a..3a52ae1409 100644 --- a/src/main/java/com/powsybl/openloadflow/equations/EquationVector.java +++ b/src/main/java/com/powsybl/openloadflow/equations/EquationVector.java @@ -42,10 +42,10 @@ protected double[] createArray() { return array; } - private void eval(double[] array, List> equations) { + private void evalLhs(double[] array, List> equations) { Arrays.fill(array, 0); // necessary? for (Equation equation : equations) { - array[equation.getColumn()] = equation.eval(); + array[equation.getColumn()] = equation.evalLhs(); } } @@ -59,7 +59,7 @@ protected void updateArray(double[] array) { throw new IllegalArgumentException("Bad equation vector length: " + array.length); } - eval(array, equations); + evalLhs(array, equations); LOGGER.debug(PERFORMANCE_MARKER, "Equation vector updated in {} us", stopwatch.elapsed(TimeUnit.MICROSECONDS)); } diff --git a/src/main/java/com/powsybl/openloadflow/equations/InjectionDerivable.java b/src/main/java/com/powsybl/openloadflow/equations/InjectionDerivable.java index b8841494b9..ea70e7937f 100644 --- a/src/main/java/com/powsybl/openloadflow/equations/InjectionDerivable.java +++ b/src/main/java/com/powsybl/openloadflow/equations/InjectionDerivable.java @@ -62,12 +62,4 @@ public boolean isActive() { return getBranchTermStream().anyMatch(EquationTerm::isActive); } - @Override - public double eval(StateVector sv) { - // The equation is - // rhs = VariableInjectionPart+ branchPart - // with rhs = - (cte injectionPart) (for ex sum of targetQ) - // -branchPart = variableInjectionPart + cteInjectionPart - return -getBranchTermStream().mapToDouble(t -> t.eval(sv)).sum(); - } } diff --git a/src/main/java/com/powsybl/openloadflow/util/Derivable.java b/src/main/java/com/powsybl/openloadflow/util/Derivable.java index f63cb62f98..53e1fe7b4c 100644 --- a/src/main/java/com/powsybl/openloadflow/util/Derivable.java +++ b/src/main/java/com/powsybl/openloadflow/util/Derivable.java @@ -9,7 +9,6 @@ import com.powsybl.math.matrix.DenseMatrix; import com.powsybl.openloadflow.equations.Quantity; -import com.powsybl.openloadflow.equations.StateVector; import com.powsybl.openloadflow.equations.Variable; /** @@ -23,5 +22,4 @@ public interface Derivable & Quantity> extends Evaluable { boolean isActive(); - double eval(StateVector sv); } diff --git a/src/test/java/com/powsybl/openloadflow/dc/DcLoadFlowTest.java b/src/test/java/com/powsybl/openloadflow/dc/DcLoadFlowTest.java index ee7b6f145b..35356c13b4 100644 --- a/src/test/java/com/powsybl/openloadflow/dc/DcLoadFlowTest.java +++ b/src/test/java/com/powsybl/openloadflow/dc/DcLoadFlowTest.java @@ -197,6 +197,53 @@ void nonImpedantBranchTest() { assertEquals(33.3333, network.getLine("L1").getTerminal1().getP(), 0.01); } + @Test + void nonImpedantBranchAndPhaseShiftingTest() { + Network network = PhaseShifterTestCaseFactory.create(); + network.getLine("L2").setX(0).setR(0); + network.getTwoWindingsTransformer("PS1").getPhaseTapChanger().getStep(1).setAlpha(2); + loadFlowRunner.run(network, parameters); + + assertEquals(16.5316, network.getLine("L1").getTerminal1().getP(), 0.01); + assertEquals(83.4683, network.getLine("L2").getTerminal1().getP(), 0.01); // Temporary comment : P without fix = 133.87 + assertEquals(-83.4683, network.getTwoWindingsTransformer("PS1").getTerminal2().getP(), 0.01); + + // With e second zero impedance line and a second load + VoltageLevel vl2 = network.getVoltageLevel("VL2"); + vl2.getBusBreakerView().newBus() + .setId("B2Bis") + .add(); + vl2.newLoad() + .setId("LD2Bis") + .setConnectableBus("B2Bis") + .setBus("B2Bis") + .setP0(100.0) + .setQ0(50.0) + .add(); + network.newLine() + .setId("L2Bis") + .setVoltageLevel1("VL3") + .setConnectableBus1("B3") + .setBus1("B3") + .setVoltageLevel2("VL2") + .setConnectableBus2("B2Bis") + .setBus2("B2Bis") + .setR(0.0) + .setX(0.0) + .setG1(0.0) + .setB1(0.0) + .setG2(0.0) + .setB2(0.0) + .add(); + network.getGenerator("G1").setMaxP(500); + LoadFlowResult result = loadFlowRunner.run(network, parameters); + assertTrue(result.isFullyConverged()); + assertEquals(49.86, network.getLine("L1").getTerminal1().getP(), 0.01); + assertEquals(150.13, network.getTwoWindingsTransformer("PS1").getTerminal1().getP(), 0.01); + assertEquals(0, network.getTwoWindingsTransformer("PS1").getTerminal2().getP() + network.getLine("L2").getTerminal1().getP() + network.getLine("L2Bis").getTerminal1().getP(), 0.01); // Temporary comment : P without fix = 133.87 + assertEquals(-200, network.getGenerator("G1").getTerminal().getP()); + } + @Test void multiCcTest() { Network network = IeeeCdfNetworkFactory.create14(); From 39ca74161bcd62d0530aa008448e7474b21a1d6e Mon Sep 17 00:00:00 2001 From: jeandemanged Date: Thu, 12 Dec 2024 13:13:42 +0100 Subject: [PATCH 2/6] Fix regression slack distribution reports (#1148) Signed-off-by: Damien Jeandemange --- .../outerloop/DistributedSlackOuterLoop.java | 47 +++++-------- ...tractActivePowerDistributionOuterLoop.java | 35 ---------- ...stractAreaInterchangeControlOuterLoop.java | 48 ++++++++----- .../ActivePowerDistributionOuterLoop.java | 8 +-- .../network/util/ActivePowerDistribution.java | 69 +++++++++++++++++-- .../ac/DistributedSlackOnGenerationTest.java | 16 +++-- .../ac/DistributedSlackOnLoadTest.java | 14 +++- .../openloadflow/util/LoadFlowAssert.java | 24 ++++++- 8 files changed, 159 insertions(+), 102 deletions(-) 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 997ef7d3d3..855f9842b2 100644 --- a/src/main/java/com/powsybl/openloadflow/ac/outerloop/DistributedSlackOuterLoop.java +++ b/src/main/java/com/powsybl/openloadflow/ac/outerloop/DistributedSlackOuterLoop.java @@ -8,7 +8,6 @@ package com.powsybl.openloadflow.ac.outerloop; import com.powsybl.commons.report.ReportNode; -import com.powsybl.openloadflow.OpenLoadFlowParameters; import com.powsybl.openloadflow.ac.AcLoadFlowContext; import com.powsybl.openloadflow.ac.AcLoadFlowParameters; import com.powsybl.openloadflow.ac.AcOuterLoopContext; @@ -18,14 +17,12 @@ import com.powsybl.openloadflow.lf.outerloop.DistributedSlackContextData; import com.powsybl.openloadflow.lf.outerloop.OuterLoopResult; import com.powsybl.openloadflow.lf.outerloop.OuterLoopStatus; -import com.powsybl.openloadflow.network.LfGenerator; import com.powsybl.openloadflow.network.util.ActivePowerDistribution; import com.powsybl.openloadflow.util.PerUnit; import com.powsybl.openloadflow.util.Reports; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Locale; import java.util.Objects; /** @@ -71,25 +68,27 @@ public OuterLoopResult check(AcOuterLoopContext context, ReportNode reportNode) } ReportNode iterationReportNode = Reports.createOuterLoopIterationReporter(reportNode, context.getOuterLoopTotalIterations() + 1); ActivePowerDistribution.Result result = activePowerDistribution.run(context.getNetwork(), slackBusActivePowerMismatch); - double remainingMismatch = result.remainingMismatch(); + ActivePowerDistribution.ResultWithFailureBehaviorHandling resultWbh = ActivePowerDistribution.handleDistributionFailureBehavior( + context.getLoadFlowContext().getParameters().getSlackDistributionFailureBehavior(), + context.getNetwork().getReferenceGenerator(), + slackBusActivePowerMismatch, + result, + "Failed to distribute slack bus active power mismatch, %.2f MW remains" + ); + double remainingMismatch = resultWbh.remainingMismatch(); double distributedActivePower = slackBusActivePowerMismatch - remainingMismatch; + if (Math.abs(remainingMismatch) > ActivePowerDistribution.P_RESIDUE_EPS) { + Reports.reportMismatchDistributionFailure(iterationReportNode, remainingMismatch * PerUnit.SB); + } else { + reportAndLogSuccess(iterationReportNode, slackBusActivePowerMismatch, resultWbh); + } DistributedSlackContextData contextData = (DistributedSlackContextData) context.getData(); contextData.addDistributedActivePower(distributedActivePower); - if (Math.abs(remainingMismatch) > ActivePowerDistribution.P_RESIDUE_EPS) { - OpenLoadFlowParameters.SlackDistributionFailureBehavior slackDistributionFailureBehavior = getSlackDistributionFailureBehavior(context); - LfGenerator referenceGenerator = context.getNetwork().getReferenceGenerator(); - String statusText = String.format(Locale.US, "Failed to distribute slack bus active power mismatch, %.2f MW remains", remainingMismatch * PerUnit.SB); - - OuterLoopResult outerLoopResult = handleDistributionFailure(context, contextData, result.movedBuses(), distributedActivePower, remainingMismatch, statusText); - - if (slackDistributionFailureBehavior == OpenLoadFlowParameters.SlackDistributionFailureBehavior.DISTRIBUTE_ON_REFERENCE_GENERATOR && referenceGenerator != null) { - LOGGER.debug("{} MW distributed to reference generator '{}'", - remainingMismatch * PerUnit.SB, referenceGenerator.getId()); - } - return outerLoopResult; + if (resultWbh.failed()) { + contextData.addDistributedActivePower(-resultWbh.failedDistributedActivePower()); + return new OuterLoopResult(this, OuterLoopStatus.FAILED, resultWbh.failedMessage()); } else { - reportAndLogSuccess(iterationReportNode, slackBusActivePowerMismatch, result); - return new OuterLoopResult(this, OuterLoopStatus.UNSTABLE); + return new OuterLoopResult(this, resultWbh.movedBuses() ? OuterLoopStatus.UNSTABLE : OuterLoopStatus.STABLE); } } @@ -98,17 +97,7 @@ public double getSlackBusActivePowerMismatch(AcOuterLoopContext context) { return context.getLastSolverResult().getSlackBusActivePowerMismatch(); } - @Override - public OpenLoadFlowParameters.SlackDistributionFailureBehavior getSlackDistributionFailureBehavior(AcOuterLoopContext context) { - OpenLoadFlowParameters.SlackDistributionFailureBehavior slackDistributionFailureBehavior = context.getLoadFlowContext().getParameters().getSlackDistributionFailureBehavior(); - if (slackDistributionFailureBehavior == OpenLoadFlowParameters.SlackDistributionFailureBehavior.DISTRIBUTE_ON_REFERENCE_GENERATOR - && context.getNetwork().getReferenceGenerator() == null) { - slackDistributionFailureBehavior = OpenLoadFlowParameters.SlackDistributionFailureBehavior.FAIL; - } - return slackDistributionFailureBehavior; - } - - private static void reportAndLogSuccess(ReportNode reportNode, double slackBusActivePowerMismatch, ActivePowerDistribution.Result result) { + 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)", diff --git a/src/main/java/com/powsybl/openloadflow/lf/outerloop/AbstractActivePowerDistributionOuterLoop.java b/src/main/java/com/powsybl/openloadflow/lf/outerloop/AbstractActivePowerDistributionOuterLoop.java index 79c9dec54a..81e6ca2556 100644 --- a/src/main/java/com/powsybl/openloadflow/lf/outerloop/AbstractActivePowerDistributionOuterLoop.java +++ b/src/main/java/com/powsybl/openloadflow/lf/outerloop/AbstractActivePowerDistributionOuterLoop.java @@ -7,14 +7,9 @@ */ package com.powsybl.openloadflow.lf.outerloop; -import com.powsybl.commons.PowsyblException; -import com.powsybl.openloadflow.OpenLoadFlowParameters; import com.powsybl.openloadflow.equations.Quantity; import com.powsybl.openloadflow.lf.AbstractLoadFlowParameters; import com.powsybl.openloadflow.lf.LoadFlowContext; -import com.powsybl.openloadflow.network.LfGenerator; - -import java.util.Objects; /** * @author Valentin Mouradian {@literal } @@ -35,34 +30,4 @@ public double getDistributedActivePower(O context) { return Double.NaN; } - @Override - public OuterLoopResult handleDistributionFailure(O context, DistributedSlackContextData contextData, boolean movedBuses, double totalDistributedActivePower, double remainingMismatch, String errorMessage) { - OpenLoadFlowParameters.SlackDistributionFailureBehavior slackDistributionFailureBehavior = getSlackDistributionFailureBehavior(context); - - switch (slackDistributionFailureBehavior) { - case THROW -> - throw new PowsyblException(errorMessage); - - case LEAVE_ON_SLACK_BUS -> { - return new OuterLoopResult(this, movedBuses ? OuterLoopStatus.UNSTABLE : OuterLoopStatus.STABLE); - } - case FAIL -> { - // Mismatches reported in LoadFlowResult on slack bus(es) are the mismatches of the last NR run. - // Since we will not be re-running an NR, revert distributedActivePower reporting which would otherwise be misleading. - // Said differently, we report that we didn't distribute anything, and this is indeed consistent with the network state. - contextData.addDistributedActivePower(-totalDistributedActivePower); - return new OuterLoopResult(this, OuterLoopStatus.FAILED, errorMessage); - } - case DISTRIBUTE_ON_REFERENCE_GENERATOR -> { - LfGenerator referenceGenerator = context.getNetwork().getReferenceGenerator(); - Objects.requireNonNull(referenceGenerator, () -> "No reference generator in " + context.getNetwork()); - // remaining goes to reference generator, without any limit consideration - contextData.addDistributedActivePower(remainingMismatch); - referenceGenerator.setTargetP(referenceGenerator.getTargetP() + remainingMismatch); - return new OuterLoopResult(this, OuterLoopStatus.UNSTABLE); - } - default -> throw new IllegalArgumentException("Unknown slackDistributionFailureBehavior"); - } - } - } diff --git a/src/main/java/com/powsybl/openloadflow/lf/outerloop/AbstractAreaInterchangeControlOuterLoop.java b/src/main/java/com/powsybl/openloadflow/lf/outerloop/AbstractAreaInterchangeControlOuterLoop.java index d6464c0bc3..83a65b9019 100644 --- a/src/main/java/com/powsybl/openloadflow/lf/outerloop/AbstractAreaInterchangeControlOuterLoop.java +++ b/src/main/java/com/powsybl/openloadflow/lf/outerloop/AbstractAreaInterchangeControlOuterLoop.java @@ -7,8 +7,8 @@ */ package com.powsybl.openloadflow.lf.outerloop; +import com.powsybl.commons.PowsyblException; import com.powsybl.commons.report.ReportNode; -import com.powsybl.openloadflow.OpenLoadFlowParameters; import com.powsybl.openloadflow.equations.Quantity; import com.powsybl.openloadflow.lf.AbstractLoadFlowParameters; import com.powsybl.openloadflow.lf.LoadFlowContext; @@ -169,12 +169,20 @@ protected double getSlackInjection(String areaId, double slackBusActivePowerMism } protected OuterLoopResult buildOuterLoopResult(Map, Double>> areas, Map resultByArea, ReportNode reportNode, O context) { - Map remainingMismatchByArea = resultByArea.entrySet().stream() - .filter(e -> !lessThanInterchangeMaxMismatch(e.getValue().remainingMismatch())) - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().remainingMismatch())); - double totalDistributedActivePower = resultByArea.entrySet().stream().mapToDouble(e -> areas.get(e.getKey()).getRight() - e.getValue().remainingMismatch()).sum(); - boolean movedBuses = resultByArea.values().stream().map(ActivePowerDistribution.Result::movedBuses).reduce(false, (a, b) -> a || b); - Map iterationsByArea = resultByArea.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().iteration())); + Map remainingMismatchByArea = new HashMap<>(); + Map iterationsByArea = new HashMap<>(); + double totalDistributedActivePower = 0.0; + boolean movedBuses = false; + for (Map.Entry e : resultByArea.entrySet()) { + String area = e.getKey(); + ActivePowerDistribution.Result result = e.getValue(); + if (!lessThanInterchangeMaxMismatch(result.remainingMismatch())) { + remainingMismatchByArea.put(area, result.remainingMismatch()); + } + totalDistributedActivePower += areas.get(area).getRight() - result.remainingMismatch(); + movedBuses |= result.movedBuses(); + iterationsByArea.put(area, result.iteration()); + } ReportNode iterationReportNode = Reports.createOuterLoopIterationReporter(reportNode, context.getOuterLoopTotalIterations() + 1); AreaInterchangeControlContextData contextData = (AreaInterchangeControlContextData) context.getData(); @@ -186,7 +194,21 @@ protected OuterLoopResult buildOuterLoopResult(Map, Doub logger.error("Remaining mismatch for Area {}: {} MW", entry.getKey(), entry.getValue() * PerUnit.SB); Reports.reportAreaInterchangeControlAreaMismatch(failureReportNode, entry.getKey(), entry.getValue() * PerUnit.SB); }); - return handleDistributionFailure(context, contextData, movedBuses, totalDistributedActivePower, Double.NaN, FAILED_TO_DISTRIBUTE_INTERCHANGE_ACTIVE_POWER_MISMATCH); + switch (context.getLoadFlowContext().getParameters().getSlackDistributionFailureBehavior()) { + case THROW -> + throw new PowsyblException(FAILED_TO_DISTRIBUTE_INTERCHANGE_ACTIVE_POWER_MISMATCH); + case LEAVE_ON_SLACK_BUS -> { + return new OuterLoopResult(this, movedBuses ? OuterLoopStatus.UNSTABLE : OuterLoopStatus.STABLE); + } + case FAIL, DISTRIBUTE_ON_REFERENCE_GENERATOR -> { + // Mismatches reported in LoadFlowResult on slack bus(es) are the mismatches of the last solver (DC, NR, ...) run. + // Since we will not be re-running the solver, revert distributedActivePower reporting which would otherwise be misleading. + // Said differently, we report that we didn't distribute anything, and this is indeed consistent with the network state. + contextData.addDistributedActivePower(-totalDistributedActivePower); + return new OuterLoopResult(this, OuterLoopStatus.FAILED, FAILED_TO_DISTRIBUTE_INTERCHANGE_ACTIVE_POWER_MISMATCH); + } + default -> throw new IllegalStateException("Unexpected SlackDistributionFailureBehavior value"); + } } else { if (movedBuses) { areas.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> { @@ -245,14 +267,4 @@ protected Map allocateSlackDistributionParticipationFactors(LfNe } return areaSlackDistributionParticipationFactor; } - - @Override - public OpenLoadFlowParameters.SlackDistributionFailureBehavior getSlackDistributionFailureBehavior(O context) { - OpenLoadFlowParameters.SlackDistributionFailureBehavior slackDistributionFailureBehavior = context.getLoadFlowContext().getParameters().getSlackDistributionFailureBehavior(); - if (OpenLoadFlowParameters.SlackDistributionFailureBehavior.DISTRIBUTE_ON_REFERENCE_GENERATOR == slackDistributionFailureBehavior) { - logger.error("Distribute on reference generator is not supported in area interchange control outer loop, falling back to FAIL mode"); - slackDistributionFailureBehavior = OpenLoadFlowParameters.SlackDistributionFailureBehavior.FAIL; - } - return slackDistributionFailureBehavior; - } } diff --git a/src/main/java/com/powsybl/openloadflow/lf/outerloop/ActivePowerDistributionOuterLoop.java b/src/main/java/com/powsybl/openloadflow/lf/outerloop/ActivePowerDistributionOuterLoop.java index 09fc2d0fea..b9fc71c0d4 100644 --- a/src/main/java/com/powsybl/openloadflow/lf/outerloop/ActivePowerDistributionOuterLoop.java +++ b/src/main/java/com/powsybl/openloadflow/lf/outerloop/ActivePowerDistributionOuterLoop.java @@ -7,7 +7,6 @@ */ package com.powsybl.openloadflow.lf.outerloop; -import com.powsybl.openloadflow.OpenLoadFlowParameters; import com.powsybl.openloadflow.equations.Quantity; import com.powsybl.openloadflow.lf.AbstractLoadFlowParameters; import com.powsybl.openloadflow.lf.LoadFlowContext; @@ -17,7 +16,7 @@ */ public interface ActivePowerDistributionOuterLoop & Quantity, E extends Enum & Quantity, - P extends AbstractLoadFlowParameters, + P extends AbstractLoadFlowParameters

, C extends LoadFlowContext, O extends OuterLoopContext> extends OuterLoop { @@ -25,9 +24,4 @@ public interface ActivePowerDistributionOuterLoop & Quantity, double getDistributedActivePower(O context); double getSlackBusActivePowerMismatch(O context); - - OpenLoadFlowParameters.SlackDistributionFailureBehavior getSlackDistributionFailureBehavior(O context); - - OuterLoopResult handleDistributionFailure(O context, DistributedSlackContextData contextData, boolean movedBuses, double totalDistributedActivePower, double remainingMismatch, String errorMessage); - } 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 29db34ad43..bdf1b061e4 100644 --- a/src/main/java/com/powsybl/openloadflow/network/util/ActivePowerDistribution.java +++ b/src/main/java/com/powsybl/openloadflow/network/util/ActivePowerDistribution.java @@ -7,15 +7,17 @@ */ package com.powsybl.openloadflow.network.util; +import com.powsybl.commons.PowsyblException; 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -24,6 +26,8 @@ */ public final class ActivePowerDistribution { + private static final Logger LOGGER = LoggerFactory.getLogger(ActivePowerDistribution.class); + /** * Active power residue epsilon: 10^-5 in p.u => 10^-3 in Mw */ @@ -104,4 +108,61 @@ public static Step getStep(LoadFlowParameters.BalanceType balanceType, boolean l new GenerationActivePowerDistributionStep(GenerationActivePowerDistributionStep.ParticipationType.REMAINING_MARGIN, useActiveLimits); }; } + + public record ResultWithFailureBehaviorHandling(boolean failed, String failedMessage, int iteration, double remainingMismatch, boolean movedBuses, double failedDistributedActivePower) { } + + public static ResultWithFailureBehaviorHandling handleDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior behavior, + LfGenerator referenceGenerator, + double activePowerMismatch, + Result result, String failMessageTemplate) { + Objects.requireNonNull(behavior); + Objects.requireNonNull(result); + ResultWithFailureBehaviorHandling resultWithFailureBehaviorHandling; + + final OpenLoadFlowParameters.SlackDistributionFailureBehavior effectiveBehavior; + // if requested behavior is to distribute on reference generator, but there is no reference generator, we fall back internally to FAIL mode + if (OpenLoadFlowParameters.SlackDistributionFailureBehavior.DISTRIBUTE_ON_REFERENCE_GENERATOR == behavior && referenceGenerator == null) { + effectiveBehavior = OpenLoadFlowParameters.SlackDistributionFailureBehavior.FAIL; + LOGGER.debug("Distribution failure behavior is DISTRIBUTE_ON_REFERENCE_GENERATOR but no reference generator selected, switching to FAIL mode"); + } else { + effectiveBehavior = behavior; + } + + final double distributedActivePower = activePowerMismatch - result.remainingMismatch(); + + if (Math.abs(result.remainingMismatch()) > ActivePowerDistribution.P_RESIDUE_EPS) { + + String statusText = String.format(Locale.US, failMessageTemplate, result.remainingMismatch() * PerUnit.SB); + switch (effectiveBehavior) { + case THROW -> + throw new PowsyblException(statusText); + + case LEAVE_ON_SLACK_BUS -> { + LOGGER.warn(statusText); + resultWithFailureBehaviorHandling = new ResultWithFailureBehaviorHandling(false, statusText, result.iteration(), result.remainingMismatch(), result.movedBuses(), 0.0); + } + case FAIL -> { + LOGGER.error(statusText); + // Mismatches reported in LoadFlowResult on slack bus(es) are the mismatches of the last solver (DC, NR, ...) run. + // Since we will not be re-running the solver, revert distributedActivePower reporting which would otherwise be misleading. + // Said differently, we report that we didn't distribute anything, and this is indeed consistent with the network state. + resultWithFailureBehaviorHandling = new ResultWithFailureBehaviorHandling(true, statusText, result.iteration(), result.remainingMismatch(), result.movedBuses(), distributedActivePower); + } + case DISTRIBUTE_ON_REFERENCE_GENERATOR -> { + Objects.requireNonNull(referenceGenerator, "No reference generator"); + // remaining goes to reference generator, without any limit consideration + LOGGER.debug("{} MW distributed to reference generator '{}'", + result.remainingMismatch() * PerUnit.SB, referenceGenerator.getId()); + referenceGenerator.setTargetP(referenceGenerator.getTargetP() + result.remainingMismatch()); + // one more iteration, no more remaining mismatch, bus moved + resultWithFailureBehaviorHandling = new ResultWithFailureBehaviorHandling(false, statusText, result.iteration() + 1, 0.0, true, 0.0); + } + default -> throw new IllegalArgumentException("Unknown slackDistributionFailureBehavior"); + } + } else { + resultWithFailureBehaviorHandling = new ResultWithFailureBehaviorHandling(false, "", result.iteration(), result.remainingMismatch(), result.movedBuses(), 0.0); + } + + return resultWithFailureBehaviorHandling; + } } diff --git a/src/test/java/com/powsybl/openloadflow/ac/DistributedSlackOnGenerationTest.java b/src/test/java/com/powsybl/openloadflow/ac/DistributedSlackOnGenerationTest.java index 078709ae8e..f4db1a2f64 100644 --- a/src/test/java/com/powsybl/openloadflow/ac/DistributedSlackOnGenerationTest.java +++ b/src/test/java/com/powsybl/openloadflow/ac/DistributedSlackOnGenerationTest.java @@ -8,6 +8,8 @@ */ package com.powsybl.openloadflow.ac; +import com.powsybl.commons.report.ReportNode; +import com.powsybl.computation.local.LocalComputationManager; import com.powsybl.iidm.network.*; import com.powsybl.iidm.network.extensions.ActivePowerControl; import com.powsybl.iidm.network.extensions.ReferencePriorities; @@ -47,6 +49,7 @@ class DistributedSlackOnGenerationTest { private LoadFlow.Runner loadFlowRunner; private LoadFlowParameters parameters; private OpenLoadFlowParameters parametersExt; + private ReportNode reportNode; @BeforeEach void setUp() { @@ -62,6 +65,7 @@ void setUp() { parametersExt = OpenLoadFlowParameters.create(parameters) .setSlackBusSelectionMode(SlackBusSelectionMode.MOST_MESHED) .setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.THROW); + reportNode = ReportNode.newRootReportNode().withMessageTemplate("test", "test").build(); } @Test @@ -337,25 +341,27 @@ void notEnoughActivePowerThrowTest() { void notEnoughActivePowerFailTest() { network.getLoad("l1").setP0(1000); parametersExt.setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.FAIL); - LoadFlowResult result = loadFlowRunner.run(network, parameters); + LoadFlowResult result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, reportNode); LoadFlowResult.ComponentResult componentResult = result.getComponentResults().get(0); assertFalse(result.isFullyConverged()); assertEquals(LoadFlowResult.ComponentResult.Status.FAILED, componentResult.getStatus()); assertEquals("Outer loop failed: Failed to distribute slack bus active power mismatch, 200.00 MW remains", componentResult.getStatusText()); assertEquals(0, componentResult.getDistributedActivePower(), 1e-4); assertEquals(520, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-4); + assertReportContains("Failed to distribute slack bus active power mismatch, [-+]?\\d*\\.\\d* MW remains", reportNode); } @Test void notEnoughActivePowerLeaveOnSlackBusTest() { network.getLoad("l1").setP0(1000); parametersExt.setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.LEAVE_ON_SLACK_BUS); - LoadFlowResult result = loadFlowRunner.run(network, parameters); + LoadFlowResult result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, reportNode); LoadFlowResult.ComponentResult componentResult = result.getComponentResults().get(0); assertTrue(result.isFullyConverged()); assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, componentResult.getStatus()); assertEquals(320, componentResult.getDistributedActivePower(), 1e-4); assertEquals(200, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-4); + assertReportContains("Failed to distribute slack bus active power mismatch, [-+]?\\d*\\.\\d* MW remains", reportNode); } @Test @@ -366,7 +372,7 @@ void notEnoughActivePowerDistributeReferenceGeneratorTest() { parametersExt .setReferenceBusSelectionMode(ReferenceBusSelectionMode.GENERATOR_REFERENCE_PRIORITY) .setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.DISTRIBUTE_ON_REFERENCE_GENERATOR); - LoadFlowResult result = loadFlowRunner.run(network, parameters); + LoadFlowResult result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, reportNode); LoadFlowResult.ComponentResult componentResult = result.getComponentResults().get(0); assertTrue(result.isFullyConverged()); assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, componentResult.getStatus()); @@ -378,6 +384,7 @@ void notEnoughActivePowerDistributeReferenceGeneratorTest() { assertAngleEquals(0., g1.getTerminal().getBusView().getBus()); // can exceed maxP (200MW) assertActivePowerEquals(-400., g1.getTerminal()); + assertReportContains("Slack bus active power \\([-+]?\\d*\\.\\d* MW\\) distributed in 3 distribution iteration\\(s\\)", reportNode); } @Test @@ -390,12 +397,13 @@ void notEnoughActivePowerDistributeNoReferenceGeneratorTest() { parametersExt .setReferenceBusSelectionMode(ReferenceBusSelectionMode.FIRST_SLACK) .setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.DISTRIBUTE_ON_REFERENCE_GENERATOR); - LoadFlowResult result = loadFlowRunner.run(network, parameters); + LoadFlowResult result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, reportNode); LoadFlowResult.ComponentResult componentResult = result.getComponentResults().get(0); assertTrue(result.isFailed()); assertEquals(LoadFlowResult.ComponentResult.Status.FAILED, componentResult.getStatus()); assertEquals("Outer loop failed: Failed to distribute slack bus active power mismatch, 200.00 MW remains", componentResult.getStatusText()); assertEquals(520., componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3); + assertReportContains("Failed to distribute slack bus active power mismatch, [-+]?\\d*\\.\\d* MW remains", reportNode); } @Test diff --git a/src/test/java/com/powsybl/openloadflow/ac/DistributedSlackOnLoadTest.java b/src/test/java/com/powsybl/openloadflow/ac/DistributedSlackOnLoadTest.java index c88f7b29f2..41b2f80eee 100644 --- a/src/test/java/com/powsybl/openloadflow/ac/DistributedSlackOnLoadTest.java +++ b/src/test/java/com/powsybl/openloadflow/ac/DistributedSlackOnLoadTest.java @@ -7,6 +7,8 @@ */ package com.powsybl.openloadflow.ac; +import com.powsybl.commons.report.ReportNode; +import com.powsybl.computation.local.LocalComputationManager; import com.powsybl.iidm.network.Load; import com.powsybl.iidm.network.LoadType; import com.powsybl.iidm.network.Network; @@ -187,19 +189,23 @@ void testNetworkWithoutConformingLoad() { .setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_CONFORM_LOAD); parametersExt .setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.LEAVE_ON_SLACK_BUS); - LoadFlowResult result = loadFlowRunner.run(network, parameters); + ReportNode reportNode = ReportNode.newRootReportNode().withMessageTemplate("test", "test").build(); + LoadFlowResult result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, reportNode); LoadFlowResult.ComponentResult componentResult = result.getComponentResults().get(0); assertTrue(result.isFullyConverged()); assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, componentResult.getStatus()); assertEquals(-60., componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-6); + assertReportContains("Failed to distribute slack bus active power mismatch, [-+]?\\d*\\.\\d* MW remains", reportNode); parametersExt .setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.FAIL); - result = loadFlowRunner.run(network, parameters); + reportNode = ReportNode.newRootReportNode().withMessageTemplate("test", "test").build(); + result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, reportNode); componentResult = result.getComponentResults().get(0); assertFalse(result.isFullyConverged()); assertEquals(LoadFlowResult.ComponentResult.Status.FAILED, componentResult.getStatus()); assertEquals(-60., componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-6); + assertReportContains("Failed to distribute slack bus active power mismatch, [-+]?\\d*\\.\\d* MW remains", reportNode); parametersExt .setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.THROW); @@ -209,12 +215,14 @@ void testNetworkWithoutConformingLoad() { .setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.DISTRIBUTE_ON_REFERENCE_GENERATOR) .setReferenceBusSelectionMode(ReferenceBusSelectionMode.GENERATOR_REFERENCE_PRIORITY); ReferencePriority.set(network.getGenerator("g1"), 1); - result = loadFlowRunner.run(network, parameters); + reportNode = ReportNode.newRootReportNode().withMessageTemplate("test", "test").build(); + result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, reportNode); componentResult = result.getComponentResults().get(0); assertTrue(result.isFullyConverged()); assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, componentResult.getStatus()); assertEquals(-60., componentResult.getDistributedActivePower(), 1e-6); assertActivePowerEquals(-40., network.getGenerator("g1").getTerminal()); + assertReportContains("Slack bus active power \\([-+]?\\d*\\.\\d* MW\\) distributed in 1 distribution iteration\\(s\\)", reportNode); } @Test diff --git a/src/test/java/com/powsybl/openloadflow/util/LoadFlowAssert.java b/src/test/java/com/powsybl/openloadflow/util/LoadFlowAssert.java index 70ee3d913b..175d806b9c 100644 --- a/src/test/java/com/powsybl/openloadflow/util/LoadFlowAssert.java +++ b/src/test/java/com/powsybl/openloadflow/util/LoadFlowAssert.java @@ -16,12 +16,14 @@ import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; +import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.util.Iterator; +import java.util.List; +import java.util.stream.Stream; import static com.powsybl.commons.test.TestUtil.normalizeLineSeparator; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; /** * @author Geoffroy Jamgotchian {@literal } @@ -117,4 +119,22 @@ public static void assertTxtReportEquals(String reportTxt, ReportNode reportNode String logExport = normalizeLineSeparator(sw.toString()); assertEquals(refLogExport, logExport); } + + public static Stream streamReportNodes(final ReportNode node) { + return Stream.concat(Stream.of(node), node.getChildren().stream().flatMap(LoadFlowAssert::streamReportNodes)); + } + + public static void assertReportContains(String regex, ReportNode reportNode) { + List matching = streamReportNodes(reportNode).filter(node -> node.getMessage().matches(regex)).toList(); + assertFalse(matching.isEmpty(), () -> { + StringWriter sw = new StringWriter(); + try { + reportNode.print(sw); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + String txtReport = normalizeLineSeparator(sw.toString()); + return "Report does not contain '" + regex + "': \n-----\n" + txtReport; + }); + } } From dde1dc69917962cf6d567c9327d1971ae52baab5 Mon Sep 17 00:00:00 2001 From: Olivier Perrin Date: Thu, 12 Dec 2024 14:32:56 +0100 Subject: [PATCH 3/6] bump powsybl-core to 6.6.0 (#1150) Signed-off-by: Didier Vidal --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 432ae06156..ed05d35ac5 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,7 @@ 1.6.3 0.3.2 - 6.6.0-RC1 + 6.6.0 From 714b35ec979b9fa1138eefff8b5a9db8c65a44a1 Mon Sep 17 00:00:00 2001 From: Sylvestre Prabakaran <38729191+SylvestreSakti@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:13:53 +0100 Subject: [PATCH 4/6] Handle slack distribution failure behavior in DC Load Flow (#1146) Signed-off-by: PRABAKARAN Sylvestre Co-authored-by: Damien Jeandemange --- .../outerloop/DistributedSlackOuterLoop.java | 8 +- .../openloadflow/dc/DcLoadFlowEngine.java | 40 +++++++++- .../network/util/ActivePowerDistribution.java | 9 +++ .../openloadflow/dc/DcLoadFlowTest.java | 74 +++++++++++++++++-- .../multipleConnectedComponentsDcReport.txt | 2 + 5 files changed, 117 insertions(+), 16 deletions(-) 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 From a69b3a043a3154f47bd502247156398036600c38 Mon Sep 17 00:00:00 2001 From: Damien Jeandemange Date: Thu, 12 Dec 2024 16:44:59 +0100 Subject: [PATCH 5/6] Bump to v1.14.0 Signed-off-by: Damien Jeandemange --- README.md | 6 +++--- pom.xml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3c8697bbc4..3ff08e6bd6 100644 --- a/README.md +++ b/README.md @@ -93,12 +93,12 @@ dependencies to respectively have access to network model, IEEE test networks an com.powsybl powsybl-iidm-impl - 6.5.1 + 6.6.0 com.powsybl powsybl-ieee-cdf-converter - 6.5.1 + 6.6.0 org.slf4j @@ -117,7 +117,7 @@ After adding a last Maven dependency on Open Load Flow implementation: com.powsybl powsybl-open-loadflow - 1.13.2 + 1.14.0 ``` diff --git a/pom.xml b/pom.xml index ed05d35ac5..ae5bb61bbb 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ powsybl-open-loadflow - 1.14.0-SNAPSHOT + 1.14.0 powsybl open loadflow An open source loadflow based on PowSyBl From 868e5e3f67627c0f4610ac2c1f5adcd49efd5954 Mon Sep 17 00:00:00 2001 From: Damien Jeandemange Date: Thu, 12 Dec 2024 16:45:22 +0100 Subject: [PATCH 6/6] Bump to v1.15.0-SNAPSHOT Signed-off-by: Damien Jeandemange --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ae5bb61bbb..b00631eb3e 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ powsybl-open-loadflow - 1.14.0 + 1.15.0-SNAPSHOT powsybl open loadflow An open source loadflow based on PowSyBl