Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fix regression slack distribution reports #1148

Merged
merged 6 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <valentin.mouradian at artelys.com>}
Expand All @@ -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");
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -169,12 +169,20 @@ protected double getSlackInjection(String areaId, double slackBusActivePowerMism
}

protected OuterLoopResult buildOuterLoopResult(Map<String, Pair<Set<LfBus>, Double>> areas, Map<String, ActivePowerDistribution.Result> resultByArea, ReportNode reportNode, O context) {
Map<String, Double> 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<String, Integer> iterationsByArea = resultByArea.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().iteration()));
Map<String, Double> remainingMismatchByArea = new HashMap<>();
Map<String, Integer> iterationsByArea = new HashMap<>();
double totalDistributedActivePower = 0.0;
boolean movedBuses = false;
for (Map.Entry<String, ActivePowerDistribution.Result> 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();
Expand All @@ -186,7 +194,21 @@ protected OuterLoopResult buildOuterLoopResult(Map<String, Pair<Set<LfBus>, 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 -> {
Expand Down Expand Up @@ -245,14 +267,4 @@ protected Map<String, Double> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,17 +16,12 @@
*/
public interface ActivePowerDistributionOuterLoop<V extends Enum<V> & Quantity,
E extends Enum<E> & Quantity,
P extends AbstractLoadFlowParameters,
P extends AbstractLoadFlowParameters<P>,
C extends LoadFlowContext<V, E, P>,
O extends OuterLoopContext<V, E, P, C>>
extends OuterLoop<V, E, P, C, O> {

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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
*/
Expand Down Expand Up @@ -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;
Copy link
Contributor

@SylvestreSakti SylvestreSakti Dec 11, 2024

Choose a reason for hiding this comment

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

A log message may be given to inform that no reference generator is defined. I think this check is not done anywhere else.

Copy link
Member Author

Choose a reason for hiding this comment

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

adding it but proposing DEBUG level, I fear this would be too noisy: it would be issued at each outerloop

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;
}
}
Loading
Loading