IncrementalTransformerReactivePowerControlOuterLoop.java
/**
* Copyright (c) 2023, Coreso SA (https://www.coreso.eu/) and TSCNET Services GmbH (https://www.tscnet.eu/)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* SPDX-License-Identifier: MPL-2.0
*/
package com.powsybl.openloadflow.ac.outerloop;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.iidm.network.TwoSides;
import com.powsybl.math.matrix.DenseMatrix;
import com.powsybl.openloadflow.ac.AcLoadFlowContext;
import com.powsybl.openloadflow.ac.AcOuterLoopContext;
import com.powsybl.openloadflow.ac.equations.AcEquationType;
import com.powsybl.openloadflow.ac.equations.AcVariableType;
import com.powsybl.openloadflow.equations.EquationSystem;
import com.powsybl.openloadflow.equations.EquationTerm;
import com.powsybl.openloadflow.equations.JacobianMatrix;
import com.powsybl.openloadflow.lf.outerloop.IncrementalContextData;
import com.powsybl.openloadflow.lf.outerloop.OuterLoopResult;
import com.powsybl.openloadflow.lf.outerloop.OuterLoopStatus;
import com.powsybl.openloadflow.network.*;
import com.powsybl.openloadflow.util.PerUnit;
import com.powsybl.openloadflow.util.Reports;
import org.apache.commons.lang3.Range;
import org.apache.commons.lang3.mutable.MutableObject;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* @author Pierre Arvy {@literal <pierre.arvy at artelys.com>}
*/
public class IncrementalTransformerReactivePowerControlOuterLoop extends AbstractTransformerReactivePowerControlOuterLoop {
private static final Logger LOGGER = LoggerFactory.getLogger(IncrementalTransformerReactivePowerControlOuterLoop.class);
public static final String NAME = "IncrementalTransformerReactivePowerControl";
private static final int MAX_DIRECTION_CHANGE = 3;
private final int maxTapShift;
public IncrementalTransformerReactivePowerControlOuterLoop(int maxTapShift) {
this.maxTapShift = maxTapShift;
}
@Override
public String getName() {
return NAME;
}
private static boolean isOutOfDeadband(TransformerReactivePowerControl reactivePowerControl) {
double diffQ = getDiffQ(reactivePowerControl);
double halfTargetDeadband = getHalfTargetDeadband(reactivePowerControl);
boolean outOfDeadband = Math.abs(diffQ) > halfTargetDeadband;
if (outOfDeadband) {
LfBranch controllerBranch = reactivePowerControl.getControllerBranch();
LfBranch controlledBranch = reactivePowerControl.getControlledBranch();
LOGGER.trace("Controlled branch '{}' ({} controller) is outside of its deadband (half is {} MVar) and could need a reactive power adjustment of {} MVar",
controlledBranch.getId(), controllerBranch.getId(), halfTargetDeadband * PerUnit.SB, diffQ * PerUnit.SB);
}
return outOfDeadband;
}
public static List<LfBranch> getControllerBranches(LfNetwork network) {
return network.getBranches().stream()
.filter(branch -> !branch.isDisabled() && branch.isTransformerReactivePowerController())
.toList();
}
public static List<LfBranch> getControlledBranchesOutOfDeadband(LfNetwork network) {
return network.getBranches().stream()
.filter(LfBranch::isTransformerReactivePowerControlled)
.filter(branch -> isOutOfDeadband(branch.getTransformerReactivePowerControl().orElseThrow()))
.filter(Predicate.not(LfBranch::isDisabled))
.toList();
}
public static List<LfBranch> getControllerBranchesOutOfDeadband(List<LfBranch> controlledBranchesOutOfDeadband) {
return controlledBranchesOutOfDeadband.stream()
.map(controlledBranch -> controlledBranch.getTransformerReactivePowerControl().orElseThrow().getControllerBranch())
.filter(Predicate.not(LfBranch::isDisabled))
.toList();
}
@Override
public void initialize(AcOuterLoopContext context) {
var contextData = new IncrementalContextData();
context.setData(contextData);
for (LfBranch branch : getControllerBranches(context.getNetwork())) {
contextData.getControllersContexts().put(branch.getId(), new IncrementalContextData.ControllerContext(MAX_DIRECTION_CHANGE));
}
}
static class SensitivityContext {
private final DenseMatrix sensitivities;
private final int[] controllerBranchIndex;
public SensitivityContext(LfNetwork network, List<LfBranch> controllerBranches,
EquationSystem<AcVariableType, AcEquationType> equationSystem,
JacobianMatrix<AcVariableType, AcEquationType> j) {
controllerBranchIndex = LfBranch.createIndex(network, controllerBranches);
sensitivities = calculateSensitivityValues(controllerBranches, controllerBranchIndex, equationSystem, j);
}
private static DenseMatrix calculateSensitivityValues(List<LfBranch> controllerBranches, int[] controllerBranchIndex,
EquationSystem<AcVariableType, AcEquationType> equationSystem,
JacobianMatrix<AcVariableType, AcEquationType> j) {
DenseMatrix rhs = new DenseMatrix(equationSystem.getIndex().getSortedEquationsToSolve().size(), controllerBranches.size());
for (LfBranch controllerBranch : controllerBranches) {
equationSystem.getEquation(controllerBranch.getNum(), AcEquationType.BRANCH_TARGET_RHO1)
.ifPresent(equation -> rhs.set(equation.getColumn(), controllerBranchIndex[controllerBranch.getNum()], 1d));
}
j.solveTransposed(rhs);
return rhs;
}
@SuppressWarnings("unchecked")
private static EquationTerm<AcVariableType, AcEquationType> getCalculatedQ(LfBranch controlledBranch, TwoSides controlledSide) {
var calculatedQ = controlledSide == TwoSides.ONE ? controlledBranch.getQ1() : controlledBranch.getQ2();
return (EquationTerm<AcVariableType, AcEquationType>) calculatedQ;
}
double calculateSensitivityFromRToQ(LfBranch controllerBranch, LfBranch controlledBranch, TwoSides controlledSide) {
return getCalculatedQ(controlledBranch, controlledSide)
.calculateSensi(sensitivities, controllerBranchIndex[controllerBranch.getNum()]);
}
}
private boolean adjustWithController(LfBranch controllerBranch, LfBranch controlledBranch, TwoSides controlledSide, IncrementalContextData contextData,
double diffQ, SensitivityContext sensitivities,
List<String> controlledBranchesWithAllItsControllersToLimit) {
// only one transformer controls a branch
var controllerContext = contextData.getControllersContexts().get(controllerBranch.getId());
double sensitivity = sensitivities.calculateSensitivityFromRToQ(controllerBranch, controlledBranch, controlledSide);
PiModel piModel = controllerBranch.getPiModel();
int previousTapPosition = piModel.getTapPosition();
double deltaR1 = diffQ / sensitivity;
return piModel.updateTapPositionToReachNewR1(deltaR1, maxTapShift, controllerContext.getAllowedDirection()).map(direction -> {
controllerContext.updateAllowedDirection(direction);
Range<Integer> tapPositionRange = piModel.getTapPositionRange();
LOGGER.debug("Controller branch '{}' change tap from {} to {} (full range: {})", controllerBranch.getId(),
previousTapPosition, piModel.getTapPosition(), tapPositionRange);
if (piModel.getTapPosition() == tapPositionRange.getMinimum()
|| piModel.getTapPosition() == tapPositionRange.getMaximum()) {
controlledBranchesWithAllItsControllersToLimit.add(controlledBranch.getId());
}
return direction;
}).isPresent();
}
@Override
public OuterLoopResult check(AcOuterLoopContext context, ReportNode reportNode) {
MutableObject<OuterLoopStatus> status = new MutableObject<>(OuterLoopStatus.STABLE);
LfNetwork network = context.getNetwork();
AcLoadFlowContext loadFlowContext = context.getLoadFlowContext();
var contextData = (IncrementalContextData) context.getData();
// branches which are out of their deadbands
List<LfBranch> controlledBranchesOutOfDeadband = getControlledBranchesOutOfDeadband(network);
List<LfBranch> controllerBranchesOutOfDeadband = getControllerBranchesOutOfDeadband(controlledBranchesOutOfDeadband);
if (controllerBranchesOutOfDeadband.isEmpty()) {
return new OuterLoopResult(this, status.getValue());
}
SensitivityContext sensitivityContext = new SensitivityContext(network, controllerBranchesOutOfDeadband,
loadFlowContext.getEquationSystem(), loadFlowContext.getJacobianMatrix());
// for synthetics logs
List<String> controlledBranchesAdjusted = new ArrayList<>();
List<String> controlledBranchesWithAllItsControllersToLimit = new ArrayList<>();
controlledBranchesOutOfDeadband.forEach(controlledBranch -> {
TransformerReactivePowerControl reactivePowerControl = controlledBranch.getTransformerReactivePowerControl().orElseThrow();
double diffQ = getDiffQ(reactivePowerControl);
LfBranch controller = reactivePowerControl.getControllerBranch();
TwoSides controlledSide = reactivePowerControl.getControlledSide();
// TODO : add case with more controllers
boolean adjusted = adjustWithController(controller, controlledBranch, controlledSide, contextData, diffQ, sensitivityContext, controlledBranchesWithAllItsControllersToLimit);
if (adjusted) {
controlledBranchesAdjusted.add(controlledBranch.getId());
status.setValue(OuterLoopStatus.UNSTABLE);
}
});
ReportNode iterationReportNode = !controlledBranchesOutOfDeadband.isEmpty() || !controlledBranchesAdjusted.isEmpty() || !controlledBranchesWithAllItsControllersToLimit.isEmpty() ?
Reports.createOuterLoopIterationReporter(reportNode, context.getOuterLoopTotalIterations() + 1) : null;
if (!controlledBranchesOutOfDeadband.isEmpty()) {
if (LOGGER.isInfoEnabled()) {
Map<String, Double> largestMismatches = controllerBranchesOutOfDeadband.stream()
.map(controlledBranch -> Pair.of(controlledBranch.getId(), Math.abs(getDiffQ(controlledBranch.getTransformerReactivePowerControl().get()))))
.sorted((p1, p2) -> Double.compare(p2.getRight() * PerUnit.SB, p1.getRight() * PerUnit.SB))
.limit(3) // 3 largest
.collect(Collectors.toMap(Pair::getLeft, Pair::getRight, (key1, key2) -> key1, LinkedHashMap::new));
LOGGER.info("{} controlled branch reactive power are outside of their target deadband, largest ones are: {}",
controllerBranchesOutOfDeadband.size(), largestMismatches);
}
Reports.reportTransformerControlBranchesOutsideDeadband(Objects.requireNonNull(iterationReportNode), controlledBranchesOutOfDeadband.size());
}
if (!controlledBranchesAdjusted.isEmpty()) {
LOGGER.info("{} controlled branch reactive power have been adjusted by changing at least one tap",
controlledBranchesAdjusted.size());
Reports.reportTransformerControlChangedTaps(Objects.requireNonNull(iterationReportNode), controlledBranchesAdjusted.size());
}
if (!controlledBranchesWithAllItsControllersToLimit.isEmpty()) {
LOGGER.info("{} controlled branches have all its controllers to a tap limit: {}",
controlledBranchesWithAllItsControllersToLimit.size(), controlledBranchesWithAllItsControllersToLimit);
Reports.reportTransformerControlTapLimit(Objects.requireNonNull(iterationReportNode), controlledBranchesWithAllItsControllersToLimit.size());
}
return new OuterLoopResult(this, status.getValue());
}
private static double getDiffQ(TransformerReactivePowerControl reactivePowerControl) {
double targetQ = getHighestPriorityReactivePowerTarget(reactivePowerControl.getControlledBranch());
double q = reactivePowerControl.getControlledSide() == TwoSides.ONE ? reactivePowerControl.getControlledBranch().getQ1().eval()
: reactivePowerControl.getControlledBranch().getQ2().eval();
return targetQ - q;
}
private static double getHighestPriorityReactivePowerTarget(LfBranch controlledBranch) {
return controlledBranch.getGeneratorReactivePowerControl()
.map(GeneratorReactivePowerControl::getTargetValue)
.orElseGet(() -> controlledBranch.getTransformerReactivePowerControl()
.orElseThrow()
.getTargetValue());
}
}