BestTapFinder.java
/*
* Copyright (c) 2021, RTE (http://www.rte-france.com)
* 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/.
*/
package com.powsybl.openrao.searchtreerao.linearoptimisation.algorithms;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.Unit;
import com.powsybl.openrao.data.crac.api.State;
import com.powsybl.openrao.data.crac.api.cnec.FlowCnec;
import com.powsybl.openrao.data.crac.api.rangeaction.PstRangeAction;
import com.powsybl.openrao.data.crac.api.rangeaction.RangeAction;
import com.powsybl.iidm.network.TwoSides;
import com.powsybl.openrao.searchtreerao.commons.RaoUtil;
import com.powsybl.openrao.searchtreerao.commons.optimizationperimeters.OptimizationPerimeter;
import com.powsybl.openrao.searchtreerao.result.api.*;
import com.powsybl.openrao.searchtreerao.result.impl.RangeActionActivationResultImpl;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.ValidationException;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.stream.Collectors;
import static com.powsybl.openrao.commons.Unit.MEGAWATT;
/**
* @author Joris Mancini {@literal <joris.mancini at rte-france.com>}
*/
public final class BestTapFinder {
private BestTapFinder() {
// Should not be instantiated
}
/**
* This function computes the best tap positions for PstRangeActions that were optimized in the linear problem.
* It is a little smarter than just rounding the optimal angle to the closest tap position:
* if the optimal angle is close to the limit between two tap positions, it will choose the one that maximizes the
* minimum margin on the 10 most limiting elements (pre-optim)
* If virtual costs are an important part of the optimization, it is highly recommended to use APPROXIMATED_INTEGERS
* taps in the linear optimization, rather than relying on the best tap finder to round the taps.
*
*/
public static RangeActionActivationResultImpl round(RangeActionActivationResult linearProblemResult,
Network network,
OptimizationPerimeter optimizationContext,
RangeActionSetpointResult prePerimeterSetpoint,
LinearOptimizationResult linearOptimizationResult,
Unit unit) {
RangeActionActivationResultImpl roundedResult = new RangeActionActivationResultImpl(prePerimeterSetpoint);
findBestTapOfPstRangeActions(linearProblemResult, network, optimizationContext, linearOptimizationResult, roundedResult, unit);
return roundedResult;
}
private static void findBestTapOfPstRangeActions(RangeActionActivationResult linearProblemResult,
Network network,
OptimizationPerimeter optimizationContext,
LinearOptimizationResult linearOptimizationResult,
RangeActionActivationResultImpl roundedResult,
Unit unit) {
for (State state : optimizationContext.getRangeActionOptimizationStates()) {
Map<PstRangeAction, Map<Integer, Double>> minMarginPerTap = new HashMap<>();
optimizationContext.getRangeActionsPerState().get(state).stream()
.filter(PstRangeAction.class::isInstance)
.map(PstRangeAction.class::cast)
.forEach(pstRangeAction -> minMarginPerTap.put(pstRangeAction, computeMinMarginsForBestTaps(network, pstRangeAction, linearProblemResult.getOptimizedSetpoint(pstRangeAction, state), linearOptimizationResult, unit)));
Map<String, Integer> bestTapPerPstGroup = computeBestTapPerPstGroup(minMarginPerTap);
for (RangeAction<?> rangeAction : optimizationContext.getRangeActionsPerState().get(state)) {
if (rangeAction instanceof PstRangeAction pstRangeAction && linearProblemResult.getActivatedRangeActions(state).contains(rangeAction)) {
Optional<String> optGroupId = pstRangeAction.getGroupId();
if (optGroupId.isPresent()) {
roundedResult.putResult(pstRangeAction, state, pstRangeAction.convertTapToAngle(bestTapPerPstGroup.get(optGroupId.get())));
} else {
int bestTap = minMarginPerTap.get(pstRangeAction).entrySet().stream().max(Comparator.comparing(Map.Entry<Integer, Double>::getValue)).orElseThrow().getKey();
roundedResult.putResult(pstRangeAction, state, pstRangeAction.convertTapToAngle(bestTap));
}
}
}
}
}
/**
* This function computes, for every group of PSTs, the common tap position that maximizes the minimum margin
*
* @param minMarginPerTap: a map containing for each PstRangeAction, a map with tap positions and resulting minimum margin
* @return a map containing for each group ID, the best common tap position for the PSTs
*/
static Map<String, Integer> computeBestTapPerPstGroup(Map<PstRangeAction, Map<Integer, Double>> minMarginPerTap) {
Map<String, Integer> bestTapPerPstGroup = new HashMap<>();
Set<PstRangeAction> pstRangeActions = minMarginPerTap.keySet();
Set<String> pstGroups = pstRangeActions.stream().map(PstRangeAction::getGroupId).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toSet());
for (String pstGroup : pstGroups) {
Set<PstRangeAction> pstsOfGroup = pstRangeActions.stream()
.filter(pstRangeAction -> pstRangeAction.getGroupId().isPresent() && pstRangeAction.getGroupId().get().equals(pstGroup))
.collect(Collectors.toSet());
Map<Integer, Double> groupMinMarginPerTap = new HashMap<>();
for (PstRangeAction pstRangeAction : pstsOfGroup) {
Map<Integer, Double> pstMinMarginPerTap = minMarginPerTap.get(pstRangeAction);
for (Map.Entry<Integer, Double> entry : pstMinMarginPerTap.entrySet()) {
int tap = entry.getKey();
if (groupMinMarginPerTap.containsKey(tap)) {
groupMinMarginPerTap.put(tap, Math.min(entry.getValue(), groupMinMarginPerTap.get(tap)));
} else {
groupMinMarginPerTap.put(tap, entry.getValue());
}
}
}
int bestGroupTap = groupMinMarginPerTap.entrySet().stream().max(Comparator.comparing(Map.Entry<Integer, Double>::getValue)).orElseThrow().getKey();
bestTapPerPstGroup.put(pstGroup, bestGroupTap);
}
return bestTapPerPstGroup;
}
/**
* This function computes the best tap positions for an optimized PST range action, using the optimal angle
* computed by the linear problem
* It first chooses the closest tap position to the angle, then the second closest one, if the angle is close enough
* (15% threshold) to the limit between two tap positions
* It computes the minimum margin among the most limiting cnecs for both tap positions and returns them in a map
* Exceptions:
* - if the closest tap position is at a min or max limit, and the angle is close to the angle limit, then only
* the closest tap is returned. The margin is not computed but replaced with Double.MAX_VALUE
* - if the angle is not close enough to the limit between two tap positions, only the closest tap is returned
* with a Double.MAX_VALUE margin
*
* @param pstRangeAction: the PstRangeAction for which we need the best taps and margins
* @param angle: the optimal angle computed by the linear problem
* @param linearOptimizationResult: allows to get flow & sensitivity values, as well as most limiting flow CNECs
* @param unit: the unit of the evaluators (MW or A)
* @return a map containing the minimum margin for each best tap position (one or two taps)
*/
static Map<Integer, Double> computeMinMarginsForBestTaps(Network network,
PstRangeAction pstRangeAction,
double angle,
LinearOptimizationResult linearOptimizationResult,
Unit unit) {
int closestTap = pstRangeAction.convertAngleToTap(angle);
double closestAngle = pstRangeAction.convertTapToAngle(closestTap);
Integer otherTap = findOtherTap(pstRangeAction, angle, closestTap, closestAngle);
// Default case
if (otherTap == null) {
return Map.of(closestTap, Double.MAX_VALUE);
}
double otherAngle = pstRangeAction.convertTapToAngle(otherTap);
double approxLimitAngle = 0.5 * (closestAngle + otherAngle);
if (Math.abs(angle - approxLimitAngle) / Math.abs(closestAngle - otherAngle) < 0.15) {
// Angle is too close to the limit between two tap positions
// Choose the tap that maximizes the margin on the most limiting element
Pair<Double, Double> margins = computeMinMargins(network, pstRangeAction, closestAngle, otherAngle, linearOptimizationResult, unit);
if (margins.getRight() > margins.getLeft()) {
return Map.of(closestTap, margins.getLeft(), otherTap, margins.getRight());
}
}
// Default case
return Map.of(closestTap, Double.MAX_VALUE);
}
/**
* Given a PST, an angle (not equal to a whole tap) and the closest whole tap & angle, this method finds
* the other closest tap (+1 or -1) that is also close to the angle
*/
private static Integer findOtherTap(PstRangeAction pstRangeAction, double angle, int closestTap, double closestAngle) {
Integer otherTap = null;
// We don't have access to min and max tap positions directly
// We have access to min and max angles, but angles and taps do not necessarily increase/decrese in the same direction
// So we have to try/catch in order to know if we're at the tap limits
boolean testTapPlus1 = true;
try {
pstRangeAction.convertTapToAngle(closestTap + 1);
} catch (OpenRaoException | ValidationException e) {
testTapPlus1 = false;
}
boolean testTapMinus1 = true;
try {
pstRangeAction.convertTapToAngle(closestTap - 1);
} catch (OpenRaoException | ValidationException e) {
testTapMinus1 = false;
}
if (testTapPlus1 && testTapMinus1) {
// We can test tap+1 and tap-1
double angleOfTapPlus1 = pstRangeAction.convertTapToAngle(closestTap + 1);
otherTap = (Math.signum(angleOfTapPlus1 - closestAngle) * Math.signum(angle - closestAngle) > 0) ? closestTap + 1 : closestTap - 1;
} else if (testTapPlus1) {
// We can only test tap+1, if the optimal angle is between the closest angle and the angle of tap+1
double angleOfTapPlus1 = pstRangeAction.convertTapToAngle(closestTap + 1);
if (Math.signum(angleOfTapPlus1 - closestAngle) * Math.signum(angle - closestAngle) > 0) {
otherTap = closestTap + 1;
}
} else if (testTapMinus1) {
// We can only test tap-1, if the optimal angle is between the closest angle and the angle of tap-1
double angleOfTapMinus1 = pstRangeAction.convertTapToAngle(closestTap - 1);
if (Math.signum(angleOfTapMinus1 - closestAngle) * Math.signum(angle - closestAngle) > 0) {
otherTap = closestTap - 1;
}
}
return otherTap;
}
/**
* This method estimates the minimum margin upon a given set of cnecs, for two angles of a given PST
*
* @param pstRangeAction: the PstRangeAction that we should test on two angles
* @param angle1: the first angle for the PST
* @param angle2: the second angle for the PST
* @param linearOptimizationResult: allows to get flow & sensitivity values, as well as most limiting flow CNECs
* @param unit: the unit of the evalutors (MW or A)
* @return a pair of two minimum margins (margin for angle1, margin for angle2)
*/
static Pair<Double, Double> computeMinMargins(Network network,
PstRangeAction pstRangeAction,
double angle1,
double angle2,
LinearOptimizationResult linearOptimizationResult,
Unit unit) {
double minMargin1 = Double.MAX_VALUE;
double minMargin2 = Double.MAX_VALUE;
for (FlowCnec flowCnec : linearOptimizationResult.getMostLimitingElements(10)) {
for (TwoSides side : flowCnec.getMonitoredSides()) {
double sensitivity = linearOptimizationResult.getSensitivityValue(flowCnec, side, pstRangeAction, MEGAWATT);
double currentSetPoint = pstRangeAction.getCurrentSetpoint(network);
double referenceFlow = linearOptimizationResult.getFlow(flowCnec, side, unit) * RaoUtil.getFlowUnitMultiplier(flowCnec, side, unit, MEGAWATT);
double flow1 = sensitivity * (angle1 - currentSetPoint) + referenceFlow;
double flow2 = sensitivity * (angle2 - currentSetPoint) + referenceFlow;
minMargin1 = Math.min(minMargin1, flowCnec.computeMargin(flow1, side, MEGAWATT));
minMargin2 = Math.min(minMargin2, flowCnec.computeMargin(flow2, side, MEGAWATT));
}
}
return Pair.of(minMargin1, minMargin2);
}
}