CoreCneCnecsCreator.java
/*
* Copyright (c) 2022, 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.data.raoresult.io.cne.core;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.Unit;
import com.powsybl.openrao.commons.logs.OpenRaoLoggerProvider;
import com.powsybl.openrao.data.raoresult.io.cne.commons.CneHelper;
import com.powsybl.openrao.data.raoresult.io.cne.core.xsd.Analog;
import com.powsybl.openrao.data.raoresult.io.cne.core.xsd.ConstraintSeries;
import com.powsybl.openrao.data.raoresult.io.cne.core.xsd.ContingencySeries;
import com.powsybl.openrao.data.raoresult.io.cne.core.xsd.MonitoredRegisteredResource;
import com.powsybl.contingency.Contingency;
import com.powsybl.openrao.data.crac.api.Instant;
import com.powsybl.openrao.data.crac.api.InstantKind;
import com.powsybl.openrao.data.crac.api.cnec.FlowCnec;
import com.powsybl.iidm.network.TwoSides;
import com.powsybl.openrao.data.crac.io.commons.api.stdcreationcontext.BranchCnecCreationContext;
import com.powsybl.openrao.data.crac.io.commons.api.stdcreationcontext.UcteCracCreationContext;
import com.powsybl.openrao.data.crac.loopflowextension.LoopFlowThreshold;
import java.util.*;
import static com.powsybl.openrao.data.raoresult.io.cne.commons.CneConstants.*;
import static com.powsybl.openrao.data.raoresult.io.cne.core.CoreCneClassCreator.*;
/**
* Creates the measurements, monitored registered resources and monitored series
*
* @author Viktor Terrier {@literal <viktor.terrier at rte-france.com>}
* @author Peter Mitri {@literal <peter.mitri at rte-france.com>}
*/
public final class CoreCneCnecsCreator {
private CneHelper cneHelper;
private UcteCracCreationContext cracCreationContext;
public CoreCneCnecsCreator(CneHelper cneHelper, UcteCracCreationContext cracCreationContext) {
this.cneHelper = cneHelper;
this.cracCreationContext = cracCreationContext;
}
private CoreCneCnecsCreator() {
}
public List<ConstraintSeries> generate() {
List<ConstraintSeries> constraintSeries = new ArrayList<>();
cracCreationContext.getBranchCnecCreationContexts().stream()
.sorted(Comparator.comparing(BranchCnecCreationContext::getNativeObjectId))
.forEach(cnec -> constraintSeries.addAll(createConstraintSeriesOfACnec(cnec, cneHelper)));
return constraintSeries;
}
private List<ConstraintSeries> createConstraintSeriesOfACnec(BranchCnecCreationContext branchCnecCreationContext, CneHelper cneHelper) {
if (!branchCnecCreationContext.isImported()) {
OpenRaoLoggerProvider.TECHNICAL_LOGS.warn("Cnec {} was not imported into the RAO, its results will be absent from the CNE file", branchCnecCreationContext.getNativeObjectId());
return new ArrayList<>();
}
List<ConstraintSeries> constraintSeries = new ArrayList<>();
String outageBranchCnecId;
String curativeBranchCnecId;
if (branchCnecCreationContext.isBaseCase()) {
outageBranchCnecId = branchCnecCreationContext.getCreatedCnecsIds().get(cneHelper.getCrac().getPreventiveInstant().getId());
curativeBranchCnecId = outageBranchCnecId;
} else {
outageBranchCnecId = branchCnecCreationContext.getCreatedCnecsIds().get(cneHelper.getCrac().getOutageInstant().getId());
curativeBranchCnecId = branchCnecCreationContext.getCreatedCnecsIds().get(cneHelper.getCrac().getInstant(InstantKind.CURATIVE).getId());
}
// A52 (CNEC)
if (cneHelper.getCrac().getFlowCnec(outageBranchCnecId).isOptimized()) {
constraintSeries.addAll(
createConstraintSeriesOfCnec(branchCnecCreationContext, outageBranchCnecId, curativeBranchCnecId, false, cneHelper)
);
} else if (cneHelper.getCrac().getFlowCnec(outageBranchCnecId).isMonitored()) {
// A49 (MNEC)
// TODO : remove 'else' when we go back to exporting CNEC+MNEC branches as both a CNEC and a MNEC
constraintSeries.addAll(
createConstraintSeriesOfCnec(branchCnecCreationContext, outageBranchCnecId, curativeBranchCnecId, true, cneHelper)
);
}
return constraintSeries;
}
private List<ConstraintSeries> createConstraintSeriesOfCnec(BranchCnecCreationContext branchCnecCreationContext, String outageCnecId, String curativeCnecId, boolean asMnec, CneHelper cneHelper) {
List<ConstraintSeries> constraintSeriesOfCnec = new ArrayList<>();
String nativeCnecId = branchCnecCreationContext.getNativeObjectId();
boolean shouldInvertBranchDirection = branchCnecCreationContext.isDirectionInvertedInNetwork();
FlowCnec outageCnec = cneHelper.getCrac().getFlowCnec(outageCnecId);
FlowCnec curativeCnec = cneHelper.getCrac().getFlowCnec(curativeCnecId);
/* Create Constraint series */
String marketStatus = asMnec ? MONITORED_MARKET_STATUS : OPTIMIZED_MARKET_STATUS;
ConstraintSeries constraintSeriesB88 = newConstraintSeries(nativeCnecId, B88_BUSINESS_TYPE, outageCnec.getOperator(), marketStatus);
ConstraintSeries constraintSeriesB57 = newConstraintSeries(nativeCnecId, B57_BUSINESS_TYPE, outageCnec.getOperator(), marketStatus);
ConstraintSeries constraintSeriesB54 = newConstraintSeries(nativeCnecId, B54_BUSINESS_TYPE, outageCnec.getOperator(), marketStatus);
/* Add contingency if exists */
Optional<Contingency> optionalContingency = outageCnec.getState().getContingency();
String contingencySuffix = "BASECASE";
if (optionalContingency.isPresent()) {
String contingencyName = optionalContingency.get().getName().orElse(optionalContingency.get().getId());
ContingencySeries contingencySeries = newContingencySeries(optionalContingency.get().getId(), contingencyName);
constraintSeriesB88.getContingencySeries().add(contingencySeries);
constraintSeriesB57.getContingencySeries().add(contingencySeries);
constraintSeriesB54.getContingencySeries().add(contingencySeries);
contingencySuffix = contingencyName;
}
contingencySuffix = "|" + contingencySuffix;
// B88
List<Analog> measurementsB88 = createB88MeasurementsOfCnec(curativeCnec, outageCnec, shouldInvertBranchDirection);
MonitoredRegisteredResource monitoredRegisteredResourceB88 = newMonitoredRegisteredResource(nativeCnecId, outageCnec.getName(), measurementsB88);
constraintSeriesB88.getMonitoredSeries().add(newMonitoredSeries(nativeCnecId, outageCnec.getName() + contingencySuffix, monitoredRegisteredResourceB88));
constraintSeriesOfCnec.add(constraintSeriesB88);
// B57
List<Analog> measurementsB57 = createB57MeasurementsOfCnec(outageCnec, shouldInvertBranchDirection);
MonitoredRegisteredResource monitoredRegisteredResourceB57 = newMonitoredRegisteredResource(nativeCnecId, outageCnec.getName(), measurementsB57);
constraintSeriesB57.getMonitoredSeries().add(newMonitoredSeries(nativeCnecId, outageCnec.getName() + contingencySuffix, monitoredRegisteredResourceB57));
constraintSeriesOfCnec.add(constraintSeriesB57);
Instant curativeInstant = cneHelper.getCrac().getInstant(InstantKind.CURATIVE);
if (optionalContingency.isPresent() &&
(!cneHelper.getRaoResult().getActivatedNetworkActionsDuringState(cneHelper.getCrac().getState(optionalContingency.get(), curativeInstant)).isEmpty()
|| !cneHelper.getRaoResult().getActivatedRangeActionsDuringState(cneHelper.getCrac().getState(optionalContingency.get(), curativeInstant)).isEmpty())) {
// B54
// TODO : remove the 'if' condition when we go back to exporting B54 series even if no CRAs are applied
List<Analog> measurementsB54 = createB54MeasurementsOfCnec(curativeCnec, shouldInvertBranchDirection);
MonitoredRegisteredResource monitoredRegisteredResourceB54 = newMonitoredRegisteredResource(nativeCnecId, curativeCnec.getName(), measurementsB54);
constraintSeriesB54.getMonitoredSeries().add(newMonitoredSeries(nativeCnecId, curativeCnec.getName() + contingencySuffix, monitoredRegisteredResourceB54));
constraintSeriesOfCnec.add(constraintSeriesB54);
}
return constraintSeriesOfCnec;
}
private static class AnalogComparator implements Comparator<Analog> {
@Override
public int compare(Analog o1, Analog o2) {
if (o1.getMeasurementType().equals(o2.getMeasurementType())) {
return o1.getUnitSymbol().compareTo(o2.getUnitSymbol());
} else {
return o1.getMeasurementType().compareTo(o2.getMeasurementType());
}
}
}
private List<Analog> createB88MeasurementsOfCnec(FlowCnec permanentCnec, FlowCnec temporaryCnec, boolean shouldInvertBranchDirection) {
List<Analog> measurements = new ArrayList<>();
measurements.addAll(createFlowMeasurementsOfFlowCnec(permanentCnec, null, true, shouldInvertBranchDirection));
measurements.addAll(createMarginMeasurementsOfFlowCnec(permanentCnec, null, false, shouldInvertBranchDirection));
measurements.addAll(createMarginMeasurementsOfFlowCnec(temporaryCnec, null, true, shouldInvertBranchDirection));
measurements.sort(new AnalogComparator());
return measurements;
}
private List<Analog> createB57MeasurementsOfCnec(FlowCnec cnec, boolean shouldInvertBranchDirection) {
List<Analog> measurements = new ArrayList<>();
measurements.addAll(createFlowMeasurementsOfFlowCnec(cnec, cracCreationContext.getCrac().getPreventiveInstant(), true, shouldInvertBranchDirection));
measurements.addAll(createMarginMeasurementsOfFlowCnec(cnec, cracCreationContext.getCrac().getPreventiveInstant(), true, shouldInvertBranchDirection));
measurements.sort(new AnalogComparator());
return measurements;
}
private List<Analog> createB54MeasurementsOfCnec(FlowCnec cnec, boolean shouldInvertBranchDirection) {
List<Analog> measurements = new ArrayList<>();
measurements.addAll(createFlowMeasurementsOfFlowCnec(cnec, cracCreationContext.getCrac().getInstant(InstantKind.CURATIVE), true, shouldInvertBranchDirection));
measurements.addAll(createMarginMeasurementsOfFlowCnec(cnec, cracCreationContext.getCrac().getInstant(InstantKind.CURATIVE), false, shouldInvertBranchDirection));
measurements.sort(new AnalogComparator());
return measurements;
}
private List<Analog> createFlowMeasurementsOfFlowCnec(FlowCnec cnec, Instant optimizedInstant, boolean withSumPtdf, boolean shouldInvertBranchDirection) {
List<Analog> measurements = new ArrayList<>();
// A01
measurements.add(createFlowMeasurement(cnec, optimizedInstant, Unit.MEGAWATT, shouldInvertBranchDirection));
// Z11
if (withSumPtdf && cneHelper.isRelativePositiveMargins()) {
measurements.add(createPtdfZonalSumMeasurement(cnec));
}
// A03
measurements.add(createFrmMeasurement(cnec));
if (cneHelper.isWithLoopFlows()) {
// Z16 & Z17
measurements.addAll(createLoopflowMeasurements(cnec, optimizedInstant, shouldInvertBranchDirection));
}
return measurements;
}
private List<Analog> createMarginMeasurementsOfFlowCnec(FlowCnec cnec, Instant optimizedInstant, boolean isTemporary, boolean shouldInvertBranchDirection) {
List<Analog> measurements = new ArrayList<>();
String measurementType;
for (Unit unit : List.of(Unit.AMPERE, Unit.MEGAWATT)) {
// A02 / A07
measurementType = isTemporary ? TATL_MEASUREMENT_TYPE : PATL_MEASUREMENT_TYPE;
measurements.add(createThresholdMeasurement(cnec, optimizedInstant, unit, measurementType, shouldInvertBranchDirection));
}
// Z12 / Z14
measurementType = isTemporary ? ABS_MARG_TATL_MEASUREMENT_TYPE : ABS_MARG_PATL_MEASUREMENT_TYPE;
measurements.add(createMarginMeasurement(cnec, optimizedInstant, Unit.MEGAWATT, measurementType));
// Z13 / Z15
measurementType = isTemporary ? OBJ_FUNC_TATL_MEASUREMENT_TYPE : OBJ_FUNC_PATL_MEASUREMENT_TYPE;
measurements.add(createObjectiveValueMeasurement(cnec, optimizedInstant, Unit.MEGAWATT, measurementType));
return measurements;
}
private double getCnecFlow(FlowCnec cnec, TwoSides side, Instant optimizedInstant) {
Instant resultState = optimizedInstant;
if (resultState != null && resultState.isCurative() && cnec.getState().getInstant().isPreventive()) {
resultState = cracCreationContext.getCrac().getPreventiveInstant();
}
return cneHelper.getRaoResult().getFlow(resultState, cnec, side, Unit.MEGAWATT);
}
private double getCnecMargin(FlowCnec cnec, Instant optimizedInstant, Unit unit, boolean deductFrmFromThreshold) {
Instant resultState = optimizedInstant;
if (resultState != null && resultState.isCurative() && cnec.getState().getInstant().isPreventive()) {
resultState = cracCreationContext.getCrac().getPreventiveInstant();
}
return getThresholdToMarginMap(cnec, resultState, unit, deductFrmFromThreshold).values().stream().min(Double::compareTo).orElseThrow();
}
private double getCnecRelativeMargin(FlowCnec cnec, Instant optimizedInstant, Unit unit) {
double absoluteMargin = getCnecMargin(cnec, optimizedInstant, unit, true);
Instant resultState = optimizedInstant;
if (resultState != null && resultState.isCurative() && cnec.getState().getInstant().isPreventive()) {
resultState = cracCreationContext.getCrac().getPreventiveInstant();
}
return absoluteMargin > 0 ? absoluteMargin / cneHelper.getRaoResult().getPtdfZonalSum(resultState, cnec, getMonitoredSide(cnec)) : absoluteMargin;
}
private Analog createFlowMeasurement(FlowCnec cnec, Instant optimizedInstant, Unit unit, boolean shouldInvertBranchDirection) {
double invert = shouldInvertBranchDirection ? -1 : 1;
return newFlowMeasurement(FLOW_MEASUREMENT_TYPE, unit, invert * getCnecFlow(cnec, getMonitoredSide(cnec), optimizedInstant));
}
private Analog createThresholdMeasurement(FlowCnec cnec, Instant optimizedInstant, Unit unit, String measurementType, boolean shouldInvertBranchDirection) {
double threshold = getClosestThreshold(cnec, optimizedInstant, unit);
double invert = shouldInvertBranchDirection ? -1 : 1;
return newFlowMeasurement(measurementType, unit, invert * threshold);
}
private Analog createMarginMeasurement(FlowCnec cnec, Instant optimizedInstant, Unit unit, String measurementType) {
boolean deductFrmFromMargin = Unit.MEGAWATT.equals(unit);
return newFlowMeasurement(measurementType, unit, getCnecMargin(cnec, optimizedInstant, unit, deductFrmFromMargin));
}
private Analog createObjectiveValueMeasurement(FlowCnec cnec, Instant optimizedInstant, Unit unit, String measurementType) {
double margin = getCnecMargin(cnec, optimizedInstant, unit, true);
if (cneHelper.isRelativePositiveMargins() && margin > 0) {
margin = getCnecRelativeMargin(cnec, optimizedInstant, unit);
}
return newFlowMeasurement(measurementType, unit, margin);
}
/**
* Select the threshold closest to the flow, that will be added in the measurement.
* This is useful when a cnec has both a Max and a Min threshold.
*/
private double getClosestThreshold(FlowCnec cnec, Instant optimizedInstant, Unit unit) {
Map<Double, Double> thresholdToMarginMap = getThresholdToMarginMap(cnec, optimizedInstant, unit, false);
if (thresholdToMarginMap.isEmpty()) {
return 0;
}
return thresholdToMarginMap.entrySet().stream().min(Map.Entry.comparingByValue()).orElseThrow().getKey();
}
/**
* Returns a map containing all threshold for a given cnec and the associated margins
*
* @param cnec the FlowCnec
* @param optimizedInstant the Instant for computing margins
* @param unit the unit of the threshold and margin
*/
private Map<Double, Double> getThresholdToMarginMap(FlowCnec cnec, Instant optimizedInstant, Unit unit, boolean deductFrmFromThreshold) {
Map<Double, Double> thresholdToMarginMap = new HashMap<>();
TwoSides side = getMonitoredSide(cnec);
double flow = getCnecFlow(cnec, side, optimizedInstant);
if (!Double.isNaN(flow)) {
getThresholdToMarginMapAsCnec(cnec, unit, deductFrmFromThreshold, thresholdToMarginMap, flow, side);
}
return thresholdToMarginMap;
}
private void getThresholdToMarginMapAsCnec(FlowCnec cnec, Unit unit, boolean deductFrmFromThreshold, Map<Double, Double> thresholdToMarginMap, double flow, TwoSides side) {
// TODO : remove this when we go back to considering FRM in the exported threshold
double flowUnitMultiplier = getFlowUnitMultiplier(cnec, side, Unit.MEGAWATT, unit);
double frm = deductFrmFromThreshold ? 0 : cnec.getReliabilityMargin() * flowUnitMultiplier;
// Only look at fixed thresholds
double maxThreshold = cnec.getUpperBound(side, unit).orElse(Double.MAX_VALUE) + frm;
maxThreshold = Double.isNaN(maxThreshold) ? Double.POSITIVE_INFINITY : maxThreshold;
thresholdToMarginMap.putIfAbsent(maxThreshold, maxThreshold - flow * flowUnitMultiplier);
double minThreshold = cnec.getLowerBound(side, unit).orElse(-Double.MAX_VALUE) - frm;
minThreshold = Double.isNaN(minThreshold) ? Double.POSITIVE_INFINITY : minThreshold;
thresholdToMarginMap.putIfAbsent(minThreshold, flow * flowUnitMultiplier - minThreshold);
}
private Analog createFrmMeasurement(FlowCnec cnec) {
return newFlowMeasurement(FRM_MEASUREMENT_TYPE, Unit.MEGAWATT, cnec.getReliabilityMargin());
}
private Analog createPtdfZonalSumMeasurement(FlowCnec cnec) {
double absPtdfSum = cneHelper.getRaoResult().getPtdfZonalSum(null, cnec, getMonitoredSide(cnec));
return newPtdfMeasurement(SUM_PTDF_MEASUREMENT_TYPE, absPtdfSum);
}
private List<Analog> createLoopflowMeasurements(FlowCnec cnec, Instant optimizedInstant, boolean shouldInvertBranchDirection) {
Instant resultOptimState = optimizedInstant;
if (resultOptimState != null && optimizedInstant.isCurative() && cnec.getState().isPreventive()) {
resultOptimState = cracCreationContext.getCrac().getPreventiveInstant();
}
List<Analog> measurements = new ArrayList<>();
try {
double loopflow = cneHelper.getRaoResult().getLoopFlow(resultOptimState, cnec, getMonitoredSide(cnec), Unit.MEGAWATT);
LoopFlowThreshold loopFlowExtension = cnec.getExtension(LoopFlowThreshold.class);
if (!Objects.isNull(loopFlowExtension) && !Double.isNaN(loopflow)) {
double invert = shouldInvertBranchDirection ? -1 : 1;
measurements.add(newFlowMeasurement(LOOPFLOW_MEASUREMENT_TYPE, Unit.MEGAWATT, invert * loopflow));
double threshold = invert * Math.signum(loopflow) * loopFlowExtension.getThreshold(Unit.MEGAWATT);
measurements.add(newFlowMeasurement(MAX_LOOPFLOW_MEASUREMENT_TYPE, Unit.MEGAWATT, threshold));
}
} catch (OpenRaoException e) {
// no commercial flow
}
return measurements;
}
public static double getFlowUnitMultiplier(FlowCnec cnec, TwoSides voltageSide, Unit unitFrom, Unit unitTo) {
if (unitFrom == unitTo) {
return 1;
}
double nominalVoltage = cnec.getNominalVoltage(voltageSide);
if (unitFrom == Unit.MEGAWATT && unitTo == Unit.AMPERE) {
return 1000 / (nominalVoltage * Math.sqrt(3));
} else if (unitFrom == Unit.AMPERE && unitTo == Unit.MEGAWATT) {
return nominalVoltage * Math.sqrt(3) / 1000;
} else {
throw new OpenRaoException("Only conversions between MW and A are supported.");
}
}
private TwoSides getMonitoredSide(FlowCnec cnec) {
return cnec.getMonitoredSides().contains(TwoSides.ONE) ? TwoSides.ONE : TwoSides.TWO;
}
}