MonitoredSeriesCreator.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.crac.io.cim.craccreator;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.Unit;
import com.powsybl.contingency.Contingency;
import com.powsybl.openrao.data.crac.io.cim.xsd.Analog;
import com.powsybl.openrao.data.crac.io.cim.xsd.ContingencySeries;
import com.powsybl.openrao.data.crac.io.cim.xsd.MonitoredRegisteredResource;
import com.powsybl.openrao.data.crac.io.cim.xsd.MonitoredSeries;
import com.powsybl.openrao.data.crac.io.cim.xsd.Series;
import com.powsybl.openrao.data.crac.io.cim.xsd.TimeSeries;
import com.powsybl.openrao.data.crac.api.Crac;
import com.powsybl.openrao.data.crac.api.Instant;
import com.powsybl.openrao.data.crac.api.InstantKind;
import com.powsybl.openrao.data.crac.api.cnec.FlowCnecAdder;
import com.powsybl.iidm.network.TwoSides;
import com.powsybl.openrao.data.crac.io.commons.api.ImportStatus;
import com.powsybl.openrao.data.crac.io.commons.cgmes.CgmesBranchHelper;
import com.powsybl.iidm.network.Branch;
import com.powsybl.iidm.network.Network;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.stream.Collectors;
import static com.powsybl.openrao.data.crac.io.cim.craccreator.CimConstants.*;
import static com.powsybl.openrao.data.crac.io.cim.craccreator.CimCracUtils.getContingencyFromCrac;
/**
* @author Philippe Edwards {@literal <philippe.edwards at rte-france.com>}
* @author Peter Mitri {@literal <peter.mitri at rte-france.com>}
*/
public class MonitoredSeriesCreator {
private final Crac crac;
private final Network network;
private final List<TimeSeries> cimTimeSeries;
private Map<String, MonitoredSeriesCreationContext> monitoredSeriesCreationContexts;
private final CimCracCreationContext cracCreationContext;
private final Set<TwoSides> defaultMonitoredSides;
public MonitoredSeriesCreator(List<TimeSeries> cimTimeSeries, Network network, CimCracCreationContext cracCreationContext, Set<TwoSides> defaultMonitoredSides) {
this.cimTimeSeries = cimTimeSeries;
this.crac = cracCreationContext.getCrac();
this.network = network;
this.cracCreationContext = cracCreationContext;
this.defaultMonitoredSides = defaultMonitoredSides;
}
public void createAndAddMonitoredSeries() {
this.monitoredSeriesCreationContexts = new HashMap<>();
for (Series cimSerie : getCnecSeries()) {
List<Contingency> contingencies = new ArrayList<>();
List<String> invalidContingencies = new ArrayList<>();
String optimizationStatus = cimSerie.getOptimizationMarketObjectStatusStatus();
for (ContingencySeries cimContingency : cimSerie.getContingencySeries()) {
Contingency contingency = getContingencyFromCrac(cimContingency, cracCreationContext);
if (Objects.nonNull(contingency)) {
contingencies.add(contingency);
} else {
invalidContingencies.add(cimContingency.getMRID());
}
}
if (cimSerie.getContingencySeries().isEmpty()) {
contingencies = new ArrayList<>(crac.getContingencies());
}
for (MonitoredSeries monitoredSeries : cimSerie.getMonitoredSeries()) {
readAndAddCnec(monitoredSeries, contingencies, optimizationStatus, invalidContingencies);
}
}
this.cracCreationContext.setMonitoredSeriesCreationContexts(monitoredSeriesCreationContexts);
}
private Set<Series> getCnecSeries() {
Set<Series> cnecSeries = new HashSet<>();
CimCracUtils.applyActionToEveryPoint(
cimTimeSeries,
cracCreationContext.getTimeStamp().toInstant(),
point -> point.getSeries().stream().filter(this::describesCnecsToImport).forEach(cnecSeries::add)
);
return cnecSeries;
}
private boolean describesCnecsToImport(Series series) {
// Read CNECs from B57 or B56 series
// WARNING: if the same CNEC is defined in multiple places, but with different information (e.g. different
// thresholds), the imported CNEC will be unpredictable
return series.getBusinessType().equals(CNECS_SERIES_BUSINESS_TYPE) || series.getBusinessType().equals(REMEDIAL_ACTIONS_SERIES_BUSINESS_TYPE);
}
private void readAndAddCnec(MonitoredSeries monitoredSeries, List<Contingency> contingencies, String optimizationStatus, List<String> invalidContingencies) {
String nativeId = monitoredSeries.getMRID();
String nativeName = monitoredSeries.getName();
List<MonitoredRegisteredResource> monitoredRegisteredResources = monitoredSeries.getRegisteredResource();
if (monitoredRegisteredResources.isEmpty()) {
saveMonitoredSeriesCreationContexts(nativeId, MonitoredSeriesCreationContext.notImported(nativeId, nativeName, null, null, ImportStatus.INCOMPLETE_DATA, "No registered resources"));
return;
}
if (monitoredRegisteredResources.size() > 1) {
saveMonitoredSeriesCreationContexts(nativeId, MonitoredSeriesCreationContext.notImported(nativeId, nativeName, null, null, ImportStatus.INCONSISTENCY_IN_DATA, "More than one registered resources"));
return;
}
MonitoredRegisteredResource monitoredRegisteredResource = monitoredRegisteredResources.get(0);
String cnecId = monitoredRegisteredResource.getName();
String resourceId = monitoredRegisteredResource.getMRID().getValue();
String resourceName = monitoredRegisteredResource.getName();
//Get network element
CgmesBranchHelper branchHelper = new CgmesBranchHelper(monitoredRegisteredResource.getMRID().getValue(), network);
if (!branchHelper.isValid()) {
saveMonitoredSeriesCreationContexts(nativeId, MonitoredSeriesCreationContext.notImported(nativeId, nativeName, resourceId, resourceName,
ImportStatus.ELEMENT_NOT_FOUND_IN_NETWORK, String.format("Network element was not found in network: %s", monitoredRegisteredResource.getMRID().getValue())));
return;
}
// Check if pure MNEC
boolean isMnec;
try {
isMnec = isMnec(optimizationStatus);
} catch (OpenRaoException e) {
saveMonitoredSeriesCreationContexts(nativeId,
MonitoredSeriesCreationContext.notImported(nativeId, nativeName, resourceId, resourceName,
ImportStatus.INCONSISTENCY_IN_DATA, e.getMessage())
);
return;
}
MonitoredSeriesCreationContext monitoredSeriesCreationContext;
if (invalidContingencies.isEmpty()) {
monitoredSeriesCreationContext = MonitoredSeriesCreationContext.imported(nativeId, nativeName, resourceId, resourceName, false, "");
} else {
String contingencyList = StringUtils.join(invalidContingencies, ", ");
monitoredSeriesCreationContext = MonitoredSeriesCreationContext.imported(nativeId, nativeName, resourceId, resourceName,
true, String.format("Contingencies %s were not imported", contingencyList));
}
// Read measurements
monitoredRegisteredResource.getMeasurements().forEach(
measurement -> monitoredSeriesCreationContext.addMeasurementCreationContext(
createCnecFromMeasurement(measurement, cnecId, isMnec, branchHelper, contingencies)
)
);
saveMonitoredSeriesCreationContexts(nativeId, monitoredSeriesCreationContext);
}
private void saveMonitoredSeriesCreationContexts(String nativeId, MonitoredSeriesCreationContext mscc) {
MonitoredSeriesCreationContext newMscc = mscc;
if (monitoredSeriesCreationContexts.containsKey(nativeId)) {
cracCreationContext.getCreationReport().warn(
String.format("Multiple Monitored_Series with same mRID \"%s\" detected; they will be merged.", nativeId)
);
// TSO can define multiple Monitored_Series with same mRID. Add information from new one to old one
MonitoredSeriesCreationContext mscc2 = monitoredSeriesCreationContexts.get(nativeId);
ImportStatus importStatus = null;
boolean isAltered = false;
if (mscc.isImported() == mscc2.isImported()) {
importStatus = mscc.getImportStatus();
} else {
importStatus = ImportStatus.IMPORTED;
isAltered = true;
}
String importStatusDetail =
mscc.getImportStatusDetail()
+ (!mscc.getImportStatusDetail().isEmpty() && !mscc2.getImportStatusDetail().isEmpty() ? " - " : "")
+ mscc2.getImportStatusDetail();
newMscc = new MonitoredSeriesCreationContext(
mscc.getNativeId(),
mscc.getNativeName(),
mscc.getNativeResourceId(),
mscc.getNativeResourceName(),
importStatus,
mscc.isAltered() || mscc2.isAltered() || isAltered,
importStatusDetail);
newMscc.addMeasurementCreationContexts(mscc.getMeasurementCreationContexts());
newMscc.addMeasurementCreationContexts(mscc2.getMeasurementCreationContexts());
}
monitoredSeriesCreationContexts.put(nativeId, newMscc);
}
private boolean isMnec(String optimizationStatus) {
return Objects.nonNull(optimizationStatus) && optimizationStatus.equals(CNECS_MNEC_MARKET_OBJECT_STATUS);
}
private InstantKind getMeasurementInstant(Analog measurement) {
return switch (measurement.getMeasurementType()) {
case CNECS_N_STATE_MEASUREMENT_TYPE -> InstantKind.PREVENTIVE;
case CNECS_OUTAGE_STATE_MEASUREMENT_TYPE -> InstantKind.OUTAGE;
case CNECS_AUTO_STATE_MEASUREMENT_TYPE -> InstantKind.AUTO;
case CNECS_CURATIVE_STATE_MEASUREMENT_TYPE -> InstantKind.CURATIVE;
default ->
throw new OpenRaoException(String.format("Unrecognized measurementType: %s", measurement.getMeasurementType()));
};
}
private Unit getMeasurementUnit(Analog measurement) {
return switch (measurement.getUnitSymbol()) {
case CNECS_PATL_UNIT_SYMBOL -> Unit.PERCENT_IMAX;
case MEGAWATT_UNIT_SYMBOL -> Unit.MEGAWATT;
case AMPERES_UNIT_SYMBOL -> Unit.AMPERE;
default ->
throw new OpenRaoException(String.format("Unrecognized unitSymbol: %s", measurement.getUnitSymbol()));
};
}
private String getMeasurementDirection(Analog measurement) {
if (Objects.isNull(measurement.getPositiveFlowIn())) {
return "both";
}
if (measurement.getPositiveFlowIn().equals(CNECS_DIRECT_DIRECTION_FLOW)
|| measurement.getPositiveFlowIn().equals(CNECS_OPPOSITE_DIRECTION_FLOW)) {
return measurement.getPositiveFlowIn();
}
throw new OpenRaoException(String.format("Unrecognized positiveFlowIn: %s", measurement.getPositiveFlowIn()));
}
private MeasurementCreationContext createCnecFromMeasurement(Analog measurement, String cnecNativeId, boolean isMnec, CgmesBranchHelper branchHelper, List<Contingency> contingencies) {
Instant instant;
Unit unit;
String direction;
double threshold;
try {
instant = crac.getInstant(getMeasurementInstant(measurement));
unit = getMeasurementUnit(measurement);
direction = getMeasurementDirection(measurement);
threshold = (unit.equals(Unit.PERCENT_IMAX) ? 0.01 : 1) * measurement.getAnalogValuesValue(); // Open RAO uses relative convention for %Imax (0 <= threshold <= 1)
} catch (OpenRaoException e) {
return MeasurementCreationContext.notImported(ImportStatus.INCONSISTENCY_IN_DATA, e.getMessage());
}
MeasurementCreationContext measurementCreationContext = MeasurementCreationContext.imported();
FlowCnecAdder flowCnecAdder = crac.newFlowCnec()
.withInstant(instant.getId())
.withNetworkElement(branchHelper.getBranch().getId());
String cnecId;
try {
cnecId = addThreshold(flowCnecAdder, unit, branchHelper, cnecNativeId, direction, threshold);
setNominalVoltage(flowCnecAdder, branchHelper);
setCurrentsLimit(flowCnecAdder, branchHelper);
} catch (OpenRaoException e) {
if (instant.isPreventive()) {
measurementCreationContext.addCnecCreationContext(null, instant, CnecCreationContext.notImported(ImportStatus.OTHER, e.getMessage()));
} else {
contingencies.forEach(contingency ->
measurementCreationContext.addCnecCreationContext(contingency.getId(), instant, CnecCreationContext.notImported(ImportStatus.OTHER, e.getMessage()))
);
}
return measurementCreationContext;
}
if (isMnec) {
flowCnecAdder.withMonitored();
cnecId += " - MONITORED";
} else {
flowCnecAdder.withOptimized();
}
if (instant.isPreventive()) {
addCnecsOnState(flowCnecAdder, cnecId, null, instant, measurementCreationContext, branchHelper.getIdInNetwork());
} else {
String finalCnecId = cnecId;
contingencies.forEach(contingency -> {
String contingencyId = contingency.getId();
flowCnecAdder.withContingency(contingencyId);
String cnecIdWithContingency = finalCnecId + " - " + contingencyId;
addCnecsOnState(flowCnecAdder, cnecIdWithContingency, contingency, instant, measurementCreationContext, branchHelper.getIdInNetwork());
});
}
return measurementCreationContext;
}
private void addCnecsOnState(FlowCnecAdder flowCnecAdder, String cnecIdWithContingency, Contingency contingency, Instant instant, MeasurementCreationContext measurementCreationContext, String networkElementId) {
String contingencyId = Objects.isNull(contingency) ? "" : contingency.getId();
String fullCnecId = cnecIdWithContingency + " - " + instant.getId();
if (Objects.isNull(crac.getFlowCnec(fullCnecId))) {
flowCnecAdder.withId(fullCnecId);
flowCnecAdder.withName(fullCnecId).add();
} else {
// If a CNEC with the same ID has already been created, we assume that the 2 CNECs are the same
// (we know network element and state are the same, we assume that thresholds are the same.
// This is true if the TSO is consistent in the definition of its CNECs; and two different TSOs can only
// share tielines, but those are distinguished by the TWO/ONE label)
cracCreationContext.getCreationReport().warn(
String.format("Multiple CNECs on same network element (%s) and same state (%s%s%s) have been detected. Only one CNEC will be created.", networkElementId, contingencyId, Objects.isNull(contingency) ? "" : " - ", instant)
);
}
measurementCreationContext.addCnecCreationContext(contingencyId, instant, CnecCreationContext.imported(fullCnecId));
}
private String addThreshold(FlowCnecAdder flowCnecAdder, Unit unit, CgmesBranchHelper branchHelper, String cnecId, String direction, double threshold) {
String modifiedCnecId = cnecId;
Set<TwoSides> monitoredSides = defaultMonitoredSides;
if (branchHelper.isHalfLine()) {
modifiedCnecId += " - " + (branchHelper.getTieLineSide() == TwoSides.ONE ? "ONE" : "TWO");
monitoredSides = Set.of(branchHelper.getTieLineSide());
} else if (unit.equals(Unit.AMPERE) &&
Math.abs(branchHelper.getBranch().getTerminal1().getVoltageLevel().getNominalV() - branchHelper.getBranch().getTerminal2().getVoltageLevel().getNominalV()) > 1.) {
// If unit is absolute amperes, monitor low voltage side
monitoredSides = branchHelper.getBranch().getTerminal1().getVoltageLevel().getNominalV() <= branchHelper.getBranch().getTerminal2().getVoltageLevel().getNominalV() ?
Set.of(TwoSides.ONE) : Set.of(TwoSides.TWO);
} else if (unit.equals(Unit.PERCENT_IMAX)) {
// If unit is %Imax, check that Imax exists
monitoredSides = monitoredSides.stream().filter(side -> hasCurrentLimit(branchHelper.getBranch(), side)).collect(Collectors.toSet());
if (monitoredSides.isEmpty()) {
throw new OpenRaoException(String.format("Cannot create any PERCENT_IMAX threshold on branch %s, as it holds no current limit at the wanted side", branchHelper.getIdInNetwork()));
}
}
Double min = -threshold;
Double max = threshold;
if (direction.equals(CNECS_DIRECT_DIRECTION_FLOW)) {
modifiedCnecId += " - DIRECT";
min = null;
} else if (direction.equals(CNECS_OPPOSITE_DIRECTION_FLOW)) {
modifiedCnecId += " - OPPOSITE";
max = null;
}
addThreshold(flowCnecAdder, unit, min, max, monitoredSides);
return modifiedCnecId;
}
private void addThreshold(FlowCnecAdder flowCnecAdder, Unit unit, Double min, Double max, Set<TwoSides> sides) {
sides.forEach(side ->
flowCnecAdder.newThreshold()
.withUnit(unit)
.withSide(side)
.withMax(max)
.withMin(min)
.add()
);
}
private void setNominalVoltage(FlowCnecAdder flowCnecAdder, CgmesBranchHelper branchHelper) {
double voltageLevelLeft = branchHelper.getBranch().getTerminal1().getVoltageLevel().getNominalV();
double voltageLevelRight = branchHelper.getBranch().getTerminal2().getVoltageLevel().getNominalV();
if (voltageLevelLeft > 1e-6 && voltageLevelRight > 1e-6) {
flowCnecAdder.withNominalVoltage(voltageLevelLeft, TwoSides.ONE);
flowCnecAdder.withNominalVoltage(voltageLevelRight, TwoSides.TWO);
} else {
throw new OpenRaoException(String.format("Voltage level for branch %s is 0 in network.", branchHelper.getBranch().getId()));
}
}
private void setCurrentsLimit(FlowCnecAdder flowCnecAdder, CgmesBranchHelper branchHelper) {
Double currentLimitLeft = getCurrentLimit(branchHelper.getBranch(), TwoSides.ONE);
Double currentLimitRight = getCurrentLimit(branchHelper.getBranch(), TwoSides.TWO);
if (Objects.nonNull(currentLimitLeft) && Objects.nonNull(currentLimitRight)) {
flowCnecAdder.withIMax(currentLimitLeft, TwoSides.ONE);
flowCnecAdder.withIMax(currentLimitRight, TwoSides.TWO);
} else {
throw new OpenRaoException(String.format("Unable to get branch current limits from network for branch %s", branchHelper.getBranch().getId()));
}
}
// This uses the same logic as the UcteCnecElementHelper which is used for CBCO cnec import for instance
private Double getCurrentLimit(Branch<?> branch, TwoSides side) {
if (hasCurrentLimit(branch, side)) {
return branch.getCurrentLimits(side).orElseThrow().getPermanentLimit();
}
if (side == TwoSides.ONE && hasCurrentLimit(branch, TwoSides.TWO)) {
return branch.getCurrentLimits(TwoSides.TWO).orElseThrow().getPermanentLimit() * branch.getTerminal1().getVoltageLevel().getNominalV() / branch.getTerminal2().getVoltageLevel().getNominalV();
}
if (side == TwoSides.TWO && hasCurrentLimit(branch, TwoSides.ONE)) {
return branch.getCurrentLimits(TwoSides.ONE).orElseThrow().getPermanentLimit() * branch.getTerminal2().getVoltageLevel().getNominalV() / branch.getTerminal1().getVoltageLevel().getNominalV();
}
return null;
}
private boolean hasCurrentLimit(Branch<?> branch, TwoSides side) {
return branch.getCurrentLimits(side).isPresent();
}
}