HvdcRangeActionCreator.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.contingency.Contingency;
import com.powsybl.openrao.data.crac.api.Crac;
import com.powsybl.openrao.data.crac.api.cnec.Cnec;
import com.powsybl.openrao.data.crac.api.rangeaction.HvdcRangeActionAdder;
import com.powsybl.openrao.data.crac.io.commons.api.ImportStatus;
import com.powsybl.openrao.data.crac.io.commons.OpenRaoImportException;
import com.powsybl.openrao.data.crac.io.cim.parameters.CimCracCreationParameters;
import com.powsybl.openrao.data.crac.io.cim.parameters.RangeActionSpeed;
import com.powsybl.openrao.data.crac.io.cim.xsd.RemedialActionRegisteredResource;
import com.powsybl.openrao.data.crac.io.cim.xsd.RemedialActionSeries;
import com.powsybl.iidm.network.Country;
import com.powsybl.iidm.network.HvdcLine;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.extensions.HvdcAngleDroopActivePowerControl;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.stream.Collectors;
import static com.powsybl.openrao.data.crac.io.cim.craccreator.CimConstants.MEGAWATT_UNIT_SYMBOL;
/**
* @author Godelaine de Montmorillon {@literal <godelaine.demontmorillon at rte-france.com>}
* @author Peter Mitri {@literal <peter.mitri at rte-france.com>}
*/
public class HvdcRangeActionCreator {
private final Crac crac;
private final Network network;
private final List<Contingency> contingencies;
private final List<String> invalidContingencies;
private final Set<Cnec<?>> cnecs;
private final Country sharedDomain;
private final CimCracCreationParameters cimCracCreationParameters;
private final Map<String, HvdcRangeActionAdder> hvdcRangeActionAdders = new HashMap<>();
private final Map<String, List<Integer>> rangeMin = new HashMap<>();
private final Map<String, List<Integer>> rangeMax = new HashMap<>();
private final Map<String, Boolean> isDirectionInverted = new HashMap<>();
private final List<String> raSeriesIds = new ArrayList<>();
private final Map<String, OpenRaoImportException> exceptions = new HashMap<>();
boolean isAltered = false;
String importStatusDetailifIsAltered = "";
public HvdcRangeActionCreator(Crac crac, Network network, List<Contingency> contingencies, List<String> invalidContingencies, Set<Cnec<?>> cnecs, Country sharedDomain, CimCracCreationParameters cimCracCreationParameters) {
this.crac = crac;
this.network = network;
this.contingencies = contingencies;
this.invalidContingencies = invalidContingencies;
this.cnecs = cnecs;
this.sharedDomain = sharedDomain;
this.cimCracCreationParameters = cimCracCreationParameters;
}
public void addDirection(RemedialActionSeries remedialActionSeries) {
raSeriesIds.add(remedialActionSeries.getMRID());
try {
if (remedialActionSeries.getRegisteredResource().size() != 4) {
throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, String.format("%s registered resources were defined in HVDC instead of 4", remedialActionSeries.getRegisteredResource().size()));
}
Set<String> networkElementIds = new HashSet<>();
Boolean isRemedialActionSeriesInverted = null;
for (RemedialActionRegisteredResource registeredResource : remedialActionSeries.getRegisteredResource()) {
// Ignore PMode registered resource
if (registeredResource.getMarketObjectStatusStatus().equals(CimConstants.MarketObjectStatus.PMODE.getStatus())) {
continue;
}
checkRegisteredResource(registeredResource);
String networkElementId = registeredResource.getMRID().getValue();
if (networkElementIds.contains(networkElementId)) {
throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, "HVDC RemedialAction_Series contains multiple RegisteredResources with the same mRID");
}
networkElementIds.add(networkElementId);
checkHvdcNetworkElementAndInitAdder(registeredResource, networkElementId);
isRemedialActionSeriesInverted = readRangeAndCheckIfInverted(isRemedialActionSeriesInverted, registeredResource, networkElementId);
}
Boolean finalIsRemedialActionSeriesInverted = isRemedialActionSeriesInverted;
if (isDirectionInverted.values().stream().anyMatch(inverted -> inverted.equals(finalIsRemedialActionSeriesInverted))) {
throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, "HVDC line should be defined in the opposite direction");
} else {
isDirectionInverted.put(remedialActionSeries.getMRID(), isRemedialActionSeriesInverted);
}
} catch (OpenRaoImportException e) {
exceptions.put(remedialActionSeries.getMRID(), e);
}
}
private Boolean readRangeAndCheckIfInverted(Boolean isRemedialActionSeriesInverted, RemedialActionRegisteredResource registeredResource, String networkElementId) {
boolean isRegisteredResourceInverted = readHvdcRange(
networkElementId,
registeredResource.getResourceCapacityMinimumCapacity().intValue(),
registeredResource.getResourceCapacityMaximumCapacity().intValue(),
registeredResource.getInAggregateNodeMRID().getValue(),
registeredResource.getOutAggregateNodeMRID().getValue());
if (Objects.nonNull(isRemedialActionSeriesInverted) && !isRemedialActionSeriesInverted.equals(isRegisteredResourceInverted)) {
throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, "HVDC registered resources reference lines in opposite directions");
} else {
return isRegisteredResourceInverted;
}
}
private void checkHvdcNetworkElementAndInitAdder(RemedialActionRegisteredResource registeredResource, String networkElementId) {
checkHvdcNetworkElement(networkElementId);
HvdcLine hvdcLine = network.getHvdcLine(networkElementId);
boolean terminal1Connected = hvdcLine.getConverterStation1().getTerminal().isConnected();
boolean terminal2Connected = hvdcLine.getConverterStation2().getTerminal().isConnected();
if (terminal1Connected && terminal2Connected) {
hvdcRangeActionAdders.putIfAbsent(networkElementId, initHvdcRangeActionAdder(registeredResource));
} else {
isAltered = true;
importStatusDetailifIsAltered = String.format("HVDC line %s has ", hvdcLine.getId());
if (!terminal1Connected && !terminal2Connected) {
importStatusDetailifIsAltered += "terminals 1 and 2 ";
} else if (!terminal1Connected) {
importStatusDetailifIsAltered += "terminal 1 ";
} else if (!terminal2Connected) {
importStatusDetailifIsAltered += "terminal 2 ";
}
importStatusDetailifIsAltered += "disconnected";
}
}
public Set<RemedialActionSeriesCreationContext> add() {
if (raSeriesIds.size() != 2) {
return raSeriesIds.stream().map(id ->
RemedialActionSeriesCreationContext.notImported(id, ImportStatus.INCONSISTENCY_IN_DATA, String.format("Expected 2 registered resources but found %s", raSeriesIds.size()))
).collect(Collectors.toSet());
}
if (!exceptions.isEmpty()) {
Set<RemedialActionSeriesCreationContext> setFromExceptions = exceptions.entrySet().stream().map(
entry -> RemedialActionSeriesCreationContext.notImported(entry.getKey(), entry.getValue().getImportStatus(), entry.getValue().getMessage())
).collect(Collectors.toSet());
// Complete for those who dd not throw an exception but could not be imported because of the others
raSeriesIds.stream().filter(key -> !exceptions.containsKey(key)).map(
id -> RemedialActionSeriesCreationContext.notImported(id, ImportStatus.INCONSISTENCY_IN_DATA, "Other RemedialActionSeries in the same HVDC Series failed")
).forEach(setFromExceptions::add);
return setFromExceptions;
}
Set<String> createdRaIds = new HashSet<>();
String groupId = hvdcRangeActionAdders.keySet().stream().sorted().collect(Collectors.joining(" + "));
for (Map.Entry<String, List<Integer>> rangeEntry : rangeMin.entrySet()) {
String neId = rangeEntry.getKey();
try {
Pair<Integer, Integer> minMax = concatenateHvdcRanges(rangeEntry.getValue(), rangeMax.get(neId));
String remedialActionId = String.join(" + ", raSeriesIds) + " - " + neId;
hvdcRangeActionAdders.get(neId)
.withId(remedialActionId).withName(remedialActionId)
.withOperator(CimConstants.readOperator(remedialActionId))
.withGroupId(groupId)
.newRange().withMin(minMax.getLeft()).withMax(minMax.getRight()).add()
.add();
createdRaIds.add(remedialActionId);
} catch (OpenRaoImportException e) {
return raSeriesIds.stream().map(id ->
RemedialActionSeriesCreationContext.notImported(id, e.getImportStatus(), e.getMessage())
).collect(Collectors.toSet());
} catch (OpenRaoException e) {
return raSeriesIds.stream().map(id ->
RemedialActionSeriesCreationContext.notImported(id, ImportStatus.INCONSISTENCY_IN_DATA, e.getMessage())).collect(Collectors.toSet());
}
}
if (createdRaIds.isEmpty()) {
return raSeriesIds.stream().map(id ->
RemedialActionSeriesCreationContext.notImported(id, ImportStatus.INCONSISTENCY_IN_DATA, String.format("All terminals on HVDC lines are disconnected"))
).collect(Collectors.toSet());
}
if (!invalidContingencies.isEmpty()) {
if (isAltered) {
importStatusDetailifIsAltered += "; ";
}
String contingencyList = StringUtils.join(invalidContingencies, ", ");
importStatusDetailifIsAltered += String.format("Contingencies %s were not imported", contingencyList);
}
return raSeriesIds.stream().map(id -> RemedialActionSeriesCreationContext.importedHvdcRa(id, createdRaIds, isAltered, isDirectionInverted.get(id), importStatusDetailifIsAltered)).collect(Collectors.toSet());
}
private HvdcRangeActionAdder initHvdcRangeActionAdder(RemedialActionRegisteredResource registeredResource) {
HvdcRangeActionAdder hvdcRangeActionAdder = crac.newHvdcRangeAction();
String hvdcId = registeredResource.getMRID().getValue();
hvdcRangeActionAdder.withNetworkElement(hvdcId);
// Speed
if (cimCracCreationParameters != null && cimCracCreationParameters.getRangeActionSpeedSet() != null) {
for (RangeActionSpeed rangeActionSpeed : cimCracCreationParameters.getRangeActionSpeedSet()) {
if (rangeActionSpeed.getRangeActionId().equals(hvdcId)) {
hvdcRangeActionAdder.withSpeed(rangeActionSpeed.getSpeed());
}
}
}
// Usage rules
RemedialActionSeriesCreator.addUsageRules(crac, CimConstants.ApplicationModeMarketObjectStatus.AUTO.getStatus(), hvdcRangeActionAdder, contingencies, invalidContingencies, cnecs, sharedDomain);
return hvdcRangeActionAdder;
}
// Return type : Pair two integers :
// -- the integers are the min and max Hvdc concatenated range.
private Pair<Integer, Integer> concatenateHvdcRanges(List<Integer> min, List<Integer> max) {
if (min.get(1) < min.get(0)) {
if (max.get(1) < min.get(0)) {
throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, "HVDC range mismatch");
} else {
return Pair.of(min.get(1), Math.max(max.get(0), max.get(1)));
}
} else {
if (min.get(1) > max.get(0)) {
throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, "HVDC range mismatch");
} else {
return Pair.of(min.get(0), Math.max(max.get(0), max.get(1)));
}
}
}
// Throws an exception if the RegisteredResource is ill defined
private void checkRegisteredResource(RemedialActionRegisteredResource registeredResource) {
// Check MarketObjectStatus
String marketObjectStatusStatus = registeredResource.getMarketObjectStatusStatus();
if (Objects.isNull(marketObjectStatusStatus)) {
throw new OpenRaoImportException(ImportStatus.INCOMPLETE_DATA, "Missing marketObjectStatusStatus");
}
if (!marketObjectStatusStatus.equals(CimConstants.MarketObjectStatus.ABSOLUTE.getStatus())) {
throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, String.format("Wrong marketObjectStatusStatus: %s", marketObjectStatusStatus));
}
// Check unit
String unit = registeredResource.getResourceCapacityUnitSymbol();
if (Objects.isNull(unit)) {
throw new OpenRaoImportException(ImportStatus.INCOMPLETE_DATA, "Missing unit");
}
if (!registeredResource.getResourceCapacityUnitSymbol().equals(MEGAWATT_UNIT_SYMBOL)) {
throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, String.format("Wrong unit: %s", unit));
}
// Check that min/max are defined
if (Objects.isNull(registeredResource.getResourceCapacityMinimumCapacity()) || Objects.isNull(registeredResource.getResourceCapacityMaximumCapacity())) {
throw new OpenRaoImportException(ImportStatus.INCOMPLETE_DATA, "Missing min or max resource capacity");
}
}
/**
* @param networkElement - HVDC line name
* @param minCapacity
* @param maxCapacity
* @param inNode - The area of the related oriented border study where the energy flows INTO.
* @param outNode - The area of the related oriented border study where the energy comes FROM.
* @return - the boolean indicates whether the Hvdc line is inverted
*/
private boolean readHvdcRange(String networkElement, int minCapacity, int maxCapacity, String inNode, String outNode) {
HvdcLine hvdcLine = network.getHvdcLine(networkElement);
boolean isInverted;
int min;
int max;
String from = hvdcLine.getConverterStation1().getTerminal().getVoltageLevel().getId();
String to = hvdcLine.getConverterStation2().getTerminal().getVoltageLevel().getId();
if (Objects.isNull(inNode) || Objects.isNull(outNode)) {
throw new OpenRaoImportException(ImportStatus.INCOMPLETE_DATA, "Missing HVDC in or out aggregate nodes");
}
if (inNode.equals(to) && outNode.equals(from)) {
isInverted = false;
min = minCapacity;
max = maxCapacity;
} else if (inNode.equals(from) && outNode.equals(to)) {
isInverted = true;
min = -maxCapacity;
max = -minCapacity;
} else {
throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, "Wrong HVDC inAggregateNode/outAggregateNode");
}
if (hvdcLine.getConverterStation1().getTerminal().isConnected() && hvdcLine.getConverterStation2().getTerminal().isConnected()) {
if (rangeMin.containsKey(networkElement)) {
rangeMin.get(networkElement).add(min);
} else {
List<Integer> list = new ArrayList<>();
list.add(min);
rangeMin.put(networkElement, list);
}
if (rangeMax.containsKey(networkElement)) {
rangeMax.get(networkElement).add(max);
} else {
List<Integer> list = new ArrayList<>();
list.add(max);
rangeMax.put(networkElement, list);
}
}
return isInverted;
}
private void checkHvdcNetworkElement(String networkElement) {
if (Objects.isNull(network.getHvdcLine(networkElement))) {
throw new OpenRaoImportException(ImportStatus.ELEMENT_NOT_FOUND_IN_NETWORK, "Not a HVDC line");
}
if (Objects.isNull(network.getHvdcLine(networkElement).getExtension(HvdcAngleDroopActivePowerControl.class))) {
throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, "HVDC does not have a HvdcAngleDroopActivePowerControl extension");
}
if (!network.getHvdcLine(networkElement).getExtension(HvdcAngleDroopActivePowerControl.class).isEnabled()) {
throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, "HvdcAngleDroopActivePowerControl extension is not enabled");
}
}
}