TRemedialActionAdder.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.data.crac.io.cse.remedialaction;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.Identifiable;
import com.powsybl.openrao.commons.OpenRaoException;
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.RemedialActionAdder;
import com.powsybl.openrao.data.crac.io.cse.xsd.TApplication;
import com.powsybl.openrao.data.crac.io.cse.xsd.THVDCNode;
import com.powsybl.openrao.data.crac.io.cse.xsd.TRemedialAction;
import com.powsybl.openrao.data.crac.io.cse.xsd.TRemedialActions;
import com.powsybl.openrao.data.crac.api.networkaction.ActionType;
import com.powsybl.openrao.data.crac.api.networkaction.NetworkActionAdder;
import com.powsybl.openrao.data.crac.api.networkaction.SingleNetworkElementActionAdder;
import com.powsybl.openrao.data.crac.api.range.RangeType;
import com.powsybl.openrao.data.crac.api.rangeaction.InjectionRangeActionAdder;
import com.powsybl.openrao.data.crac.api.rangeaction.PstRangeActionAdder;
import com.powsybl.openrao.data.crac.api.usagerule.UsageMethod;
import com.powsybl.openrao.data.crac.io.commons.api.ImportStatus;
import com.powsybl.openrao.data.crac.io.commons.api.StandardElementaryCreationContext;
import com.powsybl.openrao.data.crac.io.cse.CseCracCreationContext;
import com.powsybl.openrao.data.crac.io.cse.parameters.BusBarChangeSwitches;
import com.powsybl.openrao.data.crac.io.cse.parameters.CseCracCreationParameters;
import com.powsybl.openrao.data.crac.api.parameters.RangeActionGroup;
import com.powsybl.openrao.data.crac.io.commons.OpenRaoImportException;
import com.powsybl.openrao.data.crac.io.commons.ucte.UcteNetworkAnalyzer;
import com.powsybl.openrao.data.crac.io.commons.ucte.UctePstHelper;
import com.powsybl.openrao.data.crac.io.commons.ucte.UcteTopologicalElementHelper;
import com.powsybl.openrao.data.crac.io.cse.xsd.TBranch;
import com.powsybl.openrao.data.crac.io.cse.xsd.TCRACSeries;
import com.powsybl.openrao.data.crac.io.cse.xsd.TNode;
import com.powsybl.openrao.data.crac.io.cse.xsd.TStatusType;
import com.powsybl.openrao.data.crac.io.cse.xsd.TVariationType;
import java.util.*;
/**
* @author Alexandre Montigny {@literal <alexandre.montigny at rte-france.com>}
*/
public class TRemedialActionAdder {
private final TCRACSeries tcracSeries;
private final Crac crac;
private final Network network;
private final UcteNetworkAnalyzer ucteNetworkAnalyzer;
private final CseCracCreationContext cseCracCreationContext;
private final Map<String, Set<String>> remedialActionsForCnecsMap;
private final CseCracCreationParameters cseCracCreationParameters;
private static final String ABSOLUTE_VARIATION_TYPE = "ABSOLUTE";
public TRemedialActionAdder(TCRACSeries tcracSeries, Crac crac, Network network, UcteNetworkAnalyzer ucteNetworkAnalyzer, Map<String, Set<String>> remedialActionsForCnecsMap, CseCracCreationContext cseCracCreationContext, CseCracCreationParameters cseCracCreationParameters) {
this.tcracSeries = tcracSeries;
this.crac = crac;
this.network = network;
this.ucteNetworkAnalyzer = ucteNetworkAnalyzer;
this.cseCracCreationContext = cseCracCreationContext;
this.remedialActionsForCnecsMap = remedialActionsForCnecsMap;
this.cseCracCreationParameters = cseCracCreationParameters;
}
public void add() {
List<TRemedialActions> tRemedialActionsList = tcracSeries.getRemedialActions();
for (TRemedialActions tRemedialActions : tRemedialActionsList) {
if (tRemedialActions != null) {
tRemedialActions.getRemedialAction().forEach(tRemedialAction -> {
try {
importRemedialAction(tRemedialAction);
} catch (OpenRaoException e) {
// unsupported remedial action type
cseCracCreationContext.addRemedialActionCreationContext(
StandardElementaryCreationContext.notImported(tRemedialAction.getName().getV(), null, ImportStatus.NOT_YET_HANDLED_BY_OPEN_RAO, e.getMessage())
);
}
});
}
}
}
private void importRemedialAction(TRemedialAction tRemedialAction) {
if (tRemedialAction.getStatus() != null) {
importTopologicalAction(tRemedialAction);
} else if (tRemedialAction.getGeneration() != null) {
importInjectionAction(tRemedialAction);
} else if (tRemedialAction.getPstRange() != null) {
importPstRangeAction(tRemedialAction);
} else if (tRemedialAction.getHVDCRange() != null) {
importHvdcRangeAction(tRemedialAction);
} else if (tRemedialAction.getBusBar() != null) {
importBusBarChangeAction(tRemedialAction);
} else {
throw new OpenRaoException("unknown remedial action type");
}
}
private void importTopologicalAction(TRemedialAction tRemedialAction) {
String createdRAId = tRemedialAction.getName().getV();
NetworkActionAdder networkActionAdder = crac.newNetworkAction()
.withId(createdRAId)
.withName(tRemedialAction.getName().getV())
.withOperator(tRemedialAction.getOperator().getV());
if (tRemedialAction.getStatus().getBranch().isEmpty()) {
cseCracCreationContext.addRemedialActionCreationContext(StandardElementaryCreationContext.notImported(tRemedialAction.getName().getV(), null, ImportStatus.INCOMPLETE_DATA, "field 'Status' of a topological remedial action cannot be empty"));
return;
}
for (TBranch tBranch : tRemedialAction.getStatus().getBranch()) {
UcteTopologicalElementHelper branchHelper = new UcteTopologicalElementHelper(tBranch.getFromNode().getV(), tBranch.getToNode().getV(), String.valueOf(tBranch.getOrder().getV()), createdRAId, ucteNetworkAnalyzer);
if (!branchHelper.isValid()) {
cseCracCreationContext.addRemedialActionCreationContext(StandardElementaryCreationContext.notImported(tRemedialAction.getName().getV(), null, ImportStatus.ELEMENT_NOT_FOUND_IN_NETWORK, branchHelper.getInvalidReason()));
return;
}
Identifiable<?> ne = network.getIdentifiable(branchHelper.getIdInNetwork());
SingleNetworkElementActionAdder<?> actionAdder;
if (ne.getType() == IdentifiableType.SWITCH) {
actionAdder = networkActionAdder.newSwitchAction()
.withActionType(convertActionType(tBranch.getStatus()));
} else if (ne instanceof Branch) {
actionAdder = networkActionAdder.newTerminalsConnectionAction()
.withActionType(convertActionType(tBranch.getStatus()));
} else {
throw new OpenRaoImportException(ImportStatus.ELEMENT_NOT_FOUND_IN_NETWORK, "CSE topological action " + createdRAId + " should be on branch or on switch, not on " + network.getIdentifiable(branchHelper.getIdInNetwork()).getType());
}
actionAdder.withNetworkElement(branchHelper.getIdInNetwork()).add();
}
addUsageRules(networkActionAdder, tRemedialAction);
networkActionAdder.add();
cseCracCreationContext.addRemedialActionCreationContext(StandardElementaryCreationContext.imported(tRemedialAction.getName().getV(), null, createdRAId, false, null));
}
private void importInjectionAction(TRemedialAction tRemedialAction) {
String createdRAId = tRemedialAction.getName().getV();
NetworkActionAdder networkActionAdder = crac.newNetworkAction()
.withId(createdRAId)
.withName(tRemedialAction.getName().getV())
.withOperator(tRemedialAction.getOperator().getV());
boolean isAltered = false;
StringBuilder alteringDetail = null;
for (TNode tNode : tRemedialAction.getGeneration().getNode()) {
if (!tNode.getVariationType().getV().equals(ABSOLUTE_VARIATION_TYPE)) {
cseCracCreationContext.addRemedialActionCreationContext(
StandardElementaryCreationContext.notImported(tRemedialAction.getName().getV(), null, ImportStatus.NOT_YET_HANDLED_BY_OPEN_RAO, String.format("node %s is not defined as an ABSOLUTE injectionSetpoint (only ABSOLUTE is implemented).", tNode.getName().getV()))
);
return;
}
GeneratorHelper generatorHelper = new GeneratorHelper(tNode.getName().getV(), ucteNetworkAnalyzer);
if (!generatorHelper.isValid()) {
cseCracCreationContext.addRemedialActionCreationContext(StandardElementaryCreationContext.notImported(tRemedialAction.getName().getV(), null, generatorHelper.getImportStatus(), generatorHelper.getDetail()));
return;
} else if (generatorHelper.isAltered()) {
isAltered = true;
if (alteringDetail == null) {
alteringDetail = Optional.ofNullable(generatorHelper.getDetail()).map(StringBuilder::new).orElse(null);
} else {
alteringDetail.append(", ").append(generatorHelper.getDetail());
}
}
try {
Identifiable<?> networkElement = network.getIdentifiable(generatorHelper.getGeneratorId());
if (Objects.isNull(networkElement)) {
throw new OpenRaoImportException(ImportStatus.ELEMENT_NOT_FOUND_IN_NETWORK, String.format("%s not found in network", generatorHelper.getGeneratorId()));
}
if (networkElement.getType() != IdentifiableType.GENERATOR) {
throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, "CSE injection action " + createdRAId + " should be on generator, not on " + networkElement.getType());
}
networkActionAdder.newGeneratorAction()
.withNetworkElement(generatorHelper.getGeneratorId())
.withActivePowerValue(tNode.getValue().getV())
.add();
} catch (OpenRaoException e) {
cseCracCreationContext.addRemedialActionCreationContext(StandardElementaryCreationContext.notImported(tRemedialAction.getName().getV(), null, ImportStatus.OTHER, e.getMessage()));
return;
}
}
// After looping on all nodes
addUsageRules(networkActionAdder, tRemedialAction);
networkActionAdder.add();
cseCracCreationContext.addRemedialActionCreationContext(StandardElementaryCreationContext.imported(tRemedialAction.getName().getV(), null, createdRAId, isAltered, alteringDetail == null ? null : alteringDetail.toString()));
}
private void importPstRangeAction(TRemedialAction tRemedialAction) {
String raId = tRemedialAction.getName().getV();
tRemedialAction.getPstRange().getBranch().forEach(tBranch -> {
UctePstHelper pstHelper = new UctePstHelper(tBranch.getFromNode().getV(), tBranch.getToNode().getV(), String.valueOf(tBranch.getOrder().getV()), raId, ucteNetworkAnalyzer);
if (!pstHelper.isValid()) {
cseCracCreationContext.addRemedialActionCreationContext(CsePstCreationContext.notImported(tRemedialAction, pstHelper.getUcteId(), ImportStatus.ELEMENT_NOT_FOUND_IN_NETWORK, pstHelper.getInvalidReason()));
return;
}
String id = "PST_" + raId + "_" + pstHelper.getIdInNetwork();
int pstInitialTap = pstHelper.getInitialTap();
Map<Integer, Double> conversionMap = pstHelper.getTapToAngleConversionMap();
PstRangeActionAdder pstRangeActionAdder = crac.newPstRangeAction()
.withId(id)
.withName(tRemedialAction.getName().getV())
.withOperator(tRemedialAction.getOperator().getV())
.withNetworkElement(pstHelper.getIdInNetwork())
.withInitialTap(pstInitialTap)
.withTapToAngleConversionMap(conversionMap)
.newTapRange()
.withMinTap(tRemedialAction.getPstRange().getMin().getV())
.withMaxTap(tRemedialAction.getPstRange().getMax().getV())
.withRangeType(convertRangeType(tRemedialAction.getPstRange().getVariationType()))
.add();
addUsageRules(pstRangeActionAdder, tRemedialAction);
pstRangeActionAdder.add();
String nativeNetworkElementId = String.format("%1$-8s %2$-8s %3$s", pstHelper.getOriginalFrom(), pstHelper.getOriginalTo(), pstHelper.getSuffix());
cseCracCreationContext.addRemedialActionCreationContext(CsePstCreationContext.imported(tRemedialAction, nativeNetworkElementId, id, false, null));
});
}
private void importHvdcRangeAction(TRemedialAction tRemedialAction) {
String raId = tRemedialAction.getName().getV();
// ---- HVDC Nodes
THVDCNode hvdcNodes = tRemedialAction.getHVDCRange().getHVDCNode().get(0);
GeneratorHelper generatorFromHelper = new GeneratorHelper(hvdcNodes.getFromNode().getV(), ucteNetworkAnalyzer);
GeneratorHelper generatorToHelper = new GeneratorHelper(hvdcNodes.getToNode().getV(), ucteNetworkAnalyzer);
// ---- Only handle ABSOLUTE variation type
if (!tRemedialAction.getHVDCRange().getVariationType().getV().equals(ABSOLUTE_VARIATION_TYPE)) {
cseCracCreationContext.addRemedialActionCreationContext(
CseHvdcCreationContext.notImported(tRemedialAction,
ImportStatus.NOT_YET_HANDLED_BY_OPEN_RAO,
String.format("HVDC %s is not defined with an ABSOLUTE variation type (only ABSOLUTE is handled)", raId),
hvdcNodes.getFromNode().getV(),
hvdcNodes.getToNode().getV()));
return;
}
// ---- Only handle one HVDC Node
if (tRemedialAction.getHVDCRange().getHVDCNode().size() > 1) {
cseCracCreationContext.addRemedialActionCreationContext(
CseHvdcCreationContext.notImported(tRemedialAction,
ImportStatus.INCONSISTENCY_IN_DATA,
String.format("HVDC %s has %s (>1) HVDC nodes", raId, tRemedialAction.getHVDCRange().getHVDCNode().size()),
hvdcNodes.getFromNode().getV(),
hvdcNodes.getToNode().getV()));
return;
}
// ---- check if generators are present
if (!generatorFromHelper.isValid() || !generatorToHelper.isValid()) {
String importStatusDetails;
if (generatorToHelper.isValid()) {
importStatusDetails = generatorFromHelper.getDetail();
} else if (generatorFromHelper.isValid()) {
importStatusDetails = generatorToHelper.getDetail();
} else {
importStatusDetails = generatorFromHelper.getDetail() + " & " + generatorToHelper.getDetail();
}
cseCracCreationContext.addRemedialActionCreationContext(
CseHvdcCreationContext.notImported(tRemedialAction,
ImportStatus.ELEMENT_NOT_FOUND_IN_NETWORK,
importStatusDetails,
hvdcNodes.getFromNode().getV(),
hvdcNodes.getToNode().getV()));
return;
}
// ---- check if generator have inverted signs
if (Math.abs(generatorFromHelper.getCurrentP() + generatorToHelper.getCurrentP()) > 0.1) {
cseCracCreationContext.addRemedialActionCreationContext(
CseHvdcCreationContext.notImported(tRemedialAction,
ImportStatus.INCONSISTENCY_IN_DATA,
"the two generators of the HVDC must have opposite power output values",
hvdcNodes.getFromNode().getV(),
hvdcNodes.getToNode().getV()));
return;
}
// ---- create range action
InjectionRangeActionAdder injectionRangeActionAdder = crac.newInjectionRangeAction()
.withId(raId)
.withName(tRemedialAction.getName().getV())
.withOperator(tRemedialAction.getOperator().getV())
.withNetworkElementAndKey(-1., generatorFromHelper.getGeneratorId())
.withNetworkElementAndKey(1., generatorToHelper.getGeneratorId())
.withInitialSetpoint(generatorToHelper.getCurrentP())
.newRange()
.withMin(tRemedialAction.getHVDCRange().getMin().getV())
.withMax(tRemedialAction.getHVDCRange().getMax().getV())
.add()
.newRange()
.withMin(Math.max(-generatorFromHelper.getPmax(), generatorToHelper.getPmin()))
.withMax(Math.min(-generatorFromHelper.getPmin(), generatorToHelper.getPmax()))
.add();
// ---- add groupId if present
if (cseCracCreationParameters != null && cseCracCreationParameters.getRangeActionGroups() != null) {
List<RangeActionGroup> groups = cseCracCreationParameters.getRangeActionGroups().stream()
.filter(rangeActionGroup -> rangeActionGroup.getRangeActionsIds().contains(raId))
.toList();
if (groups.size() == 1) {
injectionRangeActionAdder.withGroupId(groups.get(0).toString());
} else if (groups.size() > 1) {
injectionRangeActionAdder.withGroupId(groups.get(0).toString());
cseCracCreationContext.getCreationReport().warn(String.format("GroupId defined multiple times for HVDC %s, only group %s is used.", raId, groups.get(0)));
}
}
addUsageRules(injectionRangeActionAdder, tRemedialAction);
injectionRangeActionAdder.add();
cseCracCreationContext.addRemedialActionCreationContext(CseHvdcCreationContext.imported(tRemedialAction,
raId,
hvdcNodes.getFromNode().getV(),
generatorFromHelper.getGeneratorId(),
hvdcNodes.getToNode().getV(),
generatorToHelper.getGeneratorId()));
}
private static ActionType convertActionType(TStatusType tStatusType) {
switch (tStatusType.getV()) {
case "CLOSE":
return ActionType.CLOSE;
case "OPEN":
default:
return ActionType.OPEN;
}
}
private static RangeType convertRangeType(TVariationType tVariationType) {
if (tVariationType.getV().equals(ABSOLUTE_VARIATION_TYPE)) {
return RangeType.ABSOLUTE;
} else {
throw new OpenRaoException(String.format("%s type is not handled by the importer", tVariationType.getV()));
}
}
private Instant getInstant(TApplication tApplication) {
switch (tApplication.getV()) {
case "PREVENTIVE":
return crac.getPreventiveInstant();
case "SPS":
return crac.getInstant(InstantKind.AUTO);
case "CURATIVE":
return crac.getInstant(InstantKind.CURATIVE);
default:
throw new OpenRaoException(String.format("%s is not a recognized application type for remedial action", tApplication.getV()));
}
}
void addUsageRules(RemedialActionAdder<?> remedialActionAdder, TRemedialAction tRemedialAction) {
Instant raApplicationInstant = getInstant(tRemedialAction.getApplication());
addOnFlowConstraintUsageRules(remedialActionAdder, tRemedialAction, raApplicationInstant);
// According to <SharedWith> tag :
String sharedWithId = tRemedialAction.getSharedWith().getV();
if (sharedWithId.equals("CSE")) {
if (raApplicationInstant.isAuto()) {
throw new OpenRaoException("Cannot import automatons from CSE CRAC yet");
} else {
addOnInstantUsageRules(remedialActionAdder, raApplicationInstant);
}
} else {
addOnFlowConstraintUsageRulesAfterSpecificCountry(remedialActionAdder, tRemedialAction, raApplicationInstant, sharedWithId);
}
}
private void addOnFlowConstraintUsageRulesAfterSpecificCountry(RemedialActionAdder<?> remedialActionAdder, TRemedialAction tRemedialAction, Instant raApplicationInstant, String sharedWithId) {
// Check that sharedWithID is a UCTE country
if (sharedWithId.equals("None")) {
return;
}
Country country;
try {
country = Country.valueOf(sharedWithId);
} catch (IllegalArgumentException e) {
cseCracCreationContext.getCreationReport().removed(String.format("RA %s has a non-UCTE sharedWith country : %s. The usage rule was not created.", tRemedialAction.getName().getV(), sharedWithId));
return;
}
// RA is available for specific UCTE country
remedialActionAdder.newOnFlowConstraintInCountryUsageRule()
.withInstant(raApplicationInstant.getId())
.withUsageMethod(UsageMethod.AVAILABLE)
.withCountry(country)
.add();
}
private void addOnInstantUsageRules(RemedialActionAdder<?> remedialActionAdder, Instant raApplicationInstant) {
// RA is available for all countries
remedialActionAdder.newOnInstantUsageRule()
.withInstant(raApplicationInstant.getId())
.withUsageMethod(UsageMethod.AVAILABLE)
.add();
}
private void addOnFlowConstraintUsageRules(RemedialActionAdder<?> remedialActionAdder, TRemedialAction tRemedialAction, Instant raApplicationInstant) {
if (remedialActionsForCnecsMap.containsKey(tRemedialAction.getName().getV())) {
for (String flowCnecId : remedialActionsForCnecsMap.get(tRemedialAction.getName().getV())) {
// Only add the usage rule if the RemedialAction can be applied before or during CNEC instant
if (!crac.getFlowCnec(flowCnecId).getState().getInstant().comesBefore(raApplicationInstant)) {
remedialActionAdder.newOnConstraintUsageRule()
.withInstant(raApplicationInstant.getId())
.withUsageMethod(UsageMethod.AVAILABLE)
.withCnec(flowCnecId)
.add();
}
}
}
}
void importBusBarChangeAction(TRemedialAction tRemedialAction) {
String raId = tRemedialAction.getName().getV();
if (cseCracCreationParameters == null || cseCracCreationParameters.getBusBarChangeSwitches(raId) == null) {
cseCracCreationContext.addRemedialActionCreationContext(StandardElementaryCreationContext.notImported(tRemedialAction.getName().getV(), null, ImportStatus.INCOMPLETE_DATA, "CSE CRAC creation parameters is missing or does not contain information for the switches to open/close"));
return;
}
BusBarChangeSwitches busBarChangeSwitches = cseCracCreationParameters.getBusBarChangeSwitches(raId);
NetworkActionAdder networkActionAdder = crac.newNetworkAction()
.withId(raId)
.withOperator(tRemedialAction.getOperator().getV());
try {
busBarChangeSwitches.getSwitchPairs().forEach(switchPairId -> {
assertIsSwitch(switchPairId.getSwitchToOpenId());
assertIsSwitch(switchPairId.getSwitchToCloseId());
networkActionAdder.newSwitchPair()
.withSwitchToOpen(switchPairId.getSwitchToOpenId())
.withSwitchToClose(switchPairId.getSwitchToCloseId())
.add();
});
} catch (OpenRaoException e) {
cseCracCreationContext.addRemedialActionCreationContext(StandardElementaryCreationContext.notImported(tRemedialAction.getName().getV(), null, ImportStatus.ELEMENT_NOT_FOUND_IN_NETWORK, e.getMessage()));
return;
}
addUsageRules(networkActionAdder, tRemedialAction);
networkActionAdder.add();
cseCracCreationContext.addRemedialActionCreationContext(StandardElementaryCreationContext.imported(tRemedialAction.getName().getV(), null, raId, false, null));
}
private void assertIsSwitch(String switchId) {
UcteTopologicalElementHelper topoHelper = new UcteTopologicalElementHelper(switchId, ucteNetworkAnalyzer);
if (!topoHelper.isValid()) {
throw new OpenRaoException(topoHelper.getInvalidReason());
}
if (network.getSwitch(topoHelper.getIdInNetwork()) == null) {
throw new OpenRaoException(String.format("%s is not a switch", switchId));
}
}
}