ActionReader.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.fbconstraint;

import com.powsybl.iidm.network.Branch;
import com.powsybl.iidm.network.Identifiable;
import com.powsybl.iidm.network.IdentifiableType;
import com.powsybl.openrao.commons.OpenRaoException;
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.rangeaction.PstRangeActionAdder;
import com.powsybl.openrao.data.crac.io.commons.api.ImportStatus;
import com.powsybl.openrao.data.crac.io.fbconstraint.xsd.ActionType;
import com.powsybl.openrao.data.crac.io.fbconstraint.xsd.RangeType;
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 jakarta.xml.bind.JAXBElement;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;

/**
 * @author Viktor Terrier {@literal <viktor.terrier at rte-france.com>}
 * @author Baptiste Seguinot{@literal <baptiste.seguinot at rte-france.com>}
 */
class ActionReader {

    private static final String PST = "PSTTAP";
    private static final String TOPO = "STATUS";

    private final ActionType action;

    private boolean isActionValid = true;
    private String invalidActionReason = "";

    private String networkElementId;
    private Type type;
    private String nativeNetworkElementId;

    //useful only for PSTs
    private UctePstHelper uctePstHelper;
    private List<ActionReader.Range> ranges;
    private String groupId;
    private boolean isInverted = false;
    private String inversionMessage = null;

    // useful only for topo
    private com.powsybl.openrao.data.crac.api.networkaction.ActionType topologicalActionType;
    private Identifiable<?> networkElement;

    enum Type {
        TOPO,
        PST
    }

    private static class Range {
        private int minTap;
        private int maxTap;
        private com.powsybl.openrao.data.crac.api.range.RangeType relativeOrAbsolute;
    }

    Type getType() {
        return type;
    }

    boolean isActionValid() {
        return isActionValid;
    }

    String getInvalidActionReason() {
        return invalidActionReason;
    }

    ActionReader(ActionType action, UcteNetworkAnalyzer ucteNetworkAnalyzer) {
        this.action = action;

        interpretWithNetwork(ucteNetworkAnalyzer);
    }

    void addAction(PstRangeActionAdder pstRangeActionAdder) {

        if (!type.equals(Type.PST)) {
            throw new OpenRaoException(String.format("This method is only applicable for Action of type %s", PST));
        }

        pstRangeActionAdder.withNetworkElement(networkElementId)
            .withInitialTap(uctePstHelper.getInitialTap())
            .withTapToAngleConversionMap(uctePstHelper.getTapToAngleConversionMap());

        for (Range range : ranges) {
            pstRangeActionAdder.newTapRange()
                .withRangeType(range.relativeOrAbsolute)
                .withMaxTap(range.maxTap)
                .withMinTap(range.minTap)
                .add();
        }

        if (!Objects.isNull(groupId)) {
            pstRangeActionAdder.withGroupId(groupId);
        }
    }

    void addAction(NetworkActionAdder networkActionAdder, String remedialActionId) {

        if (!type.equals(Type.TOPO)) {
            throw new OpenRaoException(String.format("This method is only applicable for Action of type %s", TOPO));
        }

        SingleNetworkElementActionAdder<?> actionAdder;
        if (networkElement.getType() == IdentifiableType.SWITCH) {
            actionAdder = networkActionAdder.newSwitchAction().withActionType(topologicalActionType);
        } else if (networkElement instanceof Branch) {
            actionAdder = networkActionAdder.newTerminalsConnectionAction().withActionType(topologicalActionType);
        } else {
            throw new OpenRaoImportException(ImportStatus.ELEMENT_NOT_FOUND_IN_NETWORK, "FlowBasedConstraint topological action " + remedialActionId + " should be on branch or on switch, not on " + networkElement.getType());
        }
        actionAdder.withNetworkElement(networkElementId).add();
    }

    boolean isInverted() {
        return isInverted;
    }

    String getInversionMessage() {
        return inversionMessage;
    }

    String getNativeNetworkElementId() {
        return nativeNetworkElementId;
    }

    String getNetworkElementId() {
        return networkElementId;
    }

    String getGroupId() {
        return groupId;
    }

    com.powsybl.openrao.data.crac.api.networkaction.ActionType getActionType() {
        return topologicalActionType;
    }

    private void interpretWithNetwork(UcteNetworkAnalyzer ucteNetworkAnalyzer) {

        // check first element of the action, which is assumed to be a branch
        Iterator<?> actionTypeIterator = action.getContent().stream().filter(serializable -> serializable.getClass() != String.class).iterator();
        ActionType.Branch branch = (ActionType.Branch) ((JAXBElement<?>) actionTypeIterator.next()).getValue();

        this.nativeNetworkElementId = String.format("%1$-8s %2$-8s %3$s", branch.getFrom(), branch.getTo(), branch.getOrder() != null ? branch.getOrder() : branch.getElementName());

        switch (action.getType()) {
            case PST:
                interpretAsPstRangeAction(branch, actionTypeIterator, ucteNetworkAnalyzer);
                break;
            case TOPO:
                interpretAsTopologicalAction(branch, actionTypeIterator, ucteNetworkAnalyzer);
                break;
            default:
                invalidate(String.format("action of type %s is not handled", action.getType()));
        }
    }

    private void interpretAsPstRangeAction(ActionType.Branch branch, Iterator<?> actionTypeIterator, UcteNetworkAnalyzer ucteNetworkAnalyzer) {
        this.type = Type.PST;
        this.uctePstHelper = new UctePstHelper(branch.getFrom(), branch.getTo(), branch.getOrder(), branch.getElementName(), ucteNetworkAnalyzer);

        if (!uctePstHelper.isValid()) {
            invalidate(uctePstHelper.getInvalidReason());
            return;
        }

        this.networkElementId = uctePstHelper.getIdInNetwork();
        this.isInverted = !uctePstHelper.isInvertedInNetwork(); // POWSYBL convention actually inverts transformers usually
        if (this.isInverted) {
            inversionMessage = "PST was inverted in order to match the element in the network";
        }
        getRangesAndGroupId(actionTypeIterator, isInverted);
    }

    private void interpretAsTopologicalAction(ActionType.Branch branch, Iterator<?> actionTypeIterator, UcteNetworkAnalyzer ucteNetworkAnalyzer) {
        this.type = Type.TOPO;
        UcteTopologicalElementHelper ucteElementHelper = new UcteTopologicalElementHelper(branch.getFrom(), branch.getTo(), branch.getOrder(), branch.getElementName(), ucteNetworkAnalyzer);

        if (!ucteElementHelper.isValid()) {
            invalidate(ucteElementHelper.getInvalidReason());
        }

        this.networkElementId = ucteElementHelper.getIdInNetwork();
        this.networkElement = ucteElementHelper.getIidmIdentifiable();
        getActionType(actionTypeIterator);
    }

    private void getRangesAndGroupId(Iterator<?> actionTypeIterator, boolean shouldInvertRanges) {
        ranges = new ArrayList<>();

        while (actionTypeIterator.hasNext()) {
            try {
                JAXBElement<?> jaxbElement = (JAXBElement<?>) actionTypeIterator.next();
                String elementCategory = jaxbElement.getName().getLocalPart();
                if (elementCategory.equals("relativeRange") || elementCategory.equals("range")) {
                    ranges.add(getRangesFromJaxbElement(jaxbElement, shouldInvertRanges));
                } else if (elementCategory.equals("PSTGroupId")) {
                    groupId = (String) jaxbElement.getValue();
                }
            } catch (ClassCastException e) {
                invalidate("action's elementCategory not recognized");
                break;
            }
        }
    }

    private ActionReader.Range getRangesFromJaxbElement(JAXBElement<?> jaxbElementRange, boolean shouldInvert) {
        Range range = new Range();
        String rangeCategory = jaxbElementRange.getName().getLocalPart();
        RangeType rangeType = (RangeType) jaxbElementRange.getValue();
        range.minTap = shouldInvert ? -rangeType.getMax().intValue() : rangeType.getMin().intValue();
        range.maxTap = shouldInvert ? -rangeType.getMin().intValue() : rangeType.getMax().intValue();
        if (rangeCategory.equals("relativeRange")) {
            range.relativeOrAbsolute = com.powsybl.openrao.data.crac.api.range.RangeType.RELATIVE_TO_PREVIOUS_INSTANT;
        } else if (rangeCategory.equals("range")) {
            range.relativeOrAbsolute = com.powsybl.openrao.data.crac.api.range.RangeType.ABSOLUTE;
        } else {
            invalidate(String.format("unknown type of range category %s", rangeCategory));
        }
        return range;
    }

    private void getActionType(Iterator<?> actionTypeIterator) {
        String actionAsString = ((JAXBElement<?>) actionTypeIterator.next()).getValue().toString();

        if (!actionAsString.equals("OPEN") && !actionAsString.equals("CLOSE")) {
            invalidate(String.format("unknown topological action: %s", actionAsString));
        }

        topologicalActionType = com.powsybl.openrao.data.crac.api.networkaction.ActionType.valueOf(actionAsString);
    }

    private void invalidate(String reason) {
        this.isActionValid = false;
        this.invalidActionReason = reason;
    }
}