CriticalBranchReader.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.criticalbranch;

import com.powsybl.openrao.commons.Unit;
import com.powsybl.openrao.data.crac.io.cse.xsd.TImax;
import com.powsybl.openrao.data.crac.io.cse.xsd.TOutage;
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.api.threshold.BranchThresholdAdder;
import com.powsybl.openrao.data.crac.io.commons.api.ImportStatus;
import com.powsybl.openrao.data.crac.io.commons.api.stdcreationcontext.NativeBranch;
import com.powsybl.openrao.data.crac.io.cse.xsd.TBranch;
import com.powsybl.openrao.data.crac.io.commons.ucte.UcteFlowElementHelper;
import com.powsybl.openrao.data.crac.io.commons.ucte.UcteNetworkAnalyzer;

import javax.annotation.Nullable;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author Peter Mitri {@literal <peter.mitri at rte-france.com>}
 */
public class CriticalBranchReader {
    private final String criticalBranchName;
    private final NativeBranch nativeBranch;
    private boolean isBaseCase;
    private final Map<String, String> createdCnecIds = new HashMap<>();
    private String contingencyId;
    private final boolean isImported;
    private String invalidBranchReason;
    private boolean isDirectionInverted;
    private boolean selected;
    private final Set<String> remedialActionIds = new HashSet<>();
    private final ImportStatus criticalBranchImportStatus;
    private Set<TwoSides> monitoredSides;

    public String getCriticalBranchName() {
        return criticalBranchName;
    }

    public NativeBranch getNativeBranch() {
        return nativeBranch;
    }

    public boolean isBaseCase() {
        return isBaseCase;
    }

    public Map<String, String> getCreatedCnecIds() {
        return Collections.unmodifiableMap(createdCnecIds);
    }

    public String getContingencyId() {
        return contingencyId;
    }

    public boolean isImported() {
        return isImported;
    }

    public String getInvalidBranchReason() {
        return invalidBranchReason;
    }

    public boolean isDirectionInverted() {
        return isDirectionInverted;
    }

    public boolean isSelected() {
        return selected;
    }

    public CriticalBranchReader(List<TBranch> tBranches, @Nullable TOutage tOutage, Crac crac, UcteNetworkAnalyzer ucteNetworkAnalyzer, Set<TwoSides> defaultMonitoredSides, boolean isMonitored) {
        if (tBranches.size() > 1) {
            this.criticalBranchName = tBranches.stream().map(tBranch -> tBranch.getName().getV()).collect(Collectors.joining(" ; "));
            this.nativeBranch = new NativeBranch(tBranches.get(0).getFromNode().toString(), tBranches.get(0).getToNode().toString(), tBranches.get(0).getOrder().toString());
            this.isImported = false;
            this.invalidBranchReason = "MonitoredElement has more than 1 Branch";
            this.criticalBranchImportStatus = ImportStatus.INCONSISTENCY_IN_DATA;
            return;
        }
        String outage = defineOutage(tOutage);
        TBranch tBranch = tBranches.get(0);
        this.criticalBranchName = String.join(" - ", tBranch.getName().getV(), tBranch.getFromNode().getV(), tBranch.getToNode().getV(), outage);
        UcteFlowElementHelper branchHelper = new UcteFlowElementHelper(tBranch.getFromNode().getV(), tBranch.getToNode().getV(), String.valueOf(tBranch.getOrder().getV()), ucteNetworkAnalyzer);
        this.nativeBranch = new NativeBranch(branchHelper.getOriginalFrom(), branchHelper.getOriginalTo(), branchHelper.getSuffix());
        if (!branchHelper.isValid()) {
            this.isImported = false;
            this.invalidBranchReason = branchHelper.getInvalidReason();
            this.criticalBranchImportStatus = ImportStatus.ELEMENT_NOT_FOUND_IN_NETWORK;
            return;
        }
        this.monitoredSides = branchHelper.isHalfLine() ? Set.of(branchHelper.getHalfLineSide()) : defaultMonitoredSides;
        if (tOutage != null && crac.getContingency(outage) == null) {
            this.isImported = false;
            this.criticalBranchImportStatus = ImportStatus.INCOMPLETE_DATA;
            this.invalidBranchReason = String.format("CNEC is defined on outage %s which is not defined", outage);
        } else {
            this.isImported = true;
            this.isDirectionInverted = branchHelper.isInvertedInNetwork();
            this.selected = !isMonitored && isSelected(tBranch);
            if (tOutage == null) {
                // preventive
                this.isBaseCase = true;
                this.contingencyId = null;
                this.criticalBranchImportStatus = ImportStatus.IMPORTED;
                importPreventiveCnec(tBranch, branchHelper, crac, isMonitored);
            } else {
                // curative
                this.isBaseCase = false;
                this.contingencyId = outage;
                this.criticalBranchImportStatus = ImportStatus.IMPORTED;
                importCurativeCnecs(tBranch, branchHelper, outage, crac, isMonitored);
            }
        }
    }

    private String defineOutage(TOutage tOutage) {
        if (tOutage == null) {
            return "basecase";
        }
        return tOutage.getName() == null ? tOutage.getV() : tOutage.getName().getV();
    }

    private void importPreventiveCnec(TBranch tBranch, UcteFlowElementHelper branchHelper, Crac crac, boolean isMonitored) {
        importCnec(crac, tBranch, branchHelper, isMonitored ? tBranch.getIlimitMNE() : tBranch.getImax(), null, crac.getPreventiveInstant().getId(), isMonitored);
    }

    private void importCurativeCnecs(TBranch tBranch, UcteFlowElementHelper branchHelper, String outage, Crac crac, boolean isMonitored) {
        HashMap<Instant, TImax> cnecCaracs = new HashMap<>();
        cnecCaracs.put(crac.getOutageInstant(), isMonitored ? tBranch.getIlimitMNEAfterOutage() : tBranch.getImaxAfterOutage());
        cnecCaracs.put(crac.getInstant(InstantKind.AUTO), isMonitored ? tBranch.getIlimitMNEAfterSPS() : tBranch.getImaxAfterSPS());
        cnecCaracs.put(crac.getInstant(InstantKind.CURATIVE), isMonitored ? tBranch.getIlimitMNEAfterCRA() : tBranch.getImaxAfterCRA());
        cnecCaracs.forEach((instant, iMax) -> importCnec(crac, tBranch, branchHelper, iMax, outage, instant.getId(), isMonitored));
    }

    private void importCnec(Crac crac, TBranch tBranch, UcteFlowElementHelper branchHelper, @Nullable TImax tImax, String outage, String instantId, boolean isMonitored) {
        if (tImax == null) {
            return;
        }
        String cnecId = getCnecId(tBranch, outage, instantId);
        FlowCnecAdder cnecAdder = crac.newFlowCnec()
                .withId(cnecId)
                .withName(tBranch.getName().getV())
                .withInstant(instantId)
                .withContingency(outage)
                .withOptimized(selected).withMonitored(isMonitored)
                .withNetworkElement(branchHelper.getIdInNetwork())
                .withIMax(branchHelper.getCurrentLimit(TwoSides.ONE), TwoSides.ONE)
                .withIMax(branchHelper.getCurrentLimit(TwoSides.TWO), TwoSides.TWO)
                .withNominalVoltage(branchHelper.getNominalVoltage(TwoSides.ONE), TwoSides.ONE)
                .withNominalVoltage(branchHelper.getNominalVoltage(TwoSides.TWO), TwoSides.TWO);

        Set<TwoSides> monitoredSidesForThreshold = monitoredSides;
        Unit unit = convertUnit(tImax.getUnit());
        // For transformers, if unit is absolute amperes, monitor low voltage side
        if (!branchHelper.isHalfLine() && unit.equals(Unit.AMPERE) &&
                Math.abs(branchHelper.getNominalVoltage(TwoSides.ONE) - branchHelper.getNominalVoltage(TwoSides.TWO)) > 1) {
            monitoredSidesForThreshold = (branchHelper.getNominalVoltage(TwoSides.ONE) <= branchHelper.getNominalVoltage(TwoSides.TWO)) ?
                    Set.of(TwoSides.ONE) : Set.of(TwoSides.TWO);
        }
        addThreshold(cnecAdder, tImax.getV(), unit, tBranch.getDirection().getV(), isDirectionInverted, monitoredSidesForThreshold);
        cnecAdder.add();
        createdCnecIds.put(instantId, cnecId);
        if (!isMonitored) {
            storeRemedialActions(tBranch);
        }
    }

    private void storeRemedialActions(TBranch tBranch) {
        tBranch.getRemedialActions().forEach(tRemedialActions ->
                tRemedialActions.getName().forEach(tName -> remedialActionIds.add(tName.getV()))
        );
    }

    private static void addThreshold(FlowCnecAdder cnecAdder, double positiveLimit, Unit unit, String direction, boolean invert, Set<TwoSides> monitoredSides) {
        monitoredSides.forEach(side -> {
            BranchThresholdAdder branchThresholdAdder = cnecAdder.newThreshold()
                    .withSide(side)
                    .withUnit(unit);
            convertMinMax(branchThresholdAdder, positiveLimit, direction, invert, unit.equals(Unit.PERCENT_IMAX));
            branchThresholdAdder.add();
        });
    }

    private static void convertMinMax(BranchThresholdAdder branchThresholdAdder, double positiveLimit, String direction, boolean invert, boolean isPercent) {
        double convertedPositiveLimit = positiveLimit * (isPercent ? 0.01 : 1.);
        if (direction.equals("BIDIR")) {
            branchThresholdAdder.withMax(convertedPositiveLimit);
            branchThresholdAdder.withMin(-convertedPositiveLimit);
        } else if (direction.equals("DIRECT") && !invert
            || direction.equals("OPPOSITE") && invert) {
            branchThresholdAdder.withMax(convertedPositiveLimit);
        } else if (direction.equals("DIRECT") && invert
            || direction.equals("OPPOSITE") && !invert) {
            branchThresholdAdder.withMin(-convertedPositiveLimit);
        } else {
            throw new IllegalArgumentException(String.format("%s is not a recognized direction", direction));
        }
    }

    private static String getCnecId(TBranch tBranch, String outage, String instantId) {
        if (outage == null) {
            return String.format("%s - %s->%s - %s", tBranch.getName().getV(), tBranch.getFromNode().getV(), tBranch.getToNode().getV(), instantId);
        }
        return String.format("%s - %s->%s  - %s - %s", tBranch.getName().getV(), tBranch.getFromNode().getV(), tBranch.getToNode().getV(), outage, instantId);
    }

    private static Unit convertUnit(String unit) {
        switch (unit) {
            case "Pct":
                return Unit.PERCENT_IMAX;
            case "A":
            default:
                return Unit.AMPERE;
        }
    }

    // In case it is not specified we consider it as selected
    private static boolean isSelected(TBranch tBranch) {
        return tBranch.getSelected() == null || "true".equals(tBranch.getSelected().getV());
    }

    public Set<String> getRemedialActionIds() {
        return remedialActionIds;
    }

    public ImportStatus getImportStatus() {
        return criticalBranchImportStatus;
    }
}