CriticalBranchReader.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.openrao.commons.Unit;
import com.powsybl.openrao.data.crac.api.Crac;
import com.powsybl.openrao.data.crac.api.InstantKind;
import com.powsybl.openrao.data.crac.api.cnec.FlowCnec;
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.fbconstraint.xsd.CriticalBranchType;
import com.powsybl.openrao.data.crac.io.commons.ucte.UcteFlowElementHelper;
import com.powsybl.openrao.data.crac.io.commons.ucte.UcteNetworkAnalyzer;
import com.powsybl.openrao.data.crac.loopflowextension.LoopFlowThresholdAdder;
import com.powsybl.iidm.network.Country;

import java.util.*;
import java.util.regex.Pattern;

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

    private static final List<String> DIRECT = List.of("DIRECT", "MONODIR");
    private static final List<String> OPPOSITE = List.of("OPPOSITE");

    private String importStatusDetail = null;
    private ImportStatus importStatus;

    private final CriticalBranchType criticalBranch;
    private Set<TwoSides> monitoredSides;
    private boolean isBaseCase;
    private boolean isInvertedInNetwork;
    private OutageReader outageReader;
    private UcteFlowElementHelper ucteFlowElementHelper;
    private TwoSides ampereThresholdSide = null;

    boolean isCriticialBranchValid() {
        return importStatus.equals(ImportStatus.IMPORTED);
    }

    public ImportStatus getImportStatus() {
        return importStatus;
    }

    String getImportStatusDetail() {
        return importStatusDetail;
    }

    OutageReader getOutageReader() {
        return outageReader;
    }

    boolean isBaseCase() {
        return isBaseCase;
    }

    String getBaseCaseCnecId() {
        if (isBaseCase) {
            return criticalBranch.getId().concat(" - ").concat("preventive");
        }
        return null;
    }

    boolean nullSafeIsMnec() {
        /*
         * In case of absence of element MNEC in XML input (as it is optional), no default value is set.
         * An absence of MNEC element is equivalent to the critical branch to be considered as non MNEC
         */
        return Optional.ofNullable(criticalBranch.isMNEC()).orElse(false);
    }

    String getOutageCnecId() {
        if (!isBaseCase) {
            return criticalBranch.getId().concat(" - ").concat("outage");
        }
        return null;
    }

    String getCurativeCnecId() {
        if (!isBaseCase) {
            return criticalBranch.getId().concat(" - ").concat("curative");
        }
        return null;
    }

    CriticalBranchType getCriticalBranch() {
        return criticalBranch;
    }

    NativeBranch getNativeBranch() {
        return new NativeBranch(ucteFlowElementHelper.getOriginalFrom(), ucteFlowElementHelper.getOriginalTo(), ucteFlowElementHelper.getSuffix());
    }

    boolean isInvertedInNetwork() {
        return isInvertedInNetwork;
    }

    CriticalBranchReader(CriticalBranchType criticalBranch, UcteNetworkAnalyzer ucteNetworkAnalyzer, Set<TwoSides> defaultMonitoredSides) {
        this.criticalBranch = criticalBranch;
        this.monitoredSides = defaultMonitoredSides;
        interpretWithNetwork(ucteNetworkAnalyzer);
    }

    void addCnecs(Crac crac) {
        if (isBaseCase) {
            addCnecWithPermanentThreshold(crac, crac.getPreventiveInstant().getId());
        } else {
            addOutageCnecWithTemporaryThreshold(crac);
            addCnecWithPermanentThreshold(crac, crac.getInstant(InstantKind.CURATIVE).getId());
        }
    }

    private void interpretWithNetwork(UcteNetworkAnalyzer ucteNetworkAnalyzer) {
        this.importStatus = ImportStatus.IMPORTED;
        this.ucteFlowElementHelper = new UcteFlowElementHelper(criticalBranch.getBranch().getFrom(),
            criticalBranch.getBranch().getTo(),
            criticalBranch.getBranch().getOrder(),
            criticalBranch.getBranch().getElementName(),
            ucteNetworkAnalyzer);

        if (ucteFlowElementHelper.isValid()) {
            this.isInvertedInNetwork = ucteFlowElementHelper.isInvertedInNetwork();
            if (ucteFlowElementHelper.isHalfLine()) {
                this.monitoredSides = Set.of(ucteFlowElementHelper.getHalfLineSide());
            }
        } else {
            this.importStatus = ImportStatus.ELEMENT_NOT_FOUND_IN_NETWORK;
            this.importStatusDetail = String.format("critical branch %s was removed as %s", criticalBranch.getId(), ucteFlowElementHelper.getInvalidReason());
            return;
        }

        if (Objects.isNull(criticalBranch.getOutage())) {
            this.isBaseCase = true;
        } else {
            this.isBaseCase = false;
            OutageReader tmpOutageReader = new OutageReader(criticalBranch.getOutage(), ucteNetworkAnalyzer);
            if (tmpOutageReader.isOutageValid()) {
                this.outageReader = tmpOutageReader;
            } else {
                this.importStatus = ImportStatus.INCONSISTENCY_IN_DATA;
                this.importStatusDetail = String.format("critical branch %s was removed as %s", criticalBranch.getId(), tmpOutageReader.getInvalidOutageReason());
                return;
            }
        }

        if (!criticalBranch.isCNEC() && !nullSafeIsMnec()) {
            this.importStatus = ImportStatus.NOT_FOR_RAO;
            this.importStatusDetail = String.format("critical branch %s was removed as it is neither a CNEC, nor a MNEC", criticalBranch.getId());
            return;
        }

        if (!criticalBranch.getDirection().equals("DIRECT") && !criticalBranch.getDirection().equals("MONODIR") && !criticalBranch.getDirection().equals("OPPOSITE")) {
            this.importStatus = ImportStatus.INCONSISTENCY_IN_DATA;
            this.importStatusDetail = String.format("critical branch %s was removed as its direction %s is unknown", criticalBranch.getId(), criticalBranch.getDirection());
        }

        initAmpereThresholdSide();
    }

    private void initAmpereThresholdSide() {
        if (!ucteFlowElementHelper.isHalfLine()
            && Math.abs(ucteFlowElementHelper.getNominalVoltage(TwoSides.ONE) - ucteFlowElementHelper.getNominalVoltage(TwoSides.TWO)) > 1) {
            // For transformers, if unit is absolute amperes, monitor low voltage side
            ampereThresholdSide = ucteFlowElementHelper.getNominalVoltage(TwoSides.ONE) <= ucteFlowElementHelper.getNominalVoltage(TwoSides.TWO) ?
                TwoSides.ONE : TwoSides.TWO;
        }
    }

    private void addCnecWithPermanentThreshold(Crac crac, String instantId) {
        FlowCnecAdder preventiveCnecAdder = createCnecAdder(crac, instantId);
        addPermanentThresholds(preventiveCnecAdder);
        FlowCnec cnec = preventiveCnecAdder.add();
        addLoopFlowExtension(cnec, criticalBranch);
    }

    private void addOutageCnecWithTemporaryThreshold(Crac crac) {
        FlowCnecAdder curativeCnecAdder = createCnecAdder(crac, crac.getOutageInstant().getId());
        addTemporaryThresholds(curativeCnecAdder);
        FlowCnec cnec = curativeCnecAdder.add();
        addLoopFlowExtension(cnec, criticalBranch);
    }

    private FlowCnecAdder createCnecAdder(Crac crac, String instantId) {
        CriticalBranchType.Branch branch = criticalBranch.getBranch();

        FlowCnecAdder adder = crac.newFlowCnec()
            .withId(criticalBranch.getId().concat(" - ").concat(instantId))
            .withName(branch.getName())
            .withNetworkElement(ucteFlowElementHelper.getIdInNetwork())
            .withInstant(instantId)
            .withReliabilityMargin(criticalBranch.getFrmMw())
            .withOperator(criticalBranch.getTsoOrigin())
            .withMonitored(nullSafeIsMnec())
            .withOptimized(criticalBranch.isCNEC())
            .withIMax(ucteFlowElementHelper.getCurrentLimit(TwoSides.ONE), TwoSides.ONE)
            .withIMax(ucteFlowElementHelper.getCurrentLimit(TwoSides.TWO), TwoSides.TWO)
            .withNominalVoltage(ucteFlowElementHelper.getNominalVoltage(TwoSides.ONE), TwoSides.ONE)
            .withNominalVoltage(ucteFlowElementHelper.getNominalVoltage(TwoSides.TWO), TwoSides.TWO);

        if (!isBaseCase) {
            adder.withContingency(outageReader.getOutage().getId());
        }
        return adder;
    }

    private void addPermanentThresholds(FlowCnecAdder cnecAdder) {
        if (!Objects.isNull(criticalBranch.getPermanentImaxFactor())) {
            addThreshold(cnecAdder, criticalBranch.getPermanentImaxFactor().doubleValue(), Unit.PERCENT_IMAX);
        } else if (!Objects.isNull(criticalBranch.getImaxFactor())) {
            addThreshold(cnecAdder, criticalBranch.getImaxFactor().doubleValue(), Unit.PERCENT_IMAX);
        }

        if (!Objects.isNull(criticalBranch.getPermanentImaxA())) {
            addThreshold(cnecAdder, criticalBranch.getPermanentImaxA().doubleValue(), Unit.AMPERE);
        } else if (!Objects.isNull(criticalBranch.getImaxA())) {
            addThreshold(cnecAdder, criticalBranch.getImaxA().doubleValue(), Unit.AMPERE);
        }

        if (Objects.isNull(criticalBranch.getPermanentImaxFactor()) && Objects.isNull(criticalBranch.getImaxFactor())
            && Objects.isNull(criticalBranch.getPermanentImaxA()) && Objects.isNull(criticalBranch.getImaxA())) {
            addThreshold(cnecAdder, 1., Unit.PERCENT_IMAX);
        }
    }

    private void addTemporaryThresholds(FlowCnecAdder cnecAdder) {
        if (!Objects.isNull(criticalBranch.getTemporaryImaxFactor())) {
            addThreshold(cnecAdder, criticalBranch.getTemporaryImaxFactor().doubleValue(), Unit.PERCENT_IMAX);
        } else if (!Objects.isNull(criticalBranch.getImaxFactor())) {
            addThreshold(cnecAdder, criticalBranch.getImaxFactor().doubleValue(), Unit.PERCENT_IMAX);
        }

        if (!Objects.isNull(criticalBranch.getTemporaryImaxA())) {
            addThreshold(cnecAdder, criticalBranch.getTemporaryImaxA().doubleValue(), Unit.AMPERE);
        } else if (!Objects.isNull(criticalBranch.getImaxA())) {
            addThreshold(cnecAdder, criticalBranch.getImaxA().doubleValue(), Unit.AMPERE);
        }

        if (Objects.isNull(criticalBranch.getTemporaryImaxFactor()) && Objects.isNull(criticalBranch.getImaxFactor())
            && Objects.isNull(criticalBranch.getTemporaryImaxA()) && Objects.isNull(criticalBranch.getImaxA())) {
            addThreshold(cnecAdder, 1., Unit.PERCENT_IMAX);
        }
    }

    private void addThreshold(FlowCnecAdder cnecAdder, double threshold, Unit unit) {

        //idea: create threshold in AMPERE instead of PERCENT_IMAX to avoid synchronisation afterwards
        //      can be tricky for transformers

        Set<TwoSides> monitoredSidesForThreshold = (unit.equals(Unit.AMPERE) && ampereThresholdSide != null) ?
            Set.of(ampereThresholdSide) : monitoredSides;
        monitoredSidesForThreshold.forEach(side -> {
            BranchThresholdAdder branchThresholdAdder = cnecAdder.newThreshold()
                .withUnit(unit)
                .withSide(side);
            addLimitsGivenDirection(threshold, branchThresholdAdder);
            branchThresholdAdder.add();
        });
    }

    private void addLimitsGivenDirection(double positiveLimit, BranchThresholdAdder branchThresholdAdder) {
        if (DIRECT.contains(criticalBranch.getDirection()) && !ucteFlowElementHelper.isInvertedInNetwork()
            || OPPOSITE.contains(criticalBranch.getDirection()) && ucteFlowElementHelper.isInvertedInNetwork()) {
            branchThresholdAdder.withMax(positiveLimit);
        }

        if (DIRECT.contains(criticalBranch.getDirection()) && ucteFlowElementHelper.isInvertedInNetwork()
            || OPPOSITE.contains(criticalBranch.getDirection()) && !ucteFlowElementHelper.isInvertedInNetwork()) {
            branchThresholdAdder.withMin(-positiveLimit);
        }
    }

    private static void addLoopFlowExtension(FlowCnec cnec, CriticalBranchType criticalBranch) {
        if (criticalBranch.getMinRAMfactor() != null && isCrossZonal(criticalBranch.getBranch())) {
            cnec.newExtension(LoopFlowThresholdAdder.class)
                .withUnit(Unit.PERCENT_IMAX)
                .withValue((100.0 - criticalBranch.getMinRAMfactor().doubleValue()) / 100.)
                .add();
        }
    }

    static boolean isCrossZonal(CriticalBranchType.Branch branch) {

        // check if first characters of the from/to nodes are different
        // if 1st characters are different, it means that:
        //   - the nodes come from two different countries,
        //   - or one the nodes is a Xnode

        if (!branch.getFrom().substring(0, 1).equals(branch.getTo().substring(0, 1))) {
            return true;
        }

        // check if the name of branch finishes with [XX] with XX a country code

        if (Pattern.compile("\\[[A-Za-z]{2}\\]$").matcher(branch.getName()).find()) {
            String countryCode = branch.getName().substring(branch.getName().length() - 3, branch.getName().length() - 1);
            try {
                Country.valueOf(countryCode);
                return true;
            } catch (IllegalArgumentException e) {
                return false;
            }
        }

        return false;
    }

}