FlowCnecCreator.java

/*
 * Copyright (c) 2024, 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.nc.craccreator.cnec;

import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.Unit;
import com.powsybl.contingency.Contingency;
import com.powsybl.openrao.data.crac.io.nc.craccreator.NcCracUtils;
import com.powsybl.openrao.data.crac.io.nc.craccreator.constants.LimitTypeKind;
import com.powsybl.openrao.data.crac.io.nc.craccreator.constants.OperationalLimitDirectionKind;
import com.powsybl.openrao.data.crac.io.nc.objects.AssessedElement;
import com.powsybl.openrao.data.crac.io.nc.objects.CurrentLimit;
import com.powsybl.openrao.data.crac.io.nc.parameters.NcCracCreationParameters;
import com.powsybl.openrao.data.crac.api.Crac;
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.ElementaryCreationContext;
import com.powsybl.openrao.data.crac.io.commons.api.ImportStatus;
import com.powsybl.openrao.data.crac.api.parameters.CracCreationParameters;
import com.powsybl.openrao.data.crac.io.commons.api.StandardElementaryCreationContext;
import com.powsybl.iidm.network.*;
import com.powsybl.openrao.data.crac.io.commons.OpenRaoImportException;

import java.util.*;
import java.util.stream.Collectors;

import static com.powsybl.openrao.data.crac.io.nc.craccreator.constants.NcConstants.CURRENT_LIMIT_POSSIBLE_ALIASES_BY_TYPE_LEFT;
import static com.powsybl.openrao.data.crac.io.nc.craccreator.constants.NcConstants.CURRENT_LIMIT_POSSIBLE_ALIASES_BY_TYPE_RIGHT;
import static com.powsybl.openrao.data.crac.io.nc.craccreator.constants.NcConstants.CURRENT_LIMIT_POSSIBLE_ALIASES_BY_TYPE_TIE_LINE;

/**
 * @author Thomas Bouquet {@literal <thomas.bouquet at rte-france.com>}
 */
public class FlowCnecCreator extends AbstractCnecCreator {
    private final NcCracCreationParameters ncCracCreationParameters;
    private final Set<TwoSides> defaultMonitoredSides;
    private final FlowCnecInstantHelper instantHelper;
    private final CurrentLimit nativeCurrentLimit;

    public FlowCnecCreator(Crac crac, Network network, AssessedElement nativeAssessedElement, CurrentLimit nativeCurrentLimit, Set<Contingency> linkedContingencies, Set<ElementaryCreationContext> ncCnecCreationContexts, String rejectedLinksAssessedElementContingency, CracCreationParameters cracCreationParameters, Map<String, String> borderPerTso, Map<String, String> borderPerEic) {
        super(crac, network, nativeAssessedElement, linkedContingencies, ncCnecCreationContexts, rejectedLinksAssessedElementContingency, cracCreationParameters, borderPerTso, borderPerEic);
        this.defaultMonitoredSides = cracCreationParameters.getDefaultMonitoredSides();
        this.nativeCurrentLimit = nativeCurrentLimit;
        ncCracCreationParameters = cracCreationParameters.getExtension(NcCracCreationParameters.class);
        if (ncCracCreationParameters == null) {
            throw new OpenRaoException("No NcCracCreatorParameters extension provided.");
        }
        this.instantHelper = new FlowCnecInstantHelper(ncCracCreationParameters, crac);
        checkCnecDefinitionMode();
    }

    private void checkCnecDefinitionMode() {
        if (nativeAssessedElement.conductingEquipment() == null && nativeCurrentLimit == null) {
            throw new OpenRaoImportException(ImportStatus.INCOMPLETE_DATA, writeAssessedElementIgnoredReasonMessage("no ConductingEquipment or OperationalLimit was provided"));
        }
        if (nativeAssessedElement.conductingEquipment() != null && nativeCurrentLimit != null) {
            throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, writeAssessedElementIgnoredReasonMessage("an assessed element must be defined using either a ConductingEquipment or an OperationalLimit, not both"));
        }
    }

    public void addFlowCnecs() {
        String networkElementId = nativeAssessedElement.conductingEquipment() != null ? nativeAssessedElement.conductingEquipment() : nativeCurrentLimit.terminal();
        Identifiable<?> branch = getFlowCnecBranch(networkElementId);

        // The thresholds are a map of acceptable durations to thresholds (per branch side)
        // Integer.MAX_VALUE is used for the PATL's acceptable duration
        Map<Integer, Map<TwoSides, Double>> thresholds = nativeAssessedElement.conductingEquipment() != null ? getPermanentAndTemporaryLimitsOfBranch((Branch<?>) branch) : getPermanentAndTemporaryLimitsOfOperationalLimit(branch, networkElementId);
        if (thresholds.isEmpty()) {
            throw new OpenRaoImportException(ImportStatus.INCOMPLETE_DATA, writeAssessedElementIgnoredReasonMessage("no PATL or TATLs could be retrieved for the branch " + branch.getId()));
        }

        // If the AssessedElement is defined with a conducting equipment, we use both max and min thresholds.
        boolean useMaxAndMinThresholds = true;
        if (nativeAssessedElement.conductingEquipment() == null) {
            if (OperationalLimitDirectionKind.HIGH.toString().equals(nativeCurrentLimit.direction())) {
                useMaxAndMinThresholds = false;
            } else if (!OperationalLimitDirectionKind.ABSOLUTE.toString().equals(nativeCurrentLimit.direction())) {
                throw new OpenRaoImportException(ImportStatus.NOT_FOR_RAO, writeAssessedElementIgnoredReasonMessage("OperationalLimitType.direction is neither 'absoluteValue' nor 'high'"));
            }
        }

        addAllFlowCnecsFromBranchAndOperationalLimits((Branch<?>) branch, thresholds, useMaxAndMinThresholds);
    }

    private FlowCnecAdder initFlowCnec() {
        return crac.newFlowCnec().withReliabilityMargin(0);
    }

    private Identifiable<?> getFlowCnecBranch(String networkElementId) {
        Identifiable<?> networkElement = getNetworkElementInNetwork(networkElementId);
        if (networkElement == null) {
            throw new OpenRaoImportException(ImportStatus.ELEMENT_NOT_FOUND_IN_NETWORK, writeAssessedElementIgnoredReasonMessage("the following network element is missing from the network: " + networkElementId));
        }
        if (!(networkElement instanceof Branch)) {
            throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, writeAssessedElementIgnoredReasonMessage("the network element " + networkElement.getId() + " is not a branch"));
        }
        return networkElement;
    }

    private TwoSides getSideFromNetworkElement(Identifiable<?> networkElement, String terminalId) {
        if (networkElement instanceof TieLine tieLine) {
            return getSideFromTieLine(tieLine, terminalId);
        } else {
            return getSideFromNonTieLine(networkElement, terminalId);
        }
    }

    private TwoSides getSideFromTieLine(TieLine tieLine, String terminalId) {
        for (String key : CURRENT_LIMIT_POSSIBLE_ALIASES_BY_TYPE_TIE_LINE) {
            Optional<String> oAlias = tieLine.getDanglingLine1().getAliasFromType(key);
            if (oAlias.isPresent() && oAlias.get().equals(terminalId)) {
                return TwoSides.ONE;
            }
            oAlias = tieLine.getDanglingLine2().getAliasFromType(key);
            if (oAlias.isPresent() && oAlias.get().equals(terminalId)) {
                return TwoSides.TWO;
            }
        }
        return null;
    }

    private TwoSides getSideFromNonTieLine(Identifiable<?> networkElement, String terminalId) {
        for (String key : CURRENT_LIMIT_POSSIBLE_ALIASES_BY_TYPE_LEFT) {
            Optional<String> oAlias = networkElement.getAliasFromType(key);
            if (oAlias.isPresent() && oAlias.get().equals(terminalId)) {
                return TwoSides.ONE;
            }
        }

        for (String key : CURRENT_LIMIT_POSSIBLE_ALIASES_BY_TYPE_RIGHT) {
            Optional<String> oAlias = networkElement.getAliasFromType(key);
            if (oAlias.isPresent() && oAlias.get().equals(terminalId)) {
                return TwoSides.TWO;
            }
        }

        return null;
    }

    private void addFlowCnecThreshold(FlowCnecAdder flowCnecAdder, TwoSides side, double threshold, boolean useMaxAndMinThresholds) {
        if (nativeAssessedElement.flowReliabilityMargin() < 0 || nativeAssessedElement.flowReliabilityMargin() > 100) {
            throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, writeAssessedElementIgnoredReasonMessage("of an invalid flow reliability margin (expected a value between 0 and 100)"));
        }
        double thresholdWithReliabilityMargin = threshold * (1d - nativeAssessedElement.flowReliabilityMargin() / 100d);
        BranchThresholdAdder adder = flowCnecAdder.newThreshold().withSide(side)
            .withUnit(Unit.AMPERE)
            .withMax(thresholdWithReliabilityMargin);
        if (useMaxAndMinThresholds) {
            adder.withMin(-thresholdWithReliabilityMargin);
        }
        adder.add();
    }

    private void setNominalVoltage(FlowCnecAdder flowCnecAdder, Branch<?> branch) {
        double voltageLevelLeft = branch.getTerminal1().getVoltageLevel().getNominalV();
        double voltageLevelRight = branch.getTerminal2().getVoltageLevel().getNominalV();
        if (voltageLevelLeft > 1e-6 && voltageLevelRight > 1e-6) {
            flowCnecAdder.withNominalVoltage(voltageLevelLeft, TwoSides.ONE);
            flowCnecAdder.withNominalVoltage(voltageLevelRight, TwoSides.TWO);
        } else {
            throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, "Voltage level for branch " + branch.getId() + " is 0 in network");
        }
    }

    private void setCurrentLimitsFromBranch(FlowCnecAdder flowCnecAdder, Branch<?> branch) {
        Double currentLimitLeft = getCurrentLimitFromBranch(branch, TwoSides.ONE);
        Double currentLimitRight = getCurrentLimitFromBranch(branch, TwoSides.TWO);
        if (Objects.nonNull(currentLimitLeft) && Objects.nonNull(currentLimitRight)) {
            flowCnecAdder.withIMax(currentLimitLeft, TwoSides.ONE);
            flowCnecAdder.withIMax(currentLimitRight, TwoSides.TWO);
        } else {
            throw new OpenRaoImportException(ImportStatus.INCONSISTENCY_IN_DATA, writeAssessedElementIgnoredReasonMessage("RAO was unable to retrieve the current limits of branch %s from the network".formatted(branch.getId())));
        }
    }

    private Double getCurrentLimitFromBranch(Branch<?> branch, TwoSides side) {

        if (branch.getCurrentLimits(side).isPresent()) {
            return branch.getCurrentLimits(side).orElseThrow().getPermanentLimit();
        }

        if (side == TwoSides.ONE && branch.getCurrentLimits(TwoSides.TWO).isPresent()) {
            return branch.getCurrentLimits(TwoSides.TWO).orElseThrow().getPermanentLimit() * branch.getTerminal1().getVoltageLevel().getNominalV() / branch.getTerminal2().getVoltageLevel().getNominalV();
        }

        if (side == TwoSides.TWO && branch.getCurrentLimits(TwoSides.ONE).isPresent()) {
            return branch.getCurrentLimits(TwoSides.ONE).orElseThrow().getPermanentLimit() * branch.getTerminal2().getVoltageLevel().getNominalV() / branch.getTerminal1().getVoltageLevel().getNominalV();
        }

        return null;
    }

    private void addFlowCnec(Branch<?> networkElement, Contingency contingency, String instantId, Map<TwoSides, Double> thresholdPerSide, int limitDuration, boolean useMaxAndMinThresholds) {
        if (thresholdPerSide.isEmpty()) {
            return;
        }
        FlowCnecAdder cnecAdder = initFlowCnec();
        addCnecBaseInformation(cnecAdder, contingency, instantId, limitDuration);
        thresholdPerSide.forEach((twoSides, threshold) -> addFlowCnecThreshold(cnecAdder, twoSides, threshold, useMaxAndMinThresholds));
        cnecAdder.withNetworkElement(networkElement.getId());
        setNominalVoltage(cnecAdder, networkElement);
        setCurrentLimitsFromBranch(cnecAdder, networkElement);
        cnecAdder.add();
    }

    private void addAllFlowCnecsFromBranchAndOperationalLimits(Branch<?> networkElement, Map<Integer, Map<TwoSides, Double>> thresholds, boolean useMaxAndMinThresholds) {
        // Preventive CNEC
        if (nativeAssessedElement.inBaseCase()) {
            Map<TwoSides, Double> thresholdPerSide = thresholds.getOrDefault(Integer.MAX_VALUE, Map.of());
            String cnecName = getCnecName(crac.getPreventiveInstant().getId(), null, Integer.MAX_VALUE);
            addFlowCnec(networkElement, null, crac.getPreventiveInstant().getId(), thresholdPerSide, Integer.MAX_VALUE, useMaxAndMinThresholds);
            ncCnecCreationContexts.add(StandardElementaryCreationContext.imported(nativeAssessedElement.mrid(), cnecName, cnecName, false, ""));
        }

        // Curative CNECs
        if (!linkedContingencies.isEmpty()) {
            String operatorName = NcCracUtils.getTsoNameFromUrl(nativeAssessedElement.operator());
            Map<TwoSides, Map<String, Integer>> instantToDurationMaps = Arrays.stream(TwoSides.values()).collect(Collectors.toMap(twoSides -> twoSides, twoSides -> instantHelper.mapPostContingencyInstantsAndLimitDurations(networkElement, twoSides, operatorName)));
            boolean operatorDoesNotUsePatlInFinalState = ncCracCreationParameters.getTsosWhichDoNotUsePatlInFinalState().contains(operatorName);

            // If an operator does not use the PATL for the final state but has no TATL defined, the use of PATL if forced
            Map<TwoSides, Boolean> forceUseOfPatl = Arrays.stream(TwoSides.values()).collect(Collectors.toMap(
                twoSides -> twoSides,
                twoSides -> operatorDoesNotUsePatlInFinalState
                    && (networkElement.getCurrentLimits(twoSides).isEmpty() || networkElement.getCurrentLimits(twoSides).isPresent() && networkElement.getCurrentLimits(twoSides).get().getTemporaryLimits().isEmpty())));

            linkedContingencies.forEach(
                contingency -> thresholds.forEach(
                    (acceptableDuration, thresholdPerSide) -> addCurativeFlowCnec(networkElement, useMaxAndMinThresholds, instantToDurationMaps, forceUseOfPatl, contingency, acceptableDuration, thresholdPerSide)));
        }
    }

    private void addCurativeFlowCnec(Branch<?> networkElement, boolean useMaxAndMinThresholds, Map<TwoSides, Map<String, Integer>> instantToDurationMaps, Map<TwoSides, Boolean> forceUseOfPatl, Contingency contingency, Integer acceptableDuration, Map<TwoSides, Double> thresholdPerSide) {
        Map<String, Map<TwoSides, Double>> thresholdPerSidePerInstant = new HashMap<>();
        for (TwoSides twoSides : thresholdPerSide.keySet()) {
            double threshold = thresholdPerSide.get(twoSides);
            Set<String> instantsForSide = instantHelper.getPostContingencyInstantsAssociatedToLimitDuration(instantToDurationMaps.get(twoSides), acceptableDuration);
            for (String instant : instantsForSide) {
                thresholdPerSidePerInstant.computeIfAbsent(instant, k -> new HashMap<>()).putIfAbsent(twoSides, threshold);
            }
        }
        thresholdPerSidePerInstant.forEach((instant, thresholdsOfInstant) -> {
            String cnecName = getCnecName(instant, contingency, acceptableDuration);
            addFlowCnec(networkElement, contingency, instant, thresholdsOfInstant, acceptableDuration, useMaxAndMinThresholds);
            if (acceptableDuration == Integer.MAX_VALUE && thresholdPerSide.keySet().stream().anyMatch(twoSides -> Boolean.TRUE.equals(forceUseOfPatl.get(twoSides)))) {
                ncCnecCreationContexts.add(StandardElementaryCreationContext.imported(nativeAssessedElement.mrid(), cnecName, cnecName, true, "TSO %s does not use PATL in final state but has no TATL defined for branch %s on at least one of its sides, PATL will be used".formatted(NcCracUtils.getTsoNameFromUrl(nativeAssessedElement.operator()), networkElement.getId())));
            } else {
                ncCnecCreationContexts.add(StandardElementaryCreationContext.imported(nativeAssessedElement.mrid(), cnecName, cnecName, false, ""));
            }
        });
    }

    private Map<Integer, Map<TwoSides, Double>> getPermanentAndTemporaryLimitsOfOperationalLimit(Identifiable<?> branch, String terminalId) {
        Map<Integer, Map<TwoSides, Double>> thresholds = new HashMap<>();

        TwoSides side = getSideFromNetworkElement(branch, terminalId);

        if (side != null) {
            int acceptableDuration;
            if (LimitTypeKind.PATL.toString().equals(nativeCurrentLimit.limitType())) {
                acceptableDuration = Integer.MAX_VALUE;
            } else if (LimitTypeKind.TATL.toString().equals(nativeCurrentLimit.limitType())) {
                acceptableDuration = Integer.parseInt(nativeCurrentLimit.acceptableDuration());
            } else {
                return thresholds;
            }
            thresholds.put(acceptableDuration, Map.of(side, nativeCurrentLimit.value()));
        }

        return thresholds;
    }

    private Map<Integer, Map<TwoSides, Double>> getPermanentAndTemporaryLimitsOfBranch(Branch<?> branch) {
        Set<TwoSides> sidesToCheck = getSidesToCheck(branch);

        Map<Integer, Map<TwoSides, Double>> thresholds = new HashMap<>();

        for (TwoSides side : sidesToCheck) {
            Optional<CurrentLimits> currentLimits = branch.getCurrentLimits(side);
            if (currentLimits.isPresent()) {
                // Retrieve PATL
                double permanentThreshold = currentLimits.get().getPermanentLimit();
                addLimitThreshold(thresholds, Integer.MAX_VALUE, permanentThreshold, side);
                // Retrieve TATLs
                List<LoadingLimits.TemporaryLimit> temporaryLimits = currentLimits.get().getTemporaryLimits().stream().toList();
                for (LoadingLimits.TemporaryLimit temporaryLimit : temporaryLimits) {
                    int acceptableDuration = temporaryLimit.getAcceptableDuration();
                    double temporaryThreshold = temporaryLimit.getValue();
                    addLimitThreshold(thresholds, acceptableDuration, temporaryThreshold, side);
                }
            }
        }
        return thresholds;
    }

    private static void addLimitThreshold(Map<Integer, Map<TwoSides, Double>> thresholds, int acceptableDuration, double threshold, TwoSides side) {
        if (thresholds.containsKey(acceptableDuration)) {
            thresholds.get(acceptableDuration).put(side, threshold);
        } else {
            thresholds.put(acceptableDuration, new EnumMap<>(Map.of(side, threshold)));
        }
    }

    private Set<TwoSides> getSidesToCheck(Branch<?> branch) {
        Set<TwoSides> sidesToCheck = new HashSet<>();
        if (defaultMonitoredSides.size() == 2) {
            // TODO: if TieLine, only put relevant side? ask TSOs what is the expected behavior
            sidesToCheck.add(TwoSides.ONE);
            sidesToCheck.add(TwoSides.TWO);
        } else {
            // Only one side in the set -> check the default side.
            // If no limit for the default side, check the other side.
            TwoSides defaultSide = defaultMonitoredSides.stream().toList().get(0);
            TwoSides otherSide = defaultSide == TwoSides.ONE ? TwoSides.TWO : TwoSides.ONE;
            sidesToCheck.add(branch.getCurrentLimits(defaultSide).isPresent() ? defaultSide : otherSide);
        }
        return sidesToCheck;
    }
}