LimitViolationDetection.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/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.security;

import com.powsybl.commons.PowsyblException;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.limitmodification.LimitsComputer;
import com.powsybl.iidm.network.util.LimitViolationUtils;
import com.powsybl.iidm.network.util.Networks;
import com.powsybl.iidm.network.util.PermanentLimitCheckResult;
import com.powsybl.security.detectors.LoadingLimitType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.Set;
import java.util.function.Consumer;

/**
 * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
 */
public final class LimitViolationDetection {

    private static final Logger LOGGER = LoggerFactory.getLogger(LimitViolationDetection.class);

    private LimitViolationDetection() {
    }

    /**
     * Checks whether the current and voltage values on all equipments
     * of the specified {@link Network} should be considered as {@link LimitViolation}s.
     * In case it should, feeds the consumer with it.
     *
     * @param network           The network on which physical values must be checked.
     * @param currentLimitTypes The current limit type to consider.
     * @param limitsComputer    The computer of the limit reductions to apply.
     * @param consumer          Will be fed with possibly created limit violations.
     */
    public static void checkAll(Network network, Set<LoadingLimitType> currentLimitTypes,
                                LimitsComputer<Identifiable<?>, LoadingLimits> limitsComputer, Consumer<LimitViolation> consumer) {
        network.getBranchStream().forEach(b -> checkCurrent(b, currentLimitTypes, limitsComputer, consumer));
        network.getThreeWindingsTransformerStream().forEach(t -> checkCurrent(t, currentLimitTypes, limitsComputer, consumer));
        network.getVoltageLevelStream()
                .flatMap(vl -> vl.getBusView().getBusStream())
                .forEach(b -> checkVoltage(b, consumer));
        network.getVoltageAngleLimitsStream().forEach(valOk -> checkVoltageAngle(valOk, consumer));
    }

    /**
     * Checks whether the current and voltage values on all equipments
     * of the specified {@link Network} should be considered as {@link LimitViolation}s.
     * In case it should, feeds the consumer with it.
     * In this DC power flow mode, the current is computed using the DC power factor if necessary.
     *
     * @param network             The network on which physical values must be checked.
     * @param dcPowerFactor       The DC power factor used to convert the active power into current.
     * @param currentLimitTypes   The current limit type to consider.
     * @param limitsComputer      The computer of the limit reductions to apply.
     * @param consumer            Will be fed with possibly created limit violations.
     */
    public static void checkAllDc(Network network, double dcPowerFactor, Set<LoadingLimitType> currentLimitTypes,
                                  LimitsComputer<Identifiable<?>, LoadingLimits> limitsComputer, Consumer<LimitViolation> consumer) {
        network.getBranchStream().forEach(b -> checkCurrentDc(b, dcPowerFactor, currentLimitTypes, limitsComputer, consumer));
        network.getThreeWindingsTransformerStream().forEach(b -> checkCurrentDc(b, dcPowerFactor, currentLimitTypes, limitsComputer, consumer));
        network.getVoltageAngleLimitsStream().forEach(valOk -> checkVoltageAngle(valOk, consumer));
    }

    private static void checkCurrent(Branch<?> branch, Set<LoadingLimitType> currentLimitTypes,
                                     LimitsComputer<Identifiable<?>, LoadingLimits> limitsComputer, Consumer<LimitViolation> consumer) {
        checkCurrent(branch, TwoSides.ONE, currentLimitTypes, limitsComputer, consumer);
        checkCurrent(branch, TwoSides.TWO, currentLimitTypes, limitsComputer, consumer);
    }

    private static void checkCurrent(Branch<?> branch, TwoSides side, Set<LoadingLimitType> currentLimitTypes,
                                     LimitsComputer<Identifiable<?>, LoadingLimits> limitsComputer, Consumer<LimitViolation> consumer) {
        checkLimitViolation(branch, side, branch.getTerminal(side).getI(), LimitType.CURRENT, currentLimitTypes, limitsComputer, consumer);
    }

    private static void checkCurrentDc(Branch<?> branch, double dcPowerFactor, Set<LoadingLimitType> currentLimitTypes,
                                       LimitsComputer<Identifiable<?>, LoadingLimits> limitsComputer, Consumer<LimitViolation> consumer) {
        checkCurrentDc(branch, TwoSides.ONE, dcPowerFactor, currentLimitTypes, limitsComputer, consumer);
        checkCurrentDc(branch, TwoSides.TWO, dcPowerFactor, currentLimitTypes, limitsComputer, consumer);
    }

    private static void checkCurrentDc(Branch<?> branch, TwoSides side, double dcPowerFactor,
                                       Set<LoadingLimitType> currentLimitTypes, LimitsComputer<Identifiable<?>, LoadingLimits> limitsComputer,
                                       Consumer<LimitViolation> consumer) {
        double i = getTerminalIOrAnApproximation(branch.getTerminal(side), dcPowerFactor);
        checkLimitViolation(branch, side, i, LimitType.CURRENT, currentLimitTypes, limitsComputer, consumer);
    }

    public static double getTerminalIOrAnApproximation(Terminal terminal, double dcPowerFactor) {
        // After a DC load flow, the current at terminal can be undefined (NaN). In that case, we use the DC power factor,
        // the nominal voltage and the active power at terminal in order to approximate the current following formula
        // P = sqrt(3) x Vnom x I x dcPowerFactor
        return Double.isNaN(terminal.getI()) ?
                (1000. * terminal.getP()) / (terminal.getVoltageLevel().getNominalV() * Math.sqrt(3) * dcPowerFactor)
                : terminal.getI();
    }

    static void checkLimitViolation(Branch<?> branch, TwoSides side, double value, LimitType type,
                                    Set<LoadingLimitType> currentLimitTypes, LimitsComputer<Identifiable<?>, LoadingLimits> limitsComputer,
                                    Consumer<LimitViolation> consumer) {
        boolean overloadOnTemporary = false;
        if (currentLimitTypes.contains(LoadingLimitType.TATL)) {
            Overload overload = LimitViolationUtils.checkTemporaryLimits(branch, side, limitsComputer, value, type);
            if (overload != null) {
                consumer.accept(new LimitViolation(branch.getId(),
                        branch.getOptionalName().orElse("null"),
                        toLimitViolationType(type),
                        overload.getPreviousLimitName(),
                        overload.getTemporaryLimit().getAcceptableDuration(),
                        overload.getPreviousLimit(),
                        overload.getLimitReductionCoefficient(),
                        value,
                        side));
                overloadOnTemporary = true;
            }
        }
        if (!overloadOnTemporary && currentLimitTypes.contains(LoadingLimitType.PATL)) {
            PermanentLimitCheckResult overload = LimitViolationUtils.checkPermanentLimit(branch, side, value, type, limitsComputer);
            if (overload.isOverload()) {
                double limit = branch.getLimits(type, side).map(LoadingLimits::getPermanentLimit).orElseThrow(PowsyblException::new);
                consumer.accept(new LimitViolation(branch.getId(),
                    branch.getOptionalName().orElse(null),
                    toLimitViolationType(type),
                    LimitViolationUtils.PERMANENT_LIMIT_NAME,
                    Integer.MAX_VALUE,
                    limit,
                    overload.limitReductionValue(),
                    value,
                    side));
            }
        }
    }

    private static void checkCurrent(ThreeWindingsTransformer transformer, Set<LoadingLimitType> currentLimitTypes,
                                     LimitsComputer<Identifiable<?>, LoadingLimits> limitsComputer, Consumer<LimitViolation> consumer) {
        checkCurrent(transformer, ThreeSides.ONE, currentLimitTypes, limitsComputer, consumer);
        checkCurrent(transformer, ThreeSides.TWO, currentLimitTypes, limitsComputer, consumer);
        checkCurrent(transformer, ThreeSides.THREE, currentLimitTypes, limitsComputer, consumer);
    }

    private static void checkCurrent(ThreeWindingsTransformer transformer, ThreeSides side,
                                     Set<LoadingLimitType> currentLimitTypes, LimitsComputer<Identifiable<?>, LoadingLimits> limitsComputer,
                                     Consumer<LimitViolation> consumer) {
        checkLimitViolation(transformer, side, transformer.getTerminal(side).getI(), LimitType.CURRENT, currentLimitTypes, limitsComputer, consumer
        );
    }

    private static void checkCurrentDc(ThreeWindingsTransformer transformer, double dcPowerFactor,
                                       Set<LoadingLimitType> currentLimitTypes, LimitsComputer<Identifiable<?>, LoadingLimits> limitsComputer,
                                       Consumer<LimitViolation> consumer) {
        checkCurrentDc(transformer, ThreeSides.ONE, dcPowerFactor, currentLimitTypes, limitsComputer, consumer);
        checkCurrentDc(transformer, ThreeSides.TWO, dcPowerFactor, currentLimitTypes, limitsComputer, consumer);
        checkCurrentDc(transformer, ThreeSides.THREE, dcPowerFactor, currentLimitTypes, limitsComputer, consumer);
    }

    private static void checkCurrentDc(ThreeWindingsTransformer transformer, ThreeSides side, double dcPowerFactor,
                                       Set<LoadingLimitType> currentLimitTypes, LimitsComputer<Identifiable<?>, LoadingLimits> limitsComputer,
                                       Consumer<LimitViolation> consumer) {
        double i = getTerminalIOrAnApproximation(transformer.getTerminal(side), dcPowerFactor);
        checkLimitViolation(transformer, side, i, LimitType.CURRENT, currentLimitTypes, limitsComputer, consumer);
    }

    static void checkLimitViolation(ThreeWindingsTransformer transformer, ThreeSides side, double value,
                                    LimitType type, Set<LoadingLimitType> currentLimitTypes, LimitsComputer<Identifiable<?>, LoadingLimits> limitsComputer,
                                    Consumer<LimitViolation> consumer) {
        boolean overloadOnTemporary = false;
        if (currentLimitTypes.contains(LoadingLimitType.TATL)) {
            Overload overload = LimitViolationUtils.checkTemporaryLimits(transformer, side, limitsComputer, value, type);
            if (overload != null) {
                consumer.accept(new LimitViolation(transformer.getId(),
                        transformer.getOptionalName().orElse(null),
                        toLimitViolationType(type),
                        overload.getPreviousLimitName(),
                        overload.getTemporaryLimit().getAcceptableDuration(),
                        overload.getPreviousLimit(),
                        overload.getLimitReductionCoefficient(),
                        value,
                        side));
                overloadOnTemporary = true;
            }
        }
        if (!overloadOnTemporary && currentLimitTypes.contains(LoadingLimitType.PATL)) {
            PermanentLimitCheckResult overload = LimitViolationUtils.checkPermanentLimit(transformer, side, limitsComputer, value, type);
            if (overload.isOverload()) {
                double limit = transformer.getLeg(side).getLimits(type).map(LoadingLimits::getPermanentLimit).orElseThrow(PowsyblException::new);
                consumer.accept(new LimitViolation(transformer.getId(),
                    transformer.getOptionalName().orElse(null),
                    toLimitViolationType(type),
                    LimitViolationUtils.PERMANENT_LIMIT_NAME,
                    Integer.MAX_VALUE,
                    limit,
                    overload.limitReductionValue(),
                    value,
                    side));
            }
        }
    }

    private static void checkVoltage(Bus bus, Consumer<LimitViolation> consumer) {
        double value = bus.getV();
        checkVoltage(bus, value, consumer);
    }

    static void checkVoltage(Bus bus, double value, Consumer<LimitViolation> consumer) {
        VoltageLevel vl = bus.getVoltageLevel();
        if (!Double.isNaN(vl.getLowVoltageLimit()) && value <= vl.getLowVoltageLimit()) {
            consumer.accept(new LimitViolation(vl.getId(), vl.getOptionalName().orElse(null), LimitViolationType.LOW_VOLTAGE,
                    vl.getLowVoltageLimit(), 1., value, createViolationLocation(bus)));
        }

        if (!Double.isNaN(vl.getHighVoltageLimit()) && value >= vl.getHighVoltageLimit()) {
            consumer.accept(new LimitViolation(vl.getId(), vl.getOptionalName().orElse(null), LimitViolationType.HIGH_VOLTAGE,
                    vl.getHighVoltageLimit(), 1., value, createViolationLocation(bus)));
        }
    }

    /**
     * Helper function to convert a limit type to a limit violation type
     *
     * @param type The limit type to convert.
     * @return The matching LimitViolationTYpe
     */
    public static LimitViolationType toLimitViolationType(LimitType type) {
        return switch (type) {
            case ACTIVE_POWER -> LimitViolationType.ACTIVE_POWER;
            case APPARENT_POWER -> LimitViolationType.APPARENT_POWER;
            case CURRENT -> LimitViolationType.CURRENT;
            default ->
                    throw new UnsupportedOperationException(String.format("Unsupported conversion for %s from limit type to limit violation type.", type.name()));
        };
    }

    private static void checkVoltageAngle(VoltageAngleLimit voltageAngleLimit, Consumer<LimitViolation> consumer) {
        Bus referenceBus = voltageAngleLimit.getTerminalFrom().getBusView().getBus();
        Bus otherBus = voltageAngleLimit.getTerminalTo().getBusView().getBus();
        if (referenceBus != null && otherBus != null
                && referenceBus.getConnectedComponent().getNum() == otherBus.getConnectedComponent().getNum()
                && referenceBus.getSynchronousComponent().getNum() == otherBus.getSynchronousComponent().getNum()) {
            double voltageAngleDifference = otherBus.getAngle() - referenceBus.getAngle();
            checkVoltageAngle(voltageAngleLimit, voltageAngleDifference, consumer);
        }
    }

    static void checkVoltageAngle(VoltageAngleLimit voltageAngleLimit, double value, Consumer<LimitViolation> consumer) {
        if (Double.isNaN(value)) {
            return;
        }
        voltageAngleLimit.getLowLimit().ifPresent(
                lowLimit -> {
                    if (value <= lowLimit) {
                        consumer.accept(new LimitViolation(voltageAngleLimit.getId(), LimitViolationType.LOW_VOLTAGE_ANGLE, lowLimit,
                                1., value));
                    }
                });
        voltageAngleLimit.getHighLimit().ifPresent(
                highLimit -> {
                    if (value >= highLimit) {
                        consumer.accept(new LimitViolation(voltageAngleLimit.getId(), LimitViolationType.HIGH_VOLTAGE_ANGLE, highLimit,
                                1., value));
                    }
                });
    }

    public static ViolationLocation createViolationLocation(Bus bus) {
        VoltageLevel vl = bus.getVoltageLevel();
        if (vl.getTopologyKind() == TopologyKind.NODE_BREAKER) {
            List<Integer> nodes = Networks.getNodesByBus(vl).get(bus.getId()).stream().toList();
            return new NodeBreakerViolationLocation(vl.getId(), nodes);
        } else {
            try {
                List<String> configuredBusIds = vl.getBusBreakerView().getBusStreamFromBusViewBusId(bus.getId())
                        .map(Identifiable::getId)
                        .sorted().toList();
                return new BusBreakerViolationLocation(configuredBusIds);
            } catch (Exception e) {
                LOGGER.error("Error generating ViolationLocation", e);
                return null;
            }
        }
    }
}