LoadingLimitsUtil.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.iidm.network.util;

import com.powsybl.iidm.network.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.Comparator;
import java.util.function.Function;

import static java.lang.Integer.MAX_VALUE;

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

    private LoadingLimitsUtil() {
    }

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

    /**
     * Interface for objects used to report the performed operation on limits when fixed by
     * {@link #fixMissingPermanentLimit(LoadingLimitsAdder, double, String, LimitFixLogger)}.
     */
    public interface LimitFixLogger {
        LimitFixLogger NO_OP = (what, reason, wrongValue, fixedValue) -> {
        };

        void log(String what, String reason, double wrongValue, double fixedValue);
    }

    /**
     * Comparator for temporary limits
     * to return the temporary limits ordered by descending acceptable duration
     */
    public static final Comparator<Integer> ACCEPTABLE_DURATION_COMPARATOR = (acceptableDuration1, acceptableDuration2) -> acceptableDuration2 - acceptableDuration1;

    /**
     * <p>Compute a missing permanent limit accordingly to the temporary limits and to a given percentage.</p>
     *
     * @param limitsAdder                     the LoadingLimitsAdder which permanent limit should be fixed
     * @param missingPermanentLimitPercentage The percentage to apply
     */
    public static <L extends LoadingLimits, A extends LoadingLimitsAdder<L, A>> void fixMissingPermanentLimit(LoadingLimitsAdder<L, A> limitsAdder,
                                                                                                              double missingPermanentLimitPercentage) {
        fixMissingPermanentLimit(limitsAdder, missingPermanentLimitPercentage, "", LimitFixLogger.NO_OP);
    }

    /**
     * <p>Compute a missing permanent limit accordingly to the temporary limits and to a given percentage.</p>
     *
     * @param adder                           the LoadingLimitsAdder which permanent limit should be fixed
     * @param missingPermanentLimitPercentage The percentage to apply
     * @param ownerId                         id of the limits' network element. It is only used for reporting purposes.
     * @param limitFixLogger                  the object used to report the performed operation on the permanent limit.
     */
    public static <L extends LoadingLimits, A extends LoadingLimitsAdder<L, A>> void fixMissingPermanentLimit(LoadingLimitsAdder<L, A> adder, double missingPermanentLimitPercentage,
                                                                                                              String ownerId, LimitFixLogger limitFixLogger) {
        if (!Double.isNaN(adder.getPermanentLimit())) {
            return;
        }

        double lowestTemporaryLimitWithInfiniteAcceptableDuration = MAX_VALUE;
        boolean hasTemporaryLimitWithInfiniteAcceptableDuration = false;
        for (String name : adder.getTemporaryLimitNames()) {
            if (adder.getTemporaryLimitAcceptableDuration(name) == MAX_VALUE) {
                hasTemporaryLimitWithInfiniteAcceptableDuration = true;
                lowestTemporaryLimitWithInfiniteAcceptableDuration =
                        Math.min(lowestTemporaryLimitWithInfiniteAcceptableDuration, adder.getTemporaryLimitValue(name));
                adder.removeTemporaryLimit(name);
            }
        }

        if (hasTemporaryLimitWithInfiniteAcceptableDuration) {
            limitFixLogger.log("Operational Limits of " + ownerId,
                    "Operational limits without permanent limit is considered with permanent limit " +
                            "equal to lowest temporary limit value with infinite acceptable duration",
                    Double.NaN, lowestTemporaryLimitWithInfiniteAcceptableDuration);
            adder.setPermanentLimit(lowestTemporaryLimitWithInfiniteAcceptableDuration);
        } else {
            double firstTemporaryLimit = adder.getLowestTemporaryLimitValue();
            double percentage = missingPermanentLimitPercentage / 100.;
            double fixedPermanentLimit = firstTemporaryLimit * percentage;
            limitFixLogger.log("Operational Limits of " + ownerId,
                    "Operational limits without permanent limit is considered with permanent limit " +
                            "equal to lowest temporary limit value weighted by a coefficient of " + percentage + ".",
                    Double.NaN, fixedPermanentLimit);
            adder.setPermanentLimit(fixedPermanentLimit);
        }
    }

    /**
     * <p>Initialize an adder filled with a copy of an existing limits set</p>
     *
     * @param adder  the empty adder in which we initialize the new limits
     * @param limits the limits to copy
     */
    public static <L extends LoadingLimits, A extends LoadingLimitsAdder<L, A>> A initializeFromLoadingLimits(A adder, L limits) {
        if (limits == null) {
            LOGGER.warn("Created adder is empty");
            return adder;
        }
        adder.setPermanentLimit(limits.getPermanentLimit());
        limits.getTemporaryLimits().forEach(limit ->
                adder.beginTemporaryLimit()
                        .setName(limit.getName())
                        .setAcceptableDuration(limit.getAcceptableDuration())
                        .setValue(limit.getValue())
                        .setFictitious(limit.isFictitious())
                        .endTemporaryLimit());
        return adder;
    }

    /**
     * <p>Copies every limit from each operational limits group.</p>
     *
     * @param copiedBranch the copied object
     * @param branch the object on which the copied object attributes are copied
     */
    public static <I extends Branch<I>> void copyOperationalLimits(I copiedBranch, I branch) {
        if (copiedBranch != null) {
            copyOperationalLimits(copiedBranch.getOperationalLimitsGroups1(), branch::newOperationalLimitsGroup1);
            copiedBranch.getSelectedOperationalLimitsGroupId1().ifPresent(branch::setSelectedOperationalLimitsGroup1);

            copyOperationalLimits(copiedBranch.getOperationalLimitsGroups2(), branch::newOperationalLimitsGroup2);
            copiedBranch.getSelectedOperationalLimitsGroupId2().ifPresent(branch::setSelectedOperationalLimitsGroup2);
        }
    }

    private static void copyOperationalLimits(Collection<OperationalLimitsGroup> from,
                                              Function<String, OperationalLimitsGroup> createGroup) {
        from.forEach(groupToCopy -> {
            OperationalLimitsGroup copy = createGroup.apply(groupToCopy.getId());
            groupToCopy.getCurrentLimits().ifPresent(limit -> copy.newCurrentLimits(limit).add());
            groupToCopy.getActivePowerLimits().ifPresent(limit -> copy.newActivePowerLimits(limit).add());
            groupToCopy.getApparentPowerLimits().ifPresent(limit -> copy.newApparentPowerLimits(limit).add());
        });
    }
}