ActivePowerDistribution.java

/**
 * Copyright (c) 2020, 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.openloadflow.network.util;

import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.loadflow.LoadFlowParameters;
import com.powsybl.openloadflow.OpenLoadFlowParameters;
import com.powsybl.openloadflow.network.LfBus;
import com.powsybl.openloadflow.network.LfGenerator;
import com.powsybl.openloadflow.network.LfNetwork;
import com.powsybl.openloadflow.util.PerUnit;
import com.powsybl.openloadflow.util.Reports;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
public final class ActivePowerDistribution {

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

    /**
     * Active power residue epsilon: 10^-5 in p.u => 10^-3 in Mw
     */
    public static final double P_RESIDUE_EPS = Math.pow(10, -5);

    public interface Step {

        String getElementType();

        List<ParticipatingElement> getParticipatingElements(Collection<LfBus> buses, OptionalDouble mismatch);

        double run(List<ParticipatingElement> participatingElements, int iteration, double remainingMismatch);
    }

    public record Result(int iteration, double remainingMismatch, boolean movedBuses) { }

    private final ActivePowerDistribution.Step step;

    private ActivePowerDistribution(Step step) {
        this.step = Objects.requireNonNull(step);
    }

    public String getElementType() {
        return step.getElementType();
    }

    public Result run(LfNetwork network, double activePowerMismatch) {
        return run(network.getReferenceGenerator(), network.getBuses(), activePowerMismatch);
    }

    public Result run(LfGenerator referenceGenerator, Collection<LfBus> buses, double activePowerMismatch) {
        List<ParticipatingElement> participatingElements = step.getParticipatingElements(buses, OptionalDouble.of(activePowerMismatch));
        final Map<ParticipatingElement, Double> initialP = participatingElements.stream()
                .collect(Collectors.toUnmodifiableMap(Function.identity(), ParticipatingElement::getTargetP));

        int iteration = 0;
        double remainingMismatch = activePowerMismatch;

        if (referenceGenerator != null) {
            // "undo" everything from targetP to go back to initialP for reference generator
            remainingMismatch -= referenceGenerator.getInitialTargetP() - referenceGenerator.getTargetP();
            referenceGenerator.setTargetP(referenceGenerator.getInitialTargetP());
        }

        while (!participatingElements.isEmpty()
                && Math.abs(remainingMismatch) > P_RESIDUE_EPS) {

            if (ParticipatingElement.participationFactorNorm(participatingElements) > 0.0) {
                double done = step.run(participatingElements, iteration, remainingMismatch);
                remainingMismatch -= done;
            } else {
                break;
            }
            iteration++;
        }

        // Identify if injections moved significantly, used e.g. to establish stable/unstable outer loop status.
        // The 0.9 magic factor is to handle potential rounding issues.
        final boolean movedBuses = initialP.entrySet().stream()
                .mapToDouble(e -> Math.abs(e.getKey().getTargetP() - e.getValue())).sum() > P_RESIDUE_EPS * 0.9;

        return new Result(iteration, remainingMismatch, movedBuses);
    }

    public static ActivePowerDistribution create(LoadFlowParameters.BalanceType balanceType, boolean loadPowerFactorConstant, boolean useActiveLimits) {
        return new ActivePowerDistribution(getStep(balanceType, loadPowerFactorConstant, useActiveLimits));
    }

    public static Step getStep(LoadFlowParameters.BalanceType balanceType, boolean loadPowerFactorConstant, boolean useActiveLimits) {
        return switch (balanceType) {
            case PROPORTIONAL_TO_LOAD, PROPORTIONAL_TO_CONFORM_LOAD ->
                    new LoadActivePowerDistributionStep(loadPowerFactorConstant);
            case PROPORTIONAL_TO_GENERATION_P_MAX ->
                    new GenerationActivePowerDistributionStep(GenerationActivePowerDistributionStep.ParticipationType.MAX, useActiveLimits);
            case PROPORTIONAL_TO_GENERATION_P ->
                    new GenerationActivePowerDistributionStep(GenerationActivePowerDistributionStep.ParticipationType.TARGET, useActiveLimits);
            case PROPORTIONAL_TO_GENERATION_PARTICIPATION_FACTOR ->
                    new GenerationActivePowerDistributionStep(GenerationActivePowerDistributionStep.ParticipationType.PARTICIPATION_FACTOR, useActiveLimits);
            case PROPORTIONAL_TO_GENERATION_REMAINING_MARGIN ->
                    new GenerationActivePowerDistributionStep(GenerationActivePowerDistributionStep.ParticipationType.REMAINING_MARGIN, useActiveLimits);
        };
    }

    public record ResultWithFailureBehaviorHandling(boolean failed, String failedMessage, int iteration, double remainingMismatch, boolean movedBuses, double failedDistributedActivePower) { }

    public static ResultWithFailureBehaviorHandling handleDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior behavior,
                                                                                      LfGenerator referenceGenerator,
                                                                                      double activePowerMismatch,
                                                                                      Result result, String failMessageTemplate) {
        Objects.requireNonNull(behavior);
        Objects.requireNonNull(result);
        ResultWithFailureBehaviorHandling resultWithFailureBehaviorHandling;

        final OpenLoadFlowParameters.SlackDistributionFailureBehavior effectiveBehavior;
        // if requested behavior is to distribute on reference generator, but there is no reference generator, we fall back internally to FAIL mode
        if (OpenLoadFlowParameters.SlackDistributionFailureBehavior.DISTRIBUTE_ON_REFERENCE_GENERATOR == behavior && referenceGenerator == null) {
            effectiveBehavior = OpenLoadFlowParameters.SlackDistributionFailureBehavior.FAIL;
            LOGGER.debug("Distribution failure behavior is DISTRIBUTE_ON_REFERENCE_GENERATOR but no reference generator selected, switching to FAIL mode");
        } else {
            effectiveBehavior = behavior;
        }

        final double distributedActivePower = activePowerMismatch - result.remainingMismatch();

        if (Math.abs(result.remainingMismatch()) > ActivePowerDistribution.P_RESIDUE_EPS) {

            String statusText = String.format(Locale.US, failMessageTemplate, result.remainingMismatch() * PerUnit.SB);
            switch (effectiveBehavior) {
                case THROW ->
                        throw new PowsyblException(statusText);

                case LEAVE_ON_SLACK_BUS -> {
                    LOGGER.warn(statusText);
                    resultWithFailureBehaviorHandling = new ResultWithFailureBehaviorHandling(false, statusText, result.iteration(), result.remainingMismatch(), result.movedBuses(), 0.0);
                }
                case FAIL -> {
                    LOGGER.error(statusText);
                    // Mismatches reported in LoadFlowResult on slack bus(es) are the mismatches of the last solver (DC, NR, ...) run.
                    // Since we will not be re-running the solver, revert distributedActivePower reporting which would otherwise be misleading.
                    // Said differently, we report that we didn't distribute anything, and this is indeed consistent with the network state.
                    resultWithFailureBehaviorHandling = new ResultWithFailureBehaviorHandling(true, statusText, result.iteration(), result.remainingMismatch(), result.movedBuses(), distributedActivePower);
                }
                case DISTRIBUTE_ON_REFERENCE_GENERATOR -> {
                    Objects.requireNonNull(referenceGenerator, "No reference generator");
                    // remaining goes to reference generator, without any limit consideration
                    LOGGER.debug("{} MW distributed to reference generator '{}'",
                            result.remainingMismatch() * PerUnit.SB, referenceGenerator.getId());
                    referenceGenerator.setTargetP(referenceGenerator.getTargetP() + result.remainingMismatch());
                    // one more iteration, no more remaining mismatch, bus moved
                    resultWithFailureBehaviorHandling = new ResultWithFailureBehaviorHandling(false, statusText, result.iteration() + 1, 0.0, true, 0.0);
                }
                default -> throw new IllegalArgumentException("Unknown slackDistributionFailureBehavior");
            }
        } else {
            resultWithFailureBehaviorHandling = new ResultWithFailureBehaviorHandling(false, "", result.iteration(), result.remainingMismatch(), result.movedBuses(), 0.0);
        }

        return resultWithFailureBehaviorHandling;
    }

    public static void reportAndLogSuccess(ReportNode reportNode, double slackBusActivePowerMismatch, ResultWithFailureBehaviorHandling result) {
        Reports.reportMismatchDistributionSuccess(reportNode, slackBusActivePowerMismatch * PerUnit.SB, result.iteration());

        LOGGER.info("Slack bus active power ({} MW) distributed in {} distribution iteration(s)",
                slackBusActivePowerMismatch * PerUnit.SB, result.iteration());
    }
}