GenerationActivePowerDistributionStep.java

/**
 * Copyright (c) 2019, RTE (http://www.rte-france.com)
 * Copyright (c) 2023, Coreso SA (https://www.coreso.eu/) and TSCNET Services GmbH (https://www.tscnet.eu/)
 * 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.openloadflow.network.LfBus;
import com.powsybl.openloadflow.network.LfGenerator;
import com.powsybl.openloadflow.util.PerUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

/**
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 * @author Caio Luke {@literal <caio.luke at artelys.com>}
 */
public class GenerationActivePowerDistributionStep implements ActivePowerDistribution.Step {

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

    public enum ParticipationType {
        MAX,
        TARGET,
        PARTICIPATION_FACTOR,
        REMAINING_MARGIN
    }

    private final ParticipationType participationType;

    private final boolean useActiveLimits;

    public GenerationActivePowerDistributionStep(ParticipationType pParticipationType, boolean useActiveLimits) {
        this.participationType = pParticipationType;
        this.useActiveLimits = useActiveLimits;
    }

    @Override
    public String getElementType() {
        return "generation";
    }

    @Override
    public ActivePowerDistribution.PreviousStateInfo resetToInitialState(Collection<LfBus> participatingBuses, LfGenerator referenceGenerator) {
        ActivePowerDistribution.PreviousStateInfo previousStateInfo = ActivePowerDistribution.Step.super.resetToInitialState(participatingBuses, referenceGenerator);
        double previousMismatch = 0;
        for (LfBus bus : participatingBuses) {
            for (LfGenerator generator : bus.getGenerators()) {
                if (generator.isParticipating()) {
                    previousMismatch -= generator.getInitialTargetP() - generator.getTargetP();
                    // putIfAbsent because the generator might be the reference generator (in which case it was already reinitialized)
                    previousStateInfo.previousTargetP().putIfAbsent(generator, generator.getTargetP());
                    generator.setTargetP(generator.getInitialTargetP());
                }
            }
        }
        return new ActivePowerDistribution.PreviousStateInfo(previousMismatch + previousStateInfo.previousMismatch(), previousStateInfo.previousTargetP());
    }

    @Override
    public List<ParticipatingElement> getParticipatingElements(Collection<LfBus> participatingBuses, double mismatch) {
        return participatingBuses.stream()
                .flatMap(bus -> bus.getGenerators().stream())
                .map(gen -> {
                    double factor = getParticipationFactor(gen, mismatch);
                    if (isParticipating(gen) && factor != 0) {
                        return new ParticipatingElement(gen, factor);
                    }
                    return null;
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedList::new));
    }

    @Override
    public double run(List<ParticipatingElement> participatingElements, int iteration, double remainingMismatch) {
        // normalize participation factors at each iteration start as some
        // generators might have reach a limit and have been discarded
        ParticipatingElement.normalizeParticipationFactors(participatingElements);

        double done = 0d;

        int modifiedBuses = 0;
        int generatorsAtMax = 0;
        int generatorsAtMin = 0;
        Iterator<ParticipatingElement> it = participatingElements.iterator();
        while (it.hasNext()) {
            ParticipatingElement participatingGenerator = it.next();
            LfGenerator generator = (LfGenerator) participatingGenerator.getElement();
            double factor = participatingGenerator.getFactor();

            double minTargetP = useActiveLimits ? generator.getMinTargetP() : -Double.MAX_VALUE;
            double maxTargetP = useActiveLimits ? generator.getMaxTargetP() : Double.MAX_VALUE;
            double targetP = generator.getTargetP();

            // we don't want to change the generation sign
            if (targetP < 0) {
                maxTargetP = Math.min(maxTargetP, 0);
            } else {
                minTargetP = Math.max(minTargetP, 0);
            }

            double newTargetP = targetP + remainingMismatch * factor;
            if (remainingMismatch > 0 && newTargetP > maxTargetP) {
                newTargetP = maxTargetP;
                generatorsAtMax++;
                it.remove();
            } else if (remainingMismatch < 0 && newTargetP < minTargetP) {
                newTargetP = minTargetP;
                generatorsAtMin++;
                it.remove();
            }

            if (newTargetP != targetP) {
                LOGGER.trace("Rescale '{}' active power target: {} -> {}",
                        generator.getId(), targetP * PerUnit.SB, newTargetP * PerUnit.SB);
                generator.setTargetP(newTargetP);
                done += newTargetP - targetP;
                modifiedBuses++;
            }
        }

        LOGGER.debug("{} MW / {} MW distributed at iteration {} to {} generators ({} at max power, {} at min power)",
                done * PerUnit.SB, remainingMismatch * PerUnit.SB, iteration, modifiedBuses,
                generatorsAtMax, generatorsAtMin);

        return done;
    }

    private double getParticipationFactor(LfGenerator generator, double mismatch) {
        return switch (participationType) {
            case MAX -> generator.getMaxP() / generator.getDroop();
            case TARGET -> Math.abs(generator.getTargetP());
            case PARTICIPATION_FACTOR -> generator.getParticipationFactor();
            case REMAINING_MARGIN -> {
                if (Double.isNaN(mismatch)) {
                    throw new PowsyblException("The sign of the active power mismatch is unknown, it is mandatory for REMAINING_MARGIN participation type");
                }
                // Distribution does not change sign of targetP, we take this into account in remaining margin calculation.
                // All participating generators should hit their limit together, depending on the direction and initial targetP,
                // their limit may be not only minP/maxP, but also the forbidden zero crossing.
                double targetP = generator.getTargetP();
                double minP = generator.getMinP();
                double maxP = generator.getMaxP();
                if (targetP < 0) {
                    maxP = Math.min(maxP, 0);
                } else {
                    minP = Math.max(minP, 0);
                }
                yield mismatch > 0. ? Math.max(0.0, maxP - targetP) : Math.max(0.0, targetP - minP);
            }
        };
    }

    private boolean isParticipating(LfGenerator generator) {
        // check first if generator is set to be participating
        if (!generator.isParticipating()) {
            return false;
        }
        // then depending on participation type, a generator may be found to not participate
        switch (participationType) {
            case MAX:
                return generator.getDroop() != 0;
            case PARTICIPATION_FACTOR:
                return generator.getParticipationFactor() > 0;
            case TARGET,
                 REMAINING_MARGIN:
                // nothing more to do here: the check whether TargetP is within Pmin-Pmax range
                // was already made in AbstractLfGenerator#checkActivePowerControl
                // whose result is reflected in generator.isParticipating()
                return true;
            default:
                throw new UnsupportedOperationException("Unknown balance type mode: " + participationType);
        }
    }
}