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.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.OptionalDouble;
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 List<ParticipatingElement> getParticipatingElements(Collection<LfBus> buses, OptionalDouble mismatch) {
        Boolean positiveMismatch = mismatch.isPresent() ? mismatch.getAsDouble() > 0 : null;
        return buses.stream()
                .filter(bus -> bus.isParticipating() && !bus.isDisabled() && !bus.isFictitious())
                .flatMap(bus -> bus.getGenerators().stream())
                .map(gen -> {
                    double factor = getParticipationFactor(gen, positiveMismatch);
                    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;
        double mismatch = remainingMismatch;
        if (iteration == 0) {
            // "undo" everything from targetP to go back to initialP
            for (ParticipatingElement participatingGenerator : participatingElements) {
                LfGenerator generator = (LfGenerator) participatingGenerator.getElement();
                done += generator.getInitialTargetP() - generator.getTargetP();
                mismatch -= generator.getInitialTargetP() - generator.getTargetP();
                generator.setTargetP(generator.getInitialTargetP());
            }
        }

        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 + mismatch * factor;
            if (mismatch > 0 && newTargetP > maxTargetP) {
                newTargetP = maxTargetP;
                generatorsAtMax++;
                it.remove();
            } else if (mismatch < 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, mismatch * PerUnit.SB, iteration, modifiedBuses,
                generatorsAtMax, generatorsAtMin);

        return done;
    }

    private double getParticipationFactor(LfGenerator generator, Boolean positiveMismatch) {
        return switch (participationType) {
            case MAX -> generator.getMaxP() / generator.getDroop();
            case TARGET -> Math.abs(generator.getTargetP());
            case PARTICIPATION_FACTOR -> generator.getParticipationFactor();
            case REMAINING_MARGIN -> {
                if (positiveMismatch == null) {
                    throw new PowsyblException("The sign of the active power mismatch is unknown, it is mandatory for REMAINING_MARGIN participation type");
                }
                yield Boolean.TRUE.equals(positiveMismatch) ? Math.max(0.0, generator.getMaxP() - generator.getTargetP()) : Math.max(0.0, generator.getTargetP() - generator.getMinP());
            }
        };
    }

    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);
        }
    }
}