DistributedSlackOnGenerationTest.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.ac;

import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.computation.local.LocalComputationManager;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.extensions.ActivePowerControl;
import com.powsybl.iidm.network.extensions.ReferencePriorities;
import com.powsybl.iidm.network.extensions.ReferencePriority;
import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory;
import com.powsybl.loadflow.LoadFlow;
import com.powsybl.loadflow.LoadFlowParameters;
import com.powsybl.loadflow.LoadFlowResult;
import com.powsybl.math.matrix.DenseMatrixFactory;
import com.powsybl.openloadflow.OpenLoadFlowParameters;
import com.powsybl.openloadflow.OpenLoadFlowProvider;
import com.powsybl.openloadflow.network.DistributedSlackNetworkFactory;
import com.powsybl.openloadflow.network.EurostagFactory;
import com.powsybl.openloadflow.network.FirstSlackBusSelector;
import com.powsybl.openloadflow.network.LfBus;
import com.powsybl.openloadflow.network.LfNetwork;
import com.powsybl.openloadflow.network.ReferenceBusSelectionMode;
import com.powsybl.openloadflow.network.SlackBusSelectionMode;
import com.powsybl.openloadflow.network.impl.LfNetworkLoaderImpl;
import com.powsybl.openloadflow.network.util.ActivePowerDistribution;
import com.powsybl.openloadflow.util.LoadFlowAssert;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.*;
import java.util.concurrent.CompletionException;

import static com.powsybl.openloadflow.util.LoadFlowAssert.*;
import static org.junit.jupiter.api.Assertions.*;

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

    private Network network;
    private Generator g1;
    private Generator g2;
    private Generator g3;
    private Generator g4;
    private LoadFlow.Runner loadFlowRunner;
    private LoadFlowParameters parameters;
    private OpenLoadFlowParameters parametersExt;
    private ReportNode reportNode;

    @BeforeEach
    void setUp() {
        network = DistributedSlackNetworkFactory.create();
        g1 = network.getGenerator("g1");
        g2 = network.getGenerator("g2");
        g3 = network.getGenerator("g3");
        g4 = network.getGenerator("g4");
        loadFlowRunner = new LoadFlow.Runner(new OpenLoadFlowProvider(new DenseMatrixFactory()));
        // Note that in core, default balance type is proportional to generation Pmax
        parameters = new LoadFlowParameters().setUseReactiveLimits(false)
                .setDistributedSlack(true);
        parametersExt = OpenLoadFlowParameters.create(parameters)
                .setSlackBusSelectionMode(SlackBusSelectionMode.MOST_MESHED)
                .setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.THROW);
        reportNode = ReportNode.newRootReportNode().withMessageTemplate("test").build();
    }

    @Test
    void test() {
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertEquals(1, result.getComponentResults().size());
        assertEquals(120, result.getComponentResults().get(0).getDistributedActivePower(), LoadFlowAssert.DELTA_POWER);
        assertActivePowerEquals(-115, g1.getTerminal());
        assertActivePowerEquals(-245, g2.getTerminal());
        assertActivePowerEquals(-105, g3.getTerminal());
        assertActivePowerEquals(-135, g4.getTerminal());
        assertReactivePowerEquals(159.746, g1.getTerminal());
        Line l14 = network.getLine("l14");
        Line l24 = network.getLine("l24");
        Line l34 = network.getLine("l34");
        assertActivePowerEquals(115, l14.getTerminal1());
        assertActivePowerEquals(-115, l14.getTerminal2());
        assertActivePowerEquals(245, l24.getTerminal1());
        assertActivePowerEquals(-245, l24.getTerminal2());
        assertActivePowerEquals(240, l34.getTerminal1());
        assertActivePowerEquals(-240, l34.getTerminal2());
    }

    @Test
    void testProportionalToP() {
        // decrease g1 max limit power, so that distributed slack algo reach the g1 max
        g1.setMaxP(105);
        parameters.setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_P);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);

        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-105, g1.getTerminal());
        assertActivePowerEquals(-260.526, g2.getTerminal());
        assertActivePowerEquals(-117.236, g3.getTerminal());
        assertActivePowerEquals(-117.236, g4.getTerminal());
    }

    @Test
    void testProportionalToPWithTargetLimit() {
        // decrease g1 max limit power, so that distributed slack algo reach the g1 max
        g1.setMaxP(105);
        g1.getExtension(ActivePowerControl.class).setMaxTargetP(103);
        parameters.setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_P);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);

        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-103, g1.getTerminal());
        assertActivePowerEquals(-261.579, g2.getTerminal());
        assertActivePowerEquals(-117.711, g3.getTerminal());
        assertActivePowerEquals(-117.711, g4.getTerminal());

        // now compensation down
        Load l1 = network.getLoad("l1");
        l1.setP0(400);  // was 600
        result = loadFlowRunner.run(network, parameters);

        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-83.333, g1.getTerminal());
        assertActivePowerEquals(-166.667, g2.getTerminal());
        assertActivePowerEquals(-75.000, g3.getTerminal());
        assertActivePowerEquals(-75.000, g4.getTerminal());

        // With a minTargetP for g1
        g1.getExtension(ActivePowerControl.class).setMinTargetP(85);

        result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-85, g1.getTerminal());
        assertActivePowerEquals(-165.790, g2.getTerminal());
        assertActivePowerEquals(-74.605, g3.getTerminal());
        assertActivePowerEquals(-74.605, g4.getTerminal());
    }

    @Test
    void testProportionalToPMaxWithTargetLimit() {
        g1.setMaxP(150);
        parameters.setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_P_MAX);
        //Check that maxTargetP has no influence on the computed participation factor but only on the limit hitting
        g1.getExtension(ActivePowerControl.class).setMaxTargetP(120);

        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-111.613, g1.getTerminal());
        assertActivePowerEquals(-246.452, g2.getTerminal());
        assertActivePowerEquals(-105.484, g3.getTerminal());
        assertActivePowerEquals(-136.451, g4.getTerminal());

        g1.getExtension(ActivePowerControl.class).setDroop(2.0); //Changing droop to change corresponding participation factor
        result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-120.0, g1.getTerminal()); // This time the limit is hit
        assertActivePowerEquals(-242.857, g2.getTerminal());
        assertActivePowerEquals(-104.285, g3.getTerminal());
        assertActivePowerEquals(-132.857, g4.getTerminal());
    }

    @Test
    @SuppressWarnings("unchecked")
    void testProportionalToParticipationFactor() {
        // decrease g1 max limit power, so that distributed slack algo reach the g1 max
        g1.setMaxP(100);

        // set participationFactor
        // g1 NaN participationFactor should be discarded
        g1.getExtension(ActivePowerControl.class).setParticipationFactor(Double.NaN);
        g2.getExtension(ActivePowerControl.class).setParticipationFactor(3.0);
        g3.getExtension(ActivePowerControl.class).setParticipationFactor(1.0);
        g4.getExtension(ActivePowerControl.class).setParticipationFactor(-4.0); // Should be discarded

        parameters.setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_PARTICIPATION_FACTOR);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);

        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-100, g1.getTerminal());
        assertActivePowerEquals(-290, g2.getTerminal());
        assertActivePowerEquals(-120, g3.getTerminal());
        assertActivePowerEquals(-90, g4.getTerminal());
    }

    @Test
    void testProportionalToRemainingMarginUp() {
        // decrease g1 max limit power, so that distributed slack algo reach the g1 max
        g1.setMaxP(105);

        parameters.setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_REMAINING_MARGIN);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);

        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-102.667, g1.getTerminal());
        assertActivePowerEquals(-253.333, g2.getTerminal());
        assertActivePowerEquals(-122.0, g3.getTerminal());
        assertActivePowerEquals(-122.0, g4.getTerminal());
    }

    @Test
    void testProportionalToRemainingMarginDown() {
        // Decrease load P0, so that active mismatch is negative
        network.getLoad("l1").setP0(400);

        parameters.setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_REMAINING_MARGIN);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);

        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-71.428, g1.getTerminal());
        assertActivePowerEquals(-171.428, g2.getTerminal());
        assertActivePowerEquals(-78.571, g3.getTerminal());
        assertActivePowerEquals(-78.571, g4.getTerminal());
    }

    @Test
    void testGetParticipatingElementsWithMismatch() {
        LfNetwork lfNetwork = LfNetwork.load(network, new LfNetworkLoaderImpl(), new FirstSlackBusSelector(Set.of())).get(0);
        final OptionalDouble mismatch = OptionalDouble.of(30);
        final List<LfBus> buses = lfNetwork.getBuses();
        for (LoadFlowParameters.BalanceType balanceType : LoadFlowParameters.BalanceType.values()) {
            ActivePowerDistribution.Step step = ActivePowerDistribution.getStep(balanceType, parametersExt.isLoadPowerFactorConstant(), parametersExt.isUseActiveLimits());
            switch (balanceType) {
                case PROPORTIONAL_TO_GENERATION_P_MAX, PROPORTIONAL_TO_GENERATION_P, PROPORTIONAL_TO_GENERATION_REMAINING_MARGIN -> assertEquals(4, step.getParticipatingElements(buses, mismatch).size());
                case PROPORTIONAL_TO_LOAD, PROPORTIONAL_TO_CONFORM_LOAD -> assertEquals(1, step.getParticipatingElements(buses, mismatch).size());
                case PROPORTIONAL_TO_GENERATION_PARTICIPATION_FACTOR -> assertEquals(0, step.getParticipatingElements(buses, mismatch).size());
            }
        }
    }

    @Test
    void testGetParticipatingElementsWithoutMismatch() {
        LfNetwork lfNetwork = LfNetwork.load(network, new LfNetworkLoaderImpl(), new FirstSlackBusSelector(Set.of())).get(0);
        final OptionalDouble emptyMismatch = OptionalDouble.empty();
        final List<LfBus> buses = lfNetwork.getBuses();
        for (LoadFlowParameters.BalanceType balanceType : LoadFlowParameters.BalanceType.values()) {
            ActivePowerDistribution.Step step = ActivePowerDistribution.getStep(balanceType, parametersExt.isLoadPowerFactorConstant(), parametersExt.isUseActiveLimits());
            switch (balanceType) {
                case PROPORTIONAL_TO_GENERATION_P_MAX, PROPORTIONAL_TO_GENERATION_P -> assertEquals(4, step.getParticipatingElements(buses, emptyMismatch).size());
                case PROPORTIONAL_TO_LOAD, PROPORTIONAL_TO_CONFORM_LOAD -> assertEquals(1, step.getParticipatingElements(buses, emptyMismatch).size());
                case PROPORTIONAL_TO_GENERATION_PARTICIPATION_FACTOR -> assertEquals(0, step.getParticipatingElements(buses, emptyMismatch).size());
                case PROPORTIONAL_TO_GENERATION_REMAINING_MARGIN -> assertThrows(PowsyblException.class, () -> step.getParticipatingElements(buses, emptyMismatch),
                        "The sign of the active power mismatch is unknown, it is mandatory for REMAINING_MARGIN participation type");
            }
        }
    }

    @Test
    void testProportionalToRemainingMarginPmaxBelowTargetP() {
        // decrease g1 max limit power below target P
        g1.setMaxP(90);

        parameters.setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_REMAINING_MARGIN);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);

        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-100.0, g1.getTerminal());
        assertActivePowerEquals(-254.545, g2.getTerminal());
        assertActivePowerEquals(-122.727, g3.getTerminal());
        assertActivePowerEquals(-122.727, g4.getTerminal());
    }

    @Test
    void maxTest() {
        // decrease g1 max limit power, so that distributed slack algo reach the g1 max
        g1.setMaxP(105);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-105, g1.getTerminal());
        assertActivePowerEquals(-249.285, g2.getTerminal());
        assertActivePowerEquals(-106.428, g3.getTerminal());
        assertActivePowerEquals(-139.285, g4.getTerminal());
    }

    @Test
    void minTest() {
        // increase g1 min limit power and global load so that distributed slack algo reach the g1 min
        g1.setMinP(95);
        network.getLoad("l1").setP0(400);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-95, g1.getTerminal());
        assertActivePowerEquals(-167.857, g2.getTerminal());
        assertActivePowerEquals(-79.285, g3.getTerminal());
        assertActivePowerEquals(-57.857, g4.getTerminal());
    }

    @Test
    void maxTestActivePowerLimitDisabled() {
        parametersExt.setUseActiveLimits(false);
        // decrease g1 max limit power, so that distributed slack algo reach the g1 max
        // Because we disabled active power limits, g1 will exceed max
        g1.setMaxP(105);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-108.372, g1.getTerminal());
        assertActivePowerEquals(-247.840, g2.getTerminal());
        assertActivePowerEquals(-105.946, g3.getTerminal());
        assertActivePowerEquals(-137.840, g4.getTerminal());
        assertEquals(120, result.getComponentResults().get(0).getDistributedActivePower(), LoadFlowAssert.DELTA_POWER);
    }

    @Test
    void minTestActivePowerLimitDisabled() {
        parametersExt.setUseActiveLimits(false);
        // increase g1 min limit power and global load so that distributed slack algo reach the g1 min
        // Because we disabled active power limits, g1 will exceed min
        g1.setMinP(95);
        network.getLoad("l1").setP0(400);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-90, g1.getTerminal());
        assertActivePowerEquals(-170, g2.getTerminal());
        assertActivePowerEquals(-80, g3.getTerminal());
        assertActivePowerEquals(-60, g4.getTerminal());
    }

    @Test
    void targetBelowMinAndActivePowerLimitDisabled() {
        parametersExt.setUseActiveLimits(false);
        g1.setMinP(100); // was 0
        g1.setTargetP(80);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-97.5, g1.getTerminal()); // allowed to participate even though targetP < minP
        assertActivePowerEquals(-252.5, g2.getTerminal());
        assertActivePowerEquals(-107.5, g3.getTerminal());
        assertActivePowerEquals(-142.5, g4.getTerminal());
        assertEquals(140, result.getComponentResults().get(0).getDistributedActivePower(), LoadFlowAssert.DELTA_POWER);
    }

    @Test
    void targetAboveMaxAndActivePowerLimitDisabled() {
        parametersExt.setUseActiveLimits(false);
        g1.setTargetP(240); // max is 200
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-237.5, g1.getTerminal()); // allowed to participate even though targetP > maxP
        assertActivePowerEquals(-192.5, g2.getTerminal());
        assertActivePowerEquals(-87.5, g3.getTerminal());
        assertActivePowerEquals(-82.5, g4.getTerminal());
        assertEquals(-20.0, result.getComponentResults().get(0).getDistributedActivePower(), LoadFlowAssert.DELTA_POWER);
    }

    @Test
    void targetBelowPositiveMinTest() {
        // g1 targetP below positive minP (e.g. unit starting up / ramping)
        g1.setMinP(100);
        g1.setTargetP(80);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-80.0, g1.getTerminal()); // stays at targetP
        assertActivePowerEquals(-260.0, g2.getTerminal());
        assertActivePowerEquals(-110.0, g3.getTerminal());
        assertActivePowerEquals(-150.0, g4.getTerminal());
        assertEquals(140, result.getComponentResults().get(0).getDistributedActivePower(), LoadFlowAssert.DELTA_POWER);
    }

    @Test
    void targetBelowZeroMinTest() {
        // g1 targetP below zero minP (e.g. unit modelled lumped with station supply and not producing but consuming a little bit)
        g1.setMinP(0);
        g1.setTargetP(-20);
        network.getLoad("l1").setP0(500);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(20.0, g1.getTerminal()); // stays at targetP
        assertActivePowerEquals(-260.0, g2.getTerminal());
        assertActivePowerEquals(-110.0, g3.getTerminal());
        assertActivePowerEquals(-150.0, g4.getTerminal());
        assertEquals(140, result.getComponentResults().get(0).getDistributedActivePower(), LoadFlowAssert.DELTA_POWER);
    }

    @Test
    void targetBelowNegativeMinTest() {
        // g1 targetP below negative minP (e.g. generator pumping more than tech limit)
        g1.setMinP(-100);
        g1.setTargetP(-120);
        network.getLoad("l1").setP0(400);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(120.0, g1.getTerminal()); // stays at targetP
        assertActivePowerEquals(-260.0, g2.getTerminal());
        assertActivePowerEquals(-110.0, g3.getTerminal());
        assertActivePowerEquals(-150.0, g4.getTerminal());
        assertEquals(140, result.getComponentResults().get(0).getDistributedActivePower(), LoadFlowAssert.DELTA_POWER);
    }

    @Test
    @SuppressWarnings("unchecked")
    void zeroParticipatingGeneratorsThrowTest() {
        g1.getExtension(ActivePowerControl.class).setDroop(2);
        g2.getExtension(ActivePowerControl.class).setDroop(-3);
        g3.getExtension(ActivePowerControl.class).setDroop(0);
        g4.getExtension(ActivePowerControl.class).setDroop(0);
        CompletionException thrown = assertThrows(CompletionException.class, () -> loadFlowRunner.run(network, parameters));
        assertTrue(thrown.getCause().getMessage().startsWith("Failed to distribute slack bus active power mismatch, "));
    }

    @Test
    void notEnoughActivePowerThrowTest() {
        network.getLoad("l1").setP0(1000);
        CompletionException thrown = assertThrows(CompletionException.class, () -> loadFlowRunner.run(network, parameters));
        assertTrue(thrown.getCause().getMessage().startsWith("Failed to distribute slack bus active power mismatch, "));
    }

    @Test
    void notEnoughActivePowerFailTest() {
        network.getLoad("l1").setP0(1000);
        parametersExt.setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.FAIL);
        LoadFlowResult result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, reportNode);
        LoadFlowResult.ComponentResult componentResult = result.getComponentResults().get(0);
        assertFalse(result.isFullyConverged());
        assertEquals(LoadFlowResult.ComponentResult.Status.FAILED, componentResult.getStatus());
        assertEquals("Outer loop failed: Failed to distribute slack bus active power mismatch, 200.00 MW remains", componentResult.getStatusText());
        assertEquals(0, componentResult.getDistributedActivePower(), 1e-4);
        assertEquals(520, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-4);
        assertReportContains("Failed to distribute slack bus active power mismatch, [-+]?\\d*\\.\\d* MW remains", reportNode);
    }

    @Test
    void notEnoughActivePowerLeaveOnSlackBusTest() {
        network.getLoad("l1").setP0(1000);
        parametersExt.setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.LEAVE_ON_SLACK_BUS);
        LoadFlowResult result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, reportNode);
        LoadFlowResult.ComponentResult componentResult = result.getComponentResults().get(0);
        assertTrue(result.isFullyConverged());
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, componentResult.getStatus());
        assertEquals(320, componentResult.getDistributedActivePower(), 1e-4);
        assertEquals(200, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-4);
        assertReportContains("Failed to distribute slack bus active power mismatch, [-+]?\\d*\\.\\d* MW remains", reportNode);
    }

    @Test
    void notEnoughActivePowerDistributeReferenceGeneratorTest() {
        network.getLoad("l1").setP0(1000);
        ReferencePriority.set(g1, 1);
        g1.setMaxP(200.);
        parametersExt
                .setReferenceBusSelectionMode(ReferenceBusSelectionMode.GENERATOR_REFERENCE_PRIORITY)
                .setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.DISTRIBUTE_ON_REFERENCE_GENERATOR);
        LoadFlowResult result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, reportNode);
        LoadFlowResult.ComponentResult componentResult = result.getComponentResults().get(0);
        assertTrue(result.isFullyConverged());
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, componentResult.getStatus());
        // DistributedActivePower: 520MW, breakdown:
        // - 320MW by all 4 generators hitting maxP limit
        // - 200MW by distributing on reference generator g1
        assertEquals(520., componentResult.getDistributedActivePower(), 1e-3);
        assertEquals(0., componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3);
        assertAngleEquals(0., g1.getTerminal().getBusView().getBus());
        // can exceed maxP (200MW)
        assertActivePowerEquals(-400., g1.getTerminal());
        assertReportContains("Slack bus active power \\([-+]?\\d*\\.\\d* MW\\) distributed in 3 distribution iteration\\(s\\)", reportNode);
    }

    @Test
    void notEnoughActivePowerDistributeNoReferenceGeneratorTest() {
        network.getLoad("l1").setP0(1000);
        ReferencePriority.set(g1, 1);
        g1.setMaxP(200.);
        // We request to distribute on reference generator, but ReferenceBusSelectionMode is FIRST_SLACK.
        // FIRST_SLACK mode does not select a reference generator, therefore internally we switch to FAIL mode.
        parametersExt
                .setReferenceBusSelectionMode(ReferenceBusSelectionMode.FIRST_SLACK)
                .setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.DISTRIBUTE_ON_REFERENCE_GENERATOR);
        LoadFlowResult result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, reportNode);
        LoadFlowResult.ComponentResult componentResult = result.getComponentResults().get(0);
        assertTrue(result.isFailed());
        assertEquals(LoadFlowResult.ComponentResult.Status.FAILED, componentResult.getStatus());
        assertEquals("Outer loop failed: Failed to distribute slack bus active power mismatch, 200.00 MW remains", componentResult.getStatusText());
        assertEquals(520., componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3);
        assertReportContains("Failed to distribute slack bus active power mismatch, [-+]?\\d*\\.\\d* MW remains", reportNode);
    }

    @Test
    void generatorWithNegativeTargetP() {
        network = EurostagFactory.fix(EurostagTutorialExample1Factory.create());
        network.getGenerator("GEN").setMaxP(1000);
        network.getGenerator("GEN").setTargetP(-607);
        network.getLoad("LOAD").setP0(-600);
        network.getLoad("LOAD").setQ0(-200);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(595.328, network.getGenerator("GEN").getTerminal());
    }

    @Test
    void generatorWithMaxPEqualsToMinP() {
        network = EurostagFactory.fix(EurostagTutorialExample1Factory.create());
        network.getGenerator("GEN").setMaxP(1000);
        network.getGenerator("GEN").setMinP(1000);
        network.getGenerator("GEN").setTargetP(1000);
        assertThrows(CompletionException.class, () -> loadFlowRunner.run(network, parameters),
                "Failed to distribute slack bus active power mismatch, -393.367476483181 MW remains");
    }

    @Test
    void nonParticipatingBus() {
        //B1 and B2 are located in germany the rest is in france
        Substation b1s = network.getSubstation("b1_s");
        b1s.setCountry(Country.GE);
        Substation b2s = network.getSubstation("b2_s");
        b2s.setCountry(Country.GE);

        //Only substation located in france are used
        parameters.setCountriesToBalance(EnumSet.of(Country.FR));
        LoadFlowResult result = loadFlowRunner.run(network, parameters);

        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-100, g1.getTerminal());
        assertActivePowerEquals(-200, g2.getTerminal());
        assertActivePowerEquals(-150, g3.getTerminal());
        assertActivePowerEquals(-150, g4.getTerminal());
    }

    @Test
    void generatorWithTargetPLowerThanMinP() {
        network = EurostagFactory.fix(EurostagTutorialExample1Factory.create());
        network.getGenerator("GEN").setMaxP(1000);
        network.getGenerator("GEN").setMinP(200);
        network.getGenerator("GEN").setTargetP(100);
        network.getVoltageLevel("VLGEN").newGenerator()
                .setId("g1")
                .setBus("NGEN")
                .setConnectableBus("NGEN")
                .setEnergySource(EnergySource.THERMAL)
                .setMinP(0)
                .setMaxP(0)
                .setTargetP(0)
                .setTargetV(24.5)
                .setVoltageRegulatorOn(true)
                .add();
        assertThrows(CompletionException.class, () -> loadFlowRunner.run(network, parameters),
                "Failed to distribute slack bus active power mismatch, 504.9476825313616 MW remains");
    }

    @Test
    @SuppressWarnings("unchecked")
    void notParticipatingTest() {
        g1.getExtension(ActivePowerControl.class).setParticipate(false);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertActivePowerEquals(-100, g1.getTerminal());
        assertActivePowerEquals(-251.428, g2.getTerminal());
        assertActivePowerEquals(-107.142, g3.getTerminal());
        assertActivePowerEquals(-141.428, g4.getTerminal());
    }

    @Test
    void batteryTest() {
        network = DistributedSlackNetworkFactory.createWithBattery();
        g1 = network.getGenerator("g1");
        g2 = network.getGenerator("g2");
        g3 = network.getGenerator("g3");
        g4 = network.getGenerator("g4");
        Battery bat1 = network.getBattery("bat1");
        Battery bat2 = network.getBattery("bat2");
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertEquals(1, result.getComponentResults().size());
        assertEquals(123, result.getComponentResults().get(0).getDistributedActivePower(), LoadFlowAssert.DELTA_POWER);
        assertActivePowerEquals(-115.122, g1.getTerminal());
        assertActivePowerEquals(-245.368, g2.getTerminal());
        assertActivePowerEquals(-105.123, g3.getTerminal());
        assertActivePowerEquals(-135.369, g4.getTerminal());
        assertActivePowerEquals(-2, bat1.getTerminal());
        assertActivePowerEquals(2.983, bat2.getTerminal());
    }

    @Test
    @SuppressWarnings("unchecked")
    void batteryTestProportionalToParticipationFactor() {
        network = DistributedSlackNetworkFactory.createWithBattery();
        g1 = network.getGenerator("g1");
        g2 = network.getGenerator("g2");
        g3 = network.getGenerator("g3");
        g4 = network.getGenerator("g4");
        Battery bat1 = network.getBattery("bat1");
        Battery bat2 = network.getBattery("bat2");
        g1.getExtension(ActivePowerControl.class).setParticipationFactor(Double.NaN);
        g2.getExtension(ActivePowerControl.class).setParticipationFactor(3.0);
        g3.getExtension(ActivePowerControl.class).setParticipationFactor(1.0);
        g4.getExtension(ActivePowerControl.class).setParticipationFactor(-4.0); // Should be discarded
        parameters.setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_PARTICIPATION_FACTOR);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertEquals(1, result.getComponentResults().size());
        assertEquals(123, result.getComponentResults().get(0).getDistributedActivePower(), LoadFlowAssert.DELTA_POWER);
        assertActivePowerEquals(-100, g1.getTerminal());
        assertActivePowerEquals(-288.5, g2.getTerminal());
        assertActivePowerEquals(-119.5, g3.getTerminal());
        assertActivePowerEquals(-90, g4.getTerminal());
        assertActivePowerEquals(-2, bat1.getTerminal());
        assertActivePowerEquals(0, bat2.getTerminal());
    }

    @Test
    void testDistributedActivePower() {
        parameters.setUseReactiveLimits(true).getExtension(OpenLoadFlowParameters.class).setSlackBusPMaxMismatch(0.0001);
        network = DistributedSlackNetworkFactory.createWithLossesAndPvPqTypeSwitch();
        g1 = network.getGenerator("g1");
        g2 = network.getGenerator("g2");
        g3 = network.getGenerator("g3");
        g4 = network.getGenerator("g4");
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        // we were getting 132.47279 when computing distributedActivePower as initial NR slack - final NR slack, while difference targetP - P was only 120.1961
        var expectedDistributedActivePower = -network.getGeneratorStream().mapToDouble(g -> g.getTargetP() + g.getTerminal().getP()).sum();
        assertEquals(120.1961, expectedDistributedActivePower, LoadFlowAssert.DELTA_POWER);
        assertEquals(expectedDistributedActivePower, result.getComponentResults().get(0).getDistributedActivePower(), LoadFlowAssert.DELTA_POWER);
        assertActivePowerEquals(-115.024, g1.getTerminal());
        assertActivePowerEquals(-245.073, g2.getTerminal());
        assertActivePowerEquals(-105.024, g3.getTerminal());
        assertActivePowerEquals(-135.073, g4.getTerminal());
    }

    @Test
    void testDistributedActivePowerSlackDistributionDisabled() {
        parameters.setUseReactiveLimits(true).setDistributedSlack(false);
        network = DistributedSlackNetworkFactory.createWithLossesAndPvPqTypeSwitch();
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        // we were getting 12.307 when computing distributedActivePower as initial NR slack - final NR slack, expecting zero here
        assertEquals(0.0, result.getComponentResults().get(0).getDistributedActivePower(), LoadFlowAssert.DELTA_POWER);
    }

    @Test
    void testSlackMismatchChangingSign() {
        parameters.setUseReactiveLimits(true).getExtension(OpenLoadFlowParameters.class).setSlackBusPMaxMismatch(0.0001);
        network = DistributedSlackNetworkFactory.createWithLossesAndPvPqTypeSwitch();
        g1 = network.getGenerator("g1");
        g2 = network.getGenerator("g2");
        g3 = network.getGenerator("g3");
        g4 = network.getGenerator("g4");

        parameters.setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_PARTICIPATION_FACTOR);
        for (var g : network.getGenerators()) {
            ActivePowerControl<Generator> ext = g.getExtension(ActivePowerControl.class);
            ext.setParticipationFactor(1.0);
        }

        g1.setMaxP(110.0);
        g3.setMaxP(110.0);
        g4.setMaxP(110.0);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());

        var expectedDistributedActivePower = -network.getGeneratorStream().mapToDouble(g -> g.getTargetP() + g.getTerminal().getP()).sum();
        assertEquals(120.1976, expectedDistributedActivePower, LoadFlowAssert.DELTA_POWER);
        assertEquals(expectedDistributedActivePower, result.getComponentResults().get(0).getDistributedActivePower(), LoadFlowAssert.DELTA_POWER);

        // All generators have the same participation factor, and should increase generation by 120.1976 MW
        // generator | targetP | maxP
        // ----------|---------|-------
        //   g1      |  100    |  110  --> expected to hit limit 110MW with 10MW distributed
        //   g2      |  200    |  300  --> expected to pick up the remaining slack 70.1976 MW
        //   g3      |   90    |  110  --> expected to hit limit 110MW with 20MW distributed
        //   g4      |   90    |  110  --> expected to hit limit 110MW with 20MW distributed
        assertActivePowerEquals(-110.000, g1.getTerminal());
        assertActivePowerEquals(-270.1976, g2.getTerminal());
        assertActivePowerEquals(-110.000, g3.getTerminal());
        assertActivePowerEquals(-110.000, g4.getTerminal());
    }

    @Test
    void testSlackMismatchChangingSignReferenceGenerator() {
        parameters.setUseReactiveLimits(true).getExtension(OpenLoadFlowParameters.class).setSlackBusPMaxMismatch(0.0001);
        network = DistributedSlackNetworkFactory.createWithLossesAndPvPqTypeSwitch();
        g1 = network.getGenerator("g1");
        g2 = network.getGenerator("g2");
        g3 = network.getGenerator("g3");
        g4 = network.getGenerator("g4");

        parameters.setBalanceType(LoadFlowParameters.BalanceType.PROPORTIONAL_TO_GENERATION_PARTICIPATION_FACTOR);
        parametersExt
                .setReferenceBusSelectionMode(ReferenceBusSelectionMode.GENERATOR_REFERENCE_PRIORITY)
                .setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.DISTRIBUTE_ON_REFERENCE_GENERATOR);
        for (var g : network.getGenerators()) {
            ActivePowerControl<Generator> ext = g.getExtension(ActivePowerControl.class);
            if (g.getId().equals("g1")) {
                ext.setParticipationFactor(1.0);
            } else {
                ext.setParticipationFactor(0.0);
            }
        }
        ReferencePriorities.delete(network);
        ReferencePriority.set(g2, 1);

        g1.setMaxP(110.0);
        g3.setMaxP(110.0);
        g4.setMaxP(110.0);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());

        var expectedDistributedActivePower = -network.getGeneratorStream().mapToDouble(g -> g.getTargetP() + g.getTerminal().getP()).sum();
        assertEquals(120.2021, expectedDistributedActivePower, LoadFlowAssert.DELTA_POWER);
        assertEquals(expectedDistributedActivePower, result.getComponentResults().get(0).getDistributedActivePower(), LoadFlowAssert.DELTA_POWER);

        // Only g1 gets slack "normally" distributed, and g2 being reference generator picks up the remaining slack
        // generator | targetP | Participation Factor | Reference
        // ----------|---------|----------------------|-----------
        //   g1      |  100    |         1.0          |           --> expected to hit limit 110MW with 10MW distributed
        //   g2      |  200    |          -           |     X     --> expected to pick up the remaining slack 110.2021 MW
        //   g3      |   90    |          -           |           --> unchanged
        //   g4      |   90    |          -           |           --> unchanged
        assertActivePowerEquals(-110.000, g1.getTerminal());
        assertActivePowerEquals(-310.2021, g2.getTerminal());
        assertActivePowerEquals(-90.000, g3.getTerminal());
        assertActivePowerEquals(-90.000, g4.getTerminal());
    }

    @Test
    void testEpsilonDistribution() {
        parametersExt.setSlackBusPMaxMismatch(0.1);
        network = DistributedSlackNetworkFactory.createWithEpsilonDistribution();
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertEquals(0.0, result.getComponentResults().get(0).getSlackBusResults().get(0).getActivePowerMismatch(), LoadFlowAssert.DELTA_POWER);
        assertEquals(0.15, result.getComponentResults().get(0).getDistributedActivePower(), LoadFlowAssert.DELTA_POWER);
    }
}