GeneratorRemoteControlPQSwitchTest.java

/*
 * Copyright (c) 2024-2025, 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.ac;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.commons.test.PowsyblCoreTestReportResourceBundle;
import com.powsybl.computation.local.LocalComputationManager;
import com.powsybl.iidm.network.Bus;
import com.powsybl.iidm.network.Generator;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.NetworkFactory;
import com.powsybl.iidm.network.Substation;
import com.powsybl.iidm.network.TopologyKind;
import com.powsybl.iidm.network.TwoWindingsTransformer;
import com.powsybl.iidm.network.VoltageLevel;
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.ac.outerloop.ReactiveLimitsOuterLoop;
import com.powsybl.openloadflow.network.SlackBusSelectionMode;
import com.powsybl.openloadflow.util.LoadFlowAssert;
import com.powsybl.openloadflow.util.report.PowsyblOpenLoadFlowReportResourceBundle;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.slf4j.LoggerFactory;

import java.io.IOException;

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

/**
 * @author Didier Vidal {@literal <didier.vidal_externe at rte-france.com>}
 */
class GeneratorRemoteControlPQSwitchTest {

    private Network network;
    private ReportNode reportNode;
    private Bus b1;
    private Generator g1;
    private LoadFlow.Runner loadFlowRunner;
    private LoadFlowParameters parameters;
    private OpenLoadFlowParameters parametersExt;

    /**
     * (401kV)                            (402kV)
     *  b2 ----------------l23-------------b3---l3 (200MW)
     *  |                                   |
     *  t12                                g3
     *  |                               (pMax = 10)
     *  b1
     *  |
     *  g1
     *
     */
    @BeforeEach
    void setUp() {
        network = NetworkFactory.findDefault().createNetwork("test", "java");

        Substation s12 = network.newSubstation()
                .setId("s12")
                .add();

        VoltageLevel vl1 = s12.newVoltageLevel()
                .setId("vl1")
                .setNominalV(20)
                .setTopologyKind(TopologyKind.BUS_BREAKER)
                .add();

        VoltageLevel vl2 = s12.newVoltageLevel()
                .setId("vl2")
                .setNominalV(400)
                .setTopologyKind(TopologyKind.BUS_BREAKER)
                .add();

        VoltageLevel vl3 = network.newVoltageLevel()
                .setId("vl3")
                .setNominalV(400)
                .setTopologyKind(TopologyKind.BUS_BREAKER)
                .add();

        b1 = vl1.getBusBreakerView()
                .newBus()
                .setId("b1")
                .add();

        Bus b2 = vl2.getBusBreakerView()
                .newBus()
                .setId("b2")
                .add();

        Bus b3 = vl3.getBusBreakerView()
                .newBus()
                .setId("b3")
                .add();

        network.newLine()
                .setId("l12")
                .setR(0.1)
                .setX(0.1)
                .setBus1(b2.getId())
                .setConnectableBus1(b2.getId())
                .setBus2(b3.getId())
                .setConnectableBus2(b3.getId())
                .add();

        TwoWindingsTransformer t12 = s12.newTwoWindingsTransformer()
                .setId("t12")
                .setR(0.1)
                .setX(15)
                .setBus1(b1.getId())
                .setConnectableBus1(b1.getId())
                .setBus2(b2.getId())
                .setConnectableBus2(b2.getId())
                .add();

        g1 = vl1.newGenerator()
                .setId("g1")
                .setMaxP(500)
                .setMinP(0)
                .setTargetP(200)
                .setBus(b1.getId())
                .setConnectableBus(b1.getId())
                .setVoltageRegulatorOn(true)
                .setRegulatingTerminal(t12.getTerminal2())
                .setTargetV(401)
                .add();

        vl3.newGenerator()
                .setId("g3")
                .setMaxP(10)
                .setMinP(0)
                .setTargetP(0)
                .setBus(b3.getId())
                .setConnectableBus(b3.getId())
                .setVoltageRegulatorOn(true)
                .setTargetV(402)
                .add();

        vl3.newLoad()
                .setId("l3")
                .setBus(b3.getId())
                .setConnectableBus(b3.getId())
                .setP0(200)
                .setQ0(0)
                .add();

        loadFlowRunner = new LoadFlow.Runner(new OpenLoadFlowProvider(new DenseMatrixFactory()));
        parameters = new LoadFlowParameters().setUseReactiveLimits(false)
                .setDistributedSlack(true);
        parametersExt = OpenLoadFlowParameters.create(parameters)
                .setSlackBusSelectionMode(SlackBusSelectionMode.MOST_MESHED)
                .setVoltageRemoteControl(true)
                .setSlackDistributionFailureBehavior(OpenLoadFlowParameters.SlackDistributionFailureBehavior.FAIL);

        // Activate trace logs to ensure ReactiveLimitsLoop trace logs are run at least once per build
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        loggerContext.getLogger(ReactiveLimitsOuterLoop.class).setLevel(Level.TRACE);
        reportNode = ReportNode.newRootReportNode()
                .withResourceBundles(PowsyblOpenLoadFlowReportResourceBundle.BASE_NAME, PowsyblCoreTestReportResourceBundle.TEST_BASE_NAME)
                .withMessageTemplate("test").build();
    }

    @AfterEach
    void restoreLogger() {
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        loggerContext.getLogger(ReactiveLimitsOuterLoop.class).setLevel(null);
    }

    @ParameterizedTest
    @ValueSource(booleans = {false, true})
    void testLowVoltageLargeLimits(boolean robustMode) {
        parametersExt.setMinRealisticVoltage(0.5);
        parametersExt.setMaxRealisticVoltage(2.0);
        parametersExt.setVoltageRemoteControlRobustMode(robustMode);
        parameters.setUseReactiveLimits(false);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());
        assertVoltageEquals(12.17, b1);
        assertReactivePowerEquals(2553.557, g1.getTerminal());
    }

    @ParameterizedTest
    @ValueSource(booleans = {false, true})
    void testLowVoltageRealisticLimits(boolean robustMode) {
        parametersExt.setMinRealisticVoltage(0.8);
        parametersExt.setMaxRealisticVoltage(1.2);
        parametersExt.setVoltageRemoteControlRobustMode(robustMode);
        parameters.setUseReactiveLimits(false);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertFalse(result.isFullyConverged());
        assertEquals("Unrealistic state", result.getComponentResults().get(0).getStatusText());
    }

    @ParameterizedTest
    @ValueSource(booleans = {false, true})
    void testLowVoltageQMin(boolean robustMode) throws IOException {
        parametersExt.setMinRealisticVoltage(0.8);
        parametersExt.setMaxRealisticVoltage(1.2);
        parametersExt.setVoltageRemoteControlRobustMode(robustMode);
        parameters.setUseReactiveLimits(true);
        g1.newMinMaxReactiveLimits().setMinQ(-800).setMaxQ(800).add();
        LoadFlowResult result = runWithReport();
        if (robustMode) {
            assertTrue(result.isFullyConverged());
            assertReactivePowerEquals(800, g1.getTerminal());
        } else {
            assertFalse(result.isFullyConverged());
            assertEquals("Unrealistic state", result.getComponentResults().get(0).getStatusText());
        }

        // test the report
        if (robustMode) {
            String expectedReport = """
                    + test
                       + Load flow on network 'test'
                          + Network CC0 SC0
                             + Network info
                                Network has 3 buses and 2 branches
                                Network balance: active generation=200 MW, active load=200 MW, reactive generation=0 MVar, reactive load=0 MVar
                                Angle reference bus: vl2_0
                                Slack bus: vl2_0
                             + Outer loop DistributedSlack
                                + Outer loop iteration 1
                                   Slack bus active power (22.147061 MW) distributed in 1 distribution iteration(s)
                             Outer loop VoltageMonitoring
                             + Outer loop ReactiveLimits
                                + Outer loop iteration 2
                                   + 1 buses switched PV -> PQ (1 buses remain PV)
                                      Switch bus 'vl1_0' PV -> PQ, q=-2553.557359 < minQ=-800
                             + Outer loop DistributedSlack
                                + Outer loop iteration 3
                                   Slack bus active power (-21.141636 MW) distributed in 1 distribution iteration(s)
                             Outer loop VoltageMonitoring
                             Outer loop ReactiveLimits
                             Outer loop DistributedSlack
                             Outer loop VoltageMonitoring
                             Outer loop ReactiveLimits
                             AC load flow completed successfully (solverStatus=CONVERGED, outerloopStatus=STABLE)
                    """;

            assertReportEqualsString(expectedReport, reportNode);
        }
    }

    @ParameterizedTest
    @ValueSource(booleans = {false, true})
    void testLowVoltageVMin(boolean robustMode) throws IOException {
        parametersExt.setMinRealisticVoltage(0.8);
        parametersExt.setMaxRealisticVoltage(1.2);
        parametersExt.setVoltageRemoteControlRobustMode(robustMode);
        parameters.setUseReactiveLimits(true);
        g1.newMinMaxReactiveLimits().setMinQ(-3000).setMaxQ(6000).add();
        g1.setTargetQ(10);
        LoadFlowResult result = runWithReport();
        if (robustMode) {
            assertTrue(result.isFullyConverged());
            assertReactivePowerEquals(-10, g1.getTerminal());  // The generator is set to initial targetQ
        } else {
            assertFalse(result.isFullyConverged());
            assertEquals("Unrealistic state", result.getComponentResults().get(0).getStatusText());
        }

        // test the report
        if (robustMode) {
            String expectedReport = """
                    + test
                       + Load flow on network 'test'
                          + Network CC0 SC0
                             + Network info
                                Network has 3 buses and 2 branches
                                Network balance: active generation=200 MW, active load=200 MW, reactive generation=0 MVar, reactive load=0 MVar
                                Angle reference bus: vl2_0
                                Slack bus: vl2_0
                             + Outer loop DistributedSlack
                                + Outer loop iteration 1
                                   Slack bus active power (22.147061 MW) distributed in 1 distribution iteration(s)
                             Outer loop VoltageMonitoring
                             + Outer loop ReactiveLimits
                                + Outer loop iteration 2
                                   + 1 buses switched PV -> PQ (1 buses remain PV)
                                      Switch bus 'vl1_0' PV -> PQ, q set to 10 = targetQ - because V < 16kV when remote voltage target is maintained
                             + Outer loop DistributedSlack
                                + Outer loop iteration 3
                                   Slack bus active power (-22.091658 MW) distributed in 1 distribution iteration(s)
                             Outer loop VoltageMonitoring
                             Outer loop ReactiveLimits
                             Outer loop DistributedSlack
                             Outer loop VoltageMonitoring
                             Outer loop ReactiveLimits
                             AC load flow completed successfully (solverStatus=CONVERGED, outerloopStatus=STABLE)
                    """;

            assertReportEqualsString(expectedReport, reportNode);
        }
    }

    @ParameterizedTest
    @ValueSource(booleans = {false, true})
    void testHighVoltageVMax(boolean robustMode) throws IOException {
        parametersExt.setMinRealisticVoltage(0.8);
        parametersExt.setMaxRealisticVoltage(1.2);
        parametersExt.setVoltageRemoteControlRobustMode(robustMode);
        parameters.setUseReactiveLimits(true);
        g1.newMinMaxReactiveLimits().setMinQ(-3000).setMaxQ(6000).add();
        g1.setTargetV(403);
        g1.setTargetQ(10);
        LoadFlowResult result = runWithReport();
        if (robustMode) {
            assertTrue(result.isFullyConverged());
            assertReactivePowerEquals(-10, g1.getTerminal());  // The generator is set to initial targetQ
        } else {
            assertFalse(result.isFullyConverged());
            assertEquals("Unrealistic state", result.getComponentResults().get(0).getStatusText());
        }

        // test the report
        if (robustMode) {
            String expectedReport = """
                    \
                    + test
                       + Load flow on network 'test'
                          + Network CC0 SC0
                             + Network info
                                Network has 3 buses and 2 branches
                                Network balance: active generation=200 MW, active load=200 MW, reactive generation=0 MVar, reactive load=0 MVar
                                Angle reference bus: vl2_0
                                Slack bus: vl2_0
                             + Outer loop DistributedSlack
                                + Outer loop iteration 1
                                   Slack bus active power (18.066373 MW) distributed in 1 distribution iteration(s)
                             Outer loop VoltageMonitoring
                             + Outer loop ReactiveLimits
                                + Outer loop iteration 2
                                   + 1 buses switched PV -> PQ (1 buses remain PV)
                                      Switch bus 'vl1_0' PV -> PQ, q set to 10 = targetQ - because V > 24kV when remote voltage target is maintained
                             + Outer loop DistributedSlack
                                + Outer loop iteration 3
                                   Slack bus active power (-18.01229 MW) distributed in 1 distribution iteration(s)
                             Outer loop VoltageMonitoring
                             Outer loop ReactiveLimits
                             Outer loop DistributedSlack
                             Outer loop VoltageMonitoring
                             Outer loop ReactiveLimits
                             AC load flow completed successfully (solverStatus=CONVERGED, outerloopStatus=STABLE)
                    """;

            assertReportEqualsString(expectedReport, reportNode);
        }
    }

    @ParameterizedTest
    @ValueSource(booleans = {false, true})
    void testHighVoltageQMax(boolean robustMode) throws IOException {
        parametersExt.setMinRealisticVoltage(0.8);
        parametersExt.setMaxRealisticVoltage(1.2);
        parametersExt.setVoltageRemoteControlRobustMode(robustMode);
        parameters.setUseReactiveLimits(true);
        g1.newMinMaxReactiveLimits().setMinQ(-800).setMaxQ(800).add();
        g1.setTargetV(403);
        g1.setTargetQ(10);
        LoadFlowResult result = runWithReport();
        if (robustMode) {
            assertTrue(result.isFullyConverged());
            assertReactivePowerEquals(-800, g1.getTerminal());  // The generator is set to initial targetQ
        } else {
            assertFalse(result.isFullyConverged());
            assertEquals("Unrealistic state", result.getComponentResults().get(0).getStatusText());
        }

        // test the report
        if (robustMode) {
            String expectedReport = """
                    + test
                       + Load flow on network 'test'
                          + Network CC0 SC0
                             + Network info
                                Network has 3 buses and 2 branches
                                Network balance: active generation=200 MW, active load=200 MW, reactive generation=0 MVar, reactive load=0 MVar
                                Angle reference bus: vl2_0
                                Slack bus: vl2_0
                             + Outer loop DistributedSlack
                                + Outer loop iteration 1
                                   Slack bus active power (18.066373 MW) distributed in 1 distribution iteration(s)
                             Outer loop VoltageMonitoring
                             + Outer loop ReactiveLimits
                                + Outer loop iteration 2
                                   + 1 buses switched PV -> PQ (1 buses remain PV)
                                      Switch bus 'vl1_0' PV -> PQ, q=5180.292508 > maxQ=800
                             + Outer loop DistributedSlack
                                + Outer loop iteration 3
                                   Slack bus active power (-17.327551 MW) distributed in 1 distribution iteration(s)
                             Outer loop VoltageMonitoring
                             Outer loop ReactiveLimits
                             Outer loop DistributedSlack
                             Outer loop VoltageMonitoring
                             Outer loop ReactiveLimits
                             AC load flow completed successfully (solverStatus=CONVERGED, outerloopStatus=STABLE)
                    """;

            LoadFlowAssert.assertReportEqualsString(expectedReport, reportNode);
        }
    }

    private LoadFlowResult runWithReport() {
        return loadFlowRunner.run(network,
                network.getVariantManager().getWorkingVariantId(),
                LocalComputationManager.getDefault(),
                parameters,
                reportNode);
    }

}