SecondaryVoltageControlTest.java

/*
 * Copyright (c) 2023-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 com.powsybl.commons.PowsyblException;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.commons.test.PowsyblCoreTestReportResourceBundle;
import com.powsybl.computation.local.LocalComputationManager;
import com.powsybl.ieeecdf.converter.IeeeCdfNetworkFactory;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.extensions.PilotPoint;
import com.powsybl.iidm.network.extensions.SecondaryVoltageControl;
import com.powsybl.iidm.network.extensions.SecondaryVoltageControlAdder;
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.LfNetwork;
import com.powsybl.openloadflow.network.LfNetworkParameters;
import com.powsybl.openloadflow.network.LfSecondaryVoltageControl;
import com.powsybl.openloadflow.network.impl.LfNetworkLoaderImpl;
import com.powsybl.openloadflow.util.report.PowsyblOpenLoadFlowReportResourceBundle;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.List;
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>}
 */
class SecondaryVoltageControlTest {

    private Network network;

    private Bus b4;
    private Bus b6;
    private Bus b8;
    private Bus b10;
    private Generator g6;
    private Generator g8;

    private LoadFlow.Runner loadFlowRunner;

    private LoadFlowParameters parameters;

    private OpenLoadFlowParameters parametersExt;

    @BeforeEach
    void setUp() {
        network = IeeeCdfNetworkFactory.create14();
        b4 = network.getBusBreakerView().getBus("B4");
        b6 = network.getBusBreakerView().getBus("B6");
        b8 = network.getBusBreakerView().getBus("B8");
        b10 = network.getBusBreakerView().getBus("B10");
        g6 = network.getGenerator("B6-G");
        g8 = network.getGenerator("B8-G");

        // adjust reactive limit to avoid generators be to limit
        g8.newMinMaxReactiveLimits()
                .setMinQ(-6)
                .setMaxQ(200)
                .add();

        loadFlowRunner = new LoadFlow.Runner(new OpenLoadFlowProvider(new DenseMatrixFactory()));
        parameters = new LoadFlowParameters();
        parametersExt = OpenLoadFlowParameters.create(parameters)
                .setMaxPlausibleTargetVoltage(1.6);
    }

    private static double qToK(Generator g) {
        ReactiveLimits limits = g.getReactiveLimits();
        double q = -g.getTerminal().getQ();
        double p = -g.getTerminal().getP();
        return (2 * q - limits.getMaxQ(p) - limits.getMinQ(p))
                / (limits.getMaxQ(p) - limits.getMinQ(p));
    }

    @Test
    void testNoReactiveLimits() {
        parameters.setUseReactiveLimits(false);
        network.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                .withName("z1")
                .newPilotPoint().withTargetV(13).withBusbarSectionsOrBusesIds(List.of("B10")).add()
                .newControlUnit().withId("B6-G").add()
                .newControlUnit().withId("B8-G").add()
                .add()
                .add();
        SecondaryVoltageControl control = network.getExtension(SecondaryVoltageControl.class);
        PilotPoint pilotPoint = control.getControlZone("z1").orElseThrow().getPilotPoint();

        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus());
        assertEquals(3, result.getComponentResults().get(0).getIterationCount());
        assertVoltageEquals(12.611, b10);
        assertVoltageEquals(12.84, b6);
        assertVoltageEquals(21.8, b8);
        assertEquals(0.248, qToK(g6), DELTA_POWER);
        assertEquals(-0.77, qToK(g8), DELTA_POWER);

        parametersExt.setSecondaryVoltageControl(true);

        result = loadFlowRunner.run(network, parameters);
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus());
        assertEquals(6, result.getComponentResults().get(0).getIterationCount());
        assertVoltageEquals(13, b10);
        assertVoltageEquals(12.945, b6);
        assertVoltageEquals(23.839, b8);
        assertEquals(-0.412, qToK(g6), DELTA_POWER);
        assertEquals(-0.412, qToK(g8), DELTA_POWER);

        pilotPoint.setTargetV(13.5);
        result = loadFlowRunner.run(network, parameters);
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus());
        assertEquals(6, result.getComponentResults().get(0).getIterationCount());
        assertVoltageEquals(13.5, b10);
        assertVoltageEquals(13.358, b6);
        assertVoltageEquals(25.621, b8);
        assertEquals(-0.094, qToK(g6), DELTA_POWER);
        assertEquals(-0.094, qToK(g8), DELTA_POWER);

        pilotPoint.setTargetV(12);
        result = loadFlowRunner.run(network, parameters);
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus());
        assertEquals(6, result.getComponentResults().get(0).getIterationCount());
        assertVoltageEquals(12, b10);
        assertVoltageEquals(12.151, b6);
        assertVoltageEquals(20.269, b8);
        assertEquals(-0.932, qToK(g6), DELTA_POWER);
        assertEquals(-0.932, qToK(g8), DELTA_POWER);
    }

    @Test
    void testReactiveLimits() {
        network.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                .withName("z1")
                .newPilotPoint().withTargetV(11.5).withBusbarSectionsOrBusesIds(List.of("B10")).add()
                .newControlUnit().withId("B6-G").add()
                .newControlUnit().withId("B8-G").add()
                .add()
                .add();

        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus());
        assertEquals(3, result.getComponentResults().get(0).getIterationCount());
        assertVoltageEquals(12.606, b10);
        assertVoltageEquals(12.84, b6);
        assertVoltageEquals(21.8, b8);
        assertReactivePowerEquals(-12.730, g6.getTerminal()); // [-6, 24]
        assertReactivePowerEquals(-17.623, g8.getTerminal()); // [-6, 200]

        parametersExt.setSecondaryVoltageControl(true);

        result = loadFlowRunner.run(network, parameters);
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus());
        assertEquals(8, result.getComponentResults().get(0).getIterationCount());

        assertVoltageEquals(11.736, b10); // 11.5 kV was not feasible
        assertVoltageEquals(11.924, b6);
        assertVoltageEquals(19.537, b8);
        assertReactivePowerEquals(6, g6.getTerminal()); // [-6, 24] => qmin
        assertReactivePowerEquals(6, g8.getTerminal()); // [-6, 200] => qmin
    }

    @Test
    void testUnblockGeneratorFromLimit() throws IOException {
        network.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                .withName("z1")
                .newPilotPoint().withTargetV(15).withBusbarSectionsOrBusesIds(List.of("B10")).add()
                .newControlUnit().withId("B6-G").add()
                .newControlUnit().withId("B8-G").add()
                .add()
                .add();

        // to put g6 and g8 at q min
        g6.setTargetV(11.8);
        g8.setTargetV(19.5);

        parametersExt.setSecondaryVoltageControl(true);

        ReportNode node = ReportNode.newRootReportNode()
                .withResourceBundles(PowsyblOpenLoadFlowReportResourceBundle.BASE_NAME, PowsyblCoreTestReportResourceBundle.TEST_BASE_NAME)
                .withMessageTemplate("test")
                .build();

        // try to put g6 and g8 at qmax to see if they are correctly unblock from qmin
        var result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, node);
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus());
        assertEquals(14, result.getComponentResults().get(0).getIterationCount());

        assertVoltageEquals(15, b10);
        assertVoltageEquals(14.604, b6);
        assertVoltageEquals(30.744, b8);
        assertReactivePowerEquals(-24, g6.getTerminal()); // [-6, 24] => qmax
        assertReactivePowerEquals(-200, g8.getTerminal()); // [-6, 200] => qmax

        // Note that slack distribution fails in this test that runs with the LEAVE_ON_SLACK_BUS slack failure behaviour
        String expected = """
                + test
                   + Load flow on network 'ieee14cdf'
                      + Network CC0 SC0
                         + Network info
                            Network has 14 buses and 20 branches
                            Network balance: active generation=272.4 MW, active load=259 MW, reactive generation=0 MVar, reactive load=73.5 MVar
                            Angle reference bus: VL1_0
                            Slack bus: VL1_0
                         Outer loop DistributedSlack
                         Outer loop SecondaryVoltageControl
                         Outer loop VoltageMonitoring
                         + Outer loop ReactiveLimits
                            + Outer loop iteration 3
                               + 3 buses switched PV -> PQ (2 buses remain PV)
                                  Switch bus 'VL3_0' PV -> PQ, q=-18.051176 < minQ=0
                                  Switch bus 'VL6_0' PV -> PQ, q=25.327554 > maxQ=24
                                  Switch bus 'VL8_0' PV -> PQ, q=209.071622 > maxQ=200
                            + Outer loop iteration 4
                               + 1 buses switched PV -> PQ (1 buses remain PV)
                                  Switch bus 'VL2_0' PV -> PQ, q=-46.582673 < minQ=-40
                            + Outer loop iteration 5
                               + 1 buses switched PQ -> PV (0 buses blocked PQ due to the max number of switches)
                                  Switch bus 'VL6_0' PQ -> PV, q=maxQ and v=14.604872kV > targetV=14.596348kV
                         + Outer loop DistributedSlack
                            + Outer loop iteration 6
                               Failed to distribute slack bus active power mismatch, 3.011164 MW remains
                         Outer loop SecondaryVoltageControl
                         Outer loop VoltageMonitoring
                         + Outer loop ReactiveLimits
                            + Outer loop iteration 8
                               + 2 buses switched PV -> PQ (1 buses remain PV)
                                  Switch bus 'VL6_0' PV -> PQ, q=24.012062 > maxQ=24
                                  Switch bus 'VL8_0' PV -> PQ, q=200.112821 > maxQ=200
                         + Outer loop DistributedSlack
                            + Outer loop iteration 9
                               Failed to distribute slack bus active power mismatch, 3.016519 MW remains
                         Outer loop SecondaryVoltageControl
                         Outer loop VoltageMonitoring
                         Outer loop ReactiveLimits
                         AC load flow completed successfully (solverStatus=CONVERGED, outerloopStatus=STABLE)
                """;

        assertReportEquals(new ByteArrayInputStream(expected.getBytes()), node);
    }

    @Test
    void testCannotUnblockGeneratorFromLimit() throws IOException {
        network.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                .withName("z1")
                .newPilotPoint().withTargetV(15).withBusbarSectionsOrBusesIds(List.of("B10")).add()
                .newControlUnit().withId("B6-G").add()
                .newControlUnit().withId("B8-G").add()
                .add()
                .add();

        // to put g6 and g8 at q min
        g6.setTargetV(11.8);
        g8.setTargetV(19.5);

        parametersExt.setSecondaryVoltageControl(true);
        parametersExt.setReactiveLimitsMaxPqPvSwitch(0); // Will block PQ->PV move

        ReportNode node = ReportNode.newRootReportNode()
                .withResourceBundles(PowsyblOpenLoadFlowReportResourceBundle.BASE_NAME, PowsyblCoreTestReportResourceBundle.TEST_BASE_NAME)
                .withMessageTemplate("test")
                .build();

        // try to put g6 and g8 at qmax to see if they are correctly unblock from qmin
        var result = loadFlowRunner.run(network, network.getVariantManager().getWorkingVariantId(), LocalComputationManager.getDefault(), parameters, node);
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus());
        assertEquals(10, result.getComponentResults().get(0).getIterationCount());

        assertVoltageEquals(15, b10);
        assertVoltageEquals(14.604, b6);
        assertVoltageEquals(30.744, b8);
        assertReactivePowerEquals(-24, g6.getTerminal()); // [-6, 24] => qmax
        assertReactivePowerEquals(-200, g8.getTerminal()); // [-6, 200] => qmax

        // Note that slack distribution fails in this test that runs with the LEAVE_ON_SLACK_BUS slack failure behaviour
        String expected = """
                + test
                   + Load flow on network 'ieee14cdf'
                      + Network CC0 SC0
                         + Network info
                            Network has 14 buses and 20 branches
                            Network balance: active generation=272.4 MW, active load=259 MW, reactive generation=0 MVar, reactive load=73.5 MVar
                            Angle reference bus: VL1_0
                            Slack bus: VL1_0
                         Outer loop DistributedSlack
                         Outer loop SecondaryVoltageControl
                         Outer loop VoltageMonitoring
                         + Outer loop ReactiveLimits
                            + Outer loop iteration 3
                               + 3 buses switched PV -> PQ (2 buses remain PV)
                                  Switch bus 'VL3_0' PV -> PQ, q=-18.051176 < minQ=0
                                  Switch bus 'VL6_0' PV -> PQ, q=25.327554 > maxQ=24
                                  Switch bus 'VL8_0' PV -> PQ, q=209.071622 > maxQ=200
                            + Outer loop iteration 4
                               + 1 buses switched PV -> PQ (1 buses remain PV)
                                  Switch bus 'VL2_0' PV -> PQ, q=-46.582673 < minQ=-40
                            + Outer loop iteration 5
                               + 0 buses switched PQ -> PV (1 buses blocked PQ due to the max number of switches)
                                  Bus 'VL6_0' blocked PQ as it has reached its max number of PQ -> PV switch (1)
                         + Outer loop DistributedSlack
                            + Outer loop iteration 5
                               Failed to distribute slack bus active power mismatch, 3.013536 MW remains
                         Outer loop SecondaryVoltageControl
                         Outer loop VoltageMonitoring
                         + Outer loop ReactiveLimits
                            + Outer loop iteration 5
                               + 0 buses switched PQ -> PV (1 buses blocked PQ due to the max number of switches)
                                  Bus 'VL6_0' blocked PQ as it has reached its max number of PQ -> PV switch (1)
                         AC load flow completed successfully (solverStatus=CONVERGED, outerloopStatus=STABLE)
                """;

        assertReportEquals(new ByteArrayInputStream(expected.getBytes()), node);
    }

    @Test
    void multiNoReactiveLimitsZonesTest() {
        parameters.setUseReactiveLimits(false);
        network.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                .withName("z1")
                .newPilotPoint().withTargetV(142).withBusbarSectionsOrBusesIds(List.of("B4")).add()
                .newControlUnit().withId("B1-G").add()
                .newControlUnit().withId("B2-G").add()
                .newControlUnit().withId("B3-G").add()
                .add()
                .newControlZone()
                .withName("z2")
                .newPilotPoint().withTargetV(14.5).withBusbarSectionsOrBusesIds(List.of("B10")).add()
                .newControlUnit().withId("B6-G").add()
                .newControlUnit().withId("B8-G").add()
                .add()
                .add();

        parametersExt.setSecondaryVoltageControl(true);
        var result = loadFlowRunner.run(network, parameters);
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus());
        assertEquals(6, result.getComponentResults().get(0).getIterationCount());
        assertVoltageEquals(142, b4);
        assertVoltageEquals(14.5, b10);
    }

    @Test
    void testOpenBranchIssue() {
        // open branch L6-13-1 on side 2 so that a neighbor branch of B6-G is disconnected to the other side
        network.getLine("L6-13-1").getTerminal2().disconnect();

        parameters.setUseReactiveLimits(false);
        parametersExt.setSecondaryVoltageControl(true);

        network.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                .withName("z1")
                .newPilotPoint().withTargetV(14.4).withBusbarSectionsOrBusesIds(List.of("B10")).add()
                .newControlUnit().withId("B6-G").add()
                .newControlUnit().withId("B8-G").add()
                .add()
                .add();

        var result = loadFlowRunner.run(network, parameters);
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus());
        assertEquals(7, result.getComponentResults().get(0).getIterationCount());
        assertVoltageEquals(14.4, b10);
        assertVoltageEquals(14.151, b6);
        assertVoltageEquals(28.913, b8);
    }

    @Test
    void pilotPointNotFoundTest() {
        network.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                .withName("z1")
                .newPilotPoint().withTargetV(13).withBusbarSectionsOrBusesIds(List.of("XX", "YY")).add()
                .newControlUnit().withId("B6-G").add()
                .newControlUnit().withId("B8-G").add()
                .add();

        LfNetworkParameters networkParameters = new LfNetworkParameters().setSecondaryVoltageControl(true);
        List<LfNetwork> lfNetworks = LfNetwork.load(network, new LfNetworkLoaderImpl(), networkParameters);
        LfNetwork lfNetwork = lfNetworks.get(0);
        assertTrue(lfNetwork.getSecondaryVoltageControls().isEmpty());
    }

    @Test
    void controlUnitNotFoundTest() {
        network.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                .withName("z1")
                .newPilotPoint().withTargetV(13).withBusbarSectionsOrBusesIds(List.of("B10")).add()
                .newControlUnit().withId("B99-G").add()
                .newControlUnit().withId("B8-G").add()
                .add()
                .add();

        LfNetworkParameters networkParameters = new LfNetworkParameters().setSecondaryVoltageControl(true);
        List<LfNetwork> lfNetworks = LfNetwork.load(network, new LfNetworkLoaderImpl(), networkParameters);
        LfNetwork lfNetwork = lfNetworks.get(0);
        List<LfSecondaryVoltageControl> secondaryVoltageControls = lfNetwork.getSecondaryVoltageControls();
        assertEquals(1, secondaryVoltageControls.size());
        assertEquals(1, secondaryVoltageControls.get(0).getControlledBuses().size()); // B99-G not found
    }

    @Test
    void testOptionalNoValueIssue() {
        network.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                .withName("z1")
                .newPilotPoint().withTargetV(13).withBusbarSectionsOrBusesIds(List.of("B10")).add()
                .newControlUnit().withId("B6-G").add()
                .newControlUnit().withId("B9-SH").add() // this is a shunt which is not supported
                .add()
                .add();

        parametersExt.setSecondaryVoltageControl(true);

        CompletionException e = assertThrows(CompletionException.class, () -> loadFlowRunner.run(network, parameters));
        assertEquals("Control unit 'B9-SH' of zone 'z1' is expected to be either a generator or a VSC converter station", e.getCause().getMessage());
    }

    @Test
    void testAnotherOptionalNoValueIssue() {
        Generator g6 = network.getGenerator("B6-G");
        g6.newMinMaxReactiveLimits()
                .setMinQ(100)
                .setMaxQ(100.000001)
                .add();

        network.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                .withName("z1")
                .newPilotPoint().withTargetV(13).withBusbarSectionsOrBusesIds(List.of("B10")).add()
                .newControlUnit().withId("B6-G").add()
                .newControlUnit().withId("B8-G").add()
                .add();

        parametersExt.setSecondaryVoltageControl(true);

        assertDoesNotThrow(() -> loadFlowRunner.run(network, parameters));
    }

    @Test
    void disjointControlZoneTest() {
        network.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                .withName("z1")
                .newPilotPoint().withTargetV(13).withBusbarSectionsOrBusesIds(List.of("B10")).add()
                .newControlUnit().withId("B99-G").add()
                .newControlUnit().withId("B8-G").add()
                .add()
                .newControlZone()
                .withName("z2")
                .newPilotPoint().withTargetV(13).withBusbarSectionsOrBusesIds(List.of("B10")).add()
                .newControlUnit().withId("B1-G").add()
                .newControlUnit().withId("B8-G").add()
                .add()
                .add();

        LfNetworkParameters networkParameters = new LfNetworkParameters().setSecondaryVoltageControl(true);
        LfNetworkLoaderImpl networkLoader = new LfNetworkLoaderImpl();
        PowsyblException e = assertThrows(PowsyblException.class, () -> LfNetwork.load(network, networkLoader, networkParameters));
        assertEquals("Generator voltage control of controlled bus 'VL8_0' is present in more that one control zone", e.getMessage());
    }

    @Test
    void testNotPlausibleTargetV() {
        // g8 generator is very far from pilot point bus b12, there is no way for generators of the zone to control
        // the pilot point voltage, this is detected by secondary voltage control outer loop which fails
        network.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                .withName("z1")
                .newPilotPoint().withTargetV(11.5).withBusbarSectionsOrBusesIds(List.of("B12")).add()
                .newControlUnit().withId("B8-G").add()
                .add()
                .add();

        parametersExt.setSecondaryVoltageControl(true);

        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertEquals(LoadFlowResult.ComponentResult.Status.FAILED, result.getComponentResults().get(0).getStatus());
    }

    @Test
    void testWithGeneratorRemoteVoltage() {
        // move generator 6 to bus 5 but keep voltage control on bus 6
        network.getGenerator("B6-G").remove();
        var g5 = network.getVoltageLevel("VL5").newGenerator()
                .setId("B5-G")
                .setBus("B5")
                .setTargetP(0)
                .setMinP(-9999)
                .setMaxP(9999)
                .setTargetV(12.8)
                .setVoltageRegulatorOn(true)
                .setRegulatingTerminal(network.getLoad("B6-L").getTerminal())
                .add();
        parameters.setUseReactiveLimits(false);
        parametersExt.setSecondaryVoltageControl(true);
        network.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                .withName("z1")
                .newPilotPoint().withTargetV(13).withBusbarSectionsOrBusesIds(List.of("B10")).add()
                .newControlUnit().withId("B5-G").add() // this control unit is a generator with remote voltage control
                .newControlUnit().withId("B8-G").add()
                .add()
                .add();

        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus());
        assertEquals(8, result.getComponentResults().get(0).getIterationCount());

        assertVoltageEquals(13, b10);
        assertVoltageEquals(12.682, b6);
        assertVoltageEquals(25.311, b8);
        assertReactivePowerEquals(82.904, g5.getTerminal());
        assertReactivePowerEquals(-97, g8.getTerminal());
    }

    @Test
    void testWithDiscardedGenerator() {
        g6.newMinMaxReactiveLimits()
                .setMinQ(100)
                .setMaxQ(100.00000001)
                .add();
        parametersExt.setSecondaryVoltageControl(true);
        network.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                .withName("z1")
                .newPilotPoint().withTargetV(13).withBusbarSectionsOrBusesIds(List.of("B10")).add()
                .newControlUnit().withId("B6-G").add()
                .newControlUnit().withId("B8-G").add()
                .add()
                .add();

        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus());
        assertEquals(5, result.getComponentResults().get(0).getIterationCount());

        assertVoltageEquals(13, b10);
        assertVoltageEquals(13.090, b6);
        assertVoltageEquals(23.381, b8);
    }

    @Test
    void testWithGeneratorSharedRemoteVoltage() {
        // remove generator 6 and create 2 new generators controlling voltage on bus 6
        network.getGenerator("B6-G").remove();

        var s5 = network.getSubstation("S5");
        var vl99 = s5.newVoltageLevel()
                .setId("VL99")
                .setTopologyKind(TopologyKind.BUS_BREAKER)
                .setNominalV(6)
                .add();
        var b991 = vl99.getBusBreakerView().newBus()
                .setId("B99-1")
                .add();
        var b992 = vl99.getBusBreakerView().newBus()
                .setId("B99-2")
                .add();
        var g991 = vl99.newGenerator()
                .setId("B99-G1")
                .setBus("B99-1")
                .setTargetP(1)
                .setMinP(-1000)
                .setMaxP(1000)
                .setTargetV(12.8)
                .setVoltageRegulatorOn(true)
                .setRegulatingTerminal(network.getLoad("B6-L").getTerminal())
                .add();
        g991.newMinMaxReactiveLimits()
                .setMinQ(-30)
                .setMaxQ(70)
                .add();
        var g992 = vl99.newGenerator()
                .setId("B99-G2")
                .setBus("B99-2")
                .setTargetP(2)
                .setMinP(-1000)
                .setMaxP(1000)
                .setTargetV(12.8)
                .setVoltageRegulatorOn(true)
                .setRegulatingTerminal(network.getLoad("B6-L").getTerminal())
                .add();
        g992.newMinMaxReactiveLimits()
                .setMinQ(-40)
                .setMaxQ(50)
                .add();
        s5.newTwoWindingsTransformer()
                .setId("B99-T1")
                .setVoltageLevel1("VL99")
                .setBus1("B99-1")
                .setVoltageLevel2("VL6")
                .setBus2("B6")
                .setRatedU1(5)
                .setRatedU2(12)
                .setR(0.1)
                .setX(1)
                .add();
        s5.newTwoWindingsTransformer()
                .setId("B99-T2")
                .setVoltageLevel1("VL99")
                .setBus1("B99-2")
                .setVoltageLevel2("VL6")
                .setBus2("B6")
                .setRatedU1(5)
                .setRatedU2(12)
                .setR(0.2)
                .setX(2)
                .add();

        parametersExt.setSecondaryVoltageControl(true);
        network.newExtension(SecondaryVoltageControlAdder.class)
                .newControlZone()
                .withName("z1")
                .newPilotPoint().withTargetV(12.5).withBusbarSectionsOrBusesIds(List.of("B10")).add()
                .newControlUnit().withId("B99-G1").add()
                .newControlUnit().withId("B99-G2").add()
                .add()
                .add();

        LoadFlowResult result = loadFlowRunner.run(network, parameters);
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus());
        assertEquals(7, result.getComponentResults().get(0).getIterationCount());

        assertVoltageEquals(12.5, b10);
        assertVoltageEquals(5.548, b991);
        assertVoltageEquals(4.93, b992);
        assertReactivePowerEquals(-9.172, g991.getTerminal());
        assertReactivePowerEquals(4.742, g992.getTerminal());

        // With secondary voltage set to false, check that the remote control remains active
        parametersExt.setSecondaryVoltageControl(false);
        result = loadFlowRunner.run(network, parameters);
        assertEquals(LoadFlowResult.ComponentResult.Status.CONVERGED, result.getComponentResults().get(0).getStatus());
        assertEquals(4, result.getComponentResults().get(0).getIterationCount());
        assertVoltageEquals(12.8, b6);
        assertVoltageEquals(5.52, b991);
        // Compare voltage in pu
        assertEquals(1.066, b6.getV() / b6.getVoltageLevel().getNominalV(), 1e-3);
        assertEquals(0.92, b991.getV() / b991.getVoltageLevel().getNominalV(), 1e-3);
    }
}