AreaInterchangeControlTest.java

/**
 * Copyright (c) 2024, 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.iidm.network.Area;
import com.powsybl.iidm.network.Network;
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.*;
import com.powsybl.openloadflow.network.impl.Networks;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.List;
import java.util.concurrent.CompletionException;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.*;

/**
 * @author Valentin Mouradian {@literal <valentin.mouradian at artelys.com>}
 */
class AreaInterchangeControlTest {

    private LoadFlow.Runner loadFlowRunner;
    private LoadFlowParameters parameters;

    private OpenLoadFlowParameters parametersExt;

    @BeforeEach
    void setUp() {
        loadFlowRunner = new LoadFlow.Runner(new OpenLoadFlowProvider(new DenseMatrixFactory()));
        parameters = new LoadFlowParameters();
        parametersExt = OpenLoadFlowParameters.create(parameters)
                .setAreaInterchangeControl(true)
                .setSlackBusPMaxMismatch(1e-3)
                .setAreaInterchangePMaxMismatch(1e-1);
    }

    @Test
    void twoAreasWithXnodeTest() {
        Network network = MultiAreaNetworkFactory.createTwoAreasWithXNode();
        runLfTwoAreas(network, -40, 40, -30, 2);
        parameters.setDc(true);
        runLfTwoAreas(network, -40, 40, -30, 0);
    }

    @Test
    void twoAreasWithUnpairedDanglingLine() {
        Network network = MultiAreaNetworkFactory.createTwoAreasWithDanglingLine();
        double interchangeTarget1 = -60; // area a1 has a boundary that is an unpaired dangling line with P0 = 20MW
        double interchangeTarget2 = 40;
        runLfTwoAreas(network, interchangeTarget1, interchangeTarget2, -10, 3);
        parameters.setDc(true);
        runLfTwoAreas(network, interchangeTarget1, interchangeTarget2, -10, 0);
    }

    @Test
    void zeroImpedanceBoundaryBranchesNetworkConversion() {
        Network network = MultiAreaNetworkFactory.createTwoAreasWithDanglingLine();
        network.getLine("l23_A1").setX(0);          // boundary
        network.getDanglingLine("dl1").setX(0);     // boundary
        network.getLine("l12").setX(0);             // not boundary

        LfNetwork lfNetwork = Networks.load(network, new LfNetworkParameters().setAreaInterchangeControl(false)).get(0);
        assertTrue(lfNetwork.getBranchById("dl1").isZeroImpedance(LoadFlowModel.AC));
        assertTrue(lfNetwork.getBranchById("l23_A1").isZeroImpedance(LoadFlowModel.AC));
        assertTrue(lfNetwork.getBranchById("l12").isZeroImpedance(LoadFlowModel.AC));

        lfNetwork = Networks.load(network, new LfNetworkParameters().setAreaInterchangeControl(true)).get(0);
        assertFalse(lfNetwork.getBranchById("dl1").isZeroImpedance(LoadFlowModel.AC));
        assertEquals(LfNetworkParameters.LOW_IMPEDANCE_THRESHOLD_DEFAULT_VALUE, lfNetwork.getBranchById("dl1").getPiModel().getX());
        assertFalse(lfNetwork.getBranchById("l23_A1").isZeroImpedance(LoadFlowModel.AC));
        assertEquals(LfNetworkParameters.LOW_IMPEDANCE_THRESHOLD_DEFAULT_VALUE, lfNetwork.getBranchById("l23_A1").getPiModel().getX());
        assertTrue(lfNetwork.getBranchById("l12").isZeroImpedance(LoadFlowModel.AC));

    }

    @Test
    void twoAreasWithZeroImpedanceBoundaryBranches() {
        Network network = MultiAreaNetworkFactory.createTwoAreasWithDanglingLine();
        double interchangeTarget1 = -40;
        double interchangeTarget2 = 20;
        network.getLine("l23_A1").setX(0);
        network.getDanglingLine("dl1").setX(0);
        parametersExt.setLowImpedanceBranchMode(OpenLoadFlowParameters.LowImpedanceBranchMode.REPLACE_BY_ZERO_IMPEDANCE_LINE);
        runLfTwoAreas(network, interchangeTarget1, interchangeTarget2, -10, 2);
        parameters.setDc(true);
        runLfTwoAreas(network, interchangeTarget1, interchangeTarget2, -10, 0);
    }

    @Test
    void twoAreasWithTieLineTest() {
        Network network = MultiAreaNetworkFactory.createTwoAreasWithTieLine();
        runLfTwoAreas(network, -40, 40, -30, 2);
        parameters.setDc(true);
        runLfTwoAreas(network, -40, 40, -30, 0);
    }

    @Test
    void twoAreasWithUnconsideredTlTest() {
        Network network = MultiAreaNetworkFactory.createTwoAreasWithUnconsideredTieLine();
        int expectedIterationCount = 3;
        runLfTwoAreas(network, -40, 40, -35, expectedIterationCount);
        parameters.setDc(true);
        runLfTwoAreas(network, -40, 40, -35, 0);
    }

    static Stream<Arguments> allSlackDistributionFailureBehaviors() {
        return Stream.of(Arguments.of(OpenLoadFlowParameters.SlackDistributionFailureBehavior.LEAVE_ON_SLACK_BUS, -90, -20, -10),
                Arguments.of(OpenLoadFlowParameters.SlackDistributionFailureBehavior.FAIL, Double.NaN, -30, 0),
                Arguments.of(OpenLoadFlowParameters.SlackDistributionFailureBehavior.DISTRIBUTE_ON_REFERENCE_GENERATOR, Double.NaN, -30, 0),
                Arguments.of(OpenLoadFlowParameters.SlackDistributionFailureBehavior.THROW, Double.NaN, Double.NaN, Double.NaN));
    }

    @ParameterizedTest(name = "{0}")
    @MethodSource("allSlackDistributionFailureBehaviors")
    void slackDistributionFailureBehaviorsTest(OpenLoadFlowParameters.SlackDistributionFailureBehavior slackDistributionFailureBehavior, double expectedGen1P, double expectedMismatch, double expectedDistributedP) {
        runLfOneAreaSlackDistributionFailure(slackDistributionFailureBehavior, expectedGen1P, expectedMismatch, expectedDistributedP);
        parameters.setDc(true);
        runLfOneAreaSlackDistributionFailure(slackDistributionFailureBehavior, expectedGen1P, expectedMismatch, expectedDistributedP);
    }

    private void runLfOneAreaSlackDistributionFailure(OpenLoadFlowParameters.SlackDistributionFailureBehavior slackDistributionFailureBehavior, double expectedGen1P, double expectedMismatch, double expectedDistributedP) {
        parametersExt.setSlackDistributionFailureBehavior(slackDistributionFailureBehavior);
        Network network = MultiAreaNetworkFactory.createOneAreaBase();
        network.getGenerator("g1").setMinP(90); // the generator should go down to 70MW to meet the interchange target

        if (slackDistributionFailureBehavior != OpenLoadFlowParameters.SlackDistributionFailureBehavior.THROW) {
            var result = loadFlowRunner.run(network, parameters);
            var mainComponentResult = result.getComponentResults().get(0);
            assertEquals(expectedGen1P, network.getGenerator("g1").getTerminal().getP(), 1e-3);
            assertEquals(expectedMismatch, mainComponentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3);
            assertEquals(expectedDistributedP, mainComponentResult.getDistributedActivePower(), 1e-3);
        } else {
            CompletionException thrown = assertThrows(CompletionException.class, () -> loadFlowRunner.run(network, parameters));
            assertEquals("Failed to distribute interchange active power mismatch", thrown.getCause().getMessage());
        }
    }

    @Test
    void slackBusOnBoundaryBus() {
        // The slack bus is on a boundary bus, and the flow though this boundary bus is considered in the area interchange
        Network network = MultiAreaNetworkFactory.createTwoAreasWithTwoXNodes();
        parametersExt.setSlackBusSelectionMode(SlackBusSelectionMode.NAME)
                .setSlackBusId("bx1_vl_0");
        var result = runLfTwoAreas(network, -15, 15, -30, 6);
        List<LoadFlowResult.SlackBusResult> slackBusResults = result.getComponentResults().get(0).getSlackBusResults();
        assertEquals(1, slackBusResults.size());
        assertEquals("bx1_vl_0", slackBusResults.get(0).getId());

        parameters.setDc(true);
        result = runLfTwoAreas(network, -15, 15, -30, 0);
        slackBusResults = result.getComponentResults().get(0).getSlackBusResults();
        assertEquals(1, slackBusResults.size());
        assertEquals("bx1_vl_0", slackBusResults.get(0).getId());
    }

    @Test
    void slackBusOnIgnoredBoundaryBus() {
        // The slack bus is on a boundary bus, but the flow on this boundary bus is not considered in the area interchange
        Network network = MultiAreaNetworkFactory.createTwoAreasWithTwoXNodes();
        parametersExt.setSlackBusSelectionMode(SlackBusSelectionMode.NAME)
                .setSlackBusId("bx2_vl_0");
        var result = runLfTwoAreas(network, -15, 15, -30, 6);
        List<LoadFlowResult.SlackBusResult> slackBusResults = result.getComponentResults().get(0).getSlackBusResults();
        assertEquals(1, slackBusResults.size());
        assertEquals("bx2_vl_0", slackBusResults.get(0).getId());

        parameters.setDc(true);
        result = runLfTwoAreas(network, -15, 15, -30, 0);
        slackBusResults = result.getComponentResults().get(0).getSlackBusResults();
        assertEquals(1, slackBusResults.size());
        assertEquals("bx2_vl_0", slackBusResults.get(0).getId());
    }

    @Test
    void networkWithoutAreas() {
        Network network = FourBusNetworkFactory.createBaseNetwork();
        parameters.setDistributedSlack(false);
        parametersExt.setAreaInterchangeControl(true);
        var result = loadFlowRunner.run(network, parameters);
        var componentResult = result.getComponentResults().get(0);
        assertEquals(1.998, componentResult.getDistributedActivePower(), 1e-3);
        assertEquals(0, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3);

        parameters.setDc(true);
        result = loadFlowRunner.run(network, parameters);
        componentResult = result.getComponentResults().get(0);
        assertEquals(0, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3);
    }

    @Test
    void halfNetworkWithoutArea() {
        Network network = MultiAreaNetworkFactory.createTwoAreasWithTieLine();
        network.getArea("a2").remove();

        Area area1 = network.getArea("a1");
        var result = loadFlowRunner.run(network, parameters);

        var componentResult = result.getComponentResults().get(0);
        assertEquals(area1.getInterchangeTarget().orElseThrow(), area1.getInterchange(), 1e-3);
        assertEquals(-30, componentResult.getDistributedActivePower(), 1e-3);
        assertEquals(3, componentResult.getIterationCount());
        assertEquals(0, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3);

        parameters.setDc(true);
        result = loadFlowRunner.run(network, parameters);
        componentResult = result.getComponentResults().get(0);
        assertEquals(0, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3);
        assertEquals(0, componentResult.getIterationCount());
        assertEquals(0, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3);

    }

    @Test
    void areaFragmentedBuses() {
        // Network has an area that has buses in two different components but all boundaries are in the same component
        // This Area is considered by area interchange control only in the component where all boundaries are
        Network network = MultiAreaNetworkFactory.createAreaTwoComponents();

        Area area1 = network.getArea("a1");
        Area area2 = network.getArea("a2");

        parameters.setConnectedComponentMode(LoadFlowParameters.ConnectedComponentMode.ALL);
        var result = loadFlowRunner.run(network, parameters);

        var componentResult = result.getComponentResults().get(0);
        assertEquals(area1.getInterchangeTarget().orElseThrow(), area1.getInterchange(), 1e-3);
        assertEquals(area2.getInterchangeTarget().orElseThrow(), area2.getInterchange(), 1e-3);
        assertEquals(-30, componentResult.getDistributedActivePower(), 1e-3);
        assertEquals(0, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3);
    }

    @Test
    void areaFragmentedBoundaries() {
        // Network has an area that has buses and boundaries in two different components, this area is ignored by area interchange control
        Network network = MultiAreaNetworkFactory.createAreaTwoComponentsWithBoundaries();
        Area area1 = network.getArea("a1");
        Area area2 = network.getArea("a2");

        parameters.setConnectedComponentMode(LoadFlowParameters.ConnectedComponentMode.ALL);
        var result = loadFlowRunner.run(network, parameters);

        var componentResult = result.getComponentResults().get(0);
        assertEquals(area1.getInterchangeTarget().orElseThrow(), area1.getInterchange(), 1e-3);
        assertEquals(51.1, area2.getInterchange(), 1e-3); // has been ignored by area interchange control because all boundaries are not in the same component
        assertEquals(-30, componentResult.getDistributedActivePower(), 1e-3);
        assertEquals(3, componentResult.getIterationCount());
        assertEquals(0, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), 1e-3);
    }

    private LoadFlowResult runLfTwoAreas(Network network, double interchangeTarget1, double interchangeTarget2, double expectedDistributedP, int expectedIterationCount) {
        Area area1 = network.getArea("a1");
        Area area2 = network.getArea("a2");
        area1.setInterchangeTarget(interchangeTarget1);
        area2.setInterchangeTarget(interchangeTarget2);

        var result = loadFlowRunner.run(network, parameters);
        assertTrue(result.isFullyConverged());

        var componentResult = result.getComponentResults().get(0);
        assertEquals(interchangeTarget1, area1.getInterchange(), parametersExt.getAreaInterchangePMaxMismatch());
        assertEquals(interchangeTarget2, area2.getInterchange(), parametersExt.getAreaInterchangePMaxMismatch());
        assertEquals(expectedDistributedP, componentResult.getDistributedActivePower(), 1e-3);
        assertEquals(expectedIterationCount, componentResult.getIterationCount());
        assertEquals(0, componentResult.getSlackBusResults().get(0).getActivePowerMismatch(), parametersExt.getSlackBusPMaxMismatch());
        return result;
    }

}