LfNetworkTest.java

/**
 * Copyright (c) 2019, 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.network;

import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.test.AbstractSerDeTest;
import com.powsybl.commons.test.ComparisonUtils;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.extensions.HvdcAngleDroopActivePowerControlAdder;
import com.powsybl.iidm.network.test.DanglingLineNetworkFactory;
import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory;
import com.powsybl.iidm.network.test.PhaseShifterTestCaseFactory;
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.impl.Networks;
import com.powsybl.openloadflow.sa.LimitReductionManager;
import com.powsybl.openloadflow.util.Evaluable;
import com.powsybl.openloadflow.util.EvaluableConstants;
import org.apache.commons.lang3.Range;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;

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

/**
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
class LfNetworkTest extends AbstractSerDeTest {

    @Override
    @BeforeEach
    public void setUp() throws IOException {
        super.setUp();
    }

    @Override
    @AfterEach
    public void tearDown() throws IOException {
        super.tearDown();
    }

    @Test
    void test() throws IOException {
        Network network = EurostagFactory.fix(EurostagTutorialExample1Factory.create());
        network.getVoltageLevel("VLLOAD").newShuntCompensator()
                .setId("SC")
                .setBus("NLOAD")
                .setConnectableBus("NLOAD")
                .setSectionCount(1)
                .newLinearModel()
                    .setBPerSection(3.25 * Math.pow(10, -3))
                    .setMaximumSectionCount(1)
                    .add()
                .add();

        List<LfNetwork> lfNetworks = Networks.load(network, new MostMeshedSlackBusSelector());
        LfNetwork mainNetwork = lfNetworks.get(0);
        assertEquals(1, lfNetworks.size());
        Path file = fileSystem.getPath("/work/n.json");
        mainNetwork.writeJson(file);
        try (InputStream is = Files.newInputStream(file)) {
            ComparisonUtils.assertTxtEquals(getClass().getResourceAsStream("/n.json"), is);
        }
    }

    @Test
    void testPhaseShifter() throws IOException {
        Network network = PhaseShifterTestCaseFactory.create();
        TwoWindingsTransformer ps1 = network.getTwoWindingsTransformer("PS1");
        ps1.getPhaseTapChanger()
                .setRegulationMode(PhaseTapChanger.RegulationMode.ACTIVE_POWER_CONTROL)
                .setTargetDeadband(1)
                .setRegulating(true)
                .setTapPosition(1)
                .setRegulationTerminal(ps1.getTerminal1())
                .setRegulationValue(83);

        LfNetworkParameters parameters = new LfNetworkParameters()
                .setSlackBusSelector(new MostMeshedSlackBusSelector())
                .setPhaseControl(true);
        List<LfNetwork> lfNetworks = Networks.load(network, parameters);
        LfNetwork mainNetwork = lfNetworks.get(0);
        assertEquals(1, lfNetworks.size());
        Path file = fileSystem.getPath("/work/n2.json");
        mainNetwork.writeJson(file);
        try (InputStream is = Files.newInputStream(file)) {
            ComparisonUtils.assertTxtEquals(getClass().getResourceAsStream("/n2.json"), is);
        }
    }

    @Test
    void getBranchByIdtest() {
        Network network = EurostagFactory.fix(EurostagTutorialExample1Factory.create());
        List<LfNetwork> lfNetworks = Networks.load(network, new MostMeshedSlackBusSelector());
        assertEquals(1, lfNetworks.size());
        LfNetwork lfNetwork = lfNetworks.get(0);
        assertNull(lfNetwork.getBranchById("AAA"));
        assertNotNull(lfNetwork.getBranchById("NHV1_NHV2_1"));
    }

    @Test
    void testDanglingLine() {
        Network network = DanglingLineNetworkFactory.create();
        List<LfNetwork> lfNetworks = Networks.load(network, new MostMeshedSlackBusSelector());
        assertEquals(1, lfNetworks.size());
        LfNetwork lfNetwork = lfNetworks.get(0);
        assertFalse(lfNetwork.getBusById("DL_BUS").isDisabled());
        assertTrue(lfNetwork.getBusById("DL_BUS").createBusResults().isEmpty());
    }

    @Test
    void testVsc() {
        Network network = HvdcNetworkFactory.createVsc();
        List<LfNetwork> lfNetworks = Networks.load(network, new MostMeshedSlackBusSelector());
        assertEquals(2, lfNetworks.size());
        LfNetwork lfNetwork = lfNetworks.get(0);
        assertEquals(0.0, lfNetwork.getGeneratorById("cs2").getParticipationFactor(), 1E-6);
    }

    @Test
    void testMultipleConnectedComponentsACMainComponent() {
        Network network = ConnectedComponentNetworkFactory.createTwoUnconnectedCC();
        LoadFlow.Runner loadFlowRunner = new LoadFlow.Runner(new OpenLoadFlowProvider(new DenseMatrixFactory()));
        LoadFlowParameters parameters = new LoadFlowParameters();
        LoadFlowResult result = loadFlowRunner.run(network, parameters);

        assertTrue(result.isFullyConverged());

        //Default is only compute load flow on the main component
        assertEquals(1, result.getComponentResults().size());
        assertEquals(ComponentConstants.MAIN_NUM, result.getComponentResults().get(0).getConnectedComponentNum());
    }

    @Test
    void testMultipleConnectedComponentsACAllComponents() {
        Network network = ConnectedComponentNetworkFactory.createTwoUnconnectedCC();
        LoadFlow.Runner loadFlowRunner = new LoadFlow.Runner(new OpenLoadFlowProvider(new DenseMatrixFactory()));
        LoadFlowParameters parameters = new LoadFlowParameters();
        parameters.setConnectedComponentMode(LoadFlowParameters.ConnectedComponentMode.ALL);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);

        assertTrue(result.isFullyConverged());
        assertEquals(2, result.getComponentResults().size());
    }

    @Test
    void testMultipleConnectedComponentsDCMainComponent() {
        Network network = ConnectedComponentNetworkFactory.createTwoUnconnectedCC();
        LoadFlow.Runner loadFlowRunner = new LoadFlow.Runner(new OpenLoadFlowProvider(new DenseMatrixFactory()));
        LoadFlowParameters parameters = new LoadFlowParameters();
        parameters.setDc(true);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);

        assertTrue(result.isFullyConverged());

        //Default is only compute load flow on the main component
        assertEquals(1, result.getComponentResults().size());
        assertEquals(ComponentConstants.MAIN_NUM, result.getComponentResults().get(0).getConnectedComponentNum());
    }

    @Test
    void testMultipleConnectedComponentsDCAllComponents() {
        Network network = ConnectedComponentNetworkFactory.createTwoUnconnectedCC();
        LoadFlow.Runner loadFlowRunner = new LoadFlow.Runner(new OpenLoadFlowProvider(new DenseMatrixFactory()));
        LoadFlowParameters parameters = new LoadFlowParameters();
        parameters.setConnectedComponentMode(LoadFlowParameters.ConnectedComponentMode.ALL)
                .setVoltageInitMode(LoadFlowParameters.VoltageInitMode.DC_VALUES);
        parameters.setDc(true);
        LoadFlowResult result = loadFlowRunner.run(network, parameters);

        assertTrue(result.isFullyConverged());
        assertEquals(2, result.getComponentResults().size());
    }

    private static void testGraphViz(Network network, boolean breakers, String ref) throws IOException {
        LfNetworkParameters parameters = new LfNetworkParameters().setBreakers(breakers);
        LfNetwork lfNetwork = Networks.load(network, parameters).get(0);
        try (StringWriter writer = new StringWriter()) {
            lfNetwork.writeGraphViz(writer, LoadFlowModel.AC);
            writer.flush();
            ComparisonUtils.assertTxtEquals(Objects.requireNonNull(LfNetworkTest.class.getResourceAsStream("/" + ref)), writer.toString());
        }
    }

    @Test
    void testGraphViz() throws IOException {
        testGraphViz(EurostagTutorialExample1Factory.create(), false, "sim1.dot");
        testGraphViz(NodeBreakerNetworkFactory.create(), true, "nb.dot");
        // with a disconnected line
        Network network = EurostagFactory.fix(EurostagTutorialExample1Factory.create());
        network.getLine("NHV1_NHV2_1").getTerminal1().disconnect();
        testGraphViz(network, false, "sim1_disconnected.dot");
    }

    @Test
    void testDisabledVoltageControl() {
        Network network = VoltageControlNetworkFactory.createWithDependentVoltageControls();
        List<LfNetwork> lfNetworks = Networks.load(network, new MostMeshedSlackBusSelector());
        assertEquals(1, lfNetworks.size());
        LfNetwork lfNetwork = lfNetworks.get(0);
        lfNetwork.getZeroImpedanceNetworks(LoadFlowModel.AC); // to update.
        LfBus b1 = lfNetwork.getBusById("b1_vl_0");
        assertSame(VoltageControl.MergeStatus.MAIN, b1.getGeneratorVoltageControl().orElseThrow().getMergeStatus());
        LfBus b2 = lfNetwork.getBusById("b2_vl_0");
        assertSame(VoltageControl.MergeStatus.DEPENDENT, b2.getGeneratorVoltageControl().orElseThrow().getMergeStatus());
        LfBus b3 = lfNetwork.getBusById("b3_vl_0");
        assertSame(VoltageControl.MergeStatus.DEPENDENT, b3.getGeneratorVoltageControl().orElseThrow().getMergeStatus());
        lfNetwork.getBusById("b01_vl_0").setDisabled(true); // only g1
        assertFalse(b1.getGeneratorVoltageControl().orElseThrow().isDisabled());
        assertFalse(b2.getGeneratorVoltageControl().orElseThrow().isDisabled());
        assertFalse(b3.getGeneratorVoltageControl().orElseThrow().isDisabled());
        assertFalse(b1.getGeneratorVoltageControl().orElseThrow().isHidden());
        assertFalse(b2.getGeneratorVoltageControl().orElseThrow().isHidden());
        assertFalse(b3.getGeneratorVoltageControl().orElseThrow().isHidden());

        b1.setDisabled(true);
        assertSame(VoltageControl.MergeStatus.MAIN, b1.getGeneratorVoltageControl().orElseThrow().getMergeStatus());
        assertSame(VoltageControl.MergeStatus.DEPENDENT, b2.getGeneratorVoltageControl().orElseThrow().getMergeStatus());
        assertSame(VoltageControl.MergeStatus.DEPENDENT, b3.getGeneratorVoltageControl().orElseThrow().getMergeStatus());
        assertFalse(b1.getGeneratorVoltageControl().orElseThrow().isDisabled());
        assertFalse(b2.getGeneratorVoltageControl().orElseThrow().isDisabled());
        assertFalse(b3.getGeneratorVoltageControl().orElseThrow().isDisabled());

        b2.setDisabled(true);
        b3.setDisabled(true);
        assertTrue(b1.getGeneratorVoltageControl().orElseThrow().isDisabled());
        assertTrue(b2.getGeneratorVoltageControl().orElseThrow().isDisabled());
        assertTrue(b3.getGeneratorVoltageControl().orElseThrow().isDisabled());
    }

    @Test
    void testElements() {
        Network network = HvdcNetworkFactory.createWithHvdcInAcEmulation();
        network.getHvdcLine("hvdc34").newExtension(HvdcAngleDroopActivePowerControlAdder.class)
                .withDroop(180)
                .withP0(0.f)
                .withEnabled(true)
                .add();
        List<LfNetwork> lfNetworks = Networks.load(network, new MostMeshedSlackBusSelector());
        LfNetwork mainNetwork = lfNetworks.get(0);
        assertEquals("b1_vl_0", mainNetwork.getElement(ElementType.BUS, 0).getId());
        assertEquals("hvdc34", mainNetwork.getElement(ElementType.HVDC, 0).getId());
        assertEquals("hvdc34", mainNetwork.getHvdc(0).getId());
    }

    @Test
    void testIsolatedForHvdc() {
        Network network = HvdcNetworkFactory.createWithHvdcAndGenerator();
        List<LfNetwork> lfNetworks = Networks.load(network, new MostMeshedSlackBusSelector());
        LfNetwork smallNetwork = lfNetworks.get(1);
        assertFalse(Networks.isIsolatedBusForHvdc(smallNetwork.getBusById("b4_vl_0"), Set.of(smallNetwork.getBusById("b4_vl_0"))));
    }

    @Test
    void evaluableGetterAndSetterTest() {
        Network network = EurostagFactory.fix(EurostagTutorialExample1Factory.create());
        List<LfNetwork> lfNetworks = Networks.load(network, new FirstSlackBusSelector());
        assertEquals(1, lfNetworks.size());
        LfNetwork lfNetwork = lfNetworks.get(0);
        LfBranch branch = lfNetwork.getBranch(0);
        testEvaluableGetterAndSetter(branch, LfBranch::getP1, LfBranch::setP1);
        testEvaluableGetterAndSetter(branch, LfBranch::getOpenP1, LfBranch::setOpenP1);
        testEvaluableGetterAndSetter(branch, LfBranch::getClosedP1, LfBranch::setClosedP1);
        testEvaluableGetterAndSetter(branch, LfBranch::getP2, LfBranch::setP2);
        testEvaluableGetterAndSetter(branch, LfBranch::getOpenP2, LfBranch::setOpenP2);
        testEvaluableGetterAndSetter(branch, LfBranch::getClosedP2, LfBranch::setClosedP2);
        testEvaluableGetterAndSetter(branch, LfBranch::getI1, LfBranch::setI1);
        testEvaluableGetterAndSetter(branch, LfBranch::getOpenI1, LfBranch::setOpenI1);
        testEvaluableGetterAndSetter(branch, LfBranch::getClosedI1, LfBranch::setClosedI1);
        testEvaluableGetterAndSetter(branch, LfBranch::getQ1, LfBranch::setQ1);
        testEvaluableGetterAndSetter(branch, LfBranch::getOpenQ1, LfBranch::setOpenQ1);
        testEvaluableGetterAndSetter(branch, LfBranch::getClosedQ1, LfBranch::setClosedQ1);
        testEvaluableGetterAndSetter(branch, LfBranch::getQ2, LfBranch::setQ2);
        testEvaluableGetterAndSetter(branch, LfBranch::getOpenQ2, LfBranch::setOpenQ2);
        testEvaluableGetterAndSetter(branch, LfBranch::getClosedQ2, LfBranch::setClosedQ2);
        testEvaluableGetterAndSetter(branch, LfBranch::getI2, LfBranch::setI2);
        testEvaluableGetterAndSetter(branch, LfBranch::getOpenI2, LfBranch::setOpenI2);
        testEvaluableGetterAndSetter(branch, LfBranch::getClosedI2, LfBranch::setClosedI2);
        testAdditionalEvaluableGetterAndSetter(branch, LfBranch::getAdditionalClosedP1, LfBranch::addAdditionalClosedP1);
        testAdditionalEvaluableGetterAndSetter(branch, LfBranch::getAdditionalOpenP1, LfBranch::addAdditionalOpenP1);
        testAdditionalEvaluableGetterAndSetter(branch, LfBranch::getAdditionalClosedQ1, LfBranch::addAdditionalClosedQ1);
        testAdditionalEvaluableGetterAndSetter(branch, LfBranch::getAdditionalOpenQ1, LfBranch::addAdditionalOpenQ1);
        testAdditionalEvaluableGetterAndSetter(branch, LfBranch::getAdditionalClosedP2, LfBranch::addAdditionalClosedP2);
        testAdditionalEvaluableGetterAndSetter(branch, LfBranch::getAdditionalOpenP2, LfBranch::addAdditionalOpenP2);
        testAdditionalEvaluableGetterAndSetter(branch, LfBranch::getAdditionalClosedQ2, LfBranch::addAdditionalClosedQ2);
        testAdditionalEvaluableGetterAndSetter(branch, LfBranch::getAdditionalOpenQ2, LfBranch::addAdditionalOpenQ2);
    }

    private static void testEvaluableGetterAndSetter(LfBranch branch, Function<LfBranch, Evaluable> getter, BiConsumer<LfBranch, Evaluable> setter) {
        Evaluable evaluable = () -> 0;
        assertSame(EvaluableConstants.NAN, getter.apply(branch));
        setter.accept(branch, evaluable);
        assertSame(evaluable, getter.apply(branch));
        branch.removeEvaluable(evaluable);
        assertSame(EvaluableConstants.NAN, getter.apply(branch));
    }

    private static void testAdditionalEvaluableGetterAndSetter(LfBranch branch, Function<LfBranch, List<Evaluable>> getter, BiConsumer<LfBranch, Evaluable> adder) {
        Evaluable evaluable = () -> 0;
        assertTrue(getter.apply(branch).isEmpty());
        adder.accept(branch, evaluable);
        assertEquals(1, getter.apply(branch).size());
        assertEquals(evaluable, getter.apply(branch).get(0));
        branch.removeEvaluable(evaluable);
        assertTrue(getter.apply(branch).isEmpty());
    }

    @Test
    void testLimitReductions() {
        Network network = EurostagFactory.fix(EurostagTutorialExample1Factory.createWithFixedCurrentLimits());
        // NHV1_NHV2_1 : side 1 PATL 500, side 2 PATL 1100, 1200 for 600s and 1500 for 60s then above 0s
        // NHV1_NHV2_2 : side 1 PATL 1100, 1200 for 1200s then above 60s, side 2 PATL 500
        List<LfNetwork> lfNetworks = Networks.load(network, new FirstSlackBusSelector());
        LimitReductionManager.TerminalLimitReduction terminalLimitReduction1 =
                new LimitReductionManager.TerminalLimitReduction(Range.of(300., 500.), true, null, 0.5);
        LimitReductionManager.TerminalLimitReduction terminalLimitReduction2 =
                new LimitReductionManager.TerminalLimitReduction(Range.of(300., 500.), false, Range.of(0, 60), 0.9);
        LimitReductionManager.TerminalLimitReduction terminalLimitReduction3 =
                new LimitReductionManager.TerminalLimitReduction(Range.of(300., 500.), false, Range.of(600, 1200), 0.8);
        LimitReductionManager limitReductionManager = new LimitReductionManager();
        limitReductionManager.addTerminalLimitReduction(terminalLimitReduction1);
        limitReductionManager.addTerminalLimitReduction(terminalLimitReduction2);
        limitReductionManager.addTerminalLimitReduction(terminalLimitReduction3);

        LfBranch lfBranch = lfNetworks.get(0).getBranchById("NHV1_NHV2_1");
        Branch<?> branch = network.getBranch("NHV1_NHV2_1");
        double[] reductions = lfBranch.getLimitReductions(TwoSides.ONE, limitReductionManager, branch.getNullableCurrentLimits1());
        assertEquals(1, reductions.length);
        assertEquals(0.5, reductions[0], 0.001); // PATL
        reductions = lfBranch.getLimitReductions(TwoSides.TWO, limitReductionManager, branch.getNullableCurrentLimits2());
        assertEquals(4, reductions.length);
        assertEquals(0.5, reductions[0], 0.001); // PATL
        assertEquals(0.8, reductions[1], 0.001); // TATL 600s
        assertEquals(0.9, reductions[2], 0.001); // TATL 60s
        assertEquals(0.9, reductions[3], 0.001); // TATL 0s

        lfBranch = lfNetworks.get(0).getBranchById("NHV1_NHV2_2");
        branch = network.getBranch("NHV1_NHV2_2");
        reductions = lfBranch.getLimitReductions(TwoSides.ONE, limitReductionManager, branch.getNullableCurrentLimits1());
        assertEquals(3, reductions.length);
        assertEquals(0.5, reductions[0], 0.001); // PATL
        assertEquals(0.8, reductions[1], 0.001); // TATL 1200s
        assertEquals(0.9, reductions[2], 0.001); // TATL 60s
        reductions = lfBranch.getLimitReductions(TwoSides.TWO, limitReductionManager, branch.getNullableCurrentLimits2());
        assertEquals(1, reductions.length);
        assertEquals(0.5, reductions[0], 0.001); // PATL
    }

    @Test
    void testNoLimitReductionsApplies() {
        Network network = EurostagFactory.fix(EurostagTutorialExample1Factory.createWithFixedCurrentLimits());
        // NHV1_NHV2_1 : side 1 PATL 500, side 2 PATL 1100, 1200 for 600s and 1500 for 60s then above 0s
        // NHV1_NHV2_2 : side 1 PATL 1100, 1200 for 1200s then above 60s, side 2 PATL 500
        List<LfNetwork> lfNetworks = Networks.load(network, new FirstSlackBusSelector());
        LfBranch lfBranch = lfNetworks.get(0).getBranchById("NHV1_NHV2_2");
        Branch<?> branch = network.getBranch("NHV1_NHV2_2");

        // No reductions because the LimitReductionManager is null
        double[] reductions = lfBranch.getLimitReductions(TwoSides.ONE, null, branch.getNullableCurrentLimits1());
        assertEquals(0, reductions.length);

        // No reductions because the LimitReductionManager is empty
        reductions = lfBranch.getLimitReductions(TwoSides.ONE, new LimitReductionManager(), branch.getNullableCurrentLimits1());
        assertEquals(0, reductions.length);

        // No reduction applies because the line isn't within the nominal voltage range => all values equals to 1.
        LimitReductionManager.TerminalLimitReduction terminalLimitReduction0 =
                new LimitReductionManager.TerminalLimitReduction(Range.of(100., 200.), true, null, 0.5);
        LimitReductionManager limitReductionManager0 = new LimitReductionManager();
        limitReductionManager0.addTerminalLimitReduction(terminalLimitReduction0);
        reductions = lfBranch.getLimitReductions(TwoSides.ONE, limitReductionManager0, branch.getNullableCurrentLimits1());
        assertEquals(3, reductions.length);
        assertEquals(1., reductions[0], 0.001); // PATL
        assertEquals(1., reductions[1], 0.001); // TATL 1200s
        assertEquals(1., reductions[2], 0.001); // TATL 60s

        // No reductions because only current limits are supported
        branch.newActivePowerLimits1().setPermanentLimit(100.).add();
        LimitReductionManager.TerminalLimitReduction terminalLimitReduction1 =
                new LimitReductionManager.TerminalLimitReduction(Range.of(0., Double.MAX_VALUE), true, null, 0.5);
        LimitReductionManager limitReductionManager1 = new LimitReductionManager();
        limitReductionManager1.addTerminalLimitReduction(terminalLimitReduction1);
        reductions = lfBranch.getLimitReductions(TwoSides.ONE, limitReductionManager1, branch.getNullableActivePowerLimits1());
        assertEquals(0, reductions.length);

        // No reductions because there's no limits
        reductions = lfBranch.getLimitReductions(TwoSides.ONE, limitReductionManager1, null);
        assertEquals(0, reductions.length);
    }

    @Test
    void testSeveralLimitReductionsForTheSameLimit() {
        Network network = EurostagFactory.fix(EurostagTutorialExample1Factory.createWithFixedCurrentLimits());
        // NHV1_NHV2_1 : side 1 PATL 500, side 2 PATL 1100, 1200 for 600s and 1500 for 60s then above 0s
        // NHV1_NHV2_2 : side 1 PATL 1100, 1200 for 1200s then above 60s, side 2 PATL 500
        List<LfNetwork> lfNetworks = Networks.load(network, new FirstSlackBusSelector());
        LimitReductionManager.TerminalLimitReduction terminalLimitReduction1 =
                new LimitReductionManager.TerminalLimitReduction(Range.of(300., 500.), true, null, 0.5);
        LimitReductionManager.TerminalLimitReduction terminalLimitReduction2 =
                new LimitReductionManager.TerminalLimitReduction(Range.of(300., 500.), false, Range.of(0, 60), 0.9);
        LimitReductionManager.TerminalLimitReduction terminalLimitReduction3 =
                new LimitReductionManager.TerminalLimitReduction(Range.of(300., 500.), false, Range.of(600, 1200), 0.8);
        // The following reduction overlaps `terminalLimitReduction2` for temporary limits which acceptable duration is in [0,30] seconds.
        LimitReductionManager.TerminalLimitReduction terminalLimitReduction4 =
                new LimitReductionManager.TerminalLimitReduction(Range.of(300., 500.), false, Range.of(0, 30), 0.87);
        LimitReductionManager limitReductionManager = new LimitReductionManager();
        limitReductionManager.addTerminalLimitReduction(terminalLimitReduction1);
        limitReductionManager.addTerminalLimitReduction(terminalLimitReduction2);
        limitReductionManager.addTerminalLimitReduction(terminalLimitReduction3);
        limitReductionManager.addTerminalLimitReduction(terminalLimitReduction4);

        LfBranch lfBranch = lfNetworks.get(0).getBranchById("NHV1_NHV2_1");
        Branch<?> branch = network.getBranch("NHV1_NHV2_1");
        double[] reductions = lfBranch.getLimitReductions(TwoSides.ONE, limitReductionManager, branch.getNullableCurrentLimits1());
        assertEquals(1, reductions.length);
        assertEquals(0.5, reductions[0], 0.001); // PATL
        reductions = lfBranch.getLimitReductions(TwoSides.TWO, limitReductionManager, branch.getNullableCurrentLimits2());
        assertEquals(4, reductions.length);
        assertEquals(0.5, reductions[0], 0.001); // PATL
        assertEquals(0.8, reductions[1], 0.001); // TATL 600s
        assertEquals(0.9, reductions[2], 0.001); // TATL 60s
        // `terminalLimitReduction4` is declared after `terminalLimitReduction2`, so its value is used
        assertEquals(0.87, reductions[3], 0.001); // TATL 0s

        limitReductionManager = new LimitReductionManager();
        limitReductionManager.addTerminalLimitReduction(terminalLimitReduction1);
        limitReductionManager.addTerminalLimitReduction(terminalLimitReduction4);
        limitReductionManager.addTerminalLimitReduction(terminalLimitReduction2);
        limitReductionManager.addTerminalLimitReduction(terminalLimitReduction3);
        reductions = lfBranch.getLimitReductions(TwoSides.TWO, limitReductionManager, branch.getNullableCurrentLimits2());
        assertEquals(4, reductions.length);
        assertEquals(0.5, reductions[0], 0.001); // PATL
        assertEquals(0.8, reductions[1], 0.001); // TATL 600s
        assertEquals(0.9, reductions[2], 0.001); // TATL 60s
        // `terminalLimitReduction4` is now declared before `terminalLimitReduction2`, its value is overlapped by the one of `terminalLimitReduction2`
        assertEquals(0.9, reductions[3], 0.001); // TATL 0s
    }

    @Test
    void slackBusSelectionFallback() {
        Network network = FourBusNetworkFactory.createBaseNetwork();
        network.getSubstations().forEach(substation -> substation.setCountry(Country.FR));

        LoadFlow.Runner loadFlowRunner = new LoadFlow.Runner(new OpenLoadFlowProvider(new DenseMatrixFactory()));
        LoadFlowParameters parameters = new LoadFlowParameters();
        parameters.setReadSlackBus(false);
        OpenLoadFlowParameters parametersExt = OpenLoadFlowParameters.create(parameters);

        // Setup a slack bus selection method with a filter on country that we do not have in the network (for it to fail)
        parametersExt.setSlackBusSelectionMode(SlackBusSelectionMode.FIRST);
        parametersExt.setSlackBusCountryFilter(Set.of(Country.BE));

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

    @Test
    void slackBusSelectionFallbackFails() {
        Network network = FourBusNetworkFactory.createBaseNetwork();
        network.getSubstations().forEach(substation -> substation.setCountry(Country.FR));

        // Make the initial slack bus selector fail and the fallback fail by setting all buses as excluded buses
        List<LfNetwork> lfNetworks = Networks.load(network, new FirstSlackBusSelector(Set.of(Country.BE)));
        LfNetwork first = lfNetworks.get(0);
        first.setExcludedSlackBuses(new HashSet<>(first.getBuses()));
        assertThrows(PowsyblException.class, first::updateSlackBusesAndReferenceBus,
            "No slack bus could be selected");
    }
}