TapPositionModificationTest.java

/**
 * Copyright (c) 2023, 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.iidm.modification.tapchanger;

import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.iidm.modification.AbstractNetworkModification;
import com.powsybl.iidm.modification.NetworkModification;
import com.powsybl.iidm.modification.NetworkModificationImpact;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.test.FourSubstationsNodeBreakerFactory;
import com.powsybl.iidm.network.test.ThreeWindingsTransformerNetworkFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.function.Supplier;

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

/**
 * @author Nicolas PIERRE {@literal <nicolas.pierre at artelys.com>}
 */
class TapPositionModificationTest {
    private Network network;
    private Network threeWindingNetwork;
    private TwoWindingsTransformer twoWindingsTransformer;
    private ThreeWindingsTransformer threeWindingTransformer;
    private static final ThreeSides LEG_NUM = ThreeSides.TWO; // Using leg number 2
    private ThreeWindingsTransformer.Leg threeWindingTransformerLeg;

    @BeforeEach
    public void setUp() {
        network = FourSubstationsNodeBreakerFactory.create();
        threeWindingNetwork = ThreeWindingsTransformerNetworkFactory.create();
        threeWindingTransformer = threeWindingNetwork.getThreeWindingsTransformers().iterator().next();
        twoWindingsTransformer = network.getTwoWindingsTransformerStream().findAny().orElseThrow();
        assertTrue(twoWindingsTransformer.hasPhaseTapChanger());
        assertTrue(twoWindingsTransformer.hasRatioTapChanger());
        twoWindingsTransformer.getPhaseTapChanger()
            .setTapPosition(twoWindingsTransformer.getPhaseTapChanger().getHighTapPosition());
        twoWindingsTransformer.getRatioTapChanger()
            .setTapPosition(twoWindingsTransformer.getRatioTapChanger().getHighTapPosition());
        threeWindingTransformerLeg = threeWindingTransformer.getLeg(LEG_NUM);
    }

    @Test
    void testArgumentCoherence() {
        // Good
        String id = twoWindingsTransformer.getId();
        ThreeSides leg = ThreeSides.TWO;
        assertDoesNotThrow(() -> new PhaseTapPositionModification(id, 0));
        // Log warning, but good, Leg value is ignored
        assertDoesNotThrow(() -> new PhaseTapPositionModification(id, 0));
        // good
        assertDoesNotThrow(
            () -> new PhaseTapPositionModification(id, 0, leg));
        assertDoesNotThrow(() -> new RatioTapPositionModification(id, 0));
        // Log warning, but good, Leg value is ignored
        assertDoesNotThrow(() -> new RatioTapPositionModification(id, 0));
        // good
        assertDoesNotThrow(
            () -> new RatioTapPositionModification(id, 0, leg));
    }

    @Test
    void testGetLeg() {
        RatioTapPositionModification modifRTC = new RatioTapPositionModification("ID", 1, LEG_NUM);
        assertNull(modifRTC.getLeg(null, leg -> true, true));
        // defined leg in constructor
        assertEquals(threeWindingTransformerLeg, modifRTC.getLeg(threeWindingTransformer, leg -> true, true));
        modifRTC = new RatioTapPositionModification("ID", 1);
        // no match
        assertNull(modifRTC.getLeg(threeWindingTransformer, leg -> false, false));
        // mutliple match
        assertNull(modifRTC.getLeg(threeWindingTransformer, RatioTapChangerHolder::hasRatioTapChanger, false));
        // single match
        assertEquals(threeWindingTransformerLeg,
            modifRTC.getLeg(threeWindingTransformer, leg -> leg.equals(threeWindingTransformerLeg), false));
    }

    @Test
    void testUnknownId() {
        NetworkModification modif = new PhaseTapPositionModification("UNKNOWN_ID", 5);
        assertThrows(PowsyblException.class, () -> modif.apply(network, true, ReportNode.NO_OP));
        assertDoesNotThrow(() -> modif.apply(network, false, ReportNode.NO_OP),
            "An invalid ID should not throw if throwException is false.");
        NetworkModification modif2 = new RatioTapPositionModification("UNKNOWN_ID", 5);
        assertThrows(PowsyblException.class, () -> modif2.apply(network, true, ReportNode.NO_OP));
        assertDoesNotThrow(() -> modif2.apply(network, false, ReportNode.NO_OP),
            "An invalid ID should not throw if throwException is false.");
    }

    @Test
    void testTwoWindingsModif() {
        testTapTransformer(twoWindingsTransformer.getPhaseTapChanger(), IdentifiableType.TWO_WINDINGS_TRANSFORMER,
            TapType.PHASE, twoWindingsTransformer.getId());
        testTapTransformer(twoWindingsTransformer.getRatioTapChanger(), IdentifiableType.TWO_WINDINGS_TRANSFORMER,
            TapType.RATIO, twoWindingsTransformer.getId());
    }

    @Test
    void testMissingTap() {
        ThreeWindingsTransformer.Leg leg = threeWindingTransformer.getLeg1();
        // Leg1 does not have a Rtc/Ptc. Trying to modify them must throw/log.
        // Ratio
        assertFalse(leg.hasPhaseTapChanger(), "Test assumptions are wrong.");
        NetworkModification modifRtc = new RatioTapPositionModification(threeWindingNetwork.getId(), 1,
            ThreeSides.ONE);
        assertThrows(PowsyblException.class, () -> modifRtc.apply(threeWindingNetwork, true, ReportNode.NO_OP),
            "Modifying a Ratio tap that is not present should throw.");
        assertDoesNotThrow(() -> modifRtc.apply(threeWindingNetwork, false, ReportNode.NO_OP));
        // Phase
        assertFalse(leg.hasRatioTapChanger(), "Test assumptions are wrong.");
        NetworkModification modifPtc = new PhaseTapPositionModification(threeWindingNetwork.getId(), 1,
            ThreeSides.ONE);
        assertThrows(PowsyblException.class, () -> modifPtc.apply(threeWindingNetwork, true, ReportNode.NO_OP),
            "Modifying a Phasetap that is not present should throw.");
        assertDoesNotThrow(() -> modifPtc.apply(threeWindingNetwork, false, ReportNode.NO_OP));
    }

    @Test
    void testThreeWindingsModif() {
        ThreeWindingsTransformer.Leg leg = threeWindingTransformer.getLeg2();

        leg.newPhaseTapChanger()
            .setLowTapPosition(0)
            .setTapPosition(0)
            .beginStep()
            .setR(0.01)
            .setX(0.0001)
            .setB(0)
            .setG(0)
            .setRho(1.1)
            .setAlpha(1)
            .endStep()
            .beginStep()
            .setR(0.02)
            .setX(0.0002)
            .setB(0)
            .setG(0)
            .setRho(1.2)
            .setAlpha(1.1)
            .endStep()
            .add();
        testTapTransformer(threeWindingTransformerLeg.getPhaseTapChanger(), IdentifiableType.THREE_WINDINGS_TRANSFORMER,
            TapType.PHASE, threeWindingTransformer.getId());
        testTapTransformer(threeWindingTransformerLeg.getRatioTapChanger(), IdentifiableType.THREE_WINDINGS_TRANSFORMER,
            TapType.RATIO, threeWindingTransformer.getId());
    }

    private void testTapTransformer(TapChanger<?, ?, ?, ?> tapChanger, final IdentifiableType transformerElement,
                                    final TapType tapType, final String transformerId) {
        Supplier<Integer> tapPositionSupplier = tapChanger::getTapPosition;
        assertTrue(tapChanger.getLowTapPosition() < tapChanger.getHighTapPosition());

        testValidTapPos(tapType, tapChanger.getHighTapPosition() - 1, transformerId, transformerElement,
            tapPositionSupplier);
        int actualPtcPos = tapChanger.getTapPosition();
        testInvalidTapPosition(actualPtcPos, tapType, transformerElement, tapChanger.getHighTapPosition() + 1,
            transformerId, tapPositionSupplier);
        testInvalidTapPosition(actualPtcPos, tapType, transformerElement, tapChanger.getLowTapPosition() - 1,
            transformerId, tapPositionSupplier);
    }

    private void testValidTapPos(final TapType type, final int tapPos, final String transformerId,
                                 final IdentifiableType element, final Supplier<Integer> tapPositionSupplier) {
        ThreeSides leg;
        Network networkToApply;
        NetworkModification modif;
        if (IdentifiableType.TWO_WINDINGS_TRANSFORMER.equals(element)) {
            leg = null;
            networkToApply = network;
        } else {
            leg = LEG_NUM;
            networkToApply = threeWindingNetwork;
        }
        modif = getNetworkModification(type, tapPos, transformerId, leg);
        modif.apply(networkToApply);
        assertEquals(tapPos, tapPositionSupplier.get(), "Tap Modification did not change the network");
    }

    private static NetworkModification getNetworkModification(TapType type, int tapPos, String transformerId,
                                                              ThreeSides leg) {
        NetworkModification modif;
        if (TapType.RATIO.equals(type)) {
            if (leg != null) {
                modif = new RatioTapPositionModification(transformerId, tapPos, leg);
            } else {
                modif = new RatioTapPositionModification(transformerId, tapPos);
            }
        } else { // type ==  TapType.Phase
            if (leg != null) {
                modif = new PhaseTapPositionModification(transformerId, tapPos, leg);
            } else {
                modif = new PhaseTapPositionModification(transformerId, tapPos);
            }
        }
        return modif;
    }

    private void testInvalidTapPosition(int currentTapPos, final TapType type, IdentifiableType element,
                                        int invalidTapPos, final String transformerId,
                                        final Supplier<Integer> tapPositionSupplier) {
        ThreeSides leg;
        Network networkToApply;
        NetworkModification modif;
        if (IdentifiableType.TWO_WINDINGS_TRANSFORMER.equals(element)) {
            leg = null;
            networkToApply = network;
        } else {
            leg = LEG_NUM;
            networkToApply = threeWindingNetwork;
        }
        modif = getNetworkModification(type, invalidTapPos, transformerId, leg);
        assertDoesNotThrow(() -> modif.apply(networkToApply, false, ReportNode.NO_OP));
        assertThrows(PowsyblException.class, () -> modif.apply(networkToApply, true, ReportNode.NO_OP));
        assertEquals(currentTapPos, tapPositionSupplier.get(),
            "Invalid tap position should not be applied to the network");
        modif.apply(networkToApply, false, ReportNode.NO_OP);
        assertEquals(currentTapPos, tapPositionSupplier.get(),
            "Invalid tap position should not be applied to the network");
    }

    public enum TapType {
        PHASE, RATIO
    }

    @Test
    void testGetName() {
        AbstractNetworkModification networkModification = new PhaseTapPositionModification("ID", 10);
        assertEquals("PhaseTapPositionModification", networkModification.getName());

        networkModification = new RatioTapPositionModification("ID", 10);
        assertEquals("RatioTapPositionModification", networkModification.getName());
    }

    @Test
    void testHasImpact() {
        NetworkModification modification1 = getNetworkModification(TapType.PHASE, 0, "NOT_EXISTING", ThreeSides.ONE);
        assertEquals(NetworkModificationImpact.CANNOT_BE_APPLIED, modification1.hasImpactOnNetwork(network));

        NetworkModification modification2 = getNetworkModification(TapType.RATIO, 0, "NOT_EXISTING", ThreeSides.ONE);
        assertEquals(NetworkModificationImpact.CANNOT_BE_APPLIED, modification2.hasImpactOnNetwork(network));

        NetworkModification modification3 = getNetworkModification(TapType.PHASE, 0, twoWindingsTransformer.getId(), ThreeSides.ONE);
        assertEquals(NetworkModificationImpact.HAS_IMPACT_ON_NETWORK, modification3.hasImpactOnNetwork(network));

        NetworkModification modification4 = getNetworkModification(TapType.RATIO, 0, twoWindingsTransformer.getId(), ThreeSides.ONE);
        assertEquals(NetworkModificationImpact.HAS_IMPACT_ON_NETWORK, modification4.hasImpactOnNetwork(network));

        NetworkModification modification5 = getNetworkModification(TapType.PHASE, 32, twoWindingsTransformer.getId(), ThreeSides.ONE);
        assertEquals(NetworkModificationImpact.NO_IMPACT_ON_NETWORK, modification5.hasImpactOnNetwork(network));

        NetworkModification modification6 = getNetworkModification(TapType.RATIO, 2, twoWindingsTransformer.getId(), ThreeSides.ONE);
        assertEquals(NetworkModificationImpact.NO_IMPACT_ON_NETWORK, modification6.hasImpactOnNetwork(network));

        twoWindingsTransformer.getRatioTapChanger().remove();
        twoWindingsTransformer.getPhaseTapChanger().remove();
        assertEquals(NetworkModificationImpact.CANNOT_BE_APPLIED, modification5.hasImpactOnNetwork(network));
        assertEquals(NetworkModificationImpact.CANNOT_BE_APPLIED, modification6.hasImpactOnNetwork(network));

        NetworkModification modification7 = getNetworkModification(TapType.PHASE, 0, threeWindingTransformer.getId(), ThreeSides.ONE);
        assertEquals(NetworkModificationImpact.CANNOT_BE_APPLIED, modification7.hasImpactOnNetwork(threeWindingNetwork));

        NetworkModification modification8 = getNetworkModification(TapType.RATIO, 0, threeWindingTransformer.getId(), ThreeSides.ONE);
        assertEquals(NetworkModificationImpact.CANNOT_BE_APPLIED, modification8.hasImpactOnNetwork(threeWindingNetwork));

        ThreeWindingsTransformer.Leg leg = threeWindingTransformer.getLeg1();
        leg.newPhaseTapChanger()
            .setLowTapPosition(0)
            .setTapPosition(0)
            .beginStep()
            .setR(0.01)
            .setX(0.0001)
            .setB(0)
            .setG(0)
            .setRho(1.1)
            .setAlpha(1)
            .endStep()
            .beginStep()
            .setR(0.02)
            .setX(0.0002)
            .setB(0)
            .setG(0)
            .setRho(1.2)
            .setAlpha(1.1)
            .endStep()
            .add();
        leg.newRatioTapChanger()
            .setLowTapPosition(0)
            .setTapPosition(0)
            .beginStep()
            .setRho(1.0)
            .endStep()
            .setTargetV(leg.getTerminal().getVoltageLevel().getNominalV())
            .setTargetDeadband(2.0)
            .setRegulationTerminal(leg.getTerminal())
            .add();

        NetworkModification modification9 = getNetworkModification(TapType.PHASE, 0, threeWindingTransformer.getId(), ThreeSides.ONE);
        assertEquals(NetworkModificationImpact.NO_IMPACT_ON_NETWORK, modification9.hasImpactOnNetwork(threeWindingNetwork));

        NetworkModification modification10 = getNetworkModification(TapType.RATIO, 2, threeWindingTransformer.getId(), ThreeSides.TWO);
        assertEquals(NetworkModificationImpact.NO_IMPACT_ON_NETWORK, modification10.hasImpactOnNetwork(threeWindingNetwork));

        NetworkModification modification11 = getNetworkModification(TapType.PHASE, 1, threeWindingTransformer.getId(), ThreeSides.ONE);
        assertEquals(NetworkModificationImpact.HAS_IMPACT_ON_NETWORK, modification11.hasImpactOnNetwork(threeWindingNetwork));

        NetworkModification modification12 = getNetworkModification(TapType.RATIO, 1, threeWindingTransformer.getId(), ThreeSides.TWO);
        assertEquals(NetworkModificationImpact.HAS_IMPACT_ON_NETWORK, modification12.hasImpactOnNetwork(threeWindingNetwork));

        NetworkModification modification13 = getNetworkModification(TapType.PHASE, 10, threeWindingTransformer.getId(), ThreeSides.ONE);
        assertEquals(NetworkModificationImpact.CANNOT_BE_APPLIED, modification13.hasImpactOnNetwork(threeWindingNetwork));

        NetworkModification modification14 = getNetworkModification(TapType.RATIO, 10, threeWindingTransformer.getId(), ThreeSides.TWO);
        assertEquals(NetworkModificationImpact.CANNOT_BE_APPLIED, modification14.hasImpactOnNetwork(threeWindingNetwork));

        NetworkModification modification15 = getNetworkModification(TapType.PHASE, -1, threeWindingTransformer.getId(), ThreeSides.ONE);
        assertEquals(NetworkModificationImpact.CANNOT_BE_APPLIED, modification15.hasImpactOnNetwork(threeWindingNetwork));

        NetworkModification modification16 = getNetworkModification(TapType.RATIO, -1, threeWindingTransformer.getId(), ThreeSides.TWO);
        assertEquals(NetworkModificationImpact.CANNOT_BE_APPLIED, modification16.hasImpactOnNetwork(threeWindingNetwork));
    }
}