CracValidatorTest.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/.
 */

package com.powsybl.openrao.data.crac.util;

import com.powsybl.contingency.ContingencyElement;
import com.powsybl.contingency.ContingencyElementType;
import com.powsybl.openrao.commons.Unit;
import com.powsybl.openrao.data.crac.api.Crac;
import com.powsybl.openrao.data.crac.api.CracFactory;
import com.powsybl.openrao.data.crac.api.Instant;
import com.powsybl.openrao.data.crac.api.InstantKind;
import com.powsybl.openrao.data.crac.api.cnec.FlowCnec;
import com.powsybl.iidm.network.TwoSides;
import com.powsybl.openrao.data.crac.api.networkaction.ActionType;
import com.powsybl.openrao.data.crac.api.usagerule.UsageMethod;
import com.powsybl.iidm.network.Country;
import com.powsybl.iidm.network.Network;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.List;

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

/**
 * @author Peter Mitri {@literal <peter.mitri at rte-france.com>}
 */
class CracValidatorTest {
    private static final String PREVENTIVE_INSTANT_ID = "preventive";
    private static final String OUTAGE_INSTANT_ID = "outage";
    private static final String AUTO_INSTANT_ID = "auto";

    private Crac crac;
    private Network network;
    private Instant outageInstant;

    private ContingencyElementType getContingencyType(String id) {
        return ContingencyElement.of(network.getIdentifiable(id)).getType();
    }

    @BeforeEach
    public void setUp() {
        network = Network.read("TestCase12Nodes.uct", getClass().getResourceAsStream("/TestCase12Nodes.uct"));

        crac = CracFactory.findDefault().create("crac")
            .newInstant(PREVENTIVE_INSTANT_ID, InstantKind.PREVENTIVE)
            .newInstant(OUTAGE_INSTANT_ID, InstantKind.OUTAGE)
            .newInstant(AUTO_INSTANT_ID, InstantKind.AUTO);
        outageInstant = crac.getInstant(OUTAGE_INSTANT_ID);
        crac.newContingency().withId("co-1").withContingencyElement("BBE1AA1  BBE2AA1  1", getContingencyType("BBE1AA1  BBE2AA1  1")).add();
        crac.newContingency().withId("co-2").withContingencyElement("BBE1AA1  BBE3AA1  1", getContingencyType("BBE1AA1  BBE3AA1  1")).add();

        crac.newFlowCnec()
            .withId("auto-cnec-1")
            .withNetworkElement("BBE1AA1  BBE2AA1  1")
            .withContingency("co-1").withInstant(AUTO_INSTANT_ID)
            .withNominalVoltage(400., TwoSides.ONE)
            .withNominalVoltage(200., TwoSides.TWO)
            .withIMax(2000., TwoSides.ONE)
            .withIMax(4000., TwoSides.TWO)
            .withReliabilityMargin(15.)
            .withOptimized()
            .newThreshold().withMin(-100.).withMax(100.).withUnit(Unit.MEGAWATT).withSide(TwoSides.ONE).add()
            .newThreshold().withMin(-100.).withMax(100.).withUnit(Unit.MEGAWATT).withSide(TwoSides.TWO).add()
            .newThreshold().withMin(-1.).withMax(1.).withUnit(Unit.PERCENT_IMAX).withSide(TwoSides.ONE).add()
            .add();

        crac.newFlowCnec()
            .withId("auto-cnec-2")
            .withNetworkElement("FFR2AA1  DDE3AA1  1")
            .withContingency("co-1").withInstant(AUTO_INSTANT_ID)
            .withNominalVoltage(300., TwoSides.ONE)
            .withNominalVoltage(900., TwoSides.TWO)
            .withIMax(40, TwoSides.ONE)
            .withIMax(40., TwoSides.TWO)
            .withReliabilityMargin(0.)
            .newThreshold().withMax(1000.).withUnit(Unit.AMPERE).withSide(TwoSides.ONE).add()
            .add();

        crac.newFlowCnec()
            .withId("auto-cnec-3")
            .withNetworkElement("BBE1AA1  BBE3AA1  1")
            .withContingency("co-2").withInstant(AUTO_INSTANT_ID)
            .withNominalVoltage(500., TwoSides.ONE)
            .withNominalVoltage(700., TwoSides.TWO)
            .withIMax(200., TwoSides.ONE)
            .withIMax(400., TwoSides.TWO)
            .withReliabilityMargin(1.)
            .withMonitored()
            .newThreshold().withMin(-1.).withUnit(Unit.PERCENT_IMAX).withSide(TwoSides.TWO).add()
            .add();
    }

    private void assertCnecHasOutageDuplicate(String flowCnecId) {
        FlowCnec flowCnec = crac.getFlowCnec(flowCnecId);
        assertNotNull(flowCnec);
        FlowCnec duplicate = crac.getFlowCnec(flowCnec.getId() + " - OUTAGE DUPLICATE");
        assertNotNull(duplicate);
        assertEquals(flowCnec.getNetworkElement().getId(), duplicate.getNetworkElement().getId());
        assertEquals(flowCnec.getState().getContingency(), duplicate.getState().getContingency());
        assertEquals(outageInstant, duplicate.getState().getInstant());
        assertEquals(flowCnec.isOptimized(), duplicate.isOptimized());
        assertEquals(flowCnec.isMonitored(), duplicate.isMonitored());
        assertEquals(flowCnec.getReliabilityMargin(), duplicate.getReliabilityMargin(), 1e-6);
        assertEquals(flowCnec.getIMax(TwoSides.ONE), duplicate.getIMax(TwoSides.ONE), 1e-6);
        assertEquals(flowCnec.getIMax(TwoSides.TWO), duplicate.getIMax(TwoSides.TWO), 1e-6);
        assertEquals(flowCnec.getNominalVoltage(TwoSides.ONE), duplicate.getNominalVoltage(TwoSides.ONE), 1e-6);
        assertEquals(flowCnec.getNominalVoltage(TwoSides.TWO), duplicate.getNominalVoltage(TwoSides.TWO), 1e-6);
        assertEquals(flowCnec.getThresholds(), duplicate.getThresholds());
    }

    @Test
    void testDuplicateAutoCnecs0() {
        // No auto RA in CRAC => no auto perimeter => no need to duplicate CNECs
        CracValidator.validateCrac(crac, network);

        assertEquals(3, crac.getFlowCnecs().size());
    }

    @Test
    void testDuplicateAutoCnecs1() {
        // Auto RAs in CRAC but useless for 3 CNECs => duplicate all 3 auto CNECs
        crac.newNetworkAction()
            .withId("network-action-1")
            .newSwitchAction().withNetworkElement("FFR2AA1  FFR3AA1  1").withActionType(ActionType.OPEN).add()
            .newOnFlowConstraintInCountryUsageRule().withCountry(Country.NL).withInstant(AUTO_INSTANT_ID).withUsageMethod(UsageMethod.FORCED).add()
            .add();
        List<String> report = CracValidator.validateCrac(crac, network);

        assertEquals(6, crac.getFlowCnecs().size());

        assertCnecHasOutageDuplicate("auto-cnec-1");
        assertCnecHasOutageDuplicate("auto-cnec-2");
        assertCnecHasOutageDuplicate("auto-cnec-3");

        assertEquals(3, report.size());
        assertTrue(report.contains("CNEC \"auto-cnec-1\" has no associated automaton. It will be cloned on the OUTAGE instant in order to be secured during preventive RAO."));
        assertTrue(report.contains("CNEC \"auto-cnec-2\" has no associated automaton. It will be cloned on the OUTAGE instant in order to be secured during preventive RAO."));
        assertTrue(report.contains("CNEC \"auto-cnec-3\" has no associated automaton. It will be cloned on the OUTAGE instant in order to be secured during preventive RAO."));
    }

    @Test
    void testDuplicateAutoCnecs2() {
        // 1 auto RA in CRAC for auto-cnec-1 => duplicate other 2 CNECs
        crac.newNetworkAction()
            .withId("network-action-1")
            .newSwitchAction().withNetworkElement("FFR2AA1  FFR3AA1  1").withActionType(ActionType.OPEN).add()
            .newOnConstraintUsageRule().withCnec("auto-cnec-1").withInstant(AUTO_INSTANT_ID).withUsageMethod(UsageMethod.FORCED).add()
            .newOnFlowConstraintInCountryUsageRule().withCountry(Country.NL).withInstant(AUTO_INSTANT_ID).withUsageMethod(UsageMethod.FORCED).add()
            .add();

        List<String> report = CracValidator.validateCrac(crac, network);

        assertEquals(5, crac.getFlowCnecs().size());

        assertCnecHasOutageDuplicate("auto-cnec-2");
        assertCnecHasOutageDuplicate("auto-cnec-3");

        assertEquals(2, report.size());
        assertTrue(report.contains("CNEC \"auto-cnec-2\" has no associated automaton. It will be cloned on the OUTAGE instant in order to be secured during preventive RAO."));
        assertTrue(report.contains("CNEC \"auto-cnec-3\" has no associated automaton. It will be cloned on the OUTAGE instant in order to be secured during preventive RAO."));
    }

    @Test
    void testDuplicateAutoCnecs3() {
        // 2 auto RA in CRAC for auto-cnec-1 & auto-cnec-2 => duplicate other auto-cnec-3
        crac.newNetworkAction()
            .withId("network-action-1")
            .newSwitchAction().withNetworkElement("FFR2AA1  FFR3AA1  1").withActionType(ActionType.OPEN).add()
            .newOnConstraintUsageRule().withCnec("auto-cnec-1").withInstant(AUTO_INSTANT_ID).withUsageMethod(UsageMethod.FORCED).add()
            .add();
        crac.newNetworkAction()
            .withId("network-action-2")
            .newSwitchAction().withNetworkElement("FFR2AA1  FFR3AA1  1").withActionType(ActionType.OPEN).add()
            .newOnFlowConstraintInCountryUsageRule().withCountry(Country.DE).withInstant(AUTO_INSTANT_ID).withUsageMethod(UsageMethod.FORCED).add()
            .newOnFlowConstraintInCountryUsageRule().withCountry(Country.NL).withInstant(AUTO_INSTANT_ID).withUsageMethod(UsageMethod.FORCED).add()
            .add();

        List<String> report = CracValidator.validateCrac(crac, network);

        assertEquals(4, crac.getFlowCnecs().size());

        assertCnecHasOutageDuplicate("auto-cnec-3");

        assertEquals(1, report.size());
        assertTrue(report.contains("CNEC \"auto-cnec-3\" has no associated automaton. It will be cloned on the OUTAGE instant in order to be secured during preventive RAO."));
    }

    @Test
    void testDuplicateAutoCnecs4() {
        // 2 auto RA in CRAC for contingency of auto-cnec-1 & auto-cnec-2 => duplicate other auto-cnec-3
        crac.newNetworkAction()
            .withId("network-action-1")
            .newSwitchAction().withNetworkElement("FFR2AA1  FFR3AA1  1").withActionType(ActionType.OPEN).add()
            .newOnFlowConstraintInCountryUsageRule().withCountry(Country.NL).withInstant(AUTO_INSTANT_ID).withUsageMethod(UsageMethod.FORCED).add()
            .add();
        crac.newNetworkAction()
            .withId("network-action-2")
            .newSwitchAction().withNetworkElement("FFR2AA1  FFR3AA1  1").withActionType(ActionType.OPEN).add()
            .newOnContingencyStateUsageRule().withContingency("co-1").withUsageMethod(UsageMethod.FORCED).withInstant(AUTO_INSTANT_ID).add()
            .add();

        List<String> report = CracValidator.validateCrac(crac, network);

        assertEquals(4, crac.getFlowCnecs().size());

        assertCnecHasOutageDuplicate("auto-cnec-3");

        assertEquals(1, report.size());
        assertTrue(report.contains("CNEC \"auto-cnec-3\" has no associated automaton. It will be cloned on the OUTAGE instant in order to be secured during preventive RAO."));
    }

    @Test
    void testDuplicateAutoCnecs5() {
        // 1 auto RA in CRAC forced after all contingencies => no duplicate
        crac.newNetworkAction()
            .withId("network-action-1")
            .newSwitchAction().withNetworkElement("FFR2AA1  FFR3AA1  1").withActionType(ActionType.OPEN).add()
            .newOnContingencyStateUsageRule().withContingency("co-1").withUsageMethod(UsageMethod.FORCED).withInstant(AUTO_INSTANT_ID).add()
            .newOnContingencyStateUsageRule().withContingency("co-2").withUsageMethod(UsageMethod.FORCED).withInstant(AUTO_INSTANT_ID).add()
            .add();

        CracValidator.validateCrac(crac, network);
        assertEquals(3, crac.getFlowCnecs().size());
    }

    @Test
    void testDuplicateCnecsWithOnFlowConstraints() {
        crac.newNetworkAction()
            .withId("network-action-1")
            .newTerminalsConnectionAction().withNetworkElement("FFR2AA1  FFR3AA1  1").withActionType(ActionType.OPEN).add()
            .newOnConstraintUsageRule().withCnec("auto-cnec-1").withInstant(AUTO_INSTANT_ID).withUsageMethod(UsageMethod.FORCED).add()
            .add();

        CracValidator.validateCrac(crac, network);
        assertEquals(4, crac.getFlowCnecs().size());
        assertNull(crac.getFlowCnec("auto-cnec-1 - OUTAGE DUPLICATE"));
        assertNotNull(crac.getFlowCnec("auto-cnec-2 - OUTAGE DUPLICATE"));
    }

    @Test
    void testDuplicateCnecsWithOnFlowConstraintInCountries() {
        crac.newNetworkAction()
            .withId("network-action-1")
            .newTerminalsConnectionAction().withNetworkElement("FFR2AA1  FFR3AA1  1").withActionType(ActionType.OPEN).add()
            .newOnFlowConstraintInCountryUsageRule().withCountry(Country.BE).withInstant(AUTO_INSTANT_ID).withUsageMethod(UsageMethod.FORCED).add()
            .add();

        CracValidator.validateCrac(crac, network);
        assertEquals(4, crac.getFlowCnecs().size());
        assertNull(crac.getFlowCnec("auto-cnec-1 - OUTAGE DUPLICATE"));
        assertNotNull(crac.getFlowCnec("auto-cnec-2 - OUTAGE DUPLICATE"));
        assertNull(crac.getFlowCnec("auto-cnec-3 - OUTAGE DUPLICATE"));
    }
}