CoreCneCnecsCreatorTest.java

/*
 * Copyright (c) 2022, 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.raoresult.io.cne.core;

import com.powsybl.contingency.Contingency;
import com.powsybl.contingency.ContingencyElementType;
import com.powsybl.openrao.commons.OpenRaoException;
import com.powsybl.openrao.commons.Unit;
import com.powsybl.openrao.data.raoresult.io.cne.commons.CneHelper;
import com.powsybl.openrao.data.raoresult.io.cne.commons.CneUtil;
import com.powsybl.openrao.data.crac.api.*;
import com.powsybl.openrao.data.crac.api.cnec.FlowCnec;
import com.powsybl.iidm.network.TwoSides;
import com.powsybl.openrao.data.crac.api.networkaction.NetworkAction;
import com.powsybl.openrao.data.crac.loopflowextension.LoopFlowThresholdAdder;
import com.powsybl.openrao.data.raoresult.api.RaoResult;
import com.powsybl.openrao.data.raoresult.io.cne.core.xsd.Analog;
import com.powsybl.openrao.data.raoresult.io.cne.core.xsd.ConstraintSeries;
import com.powsybl.openrao.data.raoresult.io.cne.core.xsd.MonitoredRegisteredResource;
import com.powsybl.openrao.data.raoresult.io.cne.core.xsd.MonitoredSeries;
import com.powsybl.openrao.data.raoresult.io.cne.core.xsd.PartyMarketParticipant;
import com.powsybl.openrao.raoapi.parameters.ObjectiveFunctionParameters;
import com.powsybl.openrao.raoapi.parameters.RaoParameters;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import java.util.List;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;

import static com.powsybl.openrao.data.raoresult.io.cne.core.CoreCneUtil.CORE_CNE_EXPORT_PROPERTIES_PREFIX;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

/**
 * @author Peter Mitri {@literal <peter.mitri at rte-france.com>}
 */
class CoreCneCnecsCreatorTest {
    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 static final String CURATIVE_INSTANT_ID = "curative";

    private Crac crac;
    private RaoResult raoResult;
    private RaoParameters raoParameters;
    private Instant curativeInstant;
    private Properties properties;

    @BeforeEach
    public void setUp() {
        CneUtil.initUniqueIds();
        crac = CracFactory.findDefault().create("test-crac")
            .newInstant(PREVENTIVE_INSTANT_ID, InstantKind.PREVENTIVE)
            .newInstant(OUTAGE_INSTANT_ID, InstantKind.OUTAGE)
            .newInstant(AUTO_INSTANT_ID, InstantKind.AUTO)
            .newInstant(CURATIVE_INSTANT_ID, InstantKind.CURATIVE);
        curativeInstant = crac.getInstant(CURATIVE_INSTANT_ID);
        raoResult = Mockito.mock(RaoResult.class);
        raoParameters = new RaoParameters();

        properties = new Properties();
        properties.setProperty("rao-result.export.core-cne.relative-positive-margins", "true");
        properties.setProperty("rao-result.export.core-cne.relative-positive-margins", "true");
        properties.setProperty("rao-result.export.core-cne.document-id", "22XCORESO------S-20211115-F299v1");
        properties.setProperty("rao-result.export.core-cne.revision-number", "2");
        properties.setProperty("rao-result.export.core-cne.domain-id", "10YDOM-REGION-1V");
        properties.setProperty("rao-result.export.core-cne.process-type", "A48");
        properties.setProperty("rao-result.export.core-cne.sender-id", "22XCORESO------S");
        properties.setProperty("rao-result.export.core-cne.sender-role", "A44");
        properties.setProperty("rao-result.export.core-cne.receiver-id", "17XTSO-CS------W");
        properties.setProperty("rao-result.export.core-cne.receiver-role", "A36");
        properties.setProperty("rao-result.export.core-cne.time-interval", "2021-10-30T22:00Z/2021-10-31T23:00Z");
    }

    private void checkConstraintSeriesContent(ConstraintSeries cs, FlowCnec cnec, String businessType, List<String> countries, boolean asMnec,
                                              Double flowMw, Double patlA, Double patlMw, Double frm,
                                              Double tatlA, Double tatlMw, Double ptdf, Double patlMarginMw,
                                              Double patlObjMw, Double tatlMarginMw,
                                              Double tatlObjMw, Double lfMw, Double lfThresholdMw) {
        assertEquals(businessType, cs.getBusinessType());
        assertEquals(countries.size(), cs.getPartyMarketParticipant().size());
        for (PartyMarketParticipant pmp : cs.getPartyMarketParticipant()) {
            assertTrue(countries.contains(pmp.getMRID().getValue()));
        }
        assertEquals(asMnec ? "A49" : "A52", cs.getOptimizationMarketObjectStatusStatus());

        Optional<Contingency> contingency = cnec.getState().getContingency();
        if (contingency.isPresent()) {
            assertEquals(1, cs.getContingencySeries().size());
            assertTrue(cs.getContingencySeries().get(0).getMRID().contains(contingency.get().getId()));
            assertEquals(contingency.get().getId(), cs.getContingencySeries().get(0).getName());
        } else {
            assertTrue(cs.getContingencySeries().isEmpty());
        }

        assertEquals(1, cs.getMonitoredSeries().size());
        MonitoredSeries ms = cs.getMonitoredSeries().get(0);

        assertEquals(1, ms.getRegisteredResource().size());
        MonitoredRegisteredResource rr = ms.getRegisteredResource().get(0);

        // check measurements
        List<Analog> measurements = rr.getMeasurements();
        int iMeasure = 0;
        iMeasure += checkMeasurement(measurements, iMeasure, "A01", "MAW", flowMw);
        iMeasure += checkMeasurement(measurements, iMeasure, "A02", "AMP", patlA);
        iMeasure += checkMeasurement(measurements, iMeasure, "A02", "MAW", patlMw);
        iMeasure += checkMeasurement(measurements, iMeasure, "A03", "MAW", frm);
        iMeasure += checkMeasurement(measurements, iMeasure, "A07", "AMP", tatlA);
        iMeasure += checkMeasurement(measurements, iMeasure, "A07", "MAW", tatlMw);
        iMeasure += checkMeasurement(measurements, iMeasure, "Z11", "C62", ptdf);
        iMeasure += checkMeasurement(measurements, iMeasure, "Z12", "MAW", patlMarginMw);
        iMeasure += checkMeasurement(measurements, iMeasure, "Z13", "MAW", patlObjMw);
        iMeasure += checkMeasurement(measurements, iMeasure, "Z14", "MAW", tatlMarginMw);
        iMeasure += checkMeasurement(measurements, iMeasure, "Z15", "MAW", tatlObjMw);
        iMeasure += checkMeasurement(measurements, iMeasure, "Z16", "MAW", lfMw);
        iMeasure += checkMeasurement(measurements, iMeasure, "Z17", "MAW", lfThresholdMw);

        assertEquals(iMeasure, measurements.size());
    }

    private int checkMeasurement(List<Analog> measurements, int index, String expectedType, String expectedUnit, Double expectedValue) {
        if (expectedValue != null) {
            Analog measurement = measurements.get(index);
            assertEquals(expectedType, measurement.getMeasurementType());
            assertEquals(expectedUnit, measurement.getUnitSymbol());
            assertEquals(Math.abs(expectedValue), measurement.getAnalogValuesValue(), 1e-6);
            if (expectedUnit.equals("C62")) {
                assertNull(measurement.getPositiveFlowIn());
            } else {
                assertEquals(expectedValue < 0 ? "A02" : "A01", measurement.getPositiveFlowIn());
            }
            return 1;
        } else {
            return 0;
        }
    }

    private void mockCnecResult(FlowCnec cnec, double flowMw, double marginMw, double relMarginMw, double ptdf) {
        TwoSides monitoredSide = cnec.getMonitoredSides().contains(TwoSides.ONE) ? TwoSides.ONE : TwoSides.TWO;
        Mockito.when(raoResult.getFlow(any(), eq(cnec), eq(monitoredSide), eq(Unit.AMPERE))).thenThrow(new OpenRaoException("No ampere allowed"));
        Mockito.when(raoResult.getFlow(any(), eq(cnec), eq(monitoredSide), eq(Unit.MEGAWATT))).thenReturn(flowMw);
        Mockito.when(raoResult.getMargin(any(), eq(cnec), eq(Unit.AMPERE))).thenThrow(new OpenRaoException("No ampere allowed"));
        Mockito.when(raoResult.getMargin(any(), eq(cnec), eq(Unit.MEGAWATT))).thenReturn(marginMw);
        Mockito.when(raoResult.getRelativeMargin(any(), eq(cnec), eq(Unit.AMPERE))).thenThrow(new OpenRaoException("No ampere allowed"));
        Mockito.when(raoResult.getRelativeMargin(any(), eq(cnec), eq(Unit.MEGAWATT))).thenReturn(relMarginMw);
        Mockito.when(raoResult.getPtdfZonalSum(any(), eq(cnec), eq(monitoredSide))).thenReturn(ptdf);
    }

    @Test
    void testExportTwoPreventiveCnecs() {
        FlowCnec cnec1 = crac.newFlowCnec()
            .withId("bbb_cnec1")
            .withNetworkElement("FFR2AA1  DDE3AA1  1")
            .withOperator("FR")
            .withInstant(PREVENTIVE_INSTANT_ID)
            .withOptimized()
            .withNominalVoltage(400.)
            .withReliabilityMargin(0.)
            .newThreshold().withUnit(Unit.MEGAWATT).withMax(100.).withSide(TwoSides.TWO).add()
            .add();

        mockCnecResult(cnec1, 80, 20, 200, .1);

        FlowCnec cnec2 = crac.newFlowCnec()
            .withId("aaa_cnec2")
            .withNetworkElement("NNL2AA1  NNL3AA1  1")
            .withOperator("NL")
            .withInstant(PREVENTIVE_INSTANT_ID)
            .withOptimized()
            .withNominalVoltage(400.)
            .withReliabilityMargin(10.)
            .newThreshold().withUnit(Unit.MEGAWATT).withMax(1000.).withSide(TwoSides.TWO).add()
            .add();

        mockCnecResult(cnec2, 800, -200, -999999999, .2);

        CneHelper cneHelper = new CneHelper(crac, raoResult, properties, CORE_CNE_EXPORT_PROPERTIES_PREFIX);
        CoreCneCnecsCreator cneCnecsCreator = new CoreCneCnecsCreator(cneHelper, new MockCracCreationContext(crac));

        List<ConstraintSeries> cnecsConstraintSeries = cneCnecsCreator.generate();

        // check size
        assertEquals(4, cnecsConstraintSeries.size());

        // check contents
        // start with cnec2 (name starts with aaa)
        checkConstraintSeriesContent(cnecsConstraintSeries.get(0), cnec2, "B88", List.of("10X1001A1001A361"), false,
            800., 1443., 1000., 10., 1443., 1000., .2, 190., 950.,
            190., 950., null, null);
        checkConstraintSeriesContent(cnecsConstraintSeries.get(1), cnec2, "B57", List.of("10X1001A1001A361"), false,
            800., null, null, 10., 1443., 1000., .2, null, null,
            190., 950., null, null);
        /* TODO : reactivate this when we go back to exporting B54 series even if no CRAs are applied
        checkConstraintSeriesContent(cnecsConstraintSeries.get(2), cnec2, "B54", List.of("10X1001A1001A361"), false,
            800., 1443., 1000., 10., null, null, .2, 200., 950.,
            null, null, null, null);
                 */

        // then cnec1 (name starts with bbb)
        checkConstraintSeriesContent(cnecsConstraintSeries.get(2), cnec1, "B88", List.of("10XFR-RTE------Q"), false,
            80., 144., 100., 0., 144., 100., .1, 20., 200.,
            20., 200., null, null);
        checkConstraintSeriesContent(cnecsConstraintSeries.get(3), cnec1, "B57", List.of("10XFR-RTE------Q"), false,
            80., null, null, 0., 144., 100., .1, null, null,
            20., 200., null, null);
        /* TODO : reactivate this when we go back to exporting B54 series even if no CRAs are applied
        checkConstraintSeriesContent(cnecsConstraintSeries.get(5), cnec1, "B54", List.of("10XFR-RTE------Q"), false,
            80., 144., 100., 0., null, null, .1, 20., 200.,
            null, null, null, null);
                 */
    }

    @Test
    void testExportPreventivePureMnec() {
        FlowCnec cnec1 = crac.newFlowCnec()
            .withId("cnec1")
            .withNetworkElement("FFR2AA1  DDE3AA1  1")
            .withOperator("D8")
            .withInstant(PREVENTIVE_INSTANT_ID)
            .withMonitored()
            .withNominalVoltage(400.)
            .withReliabilityMargin(0.)
            .newThreshold().withUnit(Unit.MEGAWATT).withMax(100.).withSide(TwoSides.TWO).add()
            .add();

        mockCnecResult(cnec1, 80, 20, 200, .1);

        raoParameters.getObjectiveFunctionParameters().setType(ObjectiveFunctionParameters.ObjectiveFunctionType.MAX_MIN_RELATIVE_MARGIN);
        raoParameters.getObjectiveFunctionParameters().setUnit(Unit.MEGAWATT);
        CneHelper cneHelper = new CneHelper(crac, raoResult, properties, CORE_CNE_EXPORT_PROPERTIES_PREFIX);
        CoreCneCnecsCreator cneCnecsCreator = new CoreCneCnecsCreator(cneHelper, new MockCracCreationContext(crac));

        List<ConstraintSeries> cnecsConstraintSeries = cneCnecsCreator.generate();

        // check size
        assertEquals(2, cnecsConstraintSeries.size());

        checkConstraintSeriesContent(cnecsConstraintSeries.get(0), cnec1, "B88", List.of("10XDE-VE-TRANSMK"), true,
            80., 144., 100., 0., 144., 100., .1, 20., 200.,
            20., 200., null, null);
        checkConstraintSeriesContent(cnecsConstraintSeries.get(1), cnec1, "B57", List.of("10XDE-VE-TRANSMK"), true,
            80., null, null, 0., 144., 100., .1, null, null,
            20., 200., null, null);
        /* TODO : reactivate this when we go back to exporting B54 series even if no CRAs are applied
        checkConstraintSeriesContent(cnecsConstraintSeries.get(2), cnec1, "B54", List.of("10XDE-VE-TRANSMK"), true,
            80., 144., 100., 0., null, null, null, 20., null,
            null, null, null, null);
         */
    }

    @Test
    void testExportPreventiveCnecAndMnec() {
        FlowCnec cnec1 = crac.newFlowCnec()
            .withId("cnec1")
            .withNetworkElement("FFR2AA1  DDE3AA1  1")
            .withOperator("D7")
            .withInstant(PREVENTIVE_INSTANT_ID)
            .withOptimized()
            .withMonitored()
            .withNominalVoltage(400.)
            .withReliabilityMargin(10.)
            .newThreshold().withUnit(Unit.MEGAWATT).withMax(100.).withSide(TwoSides.TWO).add()
            .add();

        mockCnecResult(cnec1, 80, 20, 200, .1);

        raoParameters.getObjectiveFunctionParameters().setType(ObjectiveFunctionParameters.ObjectiveFunctionType.MAX_MIN_RELATIVE_MARGIN);
        raoParameters.getObjectiveFunctionParameters().setUnit(Unit.MEGAWATT);
        CneHelper cneHelper = new CneHelper(crac, raoResult, properties, CORE_CNE_EXPORT_PROPERTIES_PREFIX);
        CoreCneCnecsCreator cneCnecsCreator = new CoreCneCnecsCreator(cneHelper, new MockCracCreationContext(crac));

        List<ConstraintSeries> cnecsConstraintSeries = cneCnecsCreator.generate();

        // check size
        assertEquals(2, cnecsConstraintSeries.size());

        /* TODO : reactivate this when we go back to exporting CNEC+MNEC branches as both a CNEC and a MNEC
        checkConstraintSeriesContent(cnecsConstraintSeries.get(0), cnec1, "B88", List.of("10XDE-RWENET---W"), true,
            80., 144., 100., 10., 144., 100., null, 20., null,
            20., null, null, null);
        checkConstraintSeriesContent(cnecsConstraintSeries.get(1), cnec1, "B57", List.of("10XDE-RWENET---W"), true,
            80., null, null, 10., 144., 100., null, null, null,
            20., null, null, null);
        checkConstraintSeriesContent(cnecsConstraintSeries.get(2), cnec1, "B54", List.of("10XDE-RWENET---W"), true,
            80., 144., 100., 10., null, null, null, 20., null,
            null, null, null, null);*/

        checkConstraintSeriesContent(cnecsConstraintSeries.get(0), cnec1, "B88", List.of("10XDE-RWENET---W"), false,
            80., 144., 100., 10., 144., 100., .1, 10., 100.,
            10., 100., null, null);
        checkConstraintSeriesContent(cnecsConstraintSeries.get(1), cnec1, "B57", List.of("10XDE-RWENET---W"), false,
            80., null, null, 10., 144., 100., .1, null, null,
            10., 100., null, null);
        /* TODO : reactivate this when we go back to exporting B54 series even if no CRAs are applied
        checkConstraintSeriesContent(cnecsConstraintSeries.get(2), cnec1, "B54", List.of("10XDE-RWENET---W"), false,
            80., 144., 100., 10., null, null, .1, 20., 100.,
            null, null, null, null);
         */
    }

    @Test
    void testCurativeCnecs() {
        crac.newContingency()
            .withId("contingency1")
            .withContingencyElement("FFR2AA1  DDE3AA1  1", ContingencyElementType.LINE)
            .add();
        FlowCnec cnecPrev = crac.newFlowCnec()
            .withId("zzz_cnec1 - N")
            .withNetworkElement("FFR2AA1  DDE3AA1  1")
            .withOperator("D2")
            .withInstant(PREVENTIVE_INSTANT_ID)
            .withOptimized()
            .withNominalVoltage(400.)
            .withReliabilityMargin(30.)
            .newThreshold().withUnit(Unit.MEGAWATT).withMax(100.).withSide(TwoSides.TWO).add()
            .add();
        FlowCnec cnecOutage = crac.newFlowCnec()
            .withId("cnec1 - Outage")
            .withNetworkElement("FFR2AA1  DDE3AA1  1")
            .withOperator("D2")
            .withInstant(OUTAGE_INSTANT_ID)
            .withContingency("contingency1")
            .withOptimized()
            .withNominalVoltage(400.)
            .withReliabilityMargin(20.)
            .newThreshold().withUnit(Unit.MEGAWATT).withMax(200.).withSide(TwoSides.TWO).add()
            .add();
        FlowCnec cnecCur = crac.newFlowCnec()
            .withId("cnec1 - Curative")
            .withNetworkElement("FFR2AA1  DDE3AA1  1")
            .withOperator("D2")
            .withInstant(CURATIVE_INSTANT_ID)
            .withContingency("contingency1")
            .withOptimized()
            .withNominalVoltage(400.)
            .withReliabilityMargin(20.)
            .newThreshold().withUnit(Unit.MEGAWATT).withMax(150.).withSide(TwoSides.TWO).add()
            .add();

        mockCnecResult(cnecPrev, 80, 20, 200, .1);

        mockCnecResult(cnecOutage, 85, 25, 205, .1);
        mockCnecResult(cnecCur, 85, 28, 208, .1);

        when(raoResult.getActivatedNetworkActionsDuringState(crac.getState(cnecCur.getState().getContingency().orElseThrow(), curativeInstant))).thenReturn(Set.of(Mockito.mock(NetworkAction.class)));
        CneHelper cneHelper = new CneHelper(crac, raoResult, properties, CORE_CNE_EXPORT_PROPERTIES_PREFIX);
        CoreCneCnecsCreator cneCnecsCreator = new CoreCneCnecsCreator(cneHelper, new MockCracCreationContext(crac));

        List<ConstraintSeries> cnecsConstraintSeries = cneCnecsCreator.generate();

        // check size
        assertEquals(5, cnecsConstraintSeries.size());
        List<String> tsos = List.of("10XDE-EON-NETZ-C");

        // preventive cnec
        checkConstraintSeriesContent(cnecsConstraintSeries.get(3), cnecPrev, "B88", tsos, false,
            80., 144., 100., 30., 144., 100., .1, -10., -10.,
            -10., -10., null, null);
        checkConstraintSeriesContent(cnecsConstraintSeries.get(4), cnecPrev, "B57", tsos, false,
            80., null, null, 30., 144., 100., .1, null, null,
            -10., -10., null, null);
        /* TODO : reactivate this when we go back to exporting B54 series even if no CRAs are applied
        checkConstraintSeriesContent(cnecsConstraintSeries.get(5), cnecPrev, "B54", tsos, false,
            80., 144., 100., 30., null, null, .1, 20., -10.,
            null, null, null, null);
                 */

        // curative cnecs
        checkConstraintSeriesContent(cnecsConstraintSeries.get(0), cnecCur, "B88", tsos, false,
            85., 217., 150., 20., 289., 200., .1, 45., 450.,
            95., 950., null, null);
        checkConstraintSeriesContent(cnecsConstraintSeries.get(1), cnecOutage, "B57", tsos, false,
            85., null, null, 20., 289., 200., .1, null, null,
            95., 950., null, null);
        checkConstraintSeriesContent(cnecsConstraintSeries.get(2), cnecCur, "B54", tsos, false,
            85., 217., 150., 20., null, null, .1, 45., 450.,
            null, null, null, null);
    }

    @Test
    void testWithLoopFlow() {
        FlowCnec cnec1 = crac.newFlowCnec()
            .withId("cnec1")
            .withNetworkElement("FFR2AA1  DDE3AA1  1")
            .withOperator("D4")
            .withInstant(PREVENTIVE_INSTANT_ID)
            .withOptimized()
            .withNominalVoltage(400.)
            .withReliabilityMargin(0.)
            .newThreshold().withUnit(Unit.MEGAWATT).withMax(100.).withSide(TwoSides.TWO).add()
            .add();
        cnec1.newExtension(LoopFlowThresholdAdder.class).withValue(321.).withUnit(Unit.MEGAWATT).add();

        mockCnecResult(cnec1, 80, 20, 200, .1);
        Mockito.when(raoResult.getLoopFlow(any(), eq(cnec1), eq(TwoSides.TWO), eq(Unit.MEGAWATT))).thenReturn(123.);

        properties.setProperty("rao-result.export.core-cne.with-loop-flows", "true");

        CneHelper cneHelper = new CneHelper(crac, raoResult, properties, CORE_CNE_EXPORT_PROPERTIES_PREFIX);
        CoreCneCnecsCreator cneCnecsCreator = new CoreCneCnecsCreator(cneHelper, new MockCracCreationContext(crac));

        List<ConstraintSeries> cnecsConstraintSeries = cneCnecsCreator.generate();

        // check size
        assertEquals(2, cnecsConstraintSeries.size());
        List<String> tsos = List.of("10XDE-ENBW--TNGX");

        checkConstraintSeriesContent(cnecsConstraintSeries.get(0), cnec1, "B88", tsos, false,
            80., 144., 100., 0., 144., 100., .1, 20., 200.,
            20., 200., 123., 321.);
        checkConstraintSeriesContent(cnecsConstraintSeries.get(1), cnec1, "B57", tsos, false,
            80., null, null, 0., 144., 100., .1, null, null,
            20., 200., 123., 321.);
        /* TODO : reactivate this when we go back to exporting B54 series even if no CRAs are applied
        checkConstraintSeriesContent(cnecsConstraintSeries.get(2), cnec1, "B54", countries, false,
            80., 144., 100., 0., null, null, .1, 20., 200.,
            null, null, 123., 321.);
                 */
    }
}