OperationalLimitConversionTest.java

/**
 * Copyright (c) 2024, 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.cgmes.conversion.test;

import com.powsybl.cgmes.conversion.CgmesExport;
import com.powsybl.cgmes.conversion.CgmesImport;
import com.powsybl.cgmes.conversion.Conversion;
import com.powsybl.commons.test.AbstractSerDeTest;
import com.powsybl.iidm.network.*;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;

import static com.powsybl.cgmes.conversion.test.ConversionUtil.*;
import static org.junit.jupiter.api.Assertions.*;

/**
 * @author Romain Courtier {@literal <romain.courtier at rte-france.com>}
 */

class OperationalLimitConversionTest extends AbstractSerDeTest {

    private static final Pattern OPERATIONAL_LIMIT_SET = Pattern.compile("<cim:OperationalLimitSet rdf:ID=\"(.*?)\">");
    private static final Pattern OPERATIONAL_LIMIT_TYPE = Pattern.compile("<cim:OperationalLimitType rdf:ID=\"(.*?)\">");
    private static final Pattern ACTIVE_POWER_LIMIT = Pattern.compile("<cim:ActivePowerLimit rdf:ID=\"(.*?)\">");
    private static final Pattern CURRENT_LIMIT = Pattern.compile("<cim:CurrentLimit rdf:ID=\"(.*?)\">");

    private static final String DIR = "/issues/operational-limits/";

    @Test
    void importMultipleLimitsGroupsOnSameLineEndTest() {
        // CGMES network:
        //   An ACLineSegment LN with:
        //   - On side 1, 1 OperationalLimitSet OLS_1 (Spring).
        //   - On side 2, 2 OperationalLimitSet OLS_2 (Spring) and OLS_3 (Winter).
        //   All sets contain 3 CurrentLimit. Winter set has in addition 3 ActivePowerLimit.
        // IIDM network:
        //   All limits are imported.
        //   In case there is only 1 limit group on an extremity, it becomes the active set.
        Network network = readCgmesResources(DIR, "multiple_limitsets_on_same_terminal.xml");
        assertNotNull(network);

        // There is 1 set on side 1, 2 sets on side 2.
        Line line = network.getLine("LN");
        assertEquals(1, line.getOperationalLimitsGroups1().size());
        assertEquals(2, line.getOperationalLimitsGroups2().size());

        // The winter set (OLS_3) contains current limits and active power limits.
        Optional<OperationalLimitsGroup> winterLimits = line.getOperationalLimitsGroup2("OLS_3");
        assertTrue(winterLimits.isPresent());
        assertTrue(winterLimits.get().getCurrentLimits().isPresent());
        assertTrue(winterLimits.get().getActivePowerLimits().isPresent());

        // When an end has only 1 set, this set gets selected, otherwise none is.
        assertTrue(line.getSelectedOperationalLimitsGroup1().isPresent());
        assertFalse(line.getSelectedOperationalLimitsGroup2().isPresent());

        // The CGMES id/name have been preserved in a property.
        String propertyValue = """
                {"OLS_1":"Spring","OLS_2":"Spring","OLS_3":"Winter"}""";
        assertEquals(propertyValue, line.getProperty(Conversion.PROPERTY_OPERATIONAL_LIMIT_SET_IDENTIFIERS));
    }

    @Test
    void exportSelectedLimitsGroupTest() throws IOException {
        // IIDM network:
        //   A Line LN with:
        //   - On side 1, 1 (selected) OperationalLimitsGroup.
        //   - On side 2, 2 (not selected) OperationalLimitsGroup.
        // CGMES export:
        //   When the parameter to export all limits group is set to false, only the selected groups are exported.
        //   When it is set to true (default value), all limits group are exported, whether selected or not.
        Network network = readCgmesResources(DIR, "multiple_limitsets_on_same_terminal.xml");
        Properties exportParams = new Properties();
        exportParams.put(CgmesExport.EXPORT_ALL_LIMITS_GROUP, false);
        String eqFile = writeCgmesProfile(network, "EQ", tmpDir, exportParams);

        // Only 1 OperationalLimitsGroup is selected, so only 1 is exported.
        assertEquals(1, getUniqueMatches(eqFile, OPERATIONAL_LIMIT_SET).size());
        assertEquals(3, getUniqueMatches(eqFile, OPERATIONAL_LIMIT_TYPE).size());
        assertEquals(0, getUniqueMatches(eqFile, ACTIVE_POWER_LIMIT).size());
        assertEquals(3, getUniqueMatches(eqFile, CURRENT_LIMIT).size());

        // Manually select one of the limits group on side 2 and check that 2 OperationalLimitsGroup are now exported.
        network.getLine("LN").setSelectedOperationalLimitsGroup2("OLS_2");
        eqFile = writeCgmesProfile(network, "EQ", tmpDir, exportParams);
        assertEquals(2, getUniqueMatches(eqFile, OPERATIONAL_LIMIT_SET).size());
        assertEquals(3, getUniqueMatches(eqFile, OPERATIONAL_LIMIT_TYPE).size());
        assertEquals(0, getUniqueMatches(eqFile, ACTIVE_POWER_LIMIT).size());
        assertEquals(6, getUniqueMatches(eqFile, CURRENT_LIMIT).size());

        // Export all 3 limits groups, regardless of selected (default value of the parameter).
        eqFile = writeCgmesProfile(network, "EQ", tmpDir);
        assertEquals(3, getUniqueMatches(eqFile, OPERATIONAL_LIMIT_SET).size());
        assertEquals(3, getUniqueMatches(eqFile, OPERATIONAL_LIMIT_TYPE).size());
        assertEquals(3, getUniqueMatches(eqFile, ACTIVE_POWER_LIMIT).size());
        assertEquals(9, getUniqueMatches(eqFile, CURRENT_LIMIT).size());

        // The CGMES id/name have been correctly exported.
        String regex = "<cim:OperationalLimitSet rdf:ID=\"_OLS_NUM\">.*?<cim:IdentifiedObject.name>(.*?)</cim:IdentifiedObject.name>";
        assertEquals("Spring", getFirstMatch(eqFile, Pattern.compile(regex.replace("NUM", "1"), Pattern.DOTALL)));
        assertEquals("Spring", getFirstMatch(eqFile, Pattern.compile(regex.replace("NUM", "2"), Pattern.DOTALL)));
        assertEquals("Winter", getFirstMatch(eqFile, Pattern.compile(regex.replace("NUM", "3"), Pattern.DOTALL)));
    }

    @Test
    void limitSetsAssociatedToTerminalsTest() {
        // CGMES network:
        //   OperationalLimitSet with CurrentLimit associated to the Terminal of:
        //   a DanglingLine DL, a Line ACL, a Switch SW, a TwoWindingTransformer PT2, a ThreeWindingTransformer PT3.
        // IIDM network:
        //   Limits associated to terminals of lines or transformers are imported,
        //   limits associated to terminals of switch are discarded.
        Network network = readCgmesResources(DIR, "limitsets_associated_to_terminals_EQ.xml",
                "limitsets_EQBD.xml", "limitsets_TPBD.xml");

        // OperationalLimitSet on dangling line terminal is imported smoothly.
        assertNotNull(network.getDanglingLine("DL"));
        assertTrue(network.getDanglingLine("DL").getCurrentLimits().isPresent());

        // OperationalLimitSet on ACLineSegment terminals are imported smoothly.
        assertNotNull(network.getLine("ACL"));
        assertTrue(network.getLine("ACL").getCurrentLimits1().isPresent());
        assertTrue(network.getLine("ACL").getCurrentLimits2().isPresent());

        // OperationalLimitSet on PowerTransformers terminals are imported smoothly.
        assertNotNull(network.getTwoWindingsTransformer("PT2"));
        assertTrue(network.getTwoWindingsTransformer("PT2").getCurrentLimits1().isPresent());
        assertTrue(network.getTwoWindingsTransformer("PT2").getCurrentLimits2().isPresent());

        assertNotNull(network.getThreeWindingsTransformer("PT3"));
        assertTrue(network.getThreeWindingsTransformer("PT3").getLeg1().getCurrentLimits().isPresent());
        assertTrue(network.getThreeWindingsTransformer("PT3").getLeg2().getCurrentLimits().isPresent());
        assertTrue(network.getThreeWindingsTransformer("PT3").getLeg3().getCurrentLimits().isPresent());

        // There can't be any limit associated to switches in IIDM, but check anyway that the switch has been imported.
        assertNotNull(network.getSwitch("SW"));
    }

    @Test
    void limitSetsAssociatedToEquipmentsTest() {
        // CGMES network:
        //   OperationalLimitSet with CurrentLimit associated to:
        //   a DanglingLine DL, a Line ACL, a Switch SW, a TwoWindingTransformer PT2, a ThreeWindingTransformer PT3.
        // IIDM network:
        //   Limits associated to lines are imported, limits associated to transformers or switches are discarded.
        Network network = readCgmesResources(DIR, "limitsets_associated_to_equipments_EQ.xml",
                "limitsets_EQBD.xml", "limitsets_TPBD.xml");

        // OperationalLimitSet on dangling line is imported on its single extremity.
        assertNotNull(network.getDanglingLine("DL"));
        assertTrue(network.getDanglingLine("DL").getCurrentLimits().isPresent());

        // OperationalLimitSet on ACLineSegment is imported on its two extremities.
        assertNotNull(network.getLine("ACL"));
        assertTrue(network.getLine("ACL").getCurrentLimits1().isPresent());
        assertTrue(network.getLine("ACL").getCurrentLimits2().isPresent());

        // OperationalLimitSet on PowerTransformers are discarded.
        assertNotNull(network.getTwoWindingsTransformer("PT2"));
        assertFalse(network.getTwoWindingsTransformer("PT2").getCurrentLimits1().isPresent());
        assertFalse(network.getTwoWindingsTransformer("PT2").getCurrentLimits2().isPresent());

        assertNotNull(network.getThreeWindingsTransformer("PT3"));
        assertFalse(network.getThreeWindingsTransformer("PT3").getLeg1().getCurrentLimits().isPresent());
        assertFalse(network.getThreeWindingsTransformer("PT3").getLeg2().getCurrentLimits().isPresent());
        assertFalse(network.getThreeWindingsTransformer("PT3").getLeg3().getCurrentLimits().isPresent());

        // There can't be any limit associated to switches in IIDM, but check anyway that the switch has been imported.
        assertNotNull(network.getSwitch("SW"));
    }

    @Test
    void loadingLimitTest() {
        // CGMES network:
        //   An ACLineSegment ACL with:
        //   - on side 1: CurrentLimit and ApparentPowerLimit (each time patl and tatl).
        //   - on side 2: CurrentLimit (2 patl and 2 tatl of same duration).
        // IIDM network:
        //   Limits are imported. In case of multiple limits with same terminal/kind/duration, the lowest value is kept.
        Network network = readCgmesResources(DIR, "loading_limits.xml");

        // Loading limits on side 1 have been imported smoothly.
        Line line = network.getLine("ACL");
        assertTrue(line.getCurrentLimits1().isPresent());
        assertEquals(100.0, line.getCurrentLimits1().get().getPermanentLimit());
        assertEquals(200.0, line.getCurrentLimits1().get().getTemporaryLimit(600).getValue());

        assertTrue(line.getActivePowerLimits1().isPresent());
        assertEquals(101.0, line.getActivePowerLimits1().get().getPermanentLimit());
        assertEquals(201.0, line.getActivePowerLimits1().get().getTemporaryLimit(600).getValue());

        assertTrue(line.getApparentPowerLimits1().isPresent());
        assertEquals(102.0, line.getApparentPowerLimits1().get().getPermanentLimit());
        assertEquals(202.0, line.getApparentPowerLimits1().get().getTemporaryLimit(600).getValue());

        // When several limits of same kind and duration are defined on the same terminal, only the lowest is kept.
        assertTrue(line.getCurrentLimits2().isPresent());
        assertEquals(99.0, line.getCurrentLimits2().get().getPermanentLimit());
        assertEquals(199.0, line.getCurrentLimits2().get().getTemporaryLimit(600).getValue());
    }

    @Test
    void voltageLimitTest() {
        // CGMES network:
        //   2 BusbarSection BBS_1, BBS_2 in 400 kV VoltageLevel VL_1, VL_2, with high/lowVoltageLimit 420/380 kV.
        //   BBS_1 has an OperationalLimitSet with 2 VoltageLimits 410/390 kV.
        //   BBS_2 has an OperationalLimitSet with 2 VoltageLimits 430/370 kV.
        // IIDM network:
        //   The IIDM VoltageLevel's limit is the most restrictive one between
        //   the CGMES VoltageLevel's limit and the CGMES OperationalLimit value.
        Network network = readCgmesResources(DIR, "voltage_limits.xml");

        // The most restrictive limits for VL_1 are the OperationalLimit (VoltageLimit) values.
        VoltageLevel vl1 = network.getVoltageLevel("VL_1");
        assertNotNull(vl1);
        assertEquals(410.0, vl1.getHighVoltageLimit());
        assertEquals(390.0, vl1.getLowVoltageLimit());

        // The most restrictive limits for VL_2 are the VoltageLevel's high/lowVoltageLimit.
        VoltageLevel vl2 = network.getVoltageLevel("VL_2");
        assertNotNull(vl2);
        assertEquals(420.0, vl2.getHighVoltageLimit());
        assertEquals(380.0, vl2.getLowVoltageLimit());
    }

    @Test
    void missingLimitsTest() {
        // CGMES network:
        //   An ACLineSegment ACL with 1 OperationalLimitSet on each side.
        //   On side 1, the limit set is missing the PATL. On side 2, the limit set is missing the TATL value for 1200s.
        // IIDM network:
        //   PATL are computed when missing as percentage * lowest tatl value.
        //   TATL are discarded when value is missing.
        Network network = readCgmesResources(DIR, "missing_limits.xml");

        // By default, if PATL is missing, it is set to the lowest TATL value.
        Line line = network.getLine("ACL");
        assertTrue(line.getCurrentLimits1().isPresent());
        assertEquals(125, line.getCurrentLimits1().get().getTemporaryLimits().iterator().next().getValue());
        assertEquals(125, line.getCurrentLimits1().get().getPermanentLimit());

        // It the parameter is set, the missing PATL is calculated as percentage (0.80) * lowest tatl value (125) = 100.
        Properties importParams = new Properties();
        importParams.setProperty(CgmesImport.MISSING_PERMANENT_LIMIT_PERCENTAGE, "80");
        network = readCgmesResources(importParams, DIR, "missing_limits.xml");
        line = network.getLine("ACL");
        assertTrue(line.getCurrentLimits1().isPresent());
        assertEquals(125, line.getCurrentLimits1().get().getTemporaryLimits().iterator().next().getValue());
        assertEquals(100, line.getCurrentLimits1().get().getPermanentLimit());

        // TATL for 1200s has no value, the limit is discarded.
        assertTrue(line.getCurrentLimits2().isPresent());
        assertNull(line.getCurrentLimits2().get().getTemporaryLimit(1200));
    }

    @Test
    void limitsCim100Test() {
        // CGMES network:
        //   An ACLineSegment ACL with CurrentLimit and ApparentPowerLimit (each time patl and tatl) on side 1.
        // IIDM network:
        //   Limits are imported smoothly.
        Network network = readCgmesResources(DIR, "limits_cim100.xml");

        // The difference between CIM16 and CIM100 limits lies in the naming of attributes: value and limitType.
        // It is therefore relevant to test that values and type (permanent vs temporary) are correctly converted
        // from the graph to the property bag. Everything else is common and doesn't need to be tested again.
        Line line = network.getLine("ACL");
        assertTrue(line.getCurrentLimits1().isPresent());
        assertEquals(100.0, line.getCurrentLimits1().get().getPermanentLimit());
        assertEquals(200.0, line.getCurrentLimits1().get().getTemporaryLimit(600).getValue());

        assertTrue(line.getActivePowerLimits1().isPresent());
        assertEquals(101.0, line.getActivePowerLimits1().get().getPermanentLimit());
        assertEquals(201.0, line.getActivePowerLimits1().get().getTemporaryLimit(600).getValue());

        assertTrue(line.getApparentPowerLimits1().isPresent());
        assertEquals(102.0, line.getApparentPowerLimits1().get().getPermanentLimit());
        assertEquals(202.0, line.getApparentPowerLimits1().get().getTemporaryLimit(600).getValue());
    }

}