NetworkSerDeTest.java

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

import com.google.auto.service.AutoService;
import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.extensions.AbstractExtensionSerDe;
import com.powsybl.commons.extensions.ExtensionSerDe;
import com.powsybl.commons.io.DeserializerContext;
import com.powsybl.commons.io.SerializerContext;
import com.powsybl.commons.io.TreeDataFormat;
import com.powsybl.commons.report.PowsyblCoreReportResourceBundle;
import com.powsybl.commons.test.PowsyblCoreTestReportResourceBundle;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.commons.test.TestUtil;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.test.*;
import com.powsybl.iidm.serde.extensions.util.NetworkSourceExtension;
import com.powsybl.iidm.serde.extensions.util.NetworkSourceExtensionImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

import java.io.*;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZonedDateTime;

import static com.powsybl.commons.test.ComparisonUtils.assertTxtEquals;
import static com.powsybl.iidm.serde.IidmSerDeConstants.CURRENT_IIDM_VERSION;
import static org.junit.jupiter.api.Assertions.*;

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

    static Network createEurostagTutorialExample1() {
        Network network = EurostagTutorialExample1Factory.create();
        network.setCaseDate(ZonedDateTime.parse("2013-01-15T18:45:00+01:00"));
        return network;
    }

    @Test
    void roundTripTest() throws IOException {
        allFormatsRoundTripTest(createEurostagTutorialExample1(), "eurostag-tutorial-example1.xml", CURRENT_IIDM_VERSION);

        // backward compatibility
        allFormatsRoundTripAllPreviousVersionedXmlTest("eurostag-tutorial-example1.xml");
    }

    @ParameterizedTest
    @EnumSource(value = TreeDataFormat.class, names = {"XML", "JSON"})
    void testSkippedExtension(TreeDataFormat format) throws IOException {
        Network network = NetworkSerDe.read(getNetworkAsStream("/skippedExtensions.xml"));
        Path file = tmpDir.resolve("data");
        NetworkSerDe.write(network, new ExportOptions().setFormat(format), file);

        // Read file with all extensions included (default ImportOptions)
        ReportNode reportNode1 = ReportNode.newRootReportNode()
                .withResourceBundles(PowsyblCoreTestReportResourceBundle.TEST_BASE_NAME, PowsyblCoreReportResourceBundle.BASE_NAME)
                .withMessageTemplate("root")
                .build();
        Network networkReadExtensions = NetworkSerDe.read(file,
                new ImportOptions().setFormat(format), null, NetworkFactory.findDefault(), reportNode1);
        Load load1 = networkReadExtensions.getLoad("LOAD1");
        assertNotNull(load1.getExtension(LoadBarExt.class));
        assertNotNull(load1.getExtension(LoadZipModel.class));

        StringWriter sw1 = new StringWriter();
        reportNode1.print(sw1);
        assertEquals("""
                + Root reportNode
                   Validation warnings
                   + Imported extensions
                      Extension loadBar imported.
                      Extension loadZipModel imported.
                """, TestUtil.normalizeLineSeparator(sw1.toString()));

        // Read file with only terminalMockNoSerDe and loadZipModel extensions included
        ReportNode reportNode2 = ReportNode.newRootReportNode()
                .withResourceBundles(PowsyblCoreTestReportResourceBundle.TEST_BASE_NAME, PowsyblCoreReportResourceBundle.BASE_NAME)
                .withMessageTemplate("root")
                .build();
        ImportOptions notAllExtensions = new ImportOptions()
                .addExtension("terminalMockNoSerDe").addExtension("loadZipModel")
                .setFormat(format);
        Network networkSkippedExtensions = NetworkSerDe.read(file,
                notAllExtensions, null, NetworkFactory.findDefault(), reportNode2);
        Load load2 = networkSkippedExtensions.getLoad("LOAD1");
        assertNull(load2.getExtension(LoadBarExt.class));
        LoadZipModel loadZipModelExt = load2.getExtension(LoadZipModel.class);
        assertNotNull(loadZipModelExt);
        assertEquals(3.0, loadZipModelExt.getA3(), 0.001);

        StringWriter sw2 = new StringWriter();
        reportNode2.print(sw2);
        assertEquals("""
                + Root reportNode
                   Validation warnings
                   + Imported extensions
                      Extension loadZipModel imported.
                """, TestUtil.normalizeLineSeparator(sw2.toString()));
    }

    @Test
    void testNotFoundExtension() throws IOException {
        // Read file with all extensions included (default ImportOptions)
        ReportNode reportNode1 = ReportNode.newRootReportNode()
                .withResourceBundles(PowsyblCoreTestReportResourceBundle.TEST_BASE_NAME, PowsyblCoreReportResourceBundle.BASE_NAME)
                .withMessageTemplate("root")
                .build();
        Network networkReadExtensions = NetworkSerDe.read(getNetworkAsStream("/notFoundExtension.xml"),
                new ImportOptions(), null, NetworkFactory.findDefault(), reportNode1);
        Load load1 = networkReadExtensions.getLoad("LOAD");
        assertNotNull(load1.getExtension(LoadBarExt.class));
        assertNotNull(load1.getExtension(LoadZipModel.class));

        StringWriter sw1 = new StringWriter();
        reportNode1.print(sw1);
        assertEquals("""
                + Root reportNode
                   Validation warnings
                   + Imported extensions
                      Extension loadBar imported.
                      Extension loadZipModel imported.
                   + Not found extensions
                      Extension terminalMockNoSerDe not found.
                """, TestUtil.normalizeLineSeparator(sw1.toString()));
    }

    @Test
    void testValidationIssueWithProperties() {
        Network network = createEurostagTutorialExample1();
        network.getGenerator("GEN").setProperty("test", "foo");
        Path xmlFile = tmpDir.resolve("n.xml");
        NetworkSerDe.write(network, xmlFile);
        Network readNetwork = NetworkSerDe.validateAndRead(xmlFile);
        assertEquals("foo", readNetwork.getGenerator("GEN").getProperty("test"));
    }

    @Test
    void testGzipGunzip() throws IOException {
        Network network = createEurostagTutorialExample1();
        Path file1 = tmpDir.resolve("n.xml");
        NetworkSerDe.write(network, file1);
        Network network2 = NetworkSerDe.copy(network);
        Path file2 = tmpDir.resolve("n2.xml");
        NetworkSerDe.write(network2, file2);
        assertArrayEquals(Files.readAllBytes(file1), Files.readAllBytes(file2));
    }

    @Test
    void testCopyFormat() {
        Network network = createEurostagTutorialExample1();
        Path file1 = tmpDir.resolve("n.xml");
        NetworkSerDe.write(network, file1);
        Network network2 = NetworkSerDe.copy(network);
        Path file2 = tmpDir.resolve("n2.xml");
        NetworkSerDe.write(network2, file2);
        assertTxtEquals(file1, file2);
        Network network3 = NetworkSerDe.copy(network, TreeDataFormat.BIN);
        Path file3 = tmpDir.resolve("n3.xml");
        NetworkSerDe.write(network3, file3);
        assertTxtEquals(file1, file3);
    }

    @AutoService(ExtensionSerDe.class)
    public static class BusbarSectionExtSerDe extends AbstractExtensionSerDe<BusbarSection, BusbarSectionExt> {

        public BusbarSectionExtSerDe() {
            super("busbarSectionExt", "network", BusbarSectionExt.class, "busbarSectionExt.xsd",
                    "http://www.itesla_project.eu/schema/iidm/ext/busbarSectionExt/1_0", "bbse");
        }

        @Override
        public void write(BusbarSectionExt busbarSectionExt, SerializerContext context) {
        }

        @Override
        public BusbarSectionExt read(BusbarSection busbarSection, DeserializerContext context) {
            context.getReader().readEndNode();
            var bbsExt = new BusbarSectionExt(busbarSection);
            busbarSection.addExtension(BusbarSectionExt.class, bbsExt);
            return bbsExt;
        }
    }

    private static Network writeAndRead(Network network, ExportOptions options) throws IOException {
        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
            NetworkSerDe.write(network, options, os);

            try (InputStream is = new ByteArrayInputStream(os.toByteArray())) {
                return NetworkSerDe.read(is);
            }
        }
    }

    @Test
    void busBreakerExtensions() throws IOException {
        Network network = NetworkTest1Factory.create();
        BusbarSection bb = network.getBusbarSection("voltageLevel1BusbarSection1");
        bb.addExtension(BusbarSectionExt.class, new BusbarSectionExt(bb));

        //Re-import in node breaker
        Network nodeBreakerNetwork = writeAndRead(network, new ExportOptions());

        assertNotSame(network, nodeBreakerNetwork);

        //Check that busbar and its extension is still here
        BusbarSection bb2 = nodeBreakerNetwork.getBusbarSection("voltageLevel1BusbarSection1");
        assertEquals(1, bb2.getExtensions().size());
        assertNotNull(bb2.getExtension(BusbarSectionExt.class));

        //Re-import in bus breaker
        //Check that network is correctly imported, and busbar and its extension are not here any more
        Network busBreakerNetwork = writeAndRead(network, new ExportOptions().setTopologyLevel(TopologyLevel.BUS_BREAKER));
        assertNull(busBreakerNetwork.getBusbarSection("voltageLevel1BusbarSection1"));
    }

    @Test
    void testScada() throws IOException {
        Network network = ScadaNetworkFactory.create();
        assertEquals(ValidationLevel.EQUIPMENT, network.runValidationChecks(false));
        allFormatsRoundTripTest(network, "scadaNetwork.xml", CURRENT_IIDM_VERSION);

        // backward compatibility
        allFormatsRoundTripFromVersionedXmlFromMinToCurrentVersionTest("scadaNetwork.xml", IidmVersion.V_1_7);
    }

    @Test
    void checkWithSpecificEncoding() throws IOException {
        Network network = NetworkTest1Factory.create();
        BusbarSection bb = network.getBusbarSection("voltageLevel1BusbarSection1");
        bb.addExtension(BusbarSectionExt.class, new BusbarSectionExt(bb));
        ExportOptions export = new ExportOptions();
        export.setCharset(StandardCharsets.ISO_8859_1);
        //Re-import in node breaker
        Network nodeBreakerNetwork = writeAndRead(network, export);

        //Check that busbar and its extension is still here
        BusbarSection bb2 = nodeBreakerNetwork.getBusbarSection("voltageLevel1BusbarSection1");
        assertEquals(1, bb2.getExtensions().size());
        assertNotNull(bb2.getExtension(BusbarSectionExt.class));
    }

    @Test
    void failImportWithSeveralSubnetworkLevels() throws URISyntaxException {
        Path path = Path.of(getClass().getResource(getVersionedNetworkPath("multiple-subnetwork-levels.xml",
                CURRENT_IIDM_VERSION)).toURI());
        PowsyblException e = assertThrows(PowsyblException.class, () -> NetworkSerDe.validateAndRead(path));
        assertTrue(e.getMessage().contains("Only one level of subnetworks is currently supported."));
    }

    @Test
    void roundTripWithSubnetworksTest() throws IOException {
        Network n1 = createNetwork(1);
        Network n2 = createNetwork(2);
        n1.setCaseDate(ZonedDateTime.parse("2013-01-15T18:41:00+01:00"));
        n2.setCaseDate(ZonedDateTime.parse("2013-01-15T18:42:00+01:00"));

        Network merged = Network.merge("Merged", n1, n2);
        merged.setCaseDate(ZonedDateTime.parse("2013-01-15T18:40:00+01:00"));
        // add an extension at root network level
        NetworkSourceExtension source = new NetworkSourceExtensionImpl("Source_0");
        merged.addExtension(NetworkSourceExtension.class, source);

        allFormatsRoundTripTest(merged, "subnetworks.xml", IidmSerDeConstants.CURRENT_IIDM_VERSION);

        allFormatsRoundTripFromVersionedXmlFromMinToCurrentVersionTest("subnetworks.xml", IidmVersion.V_1_5);
    }

    private Network createNetwork(int num) {
        String dlId = "dl" + num;
        String voltageLevelId = "vl" + num;
        String busId = "b" + num;

        Network network = Network.create("Network-" + num, "format");
        Substation s1 = network.newSubstation()
                .setId("s" + num)
                .setCountry(Country.FR)
                .add();
        VoltageLevel vl1 = s1.newVoltageLevel()
                .setId(voltageLevelId)
                .setNominalV(380)
                .setTopologyKind(TopologyKind.BUS_BREAKER)
                .add();
        vl1.getBusBreakerView().newBus()
                .setId(busId)
                .add();
        network.getVoltageLevel(voltageLevelId).newDanglingLine()
                .setId(dlId)
                .setName(dlId + "_name")
                .setConnectableBus(busId)
                .setBus(busId)
                .setP0(0.0)
                .setQ0(0.0)
                .setR(1.0)
                .setX(2.0)
                .setG(4.0)
                .setB(5.0)
                .setPairingKey("code")
                .add();

        // Add an extension on the network and on an inner element
        NetworkSourceExtension source = new NetworkSourceExtensionImpl("Source_" + num);
        network.addExtension(NetworkSourceExtension.class, source);

        if (num == 1) {
            Generator generator = vl1.newGenerator()
                    .setId("GEN")
                    .setBus(busId)
                    .setConnectableBus(busId)
                    .setMinP(-9999.99)
                    .setMaxP(9999.99)
                    .setVoltageRegulatorOn(true)
                    .setTargetV(24.5)
                    .setTargetP(607.0)
                    .setTargetQ(301.0)
                    .add();
            generator.newMinMaxReactiveLimits()
                    .setMinQ(-9999.99)
                    .setMaxQ(9999.99)
                    .add();
        } else if (num == 2) {
            vl1.newLoad()
                    .setId("LOAD")
                    .setBus(busId)
                    .setConnectableBus(busId)
                    .setP0(600.0)
                    .setQ0(200.0)
                    .add();

            // Add an extension on an inner element
            Load load = network.getLoad("LOAD");
            TerminalMockExt terminalMockExt = new TerminalMockExt(load);
            load.addExtension(TerminalMockExt.class, terminalMockExt);
        }
        return network;
    }
}