CgmesExportTest.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/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.cgmes.conversion.test.export;

import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import com.powsybl.cgmes.conformity.CgmesConformity1Catalog;
import com.powsybl.cgmes.conformity.CgmesConformity1ModifiedCatalog;
import com.powsybl.cgmes.conversion.CgmesExport;
import com.powsybl.cgmes.conversion.CgmesImport;
import com.powsybl.cgmes.conversion.Conversion;
import com.powsybl.cgmes.conversion.export.CgmesExportUtil;
import com.powsybl.cgmes.conversion.test.ConversionUtil;
import com.powsybl.cgmes.extensions.CgmesMetadataModels;
import com.powsybl.cgmes.model.*;
import com.powsybl.commons.config.InMemoryPlatformConfig;
import com.powsybl.commons.datasource.*;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.test.*;
import com.powsybl.iidm.network.util.Networks;
import com.powsybl.triplestore.api.TripleStoreFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

/**
 * @author Miora Vedelago {@literal <miora.ralambotiana at rte-france.com>}
 */
class CgmesExportTest {

    private Properties importParams;

    @BeforeEach
    void setUp() {
        importParams = new Properties();
        importParams.put(CgmesImport.IMPORT_CGM_WITH_SUBNETWORKS, "false");
    }

    @Test
    void testFromIidm() throws IOException {
        // Test from IIDM with configuration that does not exist in CGMES (disconnected node on switch and HVDC line)

        Network network = FictitiousSwitchFactory.create();
        VoltageLevel vl = network.getVoltageLevel("C");

        // set as WIND generator
        network.getGenerator("CB").setEnergySource(EnergySource.WIND);

        // Add disconnected node on switch (side 2)
        vl.getNodeBreakerView().newSwitch().setId("TEST_SW")
                .setKind(SwitchKind.DISCONNECTOR)
                .setOpen(true)
                .setNode1(0)
                .setNode2(6)
                .add();

        // Add disconnected node on DC converter station (side 2)
        vl.newVscConverterStation()
                .setId("C1")
                .setNode(5)
                .setLossFactor(1.1f)
                .setVoltageSetpoint(405.0)
                .setVoltageRegulatorOn(true)
                .add();
        vl.getNodeBreakerView().newInternalConnection().setNode1(0).setNode2(5).add();
        vl.newVscConverterStation()
                .setId("C2")
                .setNode(6)
                .setLossFactor(1.1f)
                .setReactivePowerSetpoint(123)
                .setVoltageRegulatorOn(false)
                .add();
        network.newHvdcLine()
                .setId("hvdc_line")
                .setR(5.0)
                .setConvertersMode(HvdcLine.ConvertersMode.SIDE_1_INVERTER_SIDE_2_RECTIFIER)
                .setNominalV(440.0)
                .setMaxP(50.0)
                .setActivePowerSetpoint(20.0)
                .setConverterStationId1("C1")
                .setConverterStationId2("C2")
                .add();

        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath("/cgmes"));
            network.write("CGMES", null, tmpDir.resolve("tmp"));
            Network n2 = Network.read(new GenericReadOnlyDataSource(tmpDir, "tmp"), importParams);
            VoltageLevel c = n2.getVoltageLevel("C");
            assertNull(Networks.getEquivalentTerminal(c, c.getNodeBreakerView().getNode2("TEST_SW")));
            assertNull(n2.getVscConverterStation("C2").getTerminal().getBusView().getBus());
        }
    }

    @Test
    void testSynchronousMachinesWithSameGeneratingUnit() throws IOException {
        ReadOnlyDataSource ds = CgmesConformity1ModifiedCatalog.microGridBaseBEGenUnitWithTwoSyncMachines().dataSource();
        Network n = Importers.importData("CGMES", ds, importParams);
        String exportFolder = "/test-gu-with-2sm";
        String baseName = "testGU2SMs";
        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath(exportFolder));
            // Export to CGMES and add boundary EQ for reimport
            n.write("CGMES", null, tmpDir.resolve(baseName));
            String eqbd = ds.listNames(".*EQ_BD.*").stream().findFirst().orElse(null);
            if (eqbd != null) {
                try (InputStream is = ds.newInputStream(eqbd)) {
                    Files.copy(is, tmpDir.resolve(baseName + "_EQ_BD.xml"));
                }
            }

            Network n2 = Network.read(new GenericReadOnlyDataSource(tmpDir, baseName), importParams);
            Generator g1 = n2.getGenerator("3a3b27be-b18b-4385-b557-6735d733baf0");
            Generator g2 = n2.getGenerator("550ebe0d-f2b2-48c1-991f-cebea43a21aa");
            String gu1 = g1.getProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + "GeneratingUnit");
            String gu2 = g2.getProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + "GeneratingUnit");
            assertEquals(gu1, gu2);
        }
    }

    @Test
    void testPhaseTapChangerFixedTapNotExported() throws IOException, XMLStreamException {
        ReadOnlyDataSource ds = CgmesConformity1Catalog.microGridBaseCaseBE().dataSource();
        Network n = Importers.importData("CGMES", ds, importParams);
        TwoWindingsTransformer transformer = n.getTwoWindingsTransformer("a708c3bc-465d-4fe7-b6ef-6fa6408a62b0");
        String regulatingControlId = "5fc492ab-fe33-423b-84f1-a47f87552427";
        String exportFolder = "/test-ptc-rc-not-exported";
        String baseName = "testPtcRcNotExported";

        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath(exportFolder));

            // With original regulating mode the regulating control should be written in the EQ output
            String baseNameWithRc = baseName + "-with-rc";
            n.write("CGMES", null, tmpDir.resolve(baseNameWithRc));
            assertTrue(cgmesFileContainsRegulatingControl(regulatingControlId, tmpDir, baseNameWithRc, "EQ"));
            assertTrue(cgmesFileContainsRegulatingControl(regulatingControlId, tmpDir, baseNameWithRc, "SSH"));

            transformer.getPhaseTapChanger().setRegulating(false);
            transformer.getPhaseTapChanger().setRegulationMode(PhaseTapChanger.RegulationMode.FIXED_TAP);
            String baseNameNoRc = baseName + "-no-rc";
            n.write("CGMES", null, tmpDir.resolve(baseNameNoRc));
            assertFalse(cgmesFileContainsRegulatingControl(regulatingControlId, tmpDir, baseNameNoRc, "EQ"));
            assertFalse(cgmesFileContainsRegulatingControl(regulatingControlId, tmpDir, baseNameNoRc, "SSH"));
        }
    }

    private static boolean cgmesFileContainsRegulatingControl(String regulatingControlId, Path folder, String baseName, String instanceFile) throws XMLStreamException, IOException {
        String file = String.format("%s_%s.xml", baseName, instanceFile);
        String rdfIdAttributeName;
        String expectedRdfIdAttributeValue;
        if (instanceFile.equals("EQ")) {
            rdfIdAttributeName = "ID";
            expectedRdfIdAttributeValue = "_" + regulatingControlId;
        } else if (instanceFile.equals("SSH")) {
            rdfIdAttributeName = "about";
            expectedRdfIdAttributeValue = "#_" + regulatingControlId;
        } else {
            return false;
        }
        return xmlFileContainsRegulatingControl(expectedRdfIdAttributeValue, rdfIdAttributeName, folder.resolve(file));
    }

    private static boolean xmlFileContainsRegulatingControl(String expectedRdfIdAttributeValue, String rdfIdAttributeName, Path file) throws IOException, XMLStreamException {
        try (InputStream is = Files.newInputStream(file)) {
            XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(is);
            while (reader.hasNext()) {
                if (reader.next() == XMLStreamConstants.START_ELEMENT && reader.getLocalName().equals("TapChangerControl")) {
                    String id = reader.getAttributeValue(CgmesNamespace.RDF_NAMESPACE, rdfIdAttributeName);
                    if (expectedRdfIdAttributeValue.equals(id)) {
                        reader.close();
                        return true;
                    }
                }
            }
            reader.close();
        }
        return false;
    }

    @Test
    void testPhaseTapChangerType16() throws IOException {
        ReadOnlyDataSource ds = CgmesConformity1Catalog.microGridBaseCaseBE().dataSource();
        String transformerId = "a708c3bc-465d-4fe7-b6ef-6fa6408a62b0";
        String phaseTapChangerId = "6ebbef67-3061-4236-a6fd-6ccc4595f6c3";
        testPhaseTapChangerType(ds, transformerId, phaseTapChangerId, 16);
        testPhaseTapChangerType(ds, transformerId, phaseTapChangerId, 100);
    }

    private void testPhaseTapChangerType(ReadOnlyDataSource ds, String transformerId, String phaseTapChangerId, int cimVersion) throws IOException {
        testPhaseTapChangerType(ds, transformerId, phaseTapChangerId, cimVersion, importParams);
    }

    private static void testPhaseTapChangerType(ReadOnlyDataSource ds, String transformerId, String phaseTapChangerId, int cimVersion, Properties importParams) throws IOException {
        Network network = Importers.importData("CGMES", ds, importParams);
        String exportFolder = "/test-ptc-type";
        String baseName = "testPtcType";
        TwoWindingsTransformer transformer = network.getTwoWindingsTransformer(transformerId);
        String typeOriginal = CgmesExportUtil.cgmesTapChangerType(transformer, phaseTapChangerId).orElseThrow(RuntimeException::new);
        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath(exportFolder));

            // When exporting only SSH (or SSH and SV), original type of tap changer should be kept
            Properties paramsOnlySsh = new Properties();
            paramsOnlySsh.put(CgmesExport.PROFILES, List.of("SSH"));
            paramsOnlySsh.put(CgmesExport.CIM_VERSION, "" + cimVersion);
            network.write("CGMES", paramsOnlySsh, tmpDir.resolve(baseName));
            String typeOnlySsh = CgmesExportUtil.cgmesTapChangerType(transformer, phaseTapChangerId).orElseThrow(RuntimeException::new);
            assertEquals(typeOriginal, typeOnlySsh);

            // If we export EQ and SSH (or all instance fiels), type of tap changer should be changed to tabular
            Properties paramsEqAndSsh = new Properties();
            paramsEqAndSsh.put(CgmesExport.CIM_VERSION, "" + cimVersion);
            network.write("CGMES", paramsEqAndSsh, tmpDir.resolve(baseName));
            String typeEqAndSsh = CgmesExportUtil.cgmesTapChangerType(transformer, phaseTapChangerId).orElseThrow(RuntimeException::new);
            assertEquals(CgmesNames.PHASE_TAP_CHANGER_TABULAR, typeEqAndSsh);
        }
    }

    @Test
    void testFromIidmBusBranch() throws IOException {
        // If we want to export an IIDM that contains dangling lines,
        // we will have to rely on some external boundaries definition

        Network network = DanglingLineNetworkFactory.create();
        DanglingLine expected = network.getDanglingLine("DL");
        Network merged = Network.merge(network, BatteryNetworkFactory.create()); // add battery
        Battery battery = merged.getBattery("BAT");

        // Before exporting, we have to define to which point
        // in the external boundary definition we want to associate this dangling line
        // For this test we chose the Conformity MicroGrid BaseCase
        ResourceSet boundaries = CgmesConformity1Catalog.microGridBaseCaseBoundaries();
        String boundaryTN = "d4affe50316740bdbbf4ae9c7cbf3cfd";
        expected.setProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.TOPOLOGICAL_NODE_BOUNDARY, boundaryTN);
        // We also inform the identifiers of the boundaries we depend on
        Properties exportParameters = new Properties();
        exportParameters.put(CgmesExport.BOUNDARY_EQ_ID, "urn:uuid:2399cbd0-9a39-11e0-aa80-0800200c9a66");
        exportParameters.put(CgmesExport.BOUNDARY_TP_ID, "urn:uuid:2399cbd1-9a39-11e0-aa80-0800200c9a66");

        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath("/cgmes"));
            merged.write("CGMES", exportParameters, tmpDir.resolve("tmp"));

            // To be able to import from the exported CGMES data we must add the external boundary definitions
            // For bus/branch we need both EQ and TP instance files of boundaries
            try (InputStream is = boundaries.newInputStream("MicroGridTestConfiguration_EQ_BD.xml")) {
                Files.copy(is, tmpDir.resolve("tmp_EQ_BD.xml"), StandardCopyOption.REPLACE_EXISTING);
            }
            try (InputStream is = boundaries.newInputStream("MicroGridTestConfiguration_TP_BD.xml")) {
                Files.copy(is, tmpDir.resolve("tmp_TP_BD.xml"), StandardCopyOption.REPLACE_EXISTING);
            }

            Network networkFromCgmes = Network.read(new GenericReadOnlyDataSource(tmpDir, "tmp"), importParams);
            DanglingLine actual = networkFromCgmes.getDanglingLine("DL");
            assertNotNull(actual);
            checkDanglingLineParams(expected, actual);
            Generator generator = networkFromCgmes.getGenerator("BAT");
            assertNotNull(generator);
            assertEquals(battery.getTargetP(), generator.getTargetP(), 0.0);
            assertEquals(battery.getTargetQ(), generator.getTargetQ(), 0.0);
            assertEquals(battery.getMinP(), generator.getMinP(), 0.0);
            assertEquals(battery.getMaxP(), generator.getMaxP(), 0.0);
        }
    }

    @Test
    void testFromIidmDanglingLineBusBranchNotBoundary() throws IOException {
        // If we want to export an IIDM that contains dangling lines,
        // we will have to rely on some external boundaries definition
        // If we do not provide this information,
        // we will re-import it as a regular line

        Network network = DanglingLineNetworkFactory.create();
        DanglingLine expected = network.getDanglingLine("DL");

        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath("/cgmes"));
            network.write("CGMES", null, tmpDir.resolve("tmp"));

            Network networkFromCgmes = Network.read(new GenericReadOnlyDataSource(tmpDir, "tmp"), importParams);
            Line actual = networkFromCgmes.getLine("DL");
            assertNotNull(actual);
            checkDanglingLineParams(expected, actual);
            // The dangling line was exported as an ACLS plus an equivalent injection.
            // Equivalent injections inside an IGM are mapped to generators,
            // So we have to check that there is a generator at side 2 (boundary) of the line
            checkDanglingLineEquivalentInjection(expected, actual);
            checkFictitiousContainerAtBoundary(expected, actual);
        }
    }

    @Test
    void testFromIidmDanglingLineNodeBreaker() throws IOException {
        // If we want to export an IIDM that contains dangling lines,
        // we will have to rely on some external boundaries definition

        Network network = DanglingLineNetworkFactory.create();
        DanglingLine expected = network.getDanglingLine("DL");

        // Before exporting, we have to define to which point
        // in the external boundary definition we want to associate this dangling line
        // For this test we chose the Conformity MicroGrid BaseCase
        ResourceSet boundaries = CgmesConformity1Catalog.microGridBaseCaseBoundaries();
        String boundaryCN = "b675a570-cb6e-11e1-bcee-406c8f32ef58";
        expected.setProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.CONNECTIVITY_NODE_BOUNDARY, boundaryCN);
        // We inform the identifier of the boundaries we depend on
        Properties exportParameters = new Properties();
        exportParameters.put(CgmesExport.BOUNDARY_EQ_ID, "urn:uuid:536f9bf1-3f8f-a546-87e3-7af2272f29b7");
        exportParameters.put(CgmesExport.TOPOLOGY_KIND, "NODE_BREAKER");

        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath("/cgmes"));
            network.write("CGMES", exportParameters, tmpDir.resolve("tmp"));

            // To be able to import from the exported CGMES data we must add the external boundary definitions
            try (InputStream is = boundaries.newInputStream("MicroGridTestConfiguration_EQ_BD.xml")) {
                Files.copy(is, tmpDir.resolve("tmp_EQ_BD.xml"), StandardCopyOption.REPLACE_EXISTING);
            }
            try (InputStream is = boundaries.newInputStream("MicroGridTestConfiguration_TP_BD.xml")) {
                Files.copy(is, tmpDir.resolve("tmp_TP_BD.xml"), StandardCopyOption.REPLACE_EXISTING);
            }

            Network networkFromCgmes = Network.read(new GenericReadOnlyDataSource(tmpDir, "tmp"), importParams);
            DanglingLine actual = networkFromCgmes.getDanglingLine("DL");
            assertNotNull(actual);
            checkDanglingLineParams(expected, actual);
        }
    }

    @Test
    void testFromIidmDanglingLineNodeBreakerNoBoundaries() throws IOException {
        // If we want to export an IIDM that contains dangling lines,
        // we will have to rely on some external boundaries definition
        // If we do not add boundary information
        // a node in the same voltage level of the dangling line will be created
        // and the re-imported network will not see it as a dangling line,
        // but as a regular transmission line

        Network network = DanglingLineNetworkFactory.create();
        DanglingLine expected = network.getDanglingLine("DL");

        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath("/cgmes"));
            Properties exportParameters = new Properties();
            exportParameters.put(CgmesExport.TOPOLOGY_KIND, "NODE_BREAKER");
            network.write("CGMES", exportParameters, tmpDir.resolve("tmp"));

            Network networkFromCgmes = Network.read(new GenericReadOnlyDataSource(tmpDir, "tmp"), importParams);
            DanglingLine actualDanglingLine = networkFromCgmes.getDanglingLine("DL");
            assertNull(actualDanglingLine);
            Line actual = networkFromCgmes.getLine("DL");
            checkDanglingLineParams(expected, actual);
            // non-network end is always exported with terminal sequence 2
            // at that node there should be only the equipment corresponding to the equivalent injection
            checkDanglingLineEquivalentInjection(expected, actual);
            checkFictitiousContainerAtBoundary(expected, actual);
        }
    }

    @Test
    void testLineContainersNotInBoundaries() throws IOException {
        ReadOnlyDataSource ds = new ResourceDataSource("Node of T-junction in line container",
                new ResourceSet("/issues/node-containers/", "line_with_t-junction.xml"));
        Network network = Network.read(ds, importParams);

        String exportFolder = "/test-line-containers-not-in-boundaries";
        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            // Export to CGMES and add boundary EQ for reimport
            Path tmpDir = Files.createDirectory(fs.getPath(exportFolder));
            String baseName = "testLineContainersNotInBoundaries";
            network.write("CGMES", null, tmpDir.resolve(baseName));
            ReadOnlyDataSource exportedCgmes = new GenericReadOnlyDataSource(tmpDir, baseName);

            // Check that the exported CGMES model contains a fictitious substation
            CgmesModel cgmes = CgmesModelFactory.create(exportedCgmes, TripleStoreFactory.defaultImplementation());
            assertTrue(cgmes.isNodeBreaker());
            assertTrue(cgmes.substations().stream().anyMatch(sub -> sub.getLocal("name").startsWith("fictS_")));

            // Verify that we re-import the exported CGMES data without problems
            Network networkReimported = Network.read(exportedCgmes, importParams);
            assertNotNull(networkReimported);
        }
    }

    @Test
    void testModelEquipmentOperationProfile() throws IOException {
        String importDir = "/issues/switches/";
        Network network = readCgmesResources(importDir, "disconnected_terminal_EQ.xml");

        String exportDir = "/testModelProfile";
        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath(exportDir));
            String eqFile = writeCgmesProfile(network, "EQ", tmpDir);

            String regex = "<md:Model.profile>http://entsoe.eu/CIM/EquipmentOperation/3/1</md:Model.profile>";
            Pattern pattern = Pattern.compile(regex);
            Matcher matcher = pattern.matcher(eqFile);
            assertEquals(1, matcher.results().count());
        }
    }

    @Test
    void testModelDescription() throws IOException {
        Network network = EurostagTutorialExample1Factory.create();

        String modelDescription = "powsybl community";
        Properties params = new Properties();
        params.put(CgmesExport.MODEL_DESCRIPTION, modelDescription);

        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fileSystem.getPath("tmp"));
            ZipArchiveDataSource zip = new ZipArchiveDataSource(tmpDir.resolve("."), "output");
            new CgmesExport().export(network, params, zip);
            Network network2 = Network.read(new GenericReadOnlyDataSource(tmpDir.resolve("output.zip")), importParams);
            CgmesMetadataModel sshMetadata = network2
                    .getExtension(CgmesMetadataModels.class)
                    .getModelForSubset(CgmesSubset.STATE_VARIABLES)
                    .orElseThrow();
            assertEquals(modelDescription, sshMetadata.getDescription());
        }
    }

    @Test
    void testModelVersion() throws IOException {
        Network network = EurostagTutorialExample1Factory.create();

        Properties params = new Properties();
        params.put(CgmesExport.MODEL_VERSION, "9");

        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fileSystem.getPath("tmp"));
            ZipArchiveDataSource zip = new ZipArchiveDataSource(tmpDir.resolve("."), "output");
            new CgmesExport().export(network, params, zip);
            Network network2 = Network.read(new GenericReadOnlyDataSource(tmpDir.resolve("output.zip")), importParams);
            CgmesMetadataModel sshMetadata = network2.getExtension(CgmesMetadataModels.class).getModelForSubset(CgmesSubset.STEADY_STATE_HYPOTHESIS).orElseThrow();
            assertEquals(9, sshMetadata.getVersion());
            CgmesMetadataModel svMetadata = network2.getExtension(CgmesMetadataModels.class).getModelForSubset(CgmesSubset.STATE_VARIABLES).orElseThrow();
            assertEquals(9, svMetadata.getVersion());
        }
    }

    @Test
    void testModelDescriptionClosingXML() throws IOException {
        Network network = EurostagTutorialExample1Factory.create();

        // Security test
        // Checking that putting end-tag does not corrupt the file
        String modelDescription = "powsybl community</md:Model.modelingAuthoritySet></md:FullModel>";
        Properties params = new Properties();
        params.put(CgmesExport.MODEL_DESCRIPTION, modelDescription);

        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fileSystem.getPath("tmp"));
            ZipArchiveDataSource zip = new ZipArchiveDataSource(tmpDir.resolve("."), "output");
            new CgmesExport().export(network, params, zip);

            // check network can be reimported and that ModelDescription still includes end-tag
            Network network2 = Network.read(new GenericReadOnlyDataSource(tmpDir.resolve("output.zip")), importParams);

            CgmesMetadataModel sshMetadata = network2.getExtension(CgmesMetadataModels.class).getModelForSubset(CgmesSubset.STEADY_STATE_HYPOTHESIS).orElseThrow();
            assertEquals(modelDescription, sshMetadata.getDescription());
            CgmesMetadataModel svMetadata = network2.getExtension(CgmesMetadataModels.class).getModelForSubset(CgmesSubset.STATE_VARIABLES).orElseThrow();
            assertEquals(modelDescription, svMetadata.getDescription());
        }

    }

    @Test
    void testExportWithModelingAuthorityFromReferenceData() throws IOException {
        // We want to test that information about sourcing actor read from reference data overrides
        // the default value for the parameter MAS URI

        // Minimal network with well-known country (BE)
        // that can be resolved to a sourcing actor (ELIA) using the reference data.
        // The reference data also contains a defined MAS URI (elia.be/OperationalPlanning) for this sourcing actor
        Network network = NetworkTest1Factory.create("minimal-network");
        network.getSubstations().iterator().next().setCountry(Country.BE);

        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix())) {
            InMemoryPlatformConfig platformConfig = new InMemoryPlatformConfig(fileSystem);

            // To export using reference data we must prepare first the boundaries that will be used
            Path boundariesDir = Files.createDirectories(fileSystem.getPath("/boundaries"));
            try (InputStream is = this.getClass().getResourceAsStream("/reference-data-provider/sample_EQBD.xml")) {
                if (is != null) {
                    Files.copy(is, boundariesDir.resolve("sample_EQBD.xml"), StandardCopyOption.REPLACE_EXISTING);
                }
            }
            platformConfig.createModuleConfig("import-export-parameters-default-value")
                    .setStringProperty(CgmesImport.BOUNDARY_LOCATION, boundariesDir.toString());

            Path tmpDir = Files.createDirectories(fileSystem.getPath("/work"));
            Properties exportParams = new Properties();
            // It is enough to check that the MAS has been set correctly in the EQ instance file
            exportParams.put(CgmesExport.PROFILES, "EQ");
            new CgmesExport(platformConfig).export(network, exportParams, new DirectoryDataSource(tmpDir, network.getNameOrId()));

            String eq = Files.readString(tmpDir.resolve(network.getNameOrId() + "_EQ.xml"));
            assertTrue(eq.contains("modelingAuthoritySet>http://www.elia.be/OperationalPlanning"));
        }
    }

    private static void checkDanglingLineParams(DanglingLine expected, DanglingLine actual) {
        assertEquals(expected.getR(), actual.getR(), EPSILON);
        assertEquals(expected.getX(), actual.getX(), EPSILON);
        assertEquals(expected.getG(), actual.getG(), EPSILON);
        assertEquals(expected.getB(), actual.getB(), EPSILON);
        assertEquals(expected.getP0(), actual.getP0(), EPSILON);
        assertEquals(expected.getQ0(), actual.getQ0(), EPSILON);
    }

    private static void checkDanglingLineParams(DanglingLine expected, Line actual) {
        assertEquals(expected.getR(), actual.getR(), EPSILON);
        assertEquals(expected.getX(), actual.getX(), EPSILON);
        assertEquals(expected.getG(), actual.getG1() + actual.getG2(), EPSILON);
        assertEquals(expected.getB(), actual.getB1() + actual.getB2(), EPSILON);
    }

    private static void checkFictitiousContainerAtBoundary(DanglingLine expected, Line actual) {
        // Check that a fictitious voltage level and substation have been created
        // Voltage level and substation must be different at both ends of line
        assertNotEquals(expected.getTerminal().getVoltageLevel().getId(), actual.getTerminal2().getVoltageLevel().getId());
        assertNotEquals(expected.getTerminal().getVoltageLevel().getSubstation().orElseThrow().getId(), actual.getTerminal2().getVoltageLevel().getSubstation().orElseThrow().getId());
        // Names should end/start with known suffixes/prefixes
        assertTrue(actual.getTerminal2().getVoltageLevel().getNameOrId().endsWith("_VL"));
        assertTrue(actual.getTerminal2().getVoltageLevel().getSubstation().orElseThrow().getNameOrId().startsWith("fictS_"));
    }

    private static void checkDanglingLineEquivalentInjection(DanglingLine expected, Line actual) {
        Connectable<?> eqAtEnd2 = actual.getTerminal2().getBusView().getBus().getConnectedTerminalStream()
                .filter(t -> t.getConnectable() != actual)
                .findFirst()
                .map(Terminal::getConnectable)
                .orElseThrow();
        assertInstanceOf(Generator.class, eqAtEnd2);
        Generator actualEquivalentInjection = (Generator) eqAtEnd2;
        assertEquals(expected.getP0(), -actualEquivalentInjection.getTargetP(), EPSILON);
        assertEquals(expected.getQ0(), -actualEquivalentInjection.getTargetQ(), EPSILON);
    }

    @Test
    void testCanGeneratorControl() throws IOException {
        ReadOnlyDataSource dataSource = CgmesConformity1Catalog.microGridBaseCaseBE().dataSource();
        Network network = new CgmesImport().importData(dataSource, NetworkFactory.findDefault(), new Properties());

        Generator generatorNoRcc = network.getGenerator("550ebe0d-f2b2-48c1-991f-cebea43a21aa");
        Generator generatorRcc = network.getGenerator("3a3b27be-b18b-4385-b557-6735d733baf0");

        generatorNoRcc.removeProperty(Conversion.PROPERTY_REGULATING_CONTROL);
        generatorRcc.removeProperty(Conversion.PROPERTY_REGULATING_CONTROL);

        String exportFolder = "/test-generator-control";
        String baseName = "testGeneratorControl";

        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath(exportFolder));
            Properties exportParams = new Properties();
            exportParams.put(CgmesExport.PROFILES, "EQ");
            new CgmesExport().export(network, exportParams, new DirectoryDataSource(tmpDir, baseName));
            String eq = Files.readString(tmpDir.resolve(baseName + "_EQ.xml"));

            // Check that RegulatingControl is properly exported
            assertTrue(eq.contains("3a3b27be-b18b-4385-b557-6735d733baf0_RC"));
            assertTrue(eq.contains("550ebe0d-f2b2-48c1-991f-cebea43a21aa_RC"));
            generatorRcc.removeProperty(Conversion.PROPERTY_REGULATING_CONTROL);
            generatorNoRcc.removeProperty(Conversion.PROPERTY_REGULATING_CONTROL);

            // RegulatingControl is exported when targetV is not NaN, even if voltage regulation is disabled
            generatorRcc.setVoltageRegulatorOn(false);
            generatorNoRcc.setVoltageRegulatorOn(false);
            new CgmesExport().export(network, exportParams, new DirectoryDataSource(tmpDir, baseName));
            eq = Files.readString(tmpDir.resolve(baseName + "_EQ.xml"));
            assertTrue(eq.contains("3a3b27be-b18b-4385-b557-6735d733baf0_RC"));
            assertTrue(eq.contains("550ebe0d-f2b2-48c1-991f-cebea43a21aa_RC"));
            generatorRcc.removeProperty(Conversion.PROPERTY_REGULATING_CONTROL);
            generatorNoRcc.removeProperty(Conversion.PROPERTY_REGULATING_CONTROL);

            // RegulatingControl isn't exported when targetV is NaN
            double rccTargetV = generatorRcc.getTargetV();
            generatorRcc.setTargetV(Double.NaN);
            double noRccTargetV = generatorNoRcc.getTargetV();
            generatorNoRcc.setTargetV(Double.NaN);
            new CgmesExport().export(network, exportParams, new DirectoryDataSource(tmpDir, baseName));
            eq = Files.readString(tmpDir.resolve(baseName + "_EQ.xml"));
            assertFalse(eq.contains("3a3b27be-b18b-4385-b557-6735d733baf0_RC"));
            assertFalse(eq.contains("550ebe0d-f2b2-48c1-991f-cebea43a21aa_RC"));
            generatorRcc.setTargetV(rccTargetV);
            generatorRcc.setVoltageRegulatorOn(true);
            generatorNoRcc.setTargetV(noRccTargetV);
            generatorNoRcc.setVoltageRegulatorOn(true);

            // RegulatingControl isn't exported when Qmin and Qmax are the same
            ReactiveCapabilityCurveAdder rccAdder = generatorRcc.newReactiveCapabilityCurve();
            ReactiveCapabilityCurve rcc = (ReactiveCapabilityCurve) generatorRcc.getReactiveLimits();
            rcc.getPoints().forEach(point -> rccAdder.beginPoint().setP(point.getP()).setMaxQ(point.getMaxQ()).setMinQ(point.getMaxQ()).endPoint());
            rccAdder.add();
            MinMaxReactiveLimitsAdder mmrlAdder = generatorNoRcc.newMinMaxReactiveLimits();
            MinMaxReactiveLimits mmrl = (MinMaxReactiveLimits) generatorNoRcc.getReactiveLimits();
            mmrlAdder.setMinQ(mmrl.getMinQ());
            mmrlAdder.setMaxQ(mmrl.getMinQ());
            mmrlAdder.add();
            new CgmesExport().export(network, exportParams, new DirectoryDataSource(tmpDir, baseName));
            eq = Files.readString(tmpDir.resolve(baseName + "_EQ.xml"));
            assertFalse(eq.contains("3a3b27be-b18b-4385-b557-6735d733baf0_RC"));
            assertFalse(eq.contains("550ebe0d-f2b2-48c1-991f-cebea43a21aa_RC"));

            // RegulatingControl is however exported when the corresponding CGMES property is present
            generatorRcc.setProperty(Conversion.PROPERTY_REGULATING_CONTROL, "3a3b27be-b18b-4385-b557-6735d733baf0_RC");
            generatorNoRcc.setProperty(Conversion.PROPERTY_REGULATING_CONTROL, "550ebe0d-f2b2-48c1-991f-cebea43a21aa_RC");
            new CgmesExport().export(network, exportParams, new DirectoryDataSource(tmpDir, baseName));
            eq = Files.readString(tmpDir.resolve(baseName + "_EQ.xml"));
            assertTrue(eq.contains("3a3b27be-b18b-4385-b557-6735d733baf0_RC"));
            assertTrue(eq.contains("550ebe0d-f2b2-48c1-991f-cebea43a21aa_RC"));

        }
    }

    @Test
    void networkWithoutControlAreaInterchange() throws IOException {
        Network network = DanglingLineNetworkFactory.create();
        assertEquals(0, network.getAreaCount());

        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath("/temp"));

            // Exporting with default behaviour, no default control area is written
            Path tmpDirNoCA = tmpDir.resolve("network-no-ca");
            Files.createDirectories(tmpDirNoCA);
            String eqFile = ConversionUtil.writeCgmesProfile(network, "EQ", tmpDirNoCA);
            assertFalse(eqFile.contains("cim:ControlArea"));

            // Explicit creation of a default control area
            new CgmesExport().createDefaultControlAreaInterchange(network);

            // Check that a control area definition has been created before export
            assertEquals(1, network.getAreaCount());
            Area defaultControlArea = network.getAreas().iterator().next();
            assertEquals(CgmesNames.CONTROL_AREA_TYPE_KIND_INTERCHANGE, defaultControlArea.getAreaType());
            assertEquals(1, defaultControlArea.getAreaBoundaryStream().count());
            assertEquals(-50, defaultControlArea.getInterchangeTarget().orElse(Double.NaN));
            assertEquals("DL", defaultControlArea.getAreaBoundaryStream().findFirst()
                    .flatMap(AreaBoundary::getBoundary)
                    .map(Boundary::getDanglingLine)
                    .map(DanglingLine::getId)
                    .orElse(null));

            // Check that exported files now have a control area definition
            // No default value for tolerance
            Path tmpDirWithCA = tmpDir.resolve("network-with-ca");
            Files.createDirectories(tmpDirWithCA);
            eqFile = ConversionUtil.writeCgmesProfile(network, "EQ", tmpDirWithCA);
            String sshFile = ConversionUtil.writeCgmesProfile(network, "SSH", tmpDirWithCA);
            assertTrue(eqFile.contains("<cim:ControlArea rdf:ID=\"_dangling-line_N_CA\">"));
            assertTrue(sshFile.contains("<cim:ControlArea.netInterchange>-50</cim:ControlArea.netInterchange>"));
            // No default value for tolerance
            assertFalse(sshFile.contains("cim:ControlArea.pTolerance"));

            // Check that tolerance is exported only if explicitly defined
            Area area = network.getAreas().iterator().next();
            area.setProperty(CgmesNames.P_TOLERANCE, "1.01");
            Path tmpDirWithCaTolerance = tmpDir.resolve("network-with-ca-tolerance");
            Files.createDirectories(tmpDirWithCaTolerance);
            sshFile = ConversionUtil.writeCgmesProfile(network, "SSH", tmpDirWithCaTolerance);
            assertTrue(sshFile.contains("<cim:ControlArea.pTolerance>1.01</cim:ControlArea.pTolerance>"));
        }
    }

    private static final double EPSILON = 1e-10;
}