SteadyStateHypothesisExportTest.java

/**
 * Copyright (c) 2020, 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.*;
import com.powsybl.cgmes.conversion.CgmesExport;
import com.powsybl.cgmes.conversion.CgmesImport;
import com.powsybl.cgmes.conversion.export.CgmesExportContext;
import com.powsybl.cgmes.conversion.export.SteadyStateHypothesisExport;
import com.powsybl.cgmes.model.CgmesNames;
import com.powsybl.cgmes.model.CgmesNamespace;
import com.powsybl.commons.datasource.DirectoryDataSource;
import com.powsybl.commons.datasource.ReadOnlyDataSource;
import com.powsybl.commons.test.AbstractSerDeTest;
import com.powsybl.commons.xml.XmlUtil;
import com.powsybl.computation.DefaultComputationManagerConfig;
import com.powsybl.iidm.network.Area;
import com.powsybl.iidm.network.ImportConfig;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.NetworkFactory;
import com.powsybl.iidm.network.extensions.RemoteReactivePowerControl;
import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory;
import com.powsybl.iidm.network.test.PhaseShifterTestCaseFactory;
import com.powsybl.iidm.network.test.ShuntTestCaseFactory;
import com.powsybl.iidm.network.test.SvcTestCaseFactory;
import com.powsybl.iidm.serde.NetworkSerDe;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.xmlunit.diff.DifferenceEvaluator;
import org.xmlunit.diff.DifferenceEvaluators;

import javax.xml.stream.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

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

/**
 * @author Miora Ralambotiana {@literal <miora.ralambotiana at rte-france.com>}
 * @author Luma Zamarre��o {@literal <zamarrenolm at aia.es>}
 */
class SteadyStateHypothesisExportTest extends AbstractSerDeTest {

    private Properties importParams;

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

    @Test
    void microGridBE() throws IOException, XMLStreamException {
        DifferenceEvaluator knownDiffsSsh = DifferenceEvaluators.chain(
            ExportXmlCompare::sameScenarioTime,
            ExportXmlCompare::ensuringIncreasedModelVersion,
            ExportXmlCompare::ignoringSynchronousMachinesSVCsWithTargetDeadband,
            ExportXmlCompare::ignoringJunctionOrBusbarTerminals);
        DifferenceEvaluator knownDiffsXiidm = DifferenceEvaluators.chain(
            DifferenceEvaluators.Default,
            ExportXmlCompare::ignoringCgmesMetadataModels);
        assertTrue(test(CgmesConformity1Catalog.microGridBaseCaseBE().dataSource(), knownDiffsSsh, knownDiffsXiidm));
    }

    @Test
    void microGridBEWithHiddenTapChangers() throws IOException, XMLStreamException {
        DifferenceEvaluator knownDiffsSsh = DifferenceEvaluators.chain(
            ExportXmlCompare::sameScenarioTime,
            ExportXmlCompare::ensuringIncreasedModelVersion,
            ExportXmlCompare::ignoringSynchronousMachinesSVCsWithTargetDeadband,
            ExportXmlCompare::ignoringJunctionOrBusbarTerminals);
        DifferenceEvaluator knownDiffsXiidm = DifferenceEvaluators.chain(
            DifferenceEvaluators.Default,
            ExportXmlCompare::ignoringCgmesMetadataModels);
        assertTrue(test(CgmesConformity1ModifiedCatalog.microGridBaseCaseBEHiddenTapChangers().dataSource(), knownDiffsSsh, knownDiffsXiidm));
    }

    @Test
    void microGridBEWithSharedRegulatingControl() throws IOException, XMLStreamException {
        DifferenceEvaluator knownDiffsSsh = DifferenceEvaluators.chain(
            ExportXmlCompare::sameScenarioTime,
            ExportXmlCompare::ensuringIncreasedModelVersion,
            ExportXmlCompare::ignoringJunctionOrBusbarTerminals);
        DifferenceEvaluator knownDiffsXiidm = DifferenceEvaluators.chain(
            DifferenceEvaluators.Default,
            ExportXmlCompare::ignoringCgmesMetadataModels);
        assertTrue(test(CgmesConformity1ModifiedCatalog.microGridBaseCaseBESharedRegulatingControl().dataSource(), knownDiffsSsh, knownDiffsXiidm));
    }

    @Test
    void smallGrid() throws IOException, XMLStreamException {
        DifferenceEvaluator knownDiffs = DifferenceEvaluators.chain(
            ExportXmlCompare::sameScenarioTime,
            ExportXmlCompare::ensuringIncreasedModelVersion,
            ExportXmlCompare::ignoringJunctionOrBusbarTerminals);
        assertTrue(test(CgmesConformity1Catalog.smallBusBranch().dataSource(), knownDiffs, DifferenceEvaluators.chain(
                DifferenceEvaluators.Default,
                ExportXmlCompare::numericDifferenceEvaluator,
                ExportXmlCompare::ignoringCgmesMetadataModels)));
    }

    @Test
    void smallGridHVDC() throws IOException, XMLStreamException {
        DifferenceEvaluator knownDiffs = DifferenceEvaluators.chain(
                ExportXmlCompare::sameScenarioTime,
                ExportXmlCompare::ensuringIncreasedModelVersion,
                ExportXmlCompare::ignoringJunctionOrBusbarTerminals);
        assertTrue(test(CgmesConformity1Catalog.smallNodeBreakerHvdc().dataSource(), knownDiffs, DifferenceEvaluators.chain(
                DifferenceEvaluators.Default,
                ExportXmlCompare::numericDifferenceEvaluator,
                ExportXmlCompare::ignoringCgmesMetadataModels,
                ExportXmlCompare::ignoringHvdcLinePmax)));
    }

    private boolean test(ReadOnlyDataSource dataSource, DifferenceEvaluator knownDiffsSsh, DifferenceEvaluator knownDiffsIidm) throws IOException, XMLStreamException {
        // Import original
        importParams.put("iidm.import.cgmes.create-cgmes-export-mapping", "true");
        Network expected = new CgmesImport().importData(dataSource, NetworkFactory.findDefault(), importParams);

        // Export SSH
        Path exportedSsh = tmpDir.resolve("exportedSsh.xml");
        try (OutputStream os = new BufferedOutputStream(Files.newOutputStream(exportedSsh))) {
            XMLStreamWriter writer = XmlUtil.initializeWriter(true, "    ", os);
            CgmesExportContext context = new CgmesExportContext(expected);
            SteadyStateHypothesisExport.write(expected, writer, context);
        }

        // Compare the exported SSH with the original one
        try (InputStream expectedssh = Repackager.newInputStream(dataSource, Repackager::ssh);
             InputStream actualssh = Files.newInputStream(exportedSsh)) {
            if (!ExportXmlCompare.compareSSH(expectedssh, actualssh, knownDiffsSsh)) {
                return false;
            }
        }

        // Zip with new SSH
        Path repackaged = tmpDir.resolve("repackaged.zip");
        Repackager r = new Repackager(dataSource)
                .with("test_EQ.xml", Repackager::eq)
                .with("test_TP.xml", Repackager::tp)
                .with("test_SV.xml", Repackager::sv)
                .with("test_SSH.xml", exportedSsh)
                .with("test_EQ_BD.xml", Repackager::eqBd)
                .with("test_TP_BD.xml", Repackager::tpBd);
        r.zip(repackaged);

        // Import with new SSH
        Network actual = Network.read(repackaged,
                DefaultComputationManagerConfig.load().createShortTimeExecutionComputationManager(), ImportConfig.load(), importParams);

        // Export original and with new SSH
        Path expectedPath = tmpDir.resolve("expected.xml");
        Path actualPath = tmpDir.resolve("actual.xml");
        NetworkSerDe.write(expected, expectedPath);
        NetworkSerDe.write(actual, actualPath);
        NetworkSerDe.validate(actualPath);

        // Compare
        return ExportXmlCompare.compareNetworks(expectedPath, actualPath, knownDiffsIidm);
    }

    @Test
    void equivalentShuntTest() throws XMLStreamException {
        ReadOnlyDataSource ds = CgmesConformity1ModifiedCatalog.microGridBaseCaseBEEquivalentShunt().dataSource();
        Network network = new CgmesImport().importData(ds, NetworkFactory.findDefault(), importParams);

        String ssh = exportSshAsString(network);

        // Equivalent shunts should not have entries in SSH
        String equivalentShuntId = "d771118f-36e9-4115-a128-cc3d9ce3e3da";
        assertNotNull(network.getShuntCompensator(equivalentShuntId));
        SshLinearShuntCompensators sshLinearShuntCompensators = readSshLinearShuntCompensator(ssh);
        assertFalse(sshLinearShuntCompensators.map.isEmpty());
        assertFalse(sshLinearShuntCompensators.map.containsKey(equivalentShuntId));
    }

    private static String exportSshAsString(Network network) throws XMLStreamException {
        CgmesExportContext context = new CgmesExportContext(network);
        StringWriter stringWriter = new StringWriter();
        XMLStreamWriter writer = XmlUtil.initializeWriter(true, "    ", stringWriter);
        SteadyStateHypothesisExport.write(network, writer, context);

        return stringWriter.toString();
    }

    private static SshLinearShuntCompensators readSshLinearShuntCompensator(String ssh) {
        final String sshLinearShuntCompensator = "LinearShuntCompensator";
        final String sshLinearShuntCompensatorSections = "ShuntCompensator.sections";
        final String sshLinearShuntCompensatorControlEnabled = "RegulatingCondEq.controlEnabled";
        final String attrAbout = "about";

        SshLinearShuntCompensators sshLinearShuntCompensators = new SshLinearShuntCompensators();
        try (InputStream is = new ByteArrayInputStream(ssh.getBytes(StandardCharsets.UTF_8))) {
            XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(is);
            Integer sections = null;
            Boolean controlEnabled = null;
            String shuntCompensatorId = null;
            while (reader.hasNext()) {
                int next = reader.next();
                if (next == XMLStreamConstants.START_ELEMENT) {
                    if (reader.getLocalName().equals(sshLinearShuntCompensator)) {
                        sections = null;
                        controlEnabled = null;
                        shuntCompensatorId = reader.getAttributeValue(CgmesNamespace.RDF_NAMESPACE, attrAbout).substring(2);
                    } else if (reader.getLocalName().equals(sshLinearShuntCompensatorSections)) {
                        String text = reader.getElementText();
                        sections = Integer.parseInt(text);
                    } else if (reader.getLocalName().equals(sshLinearShuntCompensatorControlEnabled)) {
                        String text = reader.getElementText();
                        controlEnabled = Boolean.valueOf(text);
                    }
                } else if (next == XMLStreamConstants.END_ELEMENT) {
                    if (reader.getLocalName().equals(sshLinearShuntCompensator) && sections != null && controlEnabled != null) {
                        sshLinearShuntCompensators.add(shuntCompensatorId, sections, controlEnabled);
                    }
                }
            }
            reader.close();
        } catch (XMLStreamException | IOException e) {
            throw new RuntimeException(e);
        }
        return sshLinearShuntCompensators;
    }

    private static final class SshLinearShuntCompensators {
        private final Map<String, Pair<Integer, Boolean>> map = new HashMap<>();

        void add(String shuntCompensatorId, int sections, boolean controlEnabled) {
            map.put(shuntCompensatorId, Pair.of(sections, controlEnabled));
        }
    }

    @Test
    void testUpdateControlArea() throws IOException {
        Path outputPath = tmpDir.resolve("update-control-areas");
        Files.createDirectories(outputPath);

        // Read network and check control area data
        Network be = Network.read(CgmesConformity3Catalog.microGridBaseCaseBE().dataSource(), importParams);
        long numControlAreas = be.getAreaStream().filter(a -> a.getAreaType().equals(CgmesNames.CONTROL_AREA_TYPE_KIND_INTERCHANGE)).count();
        assertEquals(1, numControlAreas);
        Area controlArea = be.getAreas().iterator().next();
        assertEquals(236.9798, controlArea.getInterchangeTarget().getAsDouble(), 1e-10);
        double pTolerance = Double.parseDouble(controlArea.getProperty("pTolerance"));
        assertEquals(10, pTolerance, 1e-10);

        // Update control area data
        controlArea.setInterchangeTarget(controlArea.getInterchangeTarget().getAsDouble() * 2);
        controlArea.setProperty("pTolerance", Double.toString(pTolerance / 2));

        // Write and read the network to check serialization of the extension
        Path updatedXiidm = outputPath.resolve("BE-updated.xiidm");
        be.write("XIIDM", null, updatedXiidm);
        Network beUpdated = Network.read(updatedXiidm);

        // Export only SSH instance file
        Properties exportParams = new Properties();
        exportParams.put(CgmesExport.PROFILES, "SSH");
        beUpdated.write("CGMES", exportParams, outputPath.resolve("BE"));

        // Check that exported SSH contains updated values for net interchange and p tolerance
        Collection<SshExportedControlArea> sshExportedControlAreas = readSshControlAreas(outputPath.resolve("BE_SSH.xml"));
        assertFalse(sshExportedControlAreas.isEmpty());
        SshExportedControlArea sshExportedControlArea = sshExportedControlAreas.iterator().next();
        assertEquals(473.9596, sshExportedControlArea.netInterchange, 1e-10);
        assertEquals(5, sshExportedControlArea.pTolerance, 1e-10);
    }

    static class SshExportedControlArea {
        String id;
        double netInterchange;
        double pTolerance;
    }

    private static final String ATTR_ABOUT = "about";
    private static final String CONTROL_AREA = "ControlArea";
    private static final String CONTROL_AREA_NET_INTERCHANGE = "ControlArea.netInterchange";
    private static final String CONTROL_AREA_P_TOLERANCE = "ControlArea.pTolerance";

    private static Collection<SshExportedControlArea> readSshControlAreas(Path ssh) {
        List<SshExportedControlArea> sshExportedControlAreas = new ArrayList<>();
        SshExportedControlArea sshExportedControlArea = null;
        try (InputStream is = Files.newInputStream(ssh)) {
            XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(is);
            while (reader.hasNext()) {
                int next = reader.next();
                if (next == XMLStreamConstants.START_ELEMENT) {
                    if (reader.getLocalName().equals(CONTROL_AREA)) {
                        sshExportedControlArea = new SshExportedControlArea();
                        sshExportedControlArea.id = reader.getAttributeValue(CgmesNamespace.RDF_NAMESPACE, ATTR_ABOUT).substring(2);
                    } else if (reader.getLocalName().equals(CONTROL_AREA_NET_INTERCHANGE) && sshExportedControlArea != null) {
                        sshExportedControlArea.netInterchange = Double.parseDouble(reader.getElementText());
                    } else if (reader.getLocalName().equals(CONTROL_AREA_P_TOLERANCE) && sshExportedControlArea != null) {
                        sshExportedControlArea.pTolerance = Double.parseDouble(reader.getElementText());
                    }
                } else if (next == XMLStreamConstants.END_ELEMENT) {
                    if (reader.getLocalName().equals(CONTROL_AREA) && sshExportedControlArea != null) {
                        sshExportedControlAreas.add(sshExportedControlArea);
                        sshExportedControlArea = null;
                    }
                }
            }
            reader.close();
        } catch (XMLStreamException | IOException e) {
            throw new RuntimeException(e);
        }
        return sshExportedControlAreas;
    }

    @Test
    void microGridCgmesExportPreservingOriginalClassesOfLoads() throws IOException, XMLStreamException {
        ReadOnlyDataSource ds = Cgmes3ModifiedCatalog.microGridBaseCaseAllTypesOfLoads().dataSource();
        Network network = new CgmesImport().importData(ds, NetworkFactory.findDefault(), importParams);

        // Export as cgmes
        Path outputPath = tmpDir.resolve("temp.cgmesExport");
        Files.createDirectories(outputPath);
        String baseName = "microGridCgmesExportPreservingOriginalClassesOfLoads";
        Properties exportParams = new Properties();
        new CgmesExport().export(network, exportParams, new DirectoryDataSource(outputPath, baseName));

        // re-import after adding the original boundary files
        copyBoundary(outputPath, baseName, ds);
        Network actual = new CgmesImport().importData(new DirectoryDataSource(outputPath, baseName), NetworkFactory.findDefault(), importParams);

        InputStream expectedSsh = Repackager.newInputStream(ds, Repackager::ssh);
        String actualSsh = exportSshAsString(actual);

        DifferenceEvaluator knownDiffsSsh = DifferenceEvaluators.chain(
                ExportXmlCompare::ignoringFullModelDependentOn,
                ExportXmlCompare::ignoringFullModelModelingAuthoritySet,
                ExportXmlCompare::ignoringRdfChildNodeListLength,
                ExportXmlCompare::ignoringChildLookupNull,
                ExportXmlCompare::ignoringTextValueShuntCompensatorControlEnabled,
                ExportXmlCompare::ignoringTextValueTapChangerControlEnabled,
                ExportXmlCompare::ignoringRdfChildLookupTerminal,
                ExportXmlCompare::ignoringRdfChildLookupEquivalentInjection,
                ExportXmlCompare::ignoringStaticVarCompensatorControlEnabled,
                ExportXmlCompare::ignoringStaticVarCompensatorQ,
                ExportXmlCompare::ignoringRegulatingControl,
                ExportXmlCompare::ignoringTextValueEquivalentInjection);
        assertTrue(ExportXmlCompare.compareSSH(expectedSsh, new ByteArrayInputStream(actualSsh.getBytes(StandardCharsets.UTF_8)), knownDiffsSsh));
    }

    @Test
    void miniGridCgmesExportPreservingOriginalClasses() throws IOException, XMLStreamException {
        ReadOnlyDataSource ds = Cgmes3Catalog.miniGrid().dataSource();
        Properties properties = new Properties();
        properties.put("iidm.import.cgmes.convert-boundary", "true");
        Network network = new CgmesImport().importData(ds, NetworkFactory.findDefault(), properties);

        // Export as cgmes
        Path outputPath = tmpDir.resolve("temp.cgmesExport");
        Files.createDirectories(outputPath);
        String baseName = "miniGridCgmesExportPreservingOriginalClasses";
        Properties exportParams = new Properties();
        new CgmesExport().export(network, exportParams, new DirectoryDataSource(outputPath, baseName));

        // re-import after adding the original boundary files
        copyBoundary(outputPath, baseName, ds);
        Network actual = new CgmesImport().importData(new DirectoryDataSource(outputPath, baseName), NetworkFactory.findDefault(), new Properties());

        InputStream expectedSsh = Repackager.newInputStream(ds, Repackager::ssh);
        String actualSsh = exportSshAsString(actual);

        DifferenceEvaluator knownDiffsSsh = DifferenceEvaluators.chain(
                ExportXmlCompare::ignoringFullModelDependentOn,
                ExportXmlCompare::ignoringFullModelModelingAuthoritySet,
                ExportXmlCompare::ignoringRdfChildNodeListLength,
                ExportXmlCompare::ignoringConformLoad,
                ExportXmlCompare::ignoringChildLookupNull);
        assertTrue(ExportXmlCompare.compareSSH(expectedSsh, new ByteArrayInputStream(actualSsh.getBytes(StandardCharsets.UTF_8)), knownDiffsSsh));
    }

    @Test
    void phaseTapChangerTapChangerControlSSHTest() throws IOException {
        String exportFolder = "/test-pst-tcc";
        String baseName = "testPstTcc";
        Network network;
        String ssh;
        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath(exportFolder));
            Properties exportParams = new Properties();
            exportParams.put(CgmesExport.PROFILES, "SSH");

            // PST with FIXED_TAP
            network = PhaseShifterTestCaseFactory.createWithTargetDeadband();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithoutAttribute(ssh, "_PS1_PTC_RC", "true", "false", "10", "200", "M");

            // PST local with ACTIVE_POWER_CONTROL
            network = PhaseShifterTestCaseFactory.createLocalActivePowerWithTargetDeadband();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "false", "10", "200", "M");
            network.getTwoWindingsTransformer("PS1").getPhaseTapChanger().setRegulating(true);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "true", "10", "200", "M");

            // PST local with CURRENT_LIMITER
            network = PhaseShifterTestCaseFactory.createLocalCurrentLimiterWithTargetDeadband();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "false", "10", "200", "none");
            network.getTwoWindingsTransformer("PS1").getPhaseTapChanger().setRegulating(true);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "true", "10", "200", "none");

            // PST remote with CURRENT_LIMITER
            network = PhaseShifterTestCaseFactory.createRemoteCurrentLimiterWithTargetDeadband();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "false", "10", "200", "none");
            network.getTwoWindingsTransformer("PS1").getPhaseTapChanger().setRegulating(true);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "true", "10", "200", "none");

            // PST remote with ACTIVE_POWER_CONTROL
            network = PhaseShifterTestCaseFactory.createRemoteActivePowerWithTargetDeadband();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "false", "10", "200", "M");
            network.getTwoWindingsTransformer("PS1").getPhaseTapChanger().setRegulating(true);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "true", "10", "200", "M");
        }
    }

    @Test
    void ratioTapChangerTapChangerControlSSHTest() throws IOException {
        String exportFolder = "/test-rtc-tcc";
        String baseName = "testRtcTcc";
        Network network;
        String ssh;
        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath(exportFolder));
            Properties exportParams = new Properties();
            exportParams.put(CgmesExport.PROFILES, "SSH");

            // RTC without control
            network = EurostagTutorialExample1Factory.createWithoutRtcControl();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithoutAttribute(ssh, "_NHV2_NLOAD_RTC_RC", "", "", "", "", "");

            // RTC local with VOLTAGE
            network = EurostagTutorialExample1Factory.create();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_NHV2_NLOAD_RTC_RC", "true", "true", "0", "158", "k");
            network.getTwoWindingsTransformer("NHV2_NLOAD").getRatioTapChanger().setRegulating(false);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_NHV2_NLOAD_RTC_RC", "true", "false", "0", "158", "k");

            // RTC local with REACTIVE_POWER
            network = EurostagTutorialExample1Factory.createWithReactiveTcc();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_NHV2_NLOAD_RTC_RC", "true", "true", "0", "100", "M");
            network.getTwoWindingsTransformer("NHV2_NLOAD").getRatioTapChanger().setRegulating(false);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_NHV2_NLOAD_RTC_RC", "true", "false", "0", "100", "M");

            // RTC remote with VOLTAGE
            network = EurostagTutorialExample1Factory.createRemoteVoltageTcc();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_NHV2_NLOAD_RTC_RC", "true", "true", "0", "158", "k");
            network.getTwoWindingsTransformer("NHV2_NLOAD").getRatioTapChanger().setRegulating(false);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_NHV2_NLOAD_RTC_RC", "true", "false", "0", "158", "k");

            // RTC remote with REACTIVE_POWER
            network = EurostagTutorialExample1Factory.createRemoteReactiveTcc();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_NHV2_NLOAD_RTC_RC", "true", "true", "0", "100", "M");
            network.getTwoWindingsTransformer("NHV2_NLOAD").getRatioTapChanger().setRegulating(false);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_NHV2_NLOAD_RTC_RC", "true", "false", "0", "100", "M");

            // 3w without control
            network = EurostagTutorialExample1Factory.createWith3wWithoutControl();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithoutAttribute(ssh, "_NGEN_V2_NHV1_RTC_RC", "", "", "", "", "");

            // 3w with local voltage control
            network = EurostagTutorialExample1Factory.createWith3wWithVoltageControl();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_NGEN_V2_NHV1_RTC_RC", "true", "true", "0", "158", "k");

            // 3w with local reactive control
            network = EurostagTutorialExample1Factory.create3wWithReactiveTcc();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_NGEN_V2_NHV1_RTC_RC", "true", "true", "0", "100", "M");
            network.getThreeWindingsTransformer("NGEN_V2_NHV1").getLeg1().getRatioTapChanger().setRegulating(false);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_NGEN_V2_NHV1_RTC_RC", "true", "false", "0", "100", "M");

            // 3w with remote voltage
            network = EurostagTutorialExample1Factory.create3wRemoteVoltageTcc();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_NGEN_V2_NHV1_RTC_RC", "true", "true", "0", "158", "k");
            network.getThreeWindingsTransformer("NGEN_V2_NHV1").getLeg1().getRatioTapChanger().setRegulating(false);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_NGEN_V2_NHV1_RTC_RC", "true", "false", "0", "158", "k");

            // 3w with remote reactive
            network = EurostagTutorialExample1Factory.create3wRemoteReactiveTcc();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_NGEN_V2_NHV1_RTC_RC", "true", "true", "0", "100", "M");
            network.getThreeWindingsTransformer("NGEN_V2_NHV1").getLeg1().getRatioTapChanger().setRegulating(false);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithAttribute(ssh, "_NGEN_V2_NHV1_RTC_RC", "true", "false", "0", "100", "M");
        }
    }

    @Test
    void staticVarCompensatorRegulatingControlSSHTest() throws IOException {
        String exportFolder = "/test-svc-rc";
        String baseName = "testSvcRc";
        Network network;
        String ssh;
        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath(exportFolder));
            Properties exportParams = new Properties();
            exportParams.put(CgmesExport.PROFILES, "SSH");

            // SVC VOLTAGE
            // Local
            network = SvcTestCaseFactory.createLocalVoltageControl();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SVC2_RC", "false", "true", "0", "390", "k");

            // Remote
            network = SvcTestCaseFactory.createRemoteVoltageControl();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SVC2_RC", "false", "true", "0", "390", "k");

            // SVC REACTIVE_POWER
            // Local
            network = SvcTestCaseFactory.createLocalReactiveControl();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SVC2_RC", "false", "true", "0", "350", "M");

            // Remote
            network = SvcTestCaseFactory.createRemoteReactiveControl();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SVC2_RC", "false", "true", "0", "350", "M");

            // SVC OFF
            // Local
            network = SvcTestCaseFactory.createLocalOffNoTarget();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRCWithoutAttribute(ssh, "_SVC2_RC");
            network = SvcTestCaseFactory.createLocalOffReactiveTarget();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SVC2_RC", "false", "false", "0", "350", "M");
            network = SvcTestCaseFactory.createLocalOffVoltageTarget();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SVC2_RC", "false", "false", "0", "390", "k");
            network = SvcTestCaseFactory.createLocalOffBothTarget();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SVC2_RC", "false", "false", "0", "390", "k");

            // Remote
            network = SvcTestCaseFactory.createRemoteOffNoTarget();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SVC2_RC", "false", "false", "0", "0", "k");
            network = SvcTestCaseFactory.createRemoteOffReactiveTarget();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SVC2_RC", "false", "false", "0", "350", "M");
            network = SvcTestCaseFactory.createRemoteOffVoltageTarget();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SVC2_RC", "false", "false", "0", "390", "k");
            network = SvcTestCaseFactory.createRemoteOffBothTarget();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SVC2_RC", "false", "false", "0", "390", "k");
        }
    }

    @Test
    void shuntCompensatorRegulatingControlSSHTest() throws IOException {
        String exportFolder = "/test-sc-rc";
        String baseName = "testScRc";
        Network network;
        String ssh;
        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath(exportFolder));
            Properties exportParams = new Properties();
            exportParams.put(CgmesExport.PROFILES, "SSH");

            // SC linear
            network = ShuntTestCaseFactory.create();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SHUNT_RC", "true", "true", "5", "200", "k");
            testTcTccWithoutAttribute(ssh, "", "", "", "", "", "M");

            network = ShuntTestCaseFactory.createLocalLinear();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SHUNT_RC", "true", "true", "5", "200", "k");
            testTcTccWithoutAttribute(ssh, "", "", "", "", "", "M");

            network = ShuntTestCaseFactory.createDisabledRemoteLinear();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SHUNT_RC", "true", "false", "5", "200", "k");
            testTcTccWithoutAttribute(ssh, "", "", "", "", "", "M");

            network = ShuntTestCaseFactory.createDisabledLocalLinear();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SHUNT_RC", "true", "false", "5", "200", "k");
            testTcTccWithoutAttribute(ssh, "", "", "", "", "", "M");

            network = ShuntTestCaseFactory.createLocalLinearNoTarget();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithoutAttribute(ssh, "_SHUNT_RC", "", "", "", "", "");

            network = ShuntTestCaseFactory.createRemoteLinearNoTarget();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SHUNT_RC", "true", "false", "5", "0", "k");

            // SC nonlinear
            network = ShuntTestCaseFactory.createNonLinear();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SHUNT_RC", "true", "true", "5", "200", "k");
            testTcTccWithoutAttribute(ssh, "", "", "", "", "", "M");

            network = ShuntTestCaseFactory.createLocalNonLinear();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SHUNT_RC", "true", "true", "5", "200", "k");
            testTcTccWithoutAttribute(ssh, "", "", "", "", "", "M");

            network = ShuntTestCaseFactory.createDisabledRemoteNonLinear();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SHUNT_RC", "true", "false", "5", "200", "k");
            testTcTccWithoutAttribute(ssh, "", "", "", "", "", "M");

            network = ShuntTestCaseFactory.createDisabledLocalNonLinear();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SHUNT_RC", "true", "false", "5", "200", "k");
            testTcTccWithoutAttribute(ssh, "", "", "", "", "", "M");

            network = ShuntTestCaseFactory.createLocalNonLinearNoTarget();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testTcTccWithoutAttribute(ssh, "_SHUNT_RC", "", "", "", "", "");

            network = ShuntTestCaseFactory.createRemoteNonLinearNoTarget();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_SHUNT_RC", "true", "false", "5", "0", "k");
        }
    }

    @Test
    void generatorRegulatingControlSSHTest() throws IOException {
        String exportFolder = "/test-gen-rc";
        String baseName = "testGenRc";
        Network network;
        String ssh;
        try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) {
            Path tmpDir = Files.createDirectory(fs.getPath(exportFolder));
            Properties exportParams = new Properties();
            exportParams.put(CgmesExport.PROFILES, "SSH");

            // Generator local voltage
            network = EurostagTutorialExample1Factory.create();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "true", "0", "24.5", "k");
            network.getGenerator("GEN").setVoltageRegulatorOn(false);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "false", "0", "24.5", "k");

            // Generator remote voltage
            network = EurostagTutorialExample1Factory.createWithRemoteVoltageGenerator();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "true", "0", "399", "k");
            network.getGenerator("GEN").setVoltageRegulatorOn(false);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "false", "0", "399", "k");

            // Generator with remote voltage regulation exported in local regulation mode
            Properties exportInLocalRegulationModeParams = new Properties();
            exportInLocalRegulationModeParams.put(CgmesExport.PROFILES, "SSH");
            exportInLocalRegulationModeParams.put(CgmesExport.EXPORT_GENERATORS_IN_LOCAL_REGULATION_MODE, true);
            network = EurostagTutorialExample1Factory.createWithRemoteVoltageGenerator();
            ssh = getSSH(network, baseName, tmpDir, exportInLocalRegulationModeParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "true", "0", "25.2", "k");

            // Generator with local reactive
            network = EurostagTutorialExample1Factory.createWithLocalReactiveGenerator();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "true", "0", "200", "M");
            network.getGenerator("GEN").getExtension(RemoteReactivePowerControl.class).setEnabled(false);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "false", "0", "200", "M");

            // Generator with remote reactive
            network = EurostagTutorialExample1Factory.createWithRemoteReactiveGenerator();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "true", "0", "200", "M");
            network.getGenerator("GEN").getExtension(RemoteReactivePowerControl.class).setEnabled(false);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "false", "0", "200", "M");

            // Generator with local reactive and voltage
            network = EurostagTutorialExample1Factory.createWithLocalReactiveAndVoltageGenerator();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "true", "0", "24.5", "k");
            network.getGenerator("GEN").setVoltageRegulatorOn(false);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "true", "0", "200", "M");
            network.getGenerator("GEN").getExtension(RemoteReactivePowerControl.class).setEnabled(false);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "false", "0", "24.5", "k");
            network.getGenerator("GEN").setVoltageRegulatorOn(true);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "true", "0", "24.5", "k");

            // Generator with remote reactive and voltage
            network = EurostagTutorialExample1Factory.createWithRemoteReactiveAndVoltageGenerators();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "true", "0", "399", "k");
            network.getGenerator("GEN").setVoltageRegulatorOn(false);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "true", "0", "200", "M");
            network.getGenerator("GEN").getExtension(RemoteReactivePowerControl.class).setEnabled(false);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "false", "0", "399", "k");
            network.getGenerator("GEN").setVoltageRegulatorOn(true);
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRcWithAttribute(ssh, "_GEN_RC", "false", "true", "0", "399", "k");

            // Generator without control
            network = EurostagTutorialExample1Factory.createWithoutControl();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRCWithoutAttribute(ssh, "_GEN_RC");

            // Generator with remote terminal without control
            network = EurostagTutorialExample1Factory.createRemoteWithoutControl();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRCWithoutAttribute(ssh, "_GEN_RC");

            // Generator without control capability
            network = EurostagTutorialExample1Factory.create();
            network.getGenerator("GEN").newMinMaxReactiveLimits().setMaxQ(0).setMinQ(0).add();
            ssh = getSSH(network, baseName, tmpDir, exportParams);
            testRcEqRCWithoutAttribute(ssh, "_GEN_RC");
        }
    }

    private void testTcTccWithoutAttribute(String ssh, String rcID, String discrete, String enabled, String deadband, String target, String multiplier) {
        assertFalse(ssh.contains("cim:TapChangerControl rdf:about=\"#" + rcID + "\""));
        assertFalse(ssh.contains("<cim:RegulatingControl.discrete>" + discrete + "</cim:RegulatingControl.discrete>"));
        assertFalse(ssh.contains("<cim:RegulatingControl.enabled>" + enabled + "</cim:RegulatingControl.enabled>"));
        assertFalse(ssh.contains("<cim:RegulatingControl.targetDeadband>" + deadband + "</cim:RegulatingControl.targetDeadband>"));
        assertFalse(ssh.contains("<cim:RegulatingControl.targetValue>" + target + "</cim:RegulatingControl.targetValue>"));
        assertFalse(ssh.contains("UnitMultiplier." + multiplier + "\""));
    }

    private void testTcTccWithAttribute(String ssh, String rcID, String discrete, String enabled, String deadband, String target, String multiplier) {
        assertTrue(ssh.contains("cim:TapChangerControl rdf:about=\"#" + rcID + "\""));
        assertTrue(ssh.contains("<cim:RegulatingControl.discrete>" + discrete + "</cim:RegulatingControl.discrete>"));
        assertTrue(ssh.contains("<cim:RegulatingControl.enabled>" + enabled + "</cim:RegulatingControl.enabled>"));
        assertTrue(ssh.contains("<cim:RegulatingControl.targetDeadband>" + deadband + "</cim:RegulatingControl.targetDeadband>"));
        assertTrue(ssh.contains("<cim:RegulatingControl.targetValue>" + target + "</cim:RegulatingControl.targetValue>"));
        assertTrue(ssh.contains("UnitMultiplier." + multiplier + "\""));
    }

    private void testRcEqRCWithoutAttribute(String ssh, String rcID) {
        assertFalse(ssh.contains("cim:RegulatingControl rdf:about=\"#" + rcID + "\""));
    }

    private void testRcEqRcWithAttribute(String ssh, String rcID, String discrete, String enabled, String deadband, String target, String multiplier) {
        assertTrue(ssh.contains("cim:RegulatingControl rdf:about=\"#" + rcID + "\""));
        assertTrue(ssh.contains("<cim:RegulatingControl.discrete>" + discrete + "</cim:RegulatingControl.discrete>"));
        assertTrue(ssh.contains("<cim:RegulatingControl.enabled>" + enabled + "</cim:RegulatingControl.enabled>"));
        assertTrue(ssh.contains("<cim:RegulatingControl.targetDeadband>" + deadband + "</cim:RegulatingControl.targetDeadband>"));
        assertTrue(ssh.contains("<cim:RegulatingControl.targetValue>" + target + "</cim:RegulatingControl.targetValue>"));
        assertTrue(ssh.contains("UnitMultiplier." + multiplier + "\""));
    }

    private String getSSH(Network network, String baseName, Path tmpDir, Properties exportParams) throws IOException {
        new CgmesExport().export(network, exportParams, new DirectoryDataSource(tmpDir, baseName));
        return Files.readString(tmpDir.resolve(baseName + "_SSH.xml"));
    }

    private static void copyBoundary(Path outputFolder, String baseName, ReadOnlyDataSource originalDataSource) throws IOException {
        String eqbd = originalDataSource.listNames(".*EQ_BD.*").stream().findFirst().orElse(null);
        if (eqbd != null) {
            try (InputStream is = originalDataSource.newInputStream(eqbd)) {
                Files.copy(is, outputFolder.resolve(baseName + "_EQ_BD.xml"));
            }
        }
    }
}