LegacyCommonGridModelExportTest.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.powsybl.cgmes.conformity.CgmesConformity1ModifiedCatalog;
import com.powsybl.cgmes.conversion.CgmesExport;
import com.powsybl.cgmes.extensions.CgmesMetadataModels;
import com.powsybl.cgmes.extensions.CgmesMetadataModelsAdder;
import com.powsybl.cgmes.model.CgmesMetadataModel;
import com.powsybl.cgmes.model.CgmesSubset;
import com.powsybl.commons.datasource.ReadOnlyDataSource;
import com.powsybl.commons.test.AbstractSerDeTest;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.util.Identifiables;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.nio.file.Files;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.powsybl.cgmes.conversion.Conversion.CGMES_PREFIX_ALIAS_PROPERTIES;
import static org.junit.jupiter.api.Assertions.*;

/**
 * @author Luma Zamarre��o {@literal <zamarrenolm at aia.es>}
 */
class LegacyCommonGridModelExportTest extends AbstractSerDeTest {

    @Test
    void testExportCgmSvDependenciesNotUpdated() throws IOException {
        // We check that prepared SV dependencies have not been modified even if we export as CGM (SSH + SV)
        // This is really not useful, since in this scenario we would want to export only SV
        // But we want to let the parameter "update-dependencies" be valid in both types of export: IGM and CGM

        ReadOnlyDataSource ds = CgmesConformity1ModifiedCatalog.microGridBaseCaseAssembledSvWithMas().dataSource();
        Network network = Network.read(ds);

        List<String> sshIds = List.of("ssh-dep1", "ssh-dep2", "ssh-dep3");
        buildSvDependenciesManaginMetadataModels(network, sshIds);

        Properties exportParams = new Properties();
        exportParams.put(CgmesExport.CGM_EXPORT, "True");
        exportParams.put(CgmesExport.UPDATE_DEPENDENCIES, "False");
        String exportBasename = "tmp-micro-bc-CGM";
        network.write("CGMES", exportParams, tmpDir.resolve(exportBasename));

        String sv = Files.readString(tmpDir.resolve(exportBasename + "_SV.xml"));

        Set<String> deps = findAll(REGEX_DEPENDENT_ON, sv);
        Set<String> prepared = network.getExtension(CgmesMetadataModels.class).getModelForSubset(CgmesSubset.STATE_VARIABLES).orElseThrow().getDependentOn();
        assertEquals(prepared, deps);
    }

    @Test
    void testExportCgmSvDependenciesOnNetworkProperties() throws IOException {
        ReadOnlyDataSource ds = CgmesConformity1ModifiedCatalog.microGridBaseCaseAssembledSvWithMas().dataSource();
        Network network = Network.read(ds);

        // This is the legacy way of preparing dependencies for SV externally,
        // It was used by projects in the FARAO community
        // The support for this way of preparing dependencies has been dropped
        network.setProperty(Identifiables.getUniqueId(CGMES_PREFIX_ALIAS_PROPERTIES + "SSH_ID", network::hasProperty), "ssh-updated-dep1");
        network.setProperty(Identifiables.getUniqueId(CGMES_PREFIX_ALIAS_PROPERTIES + "SSH_ID", network::hasProperty), "ssh-updated-dep2");
        network.setProperty(Identifiables.getUniqueId(CGMES_PREFIX_ALIAS_PROPERTIES + "SSH_ID", network::hasProperty), "ssh-updated-dep3");
        network.setProperty(Identifiables.getUniqueId(CGMES_PREFIX_ALIAS_PROPERTIES + "TP_ID", network::hasProperty), "tp-initial-dep1");
        network.setProperty(Identifiables.getUniqueId(CGMES_PREFIX_ALIAS_PROPERTIES + "TP_ID", network::hasProperty), "tp-initial-dep2");
        network.setProperty(Identifiables.getUniqueId(CGMES_PREFIX_ALIAS_PROPERTIES + "TP_ID", network::hasProperty), "tp-initial-dep3");

        Properties exportParams = new Properties();
        exportParams.put(CgmesExport.PROFILES, "SV");
        String exportBasename = "tmp-micro-bc-from-CGM";
        network.write("CGMES", exportParams, tmpDir.resolve(exportBasename));

        Set<String> deps = findAll(REGEX_DEPENDENT_ON, Files.readString(tmpDir.resolve(exportBasename + "_SV.xml")));
        // We ensure that written dependencies do not match any of the prepared through properties
        Set<String> prepared = Set.of("ssh-updated-dep1", "ssh-updated-dep2", "ssh-updated-dep3", "tp-initial-dep1", "tp-initial-dep2", "tp-initial-dep3");
        assertFalse(deps.stream().anyMatch(prepared::contains));
    }

    @Test
    void testExportCgmSvDependenciesOnMetadataModelsExtension() throws IOException {
        ReadOnlyDataSource ds = CgmesConformity1ModifiedCatalog.microGridBaseCaseAssembledSvWithMas().dataSource();
        Network network = Network.read(ds);
        List<String> initialSshIds = gatherInitialSshIds(network);

        // This test shows how to prepare the dependencies for SV in main network
        // by directly building the metadata model extension.
        // We pass only the updated SSH dependencies
        List<String> updatedSshIds = List.of("ssh-updated-dep1", "ssh-updated-dep2", "ssh-updated-dep3");
        buildSvDependenciesManaginMetadataModels(network, updatedSshIds);

        Properties exportParams = new Properties();
        exportParams.put(CgmesExport.PROFILES, "SV");
        // Because we have built the SV metadata model explicitly,
        // We do not want the CGMES export module to update dependency information
        exportParams.put(CgmesExport.UPDATE_DEPENDENCIES, "False");
        String exportBasename = "tmp-micro-bc-from-CGM";
        network.write("CGMES", exportParams, tmpDir.resolve(exportBasename));

        String sv = Files.readString(tmpDir.resolve(exportBasename + "_SV.xml"));

        Set<String> deps = findAll(REGEX_DEPENDENT_ON, sv);
        assertTrue(deps.containsAll(updatedSshIds));
        initialSshIds.forEach(initialSshId -> assertFalse(deps.contains(initialSshId)));

        // Ensure that only the prepared dependencies have been put in the output
        Set<String> prepared = network.getExtension(CgmesMetadataModels.class).getModelForSubset(CgmesSubset.STATE_VARIABLES).orElseThrow().getDependentOn();
        assertEquals(prepared, deps);

        assertEquals("MAS1", findFirst(MODELING_AUTHORITY, sv));
    }

    private void buildSvDependenciesManaginMetadataModels(Network network, List<String> updateSshIds) {
        CgmesMetadataModelsAdder networkModelsAdder = network.newExtension(CgmesMetadataModelsAdder.class);
        CgmesMetadataModelsAdder.ModelAdder svModelExport = networkModelsAdder.newModel();
        svModelExport.setSubset(CgmesSubset.STATE_VARIABLES);
        svModelExport.setId("a-unique-id");

        Network subnetwork = (Network) network.getSubnetworks().toArray()[0];
        CgmesMetadataModels modelsExtension = subnetwork.getExtension(CgmesMetadataModels.class);
        if (modelsExtension != null) {
            modelsExtension.getModelForSubset(CgmesSubset.STATE_VARIABLES).ifPresent(
                    svModel -> {
                        List<String> initialSvDependantOn = copyListDependencies(svModel);
                        removeInitialSshFromInitialDependencies(network, initialSvDependantOn);
                        initialSvDependantOn.forEach(svModelExport::addDependentOn);

                        svModelExport.setModelingAuthoritySet(svModel.getModelingAuthoritySet());
                        svModel.getProfiles().forEach(svModelExport::addProfile);
                        svModelExport.setDescription(svModel.getDescription());
                    }
            );
        }
        updateSshIds.forEach(svModelExport::addDependentOn);
        svModelExport.add();
        networkModelsAdder.add();
    }

    private static List<String> copyListDependencies(CgmesMetadataModel svModel) {
        if (svModel != null) {
            return new ArrayList<>(svModel.getDependentOn());
        }
        return new ArrayList<>();
    }

    private static List<String> gatherInitialSshIds(Network network) {
        return network.getSubnetworks()
                .stream()
                .map(subNetwork -> subNetwork.getExtension(CgmesMetadataModels.class))
                .filter(Objects::nonNull)
                .map(modelsExtension -> ((CgmesMetadataModels) modelsExtension).getModelForSubset(CgmesSubset.STEADY_STATE_HYPOTHESIS))
                .filter(Optional::isPresent)
                .map(Optional::get)
                .map(CgmesMetadataModel::getId)
                .toList();
    }

    private static void removeInitialSshFromInitialDependencies(Network network, List<String> initialSvDependantOn) {
        gatherInitialSshIds(network).forEach(initialSvDependantOn::remove);
    }

    private static Set<String> findAll(Pattern pattern, String xml) {
        Set<String> found = new HashSet<>();
        Matcher matcher = pattern.matcher(xml);
        while (matcher.find()) {
            found.add(matcher.group(1));
        }
        return found;
    }

    private static String findFirst(Pattern pattern, String xml) {
        Matcher matcher = pattern.matcher(xml);
        if (matcher.find()) {
            return matcher.group(1);
        }
        return null;
    }

    private static final Pattern REGEX_DEPENDENT_ON = Pattern.compile("<md:Model.DependentOn rdf:resource=\"(.*?)\"");
    private static final Pattern MODELING_AUTHORITY = Pattern.compile("<md:Model.modelingAuthoritySet>(.*?)</md:Model.modelingAuthoritySet>");
}