IGMmergeTests.java

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

import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import com.powsybl.cgmes.conformity.CgmesConformity1Catalog;
import com.powsybl.cgmes.conversion.CgmesExport;
import com.powsybl.cgmes.model.GridModelReferenceResources;
import com.powsybl.commons.datasource.GenericReadOnlyDataSource;
import com.powsybl.commons.datasource.ResourceSet;
import com.powsybl.iidm.network.*;
import com.powsybl.loadflow.LoadFlow;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.*;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

/**
 * @author Bertrand Rix {@literal <bertrand.rix at artelys.com>}
 */
class IGMmergeTests {

    private FileSystem fs;
    private Path tmpDir;

    @BeforeEach
    void setUp() throws IOException {
        fs = Jimfs.newFileSystem(Configuration.unix());
        tmpDir = Files.createDirectory(fs.getPath("/tmp"));
    }

    @AfterEach
    void tearDown() throws IOException {
        fs.close();
    }

    @Test
    void igmsSubnetworksMerge() throws IOException {
        Set<String> branchIds = new HashSet<>();
        Set<String> generatorIds = new HashSet<>();
        Set<String> voltageLevelIds = new HashSet<>();

        // load two IGMs BE and NL
        GridModelReferenceResources resBE = CgmesConformity1Catalog.microGridBaseCaseBE();
        Network igmBE = Network.read(resBE.dataSource());
        igmBE.getBranches().forEach(b -> branchIds.add(b.getId()));
        igmBE.getGenerators().forEach(g -> generatorIds.add(g.getId()));
        igmBE.getVoltageLevels().forEach(v -> voltageLevelIds.add(v.getId()));

        GridModelReferenceResources resNL = CgmesConformity1Catalog.microGridBaseCaseNL();
        Network igmNL = Network.read(resNL.dataSource());
        igmNL.getBranches().forEach(b -> branchIds.add(b.getId()));
        igmNL.getGenerators().forEach(g -> generatorIds.add(g.getId()));
        igmNL.getVoltageLevels().forEach(v -> voltageLevelIds.add(v.getId()));

        // merge, serialize and deserialize the network
        Network merged = Network.merge("Merged", igmBE, igmNL);

        // Check that we have subnetworks
        assertEquals(2, merged.getSubnetworks().size());

        // Once the CGM ("merged") has been built, we do not need values (p0, q0) at dangling lines
        // They are the values of the equivalent injections needed when only one IGM is considered
        for (TieLine tl : merged.getTieLineStream().toList()) {
            tl.getDanglingLine1().setP0(0).setQ0(0);
            tl.getDanglingLine2().setP0(0).setQ0(0);
        }

        LoadFlow.run(merged);

        Path mergedDir = Files.createDirectories(tmpDir.resolve("subnetworksMerge"));
        exportNetwork(merged, mergedDir, "BE_NL", Set.of("EQ", "TP", "SSH", "SV"));

        // copy the boundary set explicitly it is not serialized and is needed for reimport
        ResourceSet boundaries = CgmesConformity1Catalog.microGridBaseCaseBoundaries();
        for (String bFile : boundaries.getFileNames()) {
            Files.copy(boundaries.newInputStream(bFile), mergedDir.resolve("BE_NL" + bFile), StandardCopyOption.REPLACE_EXISTING);
        }

        // reimport and check
        Network serializedMergedNetwork = Network.read(new GenericReadOnlyDataSource(mergedDir, "BE_NL"), null);
        validate(serializedMergedNetwork, branchIds, generatorIds, voltageLevelIds);

        // compare
        compareNetwork(merged, serializedMergedNetwork);
    }

    @Test
    void testSubnetworksExports() throws IOException {
        Set<String> branchIdsBE = new HashSet<>();
        Set<String> generatorIdsBE = new HashSet<>();
        Set<String> voltageLevelIdsBE = new HashSet<>();

        // load two IGMs BE and NL
        // BE is loaded twice to keep a reference for further comparison, since "merge" is destructive
        GridModelReferenceResources resBE = CgmesConformity1Catalog.microGridBaseCaseBE();
        Network igmBE = Network.read(resBE.dataSource());
        Network igmRefBE = Network.read(resBE.dataSource());
        String idBE = igmBE.getId();
        igmBE.getBranches().forEach(b -> branchIdsBE.add(b.getId()));
        igmBE.getGenerators().forEach(g -> generatorIdsBE.add(g.getId()));
        igmBE.getVoltageLevels().forEach(v -> voltageLevelIdsBE.add(v.getId()));

        GridModelReferenceResources resNL = CgmesConformity1Catalog.microGridBaseCaseNL();
        Network igmNL = Network.read(resNL.dataSource());

        // merge, serialize and deserialize the network
        Network merged = Network.merge("Merged", igmBE, igmNL);
        Network subnetworkBE = merged.getSubnetwork(idBE);

        // Check that we have subnetworks
        assertEquals(2, merged.getSubnetworks().size());

        Network retrievedFromSubnetwork = exportAndLoad(subnetworkBE, "subnetworksBE", "BE");
        validate(retrievedFromSubnetwork, branchIdsBE, generatorIdsBE, voltageLevelIdsBE);

        // compare with ref (after its export)
        Network retrievedFromRef = exportAndLoad(igmRefBE, "refBE", "BE");

        // compare
        compareNetwork(retrievedFromSubnetwork, retrievedFromRef);
    }

    private Network exportAndLoad(Network network, String dirName, String country) throws IOException {
        Path dir = Files.createDirectories(tmpDir.resolve(dirName));
        exportNetwork(network, dir, country, Set.of("EQ", "TP", "SSH"));

        // copy the boundary set explicitly it is not serialized and is needed for reimport
        ResourceSet boundaries = CgmesConformity1Catalog.microGridBaseCaseBoundaries();
        for (String bFile : boundaries.getFileNames()) {
            Files.copy(boundaries.newInputStream(bFile), dir.resolve(country + bFile), StandardCopyOption.REPLACE_EXISTING);
        }

        // reimport
        return Network.read(new GenericReadOnlyDataSource(dir, country), null);
    }

    @Test
    void cgmToCgmes() throws IOException {
        // read resources for BE and NL, merge the resources themselves and read a network from this set of resources
        Network networkBENL = createCGM();
        // Once the CGM has been built, we do not need values (p0, q0) at dangling lines
        // They are the values of the equivalent injections needed when only one IGM is considered
        for (TieLine tl : networkBENL.getTieLineStream().toList()) {
            tl.getDanglingLine1().setP0(0).setQ0(0);
            tl.getDanglingLine2().setP0(0).setQ0(0);
        }

        Set<String> branchIds = new HashSet<>();
        Set<String> generatorIds = new HashSet<>();
        Set<String> voltageLevelIds = new HashSet<>();

        networkBENL.getBranches().forEach(b -> branchIds.add(b.getId()));
        networkBENL.getGenerators().forEach(g -> generatorIds.add(g.getId()));
        networkBENL.getVoltageLevels().forEach(v -> voltageLevelIds.add(v.getId()));

        LoadFlow.run(networkBENL);

        Path mergedResourcesDir = Files.createDirectories(tmpDir.resolve("mergedResourcesExport"));
        exportNetwork(networkBENL, mergedResourcesDir, "BE_NL", Set.of("EQ", "TP", "SSH", "SV"));

        //Copy the boundary set explicitly it is not serialized and is needed for reimport
        ResourceSet boundaries = CgmesConformity1Catalog.microGridBaseCaseBoundaries();
        for (String bFile : boundaries.getFileNames()) {
            Files.copy(boundaries.newInputStream(bFile), mergedResourcesDir.resolve("BE_NL" + bFile), StandardCopyOption.REPLACE_EXISTING);
        }
        Network serializedMergedNetwork = Network.read(new GenericReadOnlyDataSource(mergedResourcesDir, "BE_NL"), null);
        validate(serializedMergedNetwork, branchIds, generatorIds, voltageLevelIds);

        // compare the serialized and reimported network with the original one
        compareNetwork(networkBENL, serializedMergedNetwork);
    }

    @Test
    void testCompareSubnetworksMergeAgainstAssembled() {
        Network merged = Network.merge("merged",
                Network.read(CgmesConformity1Catalog.microGridBaseCaseBE().dataSource()),
                Network.read(CgmesConformity1Catalog.microGridBaseCaseNL().dataSource()));
        Network assembled = createCGM();
        compareNetwork(assembled, merged);
    }

    private static void validate(Network n, Set<String> branchIds, Set<String> generatorsId, Set<String> voltageLevelIds) {
        branchIds.forEach(b -> assertNotNull(n.getBranch(b)));
        generatorsId.forEach(g -> assertNotNull(n.getGenerator(g)));
        voltageLevelIds.forEach(v -> assertNotNull(n.getVoltageLevel(v)));
    }

    private static void exportNetwork(Network network, Path outputDir, String baseName, Set<String> profilesToExport) {
        Objects.requireNonNull(network);
        Properties exportParams = new Properties();
        exportParams.put(CgmesExport.PROFILES, String.join(",", profilesToExport));
        network.write("CGMES", exportParams, outputDir.resolve(baseName));
    }

    private static void checkDanglingLine(DanglingLine dl1, DanglingLine dl2) {
        assertEquals(dl1.getG(), dl2.getG(), TOLERANCE_GB);
        assertEquals(dl1.getB(), dl2.getB(), TOLERANCE_GB);
        assertEquals(dl1.getR(), dl2.getR(), TOLERANCE_RX);
        assertEquals(dl1.getX(), dl2.getX(), TOLERANCE_RX);
        assertEquals(dl1.getP0(), dl2.getP0(), TOLERANCE_PQ);
        assertEquals(dl1.getQ0(), dl2.getQ0(), TOLERANCE_PQ);
    }

    private static final double TOLERANCE_RX = 1e-10;
    private static final double TOLERANCE_GB = 1e-4;
    private static final double TOLERANCE_PQ = 1e-4;

    private static void checkLineCharacteristics(LineCharacteristics line1, LineCharacteristics line2) {
        boolean halvesHaveSameOrder = true;
        if (line1 instanceof TieLine) {
            String id11 = ((TieLine) line1).getDanglingLine1().getId();
            String id21 = ((TieLine) line2).getDanglingLine1().getId();
            if (!id11.equals(id21)) {
                halvesHaveSameOrder = false;
            }
        }
        assertEquals(line1.getR(), line2.getR(), TOLERANCE_RX);
        assertEquals(line1.getX(), line2.getX(), TOLERANCE_RX);
        if (halvesHaveSameOrder) {
            assertEquals(line1.getB1(), line2.getB1(), TOLERANCE_GB);
            assertEquals(line1.getB2(), line2.getB2(), TOLERANCE_GB);
            assertEquals(line1.getG1(), line2.getG1(), TOLERANCE_GB);
            assertEquals(line1.getG2(), line2.getG2(), TOLERANCE_GB);
        } else {
            assertEquals(line1.getB1(), line2.getB2(), TOLERANCE_GB);
            assertEquals(line1.getB2(), line2.getB1(), TOLERANCE_GB);
            assertEquals(line1.getG1(), line2.getG2(), TOLERANCE_GB);
            assertEquals(line1.getG2(), line2.getG1(), TOLERANCE_GB);
        }
    }

    private static void compareNetwork(Network network1, Network network2) {
        assertEquals(network1.getDanglingLineCount(), network2.getDanglingLineCount());
        assertEquals(network1.getLineCount(), network2.getLineCount());
        assertEquals(network1.getTieLineCount(), network2.getTieLineCount());
        network1.getDanglingLineStream().forEach(dl1 -> {
            DanglingLine dl2 = network2.getDanglingLine(dl1.getId());
            checkDanglingLine(dl1, dl2);
        });
        network1.getLineStream().forEach(line1 -> {
            LineCharacteristics line2 = network2.getLine(line1.getId()); // cgm should be always at network1
            checkLineCharacteristics(line1, line2);
        });
        network1.getTieLineStream().forEach(tieLine1 -> {
            TieLine tieLine2 = network2.getTieLine(tieLine1.getId());
            checkLineCharacteristics(tieLine1, tieLine2);
        });
    }

    private Network createCGM() {
        GridModelReferenceResources mergedResourcesBENL = new GridModelReferenceResources(
                "MicroGrid-BaseCase-BE_NL_MergedResources",
                null,
                new ResourceSet("/conformity/cas-1.1.3-data-4.0.3/MicroGrid/BaseCase/CGMES_v2.4.15_MicroGridTestConfiguration_BC_BE_v2/",
                        "MicroGridTestConfiguration_BC_BE_DL_V2.xml",
                        "MicroGridTestConfiguration_BC_BE_DY_V2.xml",
                        "MicroGridTestConfiguration_BC_BE_EQ_V2.xml",
                        "MicroGridTestConfiguration_BC_BE_GL_V2.xml",
                        "MicroGridTestConfiguration_BC_BE_SSH_V2.xml",
                        "MicroGridTestConfiguration_BC_BE_SV_V2.xml",
                        "MicroGridTestConfiguration_BC_BE_TP_V2.xml"),
                new ResourceSet("/conformity/cas-1.1.3-data-4.0.3/MicroGrid/BaseCase/CGMES_v2.4.15_MicroGridTestConfiguration_BC_NL_v2/",
                        "MicroGridTestConfiguration_BC_NL_DL_V2.xml",
                        "MicroGridTestConfiguration_BC_NL_DY_V2.xml",
                        "MicroGridTestConfiguration_BC_NL_EQ_V2.xml",
                        "MicroGridTestConfiguration_BC_NL_GL_V2.xml",
                        "MicroGridTestConfiguration_BC_NL_SSH_V2.xml",
                        "MicroGridTestConfiguration_BC_NL_SV_V2.xml",
                        "MicroGridTestConfiguration_BC_NL_TP_V2.xml"),
                CgmesConformity1Catalog.microGridBaseCaseBoundaries());
        return Network.read(mergedResourcesBENL.dataSource());
    }
}