diff --git a/emf/pom.xml b/emf/pom.xml
new file mode 100644
index 00000000..55d7c661
--- /dev/null
+++ b/emf/pom.xml
@@ -0,0 +1,84 @@
+
+
+
+ powsybl-entsoe
+ com.powsybl
+ 2.2.0-SNAPSHOT
+
+ 4.0.0
+
+ powsybl-emf
+ European merging function algorithm
+ Implementation of european merging function algorithm
+
+
+
+ com.powsybl
+ powsybl-commons
+
+
+ com.powsybl
+ powsybl-iidm-api
+
+
+ com.powsybl
+ powsybl-cgmes-extensions
+
+
+ com.powsybl
+ powsybl-cgmes-conversion
+
+
+ com.powsybl
+ powsybl-iidm-mergingview
+
+
+ com.powsybl
+ powsybl-cgmes-conformity
+
+
+ com.powsybl
+ powsybl-cgmes-model
+
+
+ ch.qos.logback
+ logback-classic
+ runtime
+
+
+ com.google.jimfs
+ jimfs
+ test
+
+
+ com.powsybl
+ powsybl-config-test
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit-jupiter.version}
+ test
+
+
+
+ com.powsybl
+ powsybl-iidm-impl
+ test
+
+
+ com.powsybl
+ powsybl-open-loadflow
+ test
+
+
+ com.powsybl
+ powsybl-triple-store-impl-rdf4j
+ test
+
+
+
+
diff --git a/emf/src/test/java/com/powsybl/emf/IGMmergeTests.java b/emf/src/test/java/com/powsybl/emf/IGMmergeTests.java
new file mode 100644
index 00000000..13e3c17b
--- /dev/null
+++ b/emf/src/test/java/com/powsybl/emf/IGMmergeTests.java
@@ -0,0 +1,222 @@
+/*
+ * 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.export.*;
+import com.powsybl.cgmes.model.test.TestGridModelResources;
+import com.powsybl.commons.datasource.GenericReadOnlyDataSource;
+import com.powsybl.commons.datasource.ResourceSet;
+import com.powsybl.commons.xml.XmlUtil;
+import com.powsybl.iidm.mergingview.MergingView;
+import com.powsybl.iidm.network.Network;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.function.Consumer;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * @author Bertrand Rix
+ */
+class IGMmergeTests {
+
+ private FileSystem fs;
+
+ @BeforeEach
+ public void setUp() {
+ fs = Jimfs.newFileSystem(Configuration.unix());
+ }
+
+ @AfterEach
+ public void tearDown() throws IOException {
+ fs.close();
+ }
+
+ @Test
+ void igmsDestructiveMerge() throws IOException {
+
+ Set branchIds = new HashSet<>();
+ Set generatorsId = new HashSet<>();
+ Set voltageLevelIds = new HashSet<>();
+
+ //Load two IGMs BE and NL
+ Map validNetworks = new HashMap<>();
+ TestGridModelResources resBE = CgmesConformity1Catalog.microGridBaseCaseBE();
+ Network igmBE = Network.read(resBE.dataSource());
+ validNetworks.put("BE", igmBE);
+ igmBE.getBranches().forEach(b -> branchIds.add(b.getId()));
+ igmBE.getGenerators().forEach(g -> generatorsId.add(g.getId()));
+ igmBE.getVoltageLevels().forEach(v -> voltageLevelIds.add(v.getId()));
+
+ TestGridModelResources resNL = CgmesConformity1Catalog.microGridBaseCaseNL();
+ Network igmNL = Network.read(resNL.dataSource());
+ validNetworks.put("NL", igmNL);
+ igmNL.getBranches().forEach(b -> branchIds.add(b.getId()));
+ igmNL.getGenerators().forEach(g -> generatorsId.add(g.getId()));
+ igmNL.getVoltageLevels().forEach(v -> voltageLevelIds.add(v.getId()));
+
+ //Merge, Serialize and Deserialize the network
+ igmBE.merge(igmNL);
+ validNetworks.put("Merged", igmBE);
+
+ Path destructiveMergeDir = Files.createDirectory(fs.getPath("/destructiveMerge"));
+ exportNetwork(igmBE, destructiveMergeDir, "BE_NL", validNetworks, 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), destructiveMergeDir.resolve("BE_NL" + bFile));
+ }
+
+ //Reimport and check
+ Network serializedMergedNetwork = Network.read(new GenericReadOnlyDataSource(destructiveMergeDir, "BE_NL"), null);
+ validate(serializedMergedNetwork, branchIds, generatorsId, voltageLevelIds);
+ }
+
+ @Test
+ void igmsMergeWithMergingView() throws IOException {
+
+ Set branchIds = new HashSet<>();
+ Set generatorsId = new HashSet<>();
+ Set voltageLevelIds = new HashSet<>();
+
+ Map validNetworks = new HashMap<>();
+ TestGridModelResources resBE = CgmesConformity1Catalog.microGridBaseCaseBE();
+ Network igmBE = Network.read(resBE.dataSource());
+ validNetworks.put("BE", igmBE);
+ igmBE.getBranches().forEach(b -> branchIds.add(b.getId()));
+ igmBE.getGenerators().forEach(g -> generatorsId.add(g.getId()));
+ igmBE.getVoltageLevels().forEach(v -> voltageLevelIds.add(v.getId()));
+
+ TestGridModelResources resNL = CgmesConformity1Catalog.microGridBaseCaseNL();
+ Network igmNL = Network.read(resNL.dataSource());
+
+ MergingView mergingView = MergingView.create("merged", "validation");
+ mergingView.merge(igmBE, igmNL);
+ validNetworks.put("NL", igmNL);
+ igmNL.getBranches().forEach(b -> branchIds.add(b.getId()));
+ igmNL.getGenerators().forEach(g -> generatorsId.add(g.getId()));
+ igmNL.getVoltageLevels().forEach(v -> voltageLevelIds.add(v.getId()));
+ validNetworks.put("Merged", mergingView);
+
+ Path mergingViewMergeDir = Files.createDirectory(fs.getPath("/mergingViewMerge"));
+ //Export to CGMES only state variable of the merged network, the rest is exported separately for each igms
+ exportNetwork(mergingView, mergingViewMergeDir, "BE_NL", validNetworks, Set.of("SV"));
+ exportNetwork(igmBE, mergingViewMergeDir, "BE_NL_BE", Map.of("BE", igmBE), Set.of("EQ", "TP", "SSH"));
+ exportNetwork(igmNL, mergingViewMergeDir, "BE_NL_NL", Map.of("NL", igmNL), 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), mergingViewMergeDir.resolve("BE_NL" + bFile));
+ }
+
+ Network serializedMergedNetwork = Network.read(new GenericReadOnlyDataSource(mergingViewMergeDir, "BE_NL"), null);
+ validate(serializedMergedNetwork, branchIds, generatorsId, voltageLevelIds);
+ }
+
+ @Test
+ void cgmToCgmes() throws IOException {
+ //Read resources for BE and NL, merge the resources themselves and read a network from this set of resources
+ TestGridModelResources mergedResourcesBENL = new TestGridModelResources(
+ "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());
+ Network networkBENL = Network.read(mergedResourcesBENL.dataSource());
+
+ Set branchIds = new HashSet<>();
+ Set generatorsId = new HashSet<>();
+ Set voltageLevelIds = new HashSet<>();
+
+ //networkBENL.getBranches().forEach(b -> branchIds.add(b.getId()));
+ networkBENL.getBranches().forEach(b -> branchIds.add(b.getId().replace(" ", "%20"))); // FIXME workaround before fixing CGMES export/import
+ networkBENL.getGenerators().forEach(g -> generatorsId.add(g.getId()));
+ networkBENL.getVoltageLevels().forEach(v -> voltageLevelIds.add(v.getId()));
+
+ Path mergedResourcesDir = Files.createDirectory(fs.getPath("/mergedResourcesExport"));
+ exportNetwork(networkBENL, mergedResourcesDir, "BE_NL", Map.of("BENL", networkBENL), 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));
+ }
+ Network serializedMergedNetwork = Network.read(new GenericReadOnlyDataSource(mergedResourcesDir, "BE_NL"), null);
+ validate(serializedMergedNetwork, branchIds, generatorsId, voltageLevelIds);
+ }
+
+ private static void validate(Network n, Set branchIds, Set generatorsId, Set 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, Map validNetworks, Set profilesToExport) {
+ Objects.requireNonNull(network);
+ Path filenameEq = outputDir.resolve(baseName + "_EQ.xml");
+ Path filenameTp = outputDir.resolve(baseName + "_TP.xml");
+ Path filenameSsh = outputDir.resolve(baseName + "_SSH.xml");
+ Path filenameSv = outputDir.resolve(baseName + "_SV.xml");
+ CgmesExportContext context = new CgmesExportContext();
+ context.setScenarioTime(network.getCaseDate());
+ validNetworks.forEach((name, n) -> {
+ context.addIidmMappings(n);
+ });
+
+ if (profilesToExport.contains("EQ")) {
+ export(filenameEq, writer -> EquipmentExport.write(network, writer, context));
+ }
+ if (profilesToExport.contains("TP")) {
+ export(filenameTp, writer -> TopologyExport.write(network, writer, context));
+ }
+ if (profilesToExport.contains("SSH")) {
+ export(filenameSsh, writer -> SteadyStateHypothesisExport.write(network, writer, context));
+ }
+ if (profilesToExport.contains("SV")) {
+ export(filenameSv, writer -> StateVariablesExport.write(network, writer, context));
+ }
+ }
+
+ private static void export(Path file, Consumer outConsumer) {
+ try (OutputStream out = Files.newOutputStream(file)) {
+ XMLStreamWriter writer = XmlUtil.initializeWriter(true, " ", out);
+ outConsumer.accept(writer);
+ } catch (IOException | XMLStreamException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 5c4c50ec..c4f770d0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,6 +77,7 @@
distribution-entsoe
flow-decomposition
glsk
+ emf