From 0da4ccd3b12bbc7a9ef545d4e03c37f569356178 Mon Sep 17 00:00:00 2001 From: Romain Courtier Date: Fri, 22 Mar 2024 15:39:53 +0100 Subject: [PATCH] Add a new parameter and logic for CGM export Signed-off-by: Romain Courtier --- .../powsybl/cgmes/conversion/CgmesExport.java | 153 +++++++++++++++--- .../conversion/export/CgmesExportContext.java | 30 +--- .../conversion/export/CgmesExportUtil.java | 32 ++-- .../export/StateVariablesExport.java | 7 +- .../export/SteadyStateHypothesisExport.java | 7 +- .../test/export/CgmesExportContextTest.java | 2 +- .../export/CommonGridModelExportTest.java | 125 +++++++------- 7 files changed, 230 insertions(+), 126 deletions(-) diff --git a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesExport.java b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesExport.java index 3dbf293581c..d8255ffbb5f 100644 --- a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesExport.java +++ b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesExport.java @@ -11,8 +11,10 @@ import com.powsybl.cgmes.conversion.export.*; import com.powsybl.cgmes.conversion.naming.NamingStrategy; import com.powsybl.cgmes.conversion.naming.NamingStrategyFactory; +import com.powsybl.cgmes.extensions.CgmesMetadataModels; import com.powsybl.cgmes.model.CgmesMetadataModel; import com.powsybl.cgmes.model.CgmesNamespace; +import com.powsybl.cgmes.model.CgmesSubset; import com.powsybl.commons.config.PlatformConfig; import com.powsybl.commons.datasource.DataSource; import com.powsybl.commons.exceptions.UncheckedXmlStreamException; @@ -80,11 +82,7 @@ public void export(Network network, Properties params, DataSource ds, ReportNode // If not given explicitly, // the reference data provider can try to obtain it from the country of the network // If we have multiple countries we do not pass this info to the reference data provider - Set countries = network.getSubstationStream() - .map(Substation::getCountry) - .flatMap(Optional::stream) - .map(Enum::name) - .collect(Collectors.toUnmodifiableSet()); + Set countries = getCountries(network); if (countries.size() == 1) { countryName = countries.iterator().next(); } @@ -141,41 +139,133 @@ public void export(Network network, Properties params, DataSource ds, ReportNode context.getExportedSVModel().setVersion(Integer.parseInt(modelVersion)); } - try { + if (Parameter.readBoolean(getFormat(), params, EXPORT_AS_CGM_PARAMETER, defaultValueConfig)) { + /* CGM export + This export consists in providing an updated SSH for the IGMs and an updated SV for the whole CGM + The new updated IGMs SSH shall supersede the original ones + The new updated CGM SV is dependend on the new updated IGMs SHH and on the original IGMs TP + */ + + // checkCgmConsistency(); + + Set updatedIgmSshIds = new HashSet<>(); + Set originalIgmTpIds = new HashSet<>(); + for (Network subnetwork : network.getSubnetworks()) { + // Retrieve the IGM original SSH and TP model + CgmesMetadataModels originalIgmModels = subnetwork.getExtension(CgmesMetadataModels.class); + Optional originalIgmSshModel = originalIgmModels != null ? + originalIgmModels.getModelForPart(CgmesSubset.STEADY_STATE_HYPOTHESIS) : + Optional.empty(); + Optional originalIgmTpModel = originalIgmModels != null ? + originalIgmModels.getModelForPart(CgmesSubset.TOPOLOGY) : + Optional.empty(); + originalIgmTpModel.ifPresent(m -> originalIgmTpIds.add(m.getId())); + + // Create a new IGM SSH model based on the original one + String igmMAS = originalIgmSshModel + .map(CgmesMetadataModel::getModelingAuthoritySet) + .orElseGet(() -> CgmesExportContext.DEFAULT_MODELING_AUTHORITY_SET_VALUE); + CgmesMetadataModel updatedIgmSshModel = new CgmesMetadataModel(CgmesSubset.STEADY_STATE_HYPOTHESIS, igmMAS); + context.getExportedSSHModel().getProfiles().forEach(updatedIgmSshModel::setProfile); + originalIgmSshModel.ifPresent(m -> updatedIgmSshModel.setDescription(m.getDescription())); + originalIgmSshModel.ifPresent(m -> updatedIgmSshModel.setVersion(m.getVersion() + 1)); + originalIgmSshModel.ifPresent(m -> updatedIgmSshModel.addSupersedes(m.getId())); + + // Export the IGM SSH using the updated model + Set countries = getCountries(subnetwork); + String igmName = countries.size() == 1 ? countries.iterator().next() : subnetwork.getId(); + String igmFileNameSsh = baseName + "_" + igmName + "_SSH.xml"; + context.addIidmMappings(subnetwork); + subsetExport(subnetwork, CgmesSubset.STEADY_STATE_HYPOTHESIS, igmFileNameSsh, ds, context, Optional.of(updatedIgmSshModel)); + updatedIgmSshIds.add(updatedIgmSshModel.getId()); + } + + // Check for an existing CGM SV model + CgmesMetadataModels originalCgmModels = network.getExtension(CgmesMetadataModels.class); + Optional originalCgmSvModel = originalCgmModels != null ? + originalCgmModels.getModelForPart(CgmesSubset.STATE_VARIABLES) : + Optional.empty(); + + // Create a new CGM SV model based on the original one + CgmesMetadataModel updatedCgmSvModel = new CgmesMetadataModel(CgmesSubset.STATE_VARIABLES, masUri); + context.getExportedSVModel().getProfiles().forEach(updatedCgmSvModel::setProfile); + updatedCgmSvModel.addDependentOn(updatedIgmSshIds); + updatedCgmSvModel.addDependentOn(originalIgmTpIds); + originalCgmSvModel.ifPresent(m -> updatedCgmSvModel.setDescription(m.getDescription())); + originalCgmSvModel.ifPresent(m -> updatedCgmSvModel.setVersion(m.getVersion() + 1)); + + // Export the CGM SV using the new model + subsetExport(network, CgmesSubset.STATE_VARIABLES, filenameSv, ds, context, Optional.of(updatedCgmSvModel)); + } else { + // IGM export + context.updateDependenciesIGM(); + List profiles = Parameter.readStringList(getFormat(), params, PROFILES_PARAMETER, defaultValueConfig); - checkConsistency(profiles, network, context); + checkIgmConsistency(profiles, network, context); if (profiles.contains("EQ")) { - try (OutputStream out = new BufferedOutputStream(ds.newOutputStream(filenameEq, false))) { - XMLStreamWriter writer = XmlUtil.initializeWriter(true, INDENT, out); - EquipmentExport.write(network, writer, context); - } + subsetExport(network, CgmesSubset.EQUIPMENT, filenameEq, ds, context, Optional.empty()); } else { addSubsetIdentifiers(network, "EQ", context.getExportedEQModel()); context.getExportedEQModel().setId(context.getNamingStrategy().getCgmesId(network)); } if (profiles.contains("TP")) { - try (OutputStream out = new BufferedOutputStream(ds.newOutputStream(filenameTp, false))) { - XMLStreamWriter writer = XmlUtil.initializeWriter(true, INDENT, out); - TopologyExport.write(network, writer, context); - } + subsetExport(network, CgmesSubset.TOPOLOGY, filenameTp, ds, context, Optional.empty()); } else { addSubsetIdentifiers(network, "TP", context.getExportedTPModel()); } if (profiles.contains("SSH")) { - try (OutputStream out = new BufferedOutputStream(ds.newOutputStream(filenameSsh, false))) { - XMLStreamWriter writer = XmlUtil.initializeWriter(true, INDENT, out); - SteadyStateHypothesisExport.write(network, writer, context); - } + subsetExport(network, CgmesSubset.STEADY_STATE_HYPOTHESIS, filenameSsh, ds, context, Optional.empty()); } else { addSubsetIdentifiers(network, "SSH", context.getExportedSSHModel()); } if (profiles.contains("SV")) { - try (OutputStream out = new BufferedOutputStream(ds.newOutputStream(filenameSv, false))) { - XMLStreamWriter writer = XmlUtil.initializeWriter(true, INDENT, out); - StateVariablesExport.write(network, writer, context); - } + subsetExport(network, CgmesSubset.STATE_VARIABLES, filenameSv, ds, context, Optional.empty()); } context.getNamingStrategy().debug(baseName, ds); + } + } + + /** + * Retrieve all the countries present in a network. + * @param network the network for which the countries are being looked for + * @return a Set of countries present in the network + */ + private static Set getCountries(Network network) { + return network.getSubstationStream() + .map(Substation::getCountry) + .flatMap(Optional::stream) + .map(Enum::name) + .collect(Collectors.toUnmodifiableSet()); + } + + /** + * Export a CGMES subset of a network. + * @param network the network whose subset is to be exported + * @param subset the CGMES subset to export (accepted values are: EQ/TP/SSH/SV) + * @param fileName the name of the exported file + * @param dataSource the data source used for the export + * @param context the context used for the export + * @param model if provided, the model information to use + */ + private void subsetExport(Network network, CgmesSubset subset, String fileName, DataSource dataSource, CgmesExportContext context, Optional model) { + try (OutputStream out = new BufferedOutputStream(dataSource.newOutputStream(fileName, false))) { + XMLStreamWriter writer = XmlUtil.initializeWriter(true, INDENT, out); + switch (subset) { + case EQUIPMENT: + EquipmentExport.write(network, writer, context); + break; + case TOPOLOGY: + TopologyExport.write(network, writer, context); + break; + case STEADY_STATE_HYPOTHESIS: + SteadyStateHypothesisExport.write(network, writer, context, model); + break; + case STATE_VARIABLES: + StateVariablesExport.write(network, writer, context, model); + break; + default: + throw new IllegalArgumentException("Invalid subset, one of the following value is expected: EQ/TP/SSH/SV."); + } } catch (IOException e) { throw new UncheckedIOException(e); } catch (XMLStreamException e) { @@ -183,6 +273,14 @@ public void export(Network network, Properties params, DataSource ds, ReportNode } } + private static void checkCgmConsistency() { + /* TODO + Verify that each of the subnetwork has a CgmesMetadataModel of part SSH and TP + This is necessary in order to correctly build the references (dependentOn and supersedes) + to the IGM SSH and TP export files + */ + } + private String getBoundaryId(String profile, Network network, Properties params, Parameter parameter, ReferenceDataProvider referenceDataProvider) { if (network.hasProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + profile + "_BD_ID")) { return network.getProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + profile + "_BD_ID"); @@ -206,7 +304,7 @@ private static void addSubsetIdentifiers(Network network, String profile, CgmesM .toList()); } - private static void checkConsistency(List profiles, Network network, CgmesExportContext context) { + private static void checkIgmConsistency(List profiles, Network network, CgmesExportContext context) { boolean networkIsNodeBreaker = network.getVoltageLevelStream() .map(VoltageLevel::getTopologyKind) .anyMatch(tk -> tk == TopologyKind.NODE_BREAKER); @@ -247,6 +345,7 @@ public String getFormat() { public static final String EXPORT_POWER_FLOWS_FOR_SWITCHES = "iidm.export.cgmes.export-power-flows-for-switches"; public static final String NAMING_STRATEGY = "iidm.export.cgmes.naming-strategy"; public static final String PROFILES = "iidm.export.cgmes.profiles"; + public static final String EXPORT_AS_CGM = "iidm.export.cgmes.export_as_cgm"; public static final String MODELING_AUTHORITY_SET = "iidm.export.cgmes.modeling-authority-set"; public static final String MODEL_DESCRIPTION = "iidm.export.cgmes.model-description"; public static final String EXPORT_TRANSFORMERS_WITH_HIGHEST_VOLTAGE_AT_END1 = "iidm.export.cgmes.export-transformers-with-highest-voltage-at-end1"; @@ -297,6 +396,11 @@ public String getFormat() { "Profiles to export", List.of("EQ", "TP", "SSH", "SV"), List.of("EQ", "TP", "SSH", "SV")); + private static final Parameter EXPORT_AS_CGM_PARAMETER = new Parameter( + EXPORT_AS_CGM, + ParameterType.BOOLEAN, + "True for a CGM export, False for an IGM export", + CgmesExportContext.EXPORT_AS_CGM_VALUE); private static final Parameter BOUNDARY_EQ_ID_PARAMETER = new Parameter( BOUNDARY_EQ_ID, ParameterType.STRING, @@ -375,6 +479,7 @@ public String getFormat() { EXPORT_POWER_FLOWS_FOR_SWITCHES_PARAMETER, NAMING_STRATEGY_PARAMETER, PROFILES_PARAMETER, + EXPORT_AS_CGM_PARAMETER, BOUNDARY_EQ_ID_PARAMETER, BOUNDARY_TP_ID_PARAMETER, MODELING_AUTHORITY_SET_PARAMETER, diff --git a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/CgmesExportContext.java b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/CgmesExportContext.java index 8614389a2b3..8276ce0903d 100644 --- a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/CgmesExportContext.java +++ b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/CgmesExportContext.java @@ -68,6 +68,7 @@ public class CgmesExportContext { private NamingStrategy namingStrategy = new NamingStrategy.Identity(); + public static final boolean EXPORT_AS_CGM_VALUE = false; public static final boolean EXPORT_BOUNDARY_POWER_FLOWS_DEFAULT_VALUE = true; public static final boolean EXPORT_POWER_FLOWS_FOR_SWITCHES_DEFAULT_VALUE = true; public static final boolean EXPORT_TRANSFORMERS_WITH_HIGHEST_VOLTAGE_AT_END1_DEFAULT_VALUE = false; @@ -99,34 +100,7 @@ public class CgmesExportContext { private final Map topologicalNodes = new HashMap<>(); private final ReferenceDataProvider referenceDataProvider; - public void updateDependencies(Network network) { - boolean isCGM = network.getSubnetworks().size() > 1; - if (isCGM) { - updateDependenciesCGMSolution(network); - } else { - updateDependenciesIGM(); - } - } - - private void updateDependenciesCGMSolution(Network network) { - // FIXME(Luma) work in progress - // First step: we only know how to update dependencies of CGM SV solution from IGM (Subnetwork) original TP models - Set igmOriginalTpModels = network.getSubnetworks().stream() - .map(sn -> sn.getExtension(CgmesMetadataModels.class)) - .filter(Objects::nonNull) - .map(models -> ((CgmesMetadataModels) models).getModelForPart(CgmesSubset.TOPOLOGY)) - .filter(Optional::isPresent) - .map(Optional::get) - .map(CgmesMetadataModel::getId) - .collect(Collectors.toSet()); - Set igmExportedSshModels = Collections.emptySet(); - getExportedSVModel() - .clearDependencies() - .addDependentOn(igmOriginalTpModels) - .addDependentOn(igmExportedSshModels); - } - - private void updateDependenciesIGM() { + public void updateDependenciesIGM() { // Update dependencies in a way that: // [EQ.dependentOn EQ_BD] // SV.dependentOn TP, SSH[, TP_BD] diff --git a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/CgmesExportUtil.java b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/CgmesExportUtil.java index 2180383c64d..7a9967b7d14 100644 --- a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/CgmesExportUtil.java +++ b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/CgmesExportUtil.java @@ -115,23 +115,23 @@ public static void writeRdfRoot(String cimNamespace, String euPrefix, String euN } public static void writeModelDescription(Network network, CgmesSubset subset, XMLStreamWriter writer, CgmesMetadataModel modelDescription, CgmesExportContext context) throws XMLStreamException { - // The ref to build a unique model id must contain: - // the network, the subset (EQ, SSH, SV, ...), the time of the scenario, the version, the business process and the FULL_MODEL part - // If we use name-based UUIDs this ensures that the UUID for the model will be specific enough - CgmesObjectReference[] modelRef = { - refTyped(network), - ref(subset), - ref(DATE_TIME_FORMATTER.format(context.getScenarioTime())), - ref(format(modelDescription.getVersion())), - ref(context.getBusinessProcess()), - Part.FULL_MODEL}; - String modelId = "urn:uuid:" + context.getNamingStrategy().getCgmesId(modelRef); - modelDescription.setId(modelId); - context.updateDependencies(network); - + if (modelDescription.getId() == null || modelDescription.getId().isEmpty()) { + // The ref to build a unique model id must contain: + // the network, the subset (EQ, SSH, SV, ...), the time of the scenario, the version, the business process and the FULL_MODEL part + // If we use name-based UUIDs this ensures that the UUID for the model will be specific enough + CgmesObjectReference[] modelRef = { + refTyped(network), + ref(subset), + ref(DATE_TIME_FORMATTER.format(context.getScenarioTime())), + ref(String.valueOf(modelDescription.getVersion())), + ref(context.getBusinessProcess()), + Part.FULL_MODEL}; + String modelId = "urn:uuid:" + context.getNamingStrategy().getCgmesId(modelRef); + modelDescription.setId(modelId); + } writer.writeStartElement(MD_NAMESPACE, "FullModel"); - writer.writeAttribute(RDF_NAMESPACE, CgmesNames.ABOUT, modelId); - context.getReportNode().newReportNode().withMessageTemplate("CgmesId", modelId).add(); + writer.writeAttribute(RDF_NAMESPACE, CgmesNames.ABOUT, modelDescription.getId()); + context.getReportNode().newReportNode().withMessageTemplate("CgmesId", modelDescription.getId()).add(); writer.writeStartElement(MD_NAMESPACE, CgmesNames.SCENARIO_TIME); writer.writeCharacters(DATE_TIME_FORMATTER.format(context.getScenarioTime())); writer.writeEndElement(); diff --git a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/StateVariablesExport.java b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/StateVariablesExport.java index d0b0dc1242e..4b745544f4b 100644 --- a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/StateVariablesExport.java +++ b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/StateVariablesExport.java @@ -9,6 +9,7 @@ import com.powsybl.cgmes.conversion.Conversion; import com.powsybl.cgmes.extensions.CgmesTapChanger; import com.powsybl.cgmes.extensions.CgmesTapChangers; +import com.powsybl.cgmes.model.CgmesMetadataModel; import com.powsybl.cgmes.model.CgmesNames; import com.powsybl.cgmes.model.CgmesSubset; import com.powsybl.commons.PowsyblException; @@ -46,12 +47,16 @@ public static void write(Network network, XMLStreamWriter writer) { } public static void write(Network network, XMLStreamWriter writer, CgmesExportContext context) { + write(network, writer, context, Optional.empty()); + } + + public static void write(Network network, XMLStreamWriter writer, CgmesExportContext context, Optional model) { try { String cimNamespace = context.getCim().getNamespace(); CgmesExportUtil.writeRdfRoot(cimNamespace, context.getCim().getEuPrefix(), context.getCim().getEuNamespace(), writer); if (context.getCimVersion() >= 16) { - CgmesExportUtil.writeModelDescription(network, CgmesSubset.STATE_VARIABLES, writer, context.getExportedSVModel(), context); + CgmesExportUtil.writeModelDescription(network, CgmesSubset.STATE_VARIABLES, writer, model.orElseGet(context::getExportedSVModel), context); writeTopologicalIslands(network, context, writer); // Note: unmapped topological nodes (node breaker) & boundary topological nodes are not written in topological islands } diff --git a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/SteadyStateHypothesisExport.java b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/SteadyStateHypothesisExport.java index 0607c225e44..0e2b77114a1 100644 --- a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/SteadyStateHypothesisExport.java +++ b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/SteadyStateHypothesisExport.java @@ -11,6 +11,7 @@ import com.powsybl.cgmes.extensions.CgmesControlAreas; import com.powsybl.cgmes.extensions.CgmesTapChanger; import com.powsybl.cgmes.extensions.CgmesTapChangers; +import com.powsybl.cgmes.model.CgmesMetadataModel; import com.powsybl.cgmes.model.CgmesNames; import com.powsybl.cgmes.model.CgmesSubset; import com.powsybl.commons.PowsyblException; @@ -45,6 +46,10 @@ private SteadyStateHypothesisExport() { } public static void write(Network network, XMLStreamWriter writer, CgmesExportContext context) { + write(network, writer, context, Optional.empty()); + } + + public static void write(Network network, XMLStreamWriter writer, CgmesExportContext context, Optional model) { final Map> regulatingControlViews = new HashMap<>(); String cimNamespace = context.getCim().getNamespace(); @@ -52,7 +57,7 @@ public static void write(Network network, XMLStreamWriter writer, CgmesExportCon CgmesExportUtil.writeRdfRoot(cimNamespace, context.getCim().getEuPrefix(), context.getCim().getEuNamespace(), writer); if (context.getCimVersion() >= 16) { - CgmesExportUtil.writeModelDescription(network, CgmesSubset.STEADY_STATE_HYPOTHESIS, writer, context.getExportedSSHModel(), context); + CgmesExportUtil.writeModelDescription(network, CgmesSubset.STEADY_STATE_HYPOTHESIS, writer, model.orElseGet(context::getExportedSSHModel), context); } writeLoads(network, cimNamespace, writer, context); diff --git a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesExportContextTest.java b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesExportContextTest.java index 58638e13777..f2f1e6fd578 100644 --- a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesExportContextTest.java +++ b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesExportContextTest.java @@ -33,7 +33,7 @@ class CgmesExportContextTest { void testExporter() { var exporter = new CgmesExport(); assertEquals("ENTSO-E CGMES version 2.4.15", exporter.getComment()); - assertEquals(19, exporter.getParameters().size()); + assertEquals(20, exporter.getParameters().size()); } @Test diff --git a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CommonGridModelExportTest.java b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CommonGridModelExportTest.java index 8291c7cbea5..be5e125d822 100644 --- a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CommonGridModelExportTest.java +++ b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CommonGridModelExportTest.java @@ -21,18 +21,22 @@ import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; /** * @author Luma Zamarreño {@literal } */ class CommonGridModelExportTest extends AbstractSerDeTest { + private static final Pattern REGEX_ID = Pattern.compile("FullModel rdf:about=\"(.*?)\""); + private static final Pattern REGEX_VERSION = Pattern.compile("Model.version>(.*?)<"); + private static final Pattern REGEX_DEPENDENT_ON = Pattern.compile("Model.DependentOn rdf:resource=\"(.*?)\""); + private static final Pattern REGEX_SUPERSEDES = Pattern.compile("Model.Supersedes rdf:resource=\"(.*?)\""); + private static final Pattern REGEX_MAS = Pattern.compile("Model.modelingAuthoritySet>(.*?)<"); + @Test - void testAssembled() throws IOException { + void testCgmExport() throws IOException { /* Summary from CGM Building Process Implementation Guide: @@ -53,70 +57,81 @@ void testAssembled() throws IOException { This means that the CGM SV file must contain a md:Model.DependentOn reference to each IGM’s original TP. */ - // Obtain an assembled set of IGMs - // Each IGM is a Subnetwork in the CGM Network + // Read the network (CGM) with its subnetworks (IGMs) Network network = Network.read(CgmesConformity1Catalog.microGridBaseCaseAssembled().dataSource()); assertEquals(2, network.getSubnetworks().size()); - debugCGM(network); - - Set expectedTPs = network.getSubnetworks().stream().map( - n -> n.getExtension(CgmesMetadataModels.class) - .getModelForPart(CgmesSubset.TOPOLOGY) - .map(CgmesMetadataModel::getId) - .orElseThrow()) - .collect(Collectors.toSet()); - assertEquals( - Set.of("urn:uuid:5d32d257-1646-4906-a1f6-4d7ce3f91569", "urn:uuid:f2f43818-09c8-4252-9611-7af80c398d20"), - expectedTPs); - - // Export the SV for the CGM + + // Expected values + String beMas = "http://elia.be/CGMES/2.4.15"; + String nlMas = "http://tennet.nl/CGMES/2.4.15"; + String originalBeSshId = "urn:uuid:52b712d1-f3b0-4a59-9191-79f2fb1e4c4e"; + String originalNlSshId = "urn:uuid:66085ffe-dddf-4fc8-805c-2c7aa2097b90"; + String originalBeTpId = "urn:uuid:f2f43818-09c8-4252-9611-7af80c398d20"; + String originalNlTpId = "urn:uuid:5d32d257-1646-4906-a1f6-4d7ce3f91569"; + int originalVersion = 2; + + // Check the original IGMs SSH and TP content + Network beNetwork = network.getSubnetwork("urn:uuid:d400c631-75a0-4c30-8aed-832b0d282e73"); + Network nlNetwork = network.getSubnetwork("urn:uuid:77b55f87-fc1e-4046-9599-6c6b4f991a86"); + + CgmesMetadataModel originalBeSshModel = getSubsetModel(beNetwork, CgmesSubset.STEADY_STATE_HYPOTHESIS); + assertEquals(originalBeSshId, originalBeSshModel.getId()); + assertEquals(originalVersion, originalBeSshModel.getVersion()); + assertEquals(beMas, originalBeSshModel.getModelingAuthoritySet()); + + CgmesMetadataModel originalNlSshModel = getSubsetModel(nlNetwork, CgmesSubset.STEADY_STATE_HYPOTHESIS); + assertEquals(originalNlSshId, originalNlSshModel.getId()); + assertEquals(originalVersion, originalNlSshModel.getVersion()); + assertEquals(nlMas, originalNlSshModel.getModelingAuthoritySet()); + + CgmesMetadataModel originalBeTpModel = getSubsetModel(beNetwork, CgmesSubset.TOPOLOGY); + assertEquals(originalBeTpId, originalBeTpModel.getId()); + assertEquals(originalVersion, originalBeTpModel.getVersion()); + assertEquals(beMas, originalBeTpModel.getModelingAuthoritySet()); + + CgmesMetadataModel originalNlTpModel = getSubsetModel(nlNetwork, CgmesSubset.TOPOLOGY); + assertEquals(originalNlTpId, originalNlTpModel.getId()); + assertEquals(originalVersion, originalNlTpModel.getVersion()); + assertEquals(nlMas, originalNlTpModel.getModelingAuthoritySet()); + + // Perform a CGM export and read the exported files Properties exportParams = new Properties(); - exportParams.put(CgmesExport.PROFILES, "SV"); - String basename = network.getNameOrId(); + exportParams.put(CgmesExport.EXPORT_AS_CGM, true); + String basename = "test-assembled"; + // network.write("CGMES", exportParams, Path.of(basename)); // TODO remove network.write("CGMES", exportParams, tmpDir.resolve(basename)); - Set svDependentOns = dependentOns(read(basename, "SV")); - debugSvDependentOns(svDependentOns); - - // All IGM TPs must be present in the SV dependentOns - assertTrue(svDependentOns.containsAll(expectedTPs)); + String updatedBeSshXml = Files.readString(tmpDir.resolve(basename + "_BE_SSH.xml")); + String updatedNlSshXml = Files.readString(tmpDir.resolve(basename + "_NL_SSH.xml")); + String updatedCgmSvXml = Files.readString(tmpDir.resolve(basename + "_SV.xml")); + + // Each updated IGM SSH should supersede the original one + assertEquals(originalBeSshId, getOccurrences(updatedBeSshXml, REGEX_SUPERSEDES).iterator().next()); + assertEquals(originalNlSshId, getOccurrences(updatedNlSshXml, REGEX_SUPERSEDES).iterator().next()); + + // The updated CGM SV should depend on the updated IGMs SSH and the original IGMs TP + String updatedBeSshId = getOccurrences(updatedBeSshXml, REGEX_ID).iterator().next(); + String updatedNlSshId = getOccurrences(updatedNlSshXml, REGEX_ID).iterator().next(); + Set expectedDependencies = Set.of(updatedBeSshId, updatedNlSshId, originalBeTpId, originalNlTpId); + assertEquals(expectedDependencies, getOccurrences(updatedCgmSvXml, REGEX_DEPENDENT_ON)); + + // Check MAS and version + assertEquals(beMas, getOccurrences(updatedBeSshXml, REGEX_MAS).iterator().next()); + assertEquals(nlMas, getOccurrences(updatedNlSshXml, REGEX_MAS).iterator().next()); + assertEquals(String.valueOf(originalVersion + 1), getOccurrences(updatedBeSshXml, REGEX_VERSION).iterator().next()); + assertEquals(String.valueOf(originalVersion + 1), getOccurrences(updatedNlSshXml, REGEX_VERSION).iterator().next()); + } - // FIXME(Luma) work in progress - // The CGM SV should depend on updated IGM SSHs - // As a first step, just check that the CGM dependentOns should contain more items, not only IGM TPs - assertTrue(svDependentOns.size() > expectedTPs.size()); + private CgmesMetadataModel getSubsetModel(Network network, CgmesSubset subset) { + return network.getExtension(CgmesMetadataModels.class).getModelForPart(subset).orElseThrow(); } - private static final Pattern REGEX_DEPENDENT_ON = Pattern.compile("Model.DependentOn rdf:resource=\"(.*?)\""); - - Set dependentOns(String xml) { + private Set getOccurrences(String xml, Pattern pattern) { Set matches = new HashSet<>(); - Matcher matcher = REGEX_DEPENDENT_ON.matcher(xml); + Matcher matcher = pattern.matcher(xml); while (matcher.find()) { matches.add(matcher.group(1)); } return matches; } - private String read(String basename, String profile) throws IOException { - String instanceFile = String.format("%s_%s.xml", basename, profile); - return Files.readString(tmpDir.resolve(instanceFile)); - } - - private static void debugCGM(Network network) { - System.out.println("IGM subnetworks:"); - network.getSubnetworks().forEach(n -> { - System.out.printf(" subnetwork : %s%n", n.getSubstations().iterator().next().getCountry().orElseThrow()); - System.out.printf(" SSH : %s%n", - n.getExtension(CgmesMetadataModels.class).getModelForPart(CgmesSubset.STEADY_STATE_HYPOTHESIS).map(CgmesMetadataModel::getId).orElse("uknown")); - System.out.printf(" TP : %s%n", - n.getExtension(CgmesMetadataModels.class).getModelForPart(CgmesSubset.TOPOLOGY).map(CgmesMetadataModel::getId).orElse("uknown")); - }); - } - - private void debugSvDependentOns(Set svDependentOns) { - System.out.println(); - System.out.println("SV dependentOns:"); - System.out.println(Arrays.toString(svDependentOns.toArray())); - } - }