From bb6c599077894fb070a1136210212193eac75415 Mon Sep 17 00:00:00 2001 From: Romain Courtier Date: Tue, 18 Feb 2025 14:48:01 +0100 Subject: [PATCH 1/7] Add a parameter to force a NODE_BREAKER or BUS_BRANCH export Signed-off-by: Romain Courtier --- .../powsybl/cgmes/conversion/CgmesExport.java | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 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 10812f68794..f09e13e9a3b 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 @@ -13,6 +13,7 @@ import com.powsybl.cgmes.conversion.naming.NamingStrategy; import com.powsybl.cgmes.conversion.naming.NamingStrategyFactory; import com.powsybl.cgmes.extensions.CgmesMetadataModels; +import com.powsybl.cgmes.extensions.CgmesTopologyKind; import com.powsybl.cgmes.model.*; import com.powsybl.commons.config.PlatformConfig; import com.powsybl.commons.datasource.DataSource; @@ -164,6 +165,7 @@ private CgmesExportContext createContext(Network network, Properties parameters, NamingStrategy namingStrategy = NamingStrategyFactory.create(namingStrategyImpl, uuidNamespace); CgmesExportContext context = new CgmesExportContext(network, referenceDataProvider, namingStrategy); addParametersToContext(context, parameters, reportNode, referenceDataProvider); + context.addIidmMappings(network); return context; } @@ -538,18 +540,24 @@ private void addParametersToContext(CgmesExportContext context, Properties param .setReportNode(reportNode) .setUpdateDependencies(Parameter.readBoolean(getFormat(), params, UPDATE_DEPENDENCIES_PARAMETER, defaultValueConfig)); - // If sourcing actor data has been found and the modeling authority set has not been specified explicitly, set it - PropertyBag sourcingActor = referenceDataProvider.getSourcingActor(); - if (sourcingActor.containsKey("masUri") && context.getModelingAuthoritySet() == null) { - context.setModelingAuthoritySet(sourcingActor.get("masUri")); - } - // Set CIM version String cimVersion = Parameter.readString(getFormat(), params, CIM_VERSION_PARAMETER, defaultValueConfig); if (cimVersion != null) { context.setCimVersion(Integer.parseInt(cimVersion)); } + // Set the topology kind + String topologyKind = Parameter.readString(getFormat(), params, TOPOLOGY_KIND_PARAMETER, defaultValueConfig); + if (topologyKind != null) { + context.setTopologyKind(Enum.valueOf(CgmesTopologyKind.class, topologyKind)); + } + + // If sourcing actor data has been found and the modeling authority set has not been specified explicitly, set it + PropertyBag sourcingActor = referenceDataProvider.getSourcingActor(); + if (sourcingActor.containsKey("masUri") && context.getModelingAuthoritySet() == null) { + context.setModelingAuthoritySet(sourcingActor.get("masUri")); + } + // Set boundaries String boundaryEqId = getBoundaryId(CgmesSubset.EQUIPMENT_BOUNDARY, params, BOUNDARY_EQ_ID_PARAMETER, referenceDataProvider); if (boundaryEqId != null && context.getBoundaryEqId() == null) { @@ -640,6 +648,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 TOPOLOGY_KIND = "iidm.export.cgmes.topology-kind"; public static final String CGM_EXPORT = "iidm.export.cgmes.cgm_export"; public static final String MODELING_AUTHORITY_SET = "iidm.export.cgmes.modeling-authority-set"; public static final String MODEL_DESCRIPTION = "iidm.export.cgmes.model-description"; @@ -694,6 +703,12 @@ public String getFormat() { "Profiles to export", List.of("EQ", "TP", "SSH", "SV"), List.of("EQ", "TP", "SSH", "SV")); + private static final Parameter TOPOLOGY_KIND_PARAMETER = new Parameter( + TOPOLOGY_KIND, + ParameterType.STRING, + "The topology kind of the export", + null, + List.of(CgmesTopologyKind.NODE_BREAKER.name(), CgmesTopologyKind.BUS_BRANCH.name())); private static final Parameter CGM_EXPORT_PARAMETER = new Parameter( CGM_EXPORT, ParameterType.BOOLEAN, From 69e4ada682d847da6756512a1c7a216529cd1dcf Mon Sep 17 00:00:00 2001 From: Romain Courtier Date: Tue, 18 Feb 2025 15:55:53 +0100 Subject: [PATCH 2/7] Change network topology kind computation Signed-off-by: Romain Courtier --- .../conversion/export/CgmesExportContext.java | 21 +++++++++++++++---- .../test/export/CgmesExportContextTest.java | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) 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 485e529eeea..e9545756bc9 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 @@ -55,7 +55,7 @@ public class CgmesExportContext { private static final String BOUNDARY_TP_ID_PROPERTY = Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + "TP_BD_ID"; private CgmesNamespace.Cim cim = CgmesNamespace.CIM_16; - private CgmesTopologyKind topologyKind = CgmesTopologyKind.BUS_BRANCH; + private CgmesTopologyKind topologyKind = CgmesTopologyKind.NODE_BREAKER; private ZonedDateTime scenarioTime = ZonedDateTime.now(); private ReportNode reportNode = ReportNode.NO_OP; private String businessProcess = DEFAULT_BUSINESS_PROCESS; @@ -159,12 +159,25 @@ public ReferenceDataProvider getReferenceDataProvider() { } private CgmesTopologyKind networkTopologyKind(Network network) { - for (VoltageLevel vl : network.getVoltageLevels()) { - if (vl.getTopologyKind().equals(TopologyKind.NODE_BREAKER)) { + long nodeBreakerVoltageLevelsCount = network.getVoltageLevelStream() + .filter(vl -> vl.getTopologyKind() == TopologyKind.NODE_BREAKER) + .count(); + long busBreakerVoltageLevelsCount = network.getVoltageLevelStream() + .filter(vl -> vl.getTopologyKind() == TopologyKind.BUS_BREAKER) + .count(); + + if (nodeBreakerVoltageLevelsCount > 0 && busBreakerVoltageLevelsCount == 0) { + return CgmesTopologyKind.NODE_BREAKER; + } else if (nodeBreakerVoltageLevelsCount == 0 && busBreakerVoltageLevelsCount > 0) { + return CgmesTopologyKind.BUS_BRANCH; + } else { + // For mixed-topology network, the topology kind is node/breaker for CIM 100 and bus/branch for CIM 16 + if (getCimVersion() < 100) { + return CgmesTopologyKind.BUS_BRANCH; + } else { return CgmesTopologyKind.NODE_BREAKER; } } - return CgmesTopologyKind.BUS_BRANCH; } public void addIidmMappings(Network network) { 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 c58a3ee447c..608eab9ea21 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 @@ -62,7 +62,7 @@ void emptyConstructor() { CgmesExportContext context = new CgmesExportContext(); assertEquals(16, context.getCimVersion()); assertEquals(CgmesNamespace.CIM_16_NAMESPACE, context.getCim().getNamespace()); - assertEquals(CgmesTopologyKind.BUS_BRANCH, context.getTopologyKind()); + assertEquals(CgmesTopologyKind.NODE_BREAKER, context.getTopologyKind()); assertTrue(Duration.between(ZonedDateTime.now(), context.getScenarioTime()).toMinutes() < 1); assertTrue(context.exportBoundaryPowerFlows()); assertEquals("1D", context.getBusinessProcess()); From f91b1b9a32b72c13f47fc806019a8496fd9858ae Mon Sep 17 00:00:00 2001 From: Romain Courtier Date: Tue, 18 Feb 2025 16:46:34 +0100 Subject: [PATCH 3/7] Don't write connectivity elements in case of a BUS_BRANCH export Signed-off-by: Romain Courtier --- .../conversion/export/CgmesExportContext.java | 24 +++++--- .../conversion/export/CgmesExportUtil.java | 2 +- .../conversion/export/EquipmentExport.java | 59 ++++++++++--------- .../test/export/CgmesExportTest.java | 13 ++-- .../test/export/ExportToCimVersionTest.java | 25 -------- .../export/TopologyExportCornerCasesTest.java | 4 +- .../triplestore/CgmesModelTripleStore.java | 2 +- 7 files changed, 57 insertions(+), 72 deletions(-) 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 e9545756bc9..612bfc9c409 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 @@ -298,11 +298,13 @@ public CgmesExportContext setExportEquipment(boolean exportEquipment) { } public boolean isExportedEquipment(Identifiable c) { - // We ignore fictitious loads used to model CGMES SvInjection objects that represent calculation mismatches - // We also ignore fictitious switches used to model CGMES disconnected Terminals - boolean ignored = c.isFictitious() && - (c instanceof Load - || c instanceof Switch && "true".equals(c.getProperty(Conversion.PROPERTY_IS_CREATED_FOR_DISCONNECTED_TERMINAL))); + boolean ignored = false; + if (c instanceof Load load) { + ignored = load.isFictitious(); + } else if (c instanceof Switch sw) { + ignored = sw.isFictitious() && "true".equals(sw.getProperty(Conversion.PROPERTY_IS_CREATED_FOR_DISCONNECTED_TERMINAL)) + || isBusBranchExport() && !sw.isRetained(); + } return !ignored; } @@ -686,10 +688,6 @@ public BaseVoltageMapping.BaseVoltageSource getBaseVoltageByNominalVoltage(doubl return baseVoltageByNominalVoltageMapping.get(nominalV); } - public boolean writeConnectivityNodes() { - return getCimVersion() == 100 || topologyKind == CgmesTopologyKind.NODE_BREAKER; - } - public Collection getRegionsIds() { return Collections.unmodifiableSet(regionsIdsByRegionName.values()); } @@ -792,6 +790,14 @@ public CgmesExportContext setProfiles(List profiles) { return this; } + public boolean isCim16BusBranchExport() { + return getCimVersion() == 16 && isBusBranchExport(); + } + + public boolean isBusBranchExport() { + return getTopologyKind() == CgmesTopologyKind.BUS_BRANCH; + } + public String getBaseName() { return baseName; } 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 059d0d8a07a..1536446d466 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 @@ -174,7 +174,7 @@ public static void writeModelDescription(Network network, CgmesSubset subset, XM writer.writeCharacters(profile); writer.writeEndElement(); } - if (subset == CgmesSubset.EQUIPMENT && context.getTopologyKind().equals(CgmesTopologyKind.NODE_BREAKER) && context.getCimVersion() < 100) { + if (subset == CgmesSubset.EQUIPMENT && context.getTopologyKind() == CgmesTopologyKind.NODE_BREAKER && context.getCimVersion() < 100) { // From CGMES 3 EquipmentOperation is not required to write operational limits, connectivity nodes writer.writeStartElement(MD_NAMESPACE, CgmesNames.PROFILE); writer.writeCharacters(context.getCim().getProfileUri("EQ_OP")); diff --git a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/EquipmentExport.java b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/EquipmentExport.java index 85a93388784..78a41ee06e9 100644 --- a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/EquipmentExport.java +++ b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/EquipmentExport.java @@ -78,9 +78,7 @@ public static void write(Network network, XMLStreamWriter writer, CgmesExportCon Set exportedLimitTypes = new HashSet<>(); LoadGroups loadGroups = new LoadGroups(); - if (context.writeConnectivityNodes()) { - writeConnectivityNodes(network, mapNodeKey2NodeId, cimNamespace, writer, context); - } + writeConnectivityNodes(network, mapNodeKey2NodeId, cimNamespace, writer, context); writeTerminals(network, mapTerminal2Id, mapNodeKey2NodeId, cimNamespace, writer, context); writeSwitches(network, cimNamespace, writer, context); @@ -109,22 +107,28 @@ public static void write(Network network, XMLStreamWriter writer, CgmesExportCon } private static void writeConnectivityNodes(Network network, Map mapNodeKey2NodeId, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { - for (VoltageLevel vl : network.getVoltageLevels()) { - String cgmesVlId = context.getNamingStrategy().getCgmesId(vl); - if (vl.getTopologyKind().equals(TopologyKind.NODE_BREAKER)) { - writeNodes(vl, cgmesVlId, new VoltageLevelAdjacency(vl, context), mapNodeKey2NodeId, cimNamespace, writer, context); - } else { - writeBuses(vl, cgmesVlId, mapNodeKey2NodeId, cimNamespace, writer, context); + // ConnectivityNodes are: + // - always exported from nodes and buses in case of a node-breaker or CIM 100 export + // - exported from buses in case of a CIM 100 bus-branch export + // - never exported in case of a CIM 16 bus-branch export + if (!context.isCim16BusBranchExport()) { + for (VoltageLevel vl : network.getVoltageLevels()) { + String cgmesVlId = context.getNamingStrategy().getCgmesId(vl); + if (vl.getTopologyKind() == TopologyKind.NODE_BREAKER && !context.isBusBranchExport()) { + writeNodes(vl, cgmesVlId, new VoltageLevelAdjacency(vl, context), mapNodeKey2NodeId, cimNamespace, writer, context); + writeSwitchesConnectivity(vl, cgmesVlId, mapNodeKey2NodeId, cimNamespace, writer, context); + writeBusbarSectionsConnectivity(vl, mapNodeKey2NodeId, cimNamespace, writer, context); + } else { + writeBuses(vl, cgmesVlId, mapNodeKey2NodeId, cimNamespace, writer, context); + } } - writeSwitchesConnectivity(vl, cgmesVlId, mapNodeKey2NodeId, cimNamespace, writer, context); } - writeBusbarSectionsConnectivity(network, mapNodeKey2NodeId, cimNamespace, writer, context); } private static void writeSwitchesConnectivity(VoltageLevel vl, String cgmesVlId, Map mapNodeKey2NodeId, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) { String[] nodeKeys = new String[2]; for (Switch sw : vl.getSwitches()) { - fillSwitchNodeKeys(vl, sw, nodeKeys); + fillSwitchNodeKeys(vl, sw, nodeKeys, context); // We have to go through all switches, even if they are not exported as equipment, // to be sure that all required mappings between IIDM node number and CGMES Connectivity Node are created writeSwitchConnectivity(nodeKeys[0], vl, cgmesVlId, mapNodeKey2NodeId, cimNamespace, writer, context); @@ -152,15 +156,14 @@ private static String buildNodeKey(Bus bus) { return bus.getId(); } - private static void writeBusbarSectionsConnectivity(Network network, Map mapNodeKey2NodeId, String cimNamespace, + private static void writeBusbarSectionsConnectivity(VoltageLevel vl, Map mapNodeKey2NodeId, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { - for (BusbarSection bus : network.getBusbarSections()) { - String connectivityNodeId = connectivityNodeId(mapNodeKey2NodeId, bus.getTerminal()); + for (BusbarSection bbs : vl.getNodeBreakerView().getBusbarSections()) { + String connectivityNodeId = connectivityNodeId(mapNodeKey2NodeId, bbs.getTerminal(), context); if (connectivityNodeId == null) { - VoltageLevel vl = bus.getTerminal().getVoltageLevel(); - String node = context.getNamingStrategy().getCgmesId(refTyped(bus), CONNECTIVITY_NODE); - ConnectivityNodeEq.write(node, bus.getNameOrId(), context.getNamingStrategy().getCgmesId(vl), cimNamespace, writer, context); - String key = buildNodeKey(vl, bus.getTerminal().getNodeBreakerView().getNode()); + String node = context.getNamingStrategy().getCgmesId(refTyped(bbs), CONNECTIVITY_NODE); + ConnectivityNodeEq.write(node, bbs.getNameOrId(), context.getNamingStrategy().getCgmesId(vl), cimNamespace, writer, context); + String key = buildNodeKey(vl, bbs.getTerminal().getNodeBreakerView().getNode()); mapNodeKey2NodeId.put(key, node); } } @@ -205,8 +208,8 @@ private static boolean hasDifferentTNsAtBothEnds(Switch sw) { return bus1 != bus2; } - private static void fillSwitchNodeKeys(VoltageLevel vl, Switch sw, String[] nodeKeys) { - if (vl.getTopologyKind().equals(TopologyKind.NODE_BREAKER)) { + private static void fillSwitchNodeKeys(VoltageLevel vl, Switch sw, String[] nodeKeys, CgmesExportContext context) { + if (vl.getTopologyKind().equals(TopologyKind.NODE_BREAKER) && !context.isBusBranchExport()) { nodeKeys[0] = buildNodeKey(vl, vl.getNodeBreakerView().getNode1(sw.getId())); nodeKeys[1] = buildNodeKey(vl, vl.getNodeBreakerView().getNode2(sw.getId())); } else { @@ -1045,7 +1048,7 @@ private static String writeDanglingLinesBaseVoltage(List danglingL private static String writeDanglingLinesConnectivity(List danglingLineList, String baseVoltageId, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { String connectivityNodeId = null; - if (context.writeConnectivityNodes()) { + if (!context.isCim16BusBranchExport()) { connectivityNodeId = writeDanglingLinesConnectivityNode(danglingLineList, baseVoltageId, cimNamespace, writer, context); } else { writeDanglingLinesFictitiousContainer(danglingLineList, baseVoltageId, cimNamespace, writer, context); @@ -1298,14 +1301,14 @@ private static void writeHvdcLines(Network network, Map mapTer HvdcConverterStation converter = line.getConverterStation1(); terminalId = context.getNamingStrategy().getCgmesId(refTyped(line), refTyped(converter), CONVERTER_STATION, ref(1)); - writeTerminal(converter.getTerminal(), mapTerminal2Id, terminalId, converter1Id, connectivityNodeId(mapNodeKey2NodeId, converter.getTerminal()), 1, cimNamespace, writer, context); + writeTerminal(converter.getTerminal(), mapTerminal2Id, terminalId, converter1Id, connectivityNodeId(mapNodeKey2NodeId, converter.getTerminal(), context), 1, cimNamespace, writer, context); String capabilityCurveId1 = writeVsCapabilityCurve(converter, cimNamespace, writer, context); String acdcConverterDcTerminal1 = converter.getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + AC_DC_CONVERTER_DC_TERMINAL).orElseThrow(PowsyblException::new); writeAcdcConverterDCTerminal(acdcConverterDcTerminal1, converter1Id, dcNode1, 2, cimNamespace, writer, context); converter = line.getConverterStation2(); terminalId = context.getNamingStrategy().getCgmesId(refTyped(line), refTyped(converter), CONVERTER_STATION, ref(2)); - writeTerminal(converter.getTerminal(), mapTerminal2Id, terminalId, converter2Id, connectivityNodeId(mapNodeKey2NodeId, converter.getTerminal()), 1, cimNamespace, writer, context); + writeTerminal(converter.getTerminal(), mapTerminal2Id, terminalId, converter2Id, connectivityNodeId(mapNodeKey2NodeId, converter.getTerminal(), context), 1, cimNamespace, writer, context); String capabilityCurveId2 = writeVsCapabilityCurve(converter, cimNamespace, writer, context); String acdcConverterDcTerminal2 = converter.getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + AC_DC_CONVERTER_DC_TERMINAL).orElseThrow(PowsyblException::new); writeAcdcConverterDCTerminal(acdcConverterDcTerminal2, converter2Id, dcNode2, 2, cimNamespace, writer, context); @@ -1448,7 +1451,7 @@ private static void writeTerminals(Network network, Map mapTer for (Switch sw : network.getSwitches()) { if (context.isExportedEquipment(sw)) { VoltageLevel vl = sw.getVoltageLevel(); - fillSwitchNodeKeys(vl, sw, switchNodesKeys); + fillSwitchNodeKeys(vl, sw, switchNodesKeys, context); String nodeId1 = mapNodeKey2NodeId.get(switchNodesKeys[0]); String terminalId1 = context.getNamingStrategy().getCgmesIdFromAlias(sw, Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.TERMINAL + 1); TerminalEq.write(terminalId1, context.getNamingStrategy().getCgmesId(sw), nodeId1, 1, cimNamespace, writer, context); @@ -1462,7 +1465,7 @@ private static void writeTerminals(Network network, Map mapTer private static void writeTerminal(Terminal t, Map mapTerminal2Id, Map mapNodeKey2NodeId, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) { String equipmentId = context.getNamingStrategy().getCgmesId(t.getConnectable()); - writeTerminal(t, mapTerminal2Id, CgmesExportUtil.getTerminalId(t, context), equipmentId, connectivityNodeId(mapNodeKey2NodeId, t), + writeTerminal(t, mapTerminal2Id, CgmesExportUtil.getTerminalId(t, context), equipmentId, connectivityNodeId(mapNodeKey2NodeId, t, context), CgmesExportUtil.getTerminalSequenceNumber(t), cimNamespace, writer, context); } @@ -1485,9 +1488,9 @@ private static String exportedTerminalId(Map mapTerminal2Id, T } } - private static String connectivityNodeId(Map mapNodeKey2NodeId, Terminal terminal) { + private static String connectivityNodeId(Map mapNodeKey2NodeId, Terminal terminal, CgmesExportContext context) { String key; - if (terminal.getVoltageLevel().getTopologyKind().equals(TopologyKind.NODE_BREAKER)) { + if (terminal.getVoltageLevel().getTopologyKind() == TopologyKind.NODE_BREAKER && !context.isBusBranchExport()) { key = buildNodeKey(terminal.getVoltageLevel(), terminal.getNodeBreakerView().getNode()); } else { key = buildNodeKey(terminal.getBusBreakerView().getConnectableBus()); diff --git a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesExportTest.java b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesExportTest.java index aaa2037e089..787a02d847d 100644 --- a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesExportTest.java +++ b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesExportTest.java @@ -9,7 +9,6 @@ import com.google.common.jimfs.Configuration; import com.google.common.jimfs.Jimfs; -import com.powsybl.cgmes.conformity.Cgmes3Catalog; import com.powsybl.cgmes.conformity.CgmesConformity1Catalog; import com.powsybl.cgmes.conformity.CgmesConformity1ModifiedCatalog; import com.powsybl.cgmes.conversion.CgmesExport; @@ -327,23 +326,25 @@ void testFromIidmDanglingLineNodeBreaker() throws IOException { // Before exporting, we have to define to which point // in the external boundary definition we want to associate this dangling line // For this test we chose the Conformity MicroGrid BaseCase - ResourceSet boundaries = Cgmes3Catalog.microGridBaseCaseBoundaries(); + ResourceSet boundaries = CgmesConformity1Catalog.microGridBaseCaseBoundaries(); String boundaryCN = "b675a570-cb6e-11e1-bcee-406c8f32ef58"; expected.setProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.CONNECTIVITY_NODE_BOUNDARY, boundaryCN); // We inform the identifier of the boundaries we depend on Properties exportParameters = new Properties(); exportParameters.put(CgmesExport.BOUNDARY_EQ_ID, "urn:uuid:536f9bf1-3f8f-a546-87e3-7af2272f29b7"); - exportParameters.put(CgmesExport.CIM_VERSION, "100"); + exportParameters.put(CgmesExport.TOPOLOGY_KIND, "NODE_BREAKER"); try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { Path tmpDir = Files.createDirectory(fs.getPath("/cgmes")); network.write("CGMES", exportParameters, tmpDir.resolve("tmp")); // To be able to import from the exported CGMES data we must add the external boundary definitions - // Because we work with node/breaker we only need the boundary EQ instance file - try (InputStream is = boundaries.newInputStream("20171002T0930Z_ENTSO-E_EQ_BD_2.xml")) { + try (InputStream is = boundaries.newInputStream("MicroGridTestConfiguration_EQ_BD.xml")) { Files.copy(is, tmpDir.resolve("tmp_EQ_BD.xml"), StandardCopyOption.REPLACE_EXISTING); } + try (InputStream is = boundaries.newInputStream("MicroGridTestConfiguration_TP_BD.xml")) { + Files.copy(is, tmpDir.resolve("tmp_TP_BD.xml"), StandardCopyOption.REPLACE_EXISTING); + } Network networkFromCgmes = Network.read(new GenericReadOnlyDataSource(tmpDir, "tmp"), importParams); DanglingLine actual = networkFromCgmes.getDanglingLine("DL"); @@ -367,7 +368,7 @@ void testFromIidmDanglingLineNodeBreakerNoBoundaries() throws IOException { try (FileSystem fs = Jimfs.newFileSystem(Configuration.unix())) { Path tmpDir = Files.createDirectory(fs.getPath("/cgmes")); Properties exportParameters = new Properties(); - exportParameters.put(CgmesExport.CIM_VERSION, "100"); + exportParameters.put(CgmesExport.TOPOLOGY_KIND, "NODE_BREAKER"); network.write("CGMES", exportParameters, tmpDir.resolve("tmp")); Network networkFromCgmes = Network.read(new GenericReadOnlyDataSource(tmpDir, "tmp"), importParams); diff --git a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/ExportToCimVersionTest.java b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/ExportToCimVersionTest.java index e0d1caf512b..9e478b8e94b 100644 --- a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/ExportToCimVersionTest.java +++ b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/ExportToCimVersionTest.java @@ -9,9 +9,7 @@ import com.powsybl.cgmes.conversion.CgmesExport; import com.powsybl.cgmes.conversion.CgmesImport; -import com.powsybl.cgmes.conversion.CgmesModelExtension; import com.powsybl.cgmes.extensions.CimCharacteristics; -import com.powsybl.cgmes.model.CgmesModel; import com.powsybl.cgmes.model.test.Cim14SmallCasesCatalog; import com.powsybl.commons.datasource.*; import com.powsybl.commons.test.AbstractSerDeTest; @@ -62,29 +60,6 @@ void testExportIEEE14Cim14ToCim100() { testExportToCim(network, "IEEE14", 100); } - @Test - void testExportIEEE14ToCim100CheckIsNodeBreaker() { - // Testing export to CGMES 3 is interpreted as a node/breaker CGMES model - // Input was a bus/branch model - - Network network = ieee14Cim14(); - CgmesModel cgmesModel14 = network.getExtension(CgmesModelExtension.class).getCgmesModel(); - assertFalse(cgmesModel14.isNodeBreaker()); - - String cimZipFilename = "ieee14_CIM100"; - Properties params = new Properties(); - params.put(CgmesExport.CIM_VERSION, "100"); - ZipArchiveDataSource zip = new ZipArchiveDataSource(tmpDir.resolve("."), cimZipFilename); - new CgmesExport().export(network, params, zip); - - Properties importParams = new Properties(); - importParams.put(CgmesImport.IMPORT_CGM_WITH_SUBNETWORKS, "false"); - Network network100 = Network.read(zip, importParams); - - CgmesModel cgmesModel100 = network100.getExtension(CgmesModelExtension.class).getCgmesModel(); - assertTrue(cgmesModel100.isNodeBreaker()); - } - @Test void testExportMasterResourceIdentifierOnlyForCim100OrGreater() throws IOException { Network n = NetworkTest1Factory.create(); diff --git a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/TopologyExportCornerCasesTest.java b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/TopologyExportCornerCasesTest.java index 4bb61a53619..ae643ac3683 100644 --- a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/TopologyExportCornerCasesTest.java +++ b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/TopologyExportCornerCasesTest.java @@ -75,9 +75,9 @@ private void test(Network network, checkAllTerminalsConnected(network, name); } - // Export as CGMES 3 + // Export as node-breaker Properties params = new Properties(); - params.put(CgmesExport.CIM_VERSION, "100"); + params.put(CgmesExport.TOPOLOGY_KIND, "NODE_BREAKER"); ZipArchiveDataSource zip = new ZipArchiveDataSource(tmpDir.resolve("."), name); new CgmesExport().export(network, params, zip); Properties importParams = new Properties(); diff --git a/cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/triplestore/CgmesModelTripleStore.java b/cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/triplestore/CgmesModelTripleStore.java index 6691196d6e4..4bac3078c17 100644 --- a/cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/triplestore/CgmesModelTripleStore.java +++ b/cgmes/cgmes-model/src/main/java/com/powsybl/cgmes/model/triplestore/CgmesModelTripleStore.java @@ -175,7 +175,7 @@ private boolean computeIsNodeBreaker() { if (r == null) { return false; } - if (allEqCgmes3OrGreater(r)) { + if (allEqCgmes3OrGreater(r) && !connectivityNodes().isEmpty()) { return true; } // Only consider is node breaker if all models that have profile From df9eb1d2a6b8f389cda0ad480de0df2d0e0b4852 Mon Sep 17 00:00:00 2001 From: Romain Courtier Date: Tue, 18 Feb 2025 17:02:56 +0100 Subject: [PATCH 4/7] Don't write CIM 16 Equipment Operation elements in case of a CIM16 BUS_BRANCH export Signed-off-by: Romain Courtier --- .../conversion/export/CgmesExportContext.java | 4 +++- .../conversion/export/EquipmentExport.java | 22 +++++++++++++++---- .../export/StateVariablesExport.java | 14 +++++++----- .../export/elements/ControlAreaEq.java | 4 +++- 4 files changed, 32 insertions(+), 12 deletions(-) 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 612bfc9c409..615fa99ff79 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 @@ -300,9 +300,11 @@ public CgmesExportContext setExportEquipment(boolean exportEquipment) { public boolean isExportedEquipment(Identifiable c) { boolean ignored = false; if (c instanceof Load load) { - ignored = load.isFictitious(); + ignored = load.isFictitious() + || isCim16BusBranchExport() && CgmesNames.STATION_SUPPLY.equals(CgmesExportUtil.loadClassName(load)); } else if (c instanceof Switch sw) { ignored = sw.isFictitious() && "true".equals(sw.getProperty(Conversion.PROPERTY_IS_CREATED_FOR_DISCONNECTED_TERMINAL)) + || isCim16BusBranchExport() && sw.getProperty(Conversion.PROPERTY_CGMES_ORIGINAL_CLASS, "").equals("GroundDisconnector") || isBusBranchExport() && !sw.isRetained(); } return !ignored; diff --git a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/EquipmentExport.java b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/EquipmentExport.java index 78a41ee06e9..f6d90c594c1 100644 --- a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/EquipmentExport.java +++ b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/EquipmentExport.java @@ -286,14 +286,18 @@ private static void writeBusbarSections(Network network, String cimNamespace, XM // one load area and one sub load area is created in any case private static String writeLoadGroups(Network network, Collection foundLoadGroups, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { // Write one load area and one sub load area for the whole network - String baseName = network.getNameOrId(); String loadAreaId = context.getNamingStrategy().getCgmesId(refTyped(network), LOAD_AREA); - LoadAreaEq.write(loadAreaId, baseName, cimNamespace, writer, context); String subLoadAreaId = context.getNamingStrategy().getCgmesId(refTyped(network), SUB_LOAD_AREA); - LoadAreaEq.writeSubArea(subLoadAreaId, loadAreaId, baseName, cimNamespace, writer, context); + if (!context.isCim16BusBranchExport()) { + String baseName = network.getNameOrId(); + LoadAreaEq.write(loadAreaId, baseName, cimNamespace, writer, context); + LoadAreaEq.writeSubArea(subLoadAreaId, loadAreaId, baseName, cimNamespace, writer, context); + } for (LoadGroup loadGroup : foundLoadGroups) { CgmesExportUtil.writeStartIdName(loadGroup.className, loadGroup.id, loadGroup.name, cimNamespace, writer, context); - CgmesExportUtil.writeReference("LoadGroup.SubLoadArea", subLoadAreaId, cimNamespace, writer, context); + if (!context.isCim16BusBranchExport()) { + CgmesExportUtil.writeReference("LoadGroup.SubLoadArea", subLoadAreaId, cimNamespace, writer, context); + } writer.writeEndElement(); } return loadAreaId; @@ -1207,6 +1211,11 @@ private static void writeFlowsLimits(Identifiable identifiable, FlowsLimitsHo } private static void writeLimitsGroup(Identifiable identifiable, OperationalLimitsGroup limitsGroup, String terminalId, String cimNamespace, String euNamespace, Set exportedLimitTypes, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { + if (limitsGroup.getCurrentLimits().isEmpty() && context.isCim16BusBranchExport()) { + // In CIM16, ActivePowerLimit and ApparentPowerLimit are part of EquipmentOperation profile + return; + } + // Write the OperationalLimitSet String operationalLimitSetId; String operationalLimitSetName; @@ -1242,6 +1251,11 @@ private static void writeLimitsGroup(Identifiable identifiable, OperationalLi } private static void writeLoadingLimits(LoadingLimits limits, String cimNamespace, String euNamespace, String operationalLimitSetId, Set exportedLimitTypes, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { + if (!(limits instanceof CurrentLimits) && context.isCim16BusBranchExport()) { + // In CIM16, ActivePowerLimit and ApparentPowerLimit are part of EquipmentOperation profile + return; + } + // Write the permanent limit type (if not already written) String operationalLimitTypeId = context.getNamingStrategy().getCgmesId(PATL, OPERATIONAL_LIMIT_TYPE); if (!exportedLimitTypes.contains(operationalLimitTypeId)) { 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 e5cac989aad..27613291fe4 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 @@ -605,12 +605,14 @@ private static void writeSvTapStep(String tapChangerId, int tapPosition, String } private static void writeStatus(Network network, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) { - // create SvStatus, iterate on Connectables, check Terminal status, add to SvStatus - network.getConnectableStream().forEach(c -> { - if (context.isExportedEquipment(c)) { - writeConnectableStatus(c, cimNamespace, writer, context); - } - }); + if (!context.isCim16BusBranchExport()) { + // create SvStatus, iterate on Connectables, check Terminal status, add to SvStatus + network.getConnectableStream().forEach(c -> { + if (context.isExportedEquipment(c)) { + writeConnectableStatus(c, cimNamespace, writer, context); + } + }); + } // RK: For dangling lines (boundaries), the AC Line Segment is considered in service if and only if it is connected on the network side. // If it is disconnected on the boundary side, it might not appear on the SV file. diff --git a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/elements/ControlAreaEq.java b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/elements/ControlAreaEq.java index f8b3dac5317..c094013ed3f 100644 --- a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/elements/ControlAreaEq.java +++ b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/elements/ControlAreaEq.java @@ -29,7 +29,9 @@ public static void write(String id, String controlAreaName, String energyIdentCo writer.writeEndElement(); writer.writeEmptyElement(cimNamespace, "ControlArea.type"); writer.writeAttribute(RDF_NAMESPACE, CgmesNames.RESOURCE, cimNamespace + CONTROL_AREA_TYPE); - CgmesExportUtil.writeReference("ControlArea.EnergyArea", energyAreaId, cimNamespace, writer, context); + if (!context.isCim16BusBranchExport()) { + CgmesExportUtil.writeReference("ControlArea.EnergyArea", energyAreaId, cimNamespace, writer, context); + } writer.writeEndElement(); } From 1448ed7d7e47e9b548f28690c30dfdf8c10b04e6 Mon Sep 17 00:00:00 2001 From: Romain Courtier Date: Tue, 18 Feb 2025 17:08:10 +0100 Subject: [PATCH 5/7] Add unit tests Signed-off-by: Romain Courtier --- .../cgmes/conversion/test/ConversionUtil.java | 7 + .../test/export/CgmesTopologyKindTest.java | 256 ++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesTopologyKindTest.java diff --git a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/ConversionUtil.java b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/ConversionUtil.java index 05934bb480f..31c77ffcdec 100644 --- a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/ConversionUtil.java +++ b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/ConversionUtil.java @@ -130,4 +130,11 @@ public static String getElement(String xmlFile, String className, String rdfId) Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); return getFirstMatch(xmlFile, pattern); } + + public static long getElementCount(String xmlFile, String className) { + String regex = "("; + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(xmlFile); + return matcher.results().count(); + } } diff --git a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesTopologyKindTest.java b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesTopologyKindTest.java new file mode 100644 index 00000000000..8567f9613aa --- /dev/null +++ b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesTopologyKindTest.java @@ -0,0 +1,256 @@ +/** + * Copyright (c) 2025, 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.conversion.CgmesExport; +import com.powsybl.cgmes.conversion.Conversion; +import com.powsybl.cgmes.conversion.test.ConversionUtil; +import com.powsybl.cgmes.extensions.CgmesTopologyKind; +import com.powsybl.cgmes.model.CgmesNames; +import com.powsybl.cgmes.model.CgmesNamespace; +import com.powsybl.commons.test.AbstractSerDeTest; +import com.powsybl.iidm.network.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import java.io.IOException; +import java.util.Properties; + +import static com.powsybl.cgmes.conversion.test.ConversionUtil.getElement; +import static com.powsybl.cgmes.conversion.test.ConversionUtil.getElementCount; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Romain Courtier {@literal } + */ +class CgmesTopologyKindTest extends AbstractSerDeTest { + + @ParameterizedTest + @EnumSource(value = CgmesTopologyKind.class, names = {"NODE_BREAKER", "BUS_BRANCH"}) + void cgmesTopologyKindTest(CgmesTopologyKind topologyKind) throws IOException { + Network network = mixedTopologyNetwork(); + + // Assert the CIM 16 and CIM 100 exports with given topology kind are valid + assertValidExport(network, topologyKind, false); + assertValidExport(network, topologyKind, true); + } + + private void assertValidExport(Network network, CgmesTopologyKind topologyKind, boolean cim100Export) throws IOException { + // Build the export parameters + Properties exportParams = new Properties(); + exportParams.put(CgmesExport.TOPOLOGY_KIND, topologyKind.name()); + if (cim100Export) { + exportParams.put(CgmesExport.CIM_VERSION, "100"); + } + + // Export to CGMES + String eqFile = ConversionUtil.writeCgmesProfile(network, "EQ", tmpDir, exportParams); + String sshFile = ConversionUtil.writeCgmesProfile(network, "SSH", tmpDir, exportParams); + String svFile = ConversionUtil.writeCgmesProfile(network, "SV", tmpDir, exportParams); + + // Assert the exports are valid + assertValidProfileInHeader(eqFile, topologyKind, cim100Export); + assertValidCim16EquipmentOperationElements(eqFile, sshFile, svFile, topologyKind, cim100Export); + assertValidNodeBreakerElements(eqFile, topologyKind, cim100Export); + assertValidTerminalCount(eqFile, topologyKind, cim100Export); + } + + private void assertValidProfileInHeader(String eqFile, CgmesTopologyKind topologyKind, boolean cim100Export) { + if (topologyKind == CgmesTopologyKind.NODE_BREAKER && !cim100Export) { + assertTrue(eqFile.contains(CgmesNamespace.CIM_16_EQ_OPERATION_PROFILE)); + } else { + assertFalse(eqFile.contains(CgmesNamespace.CIM_100_EQ_OPERATION_PROFILE)); + } + } + + private void assertValidCim16EquipmentOperationElements(String eqFile, String sshFile, String svFile, CgmesTopologyKind topologyKind, boolean cim100Export) { + if (topologyKind == CgmesTopologyKind.NODE_BREAKER || cim100Export) { + assertEquals(1, getElementCount(eqFile, "CurrentLimit")); + assertEquals(1, getElementCount(eqFile, "ActivePowerLimit")); + assertEquals(2, getElementCount(eqFile, "ApparentPowerLimit")); + assertEquals(1, getElementCount(eqFile, "StationSupply")); + assertEquals(1, getElementCount(eqFile, "GroundDisconnector")); + assertEquals(1, getElementCount(eqFile, "LoadArea")); + assertEquals(1, getElementCount(eqFile, "SubLoadArea")); + assertTrue(getElement(eqFile, "ConformLoadGroup", "ConformLoad_LG").contains("cim:LoadGroup.SubLoadArea")); + assertTrue(getElement(eqFile, "NonConformLoadGroup", "NonConformLoad_LG").contains("cim:LoadGroup.SubLoadArea")); + assertTrue(getElement(eqFile, "ControlArea", "Interchange").contains("cim:ControlArea.EnergyArea")); + assertEquals(1, getElementCount(sshFile, "StationSupply")); + assertEquals(1, getElementCount(sshFile, "GroundDisconnector")); + assertEquals(7, getElementCount(svFile, "SvStatus")); + } else { + assertEquals(1, getElementCount(eqFile, "CurrentLimit")); // CurrentLimit are NOT part of CIM16 EQ_OP + assertEquals(0, getElementCount(eqFile, "ActivePowerLimit")); + assertEquals(0, getElementCount(eqFile, "ApparentPowerLimit")); + assertEquals(0, getElementCount(eqFile, "StationSupply")); + assertEquals(0, getElementCount(eqFile, "GroundDisconnector")); + assertEquals(0, getElementCount(eqFile, "LoadArea")); + assertEquals(0, getElementCount(eqFile, "SubLoadArea")); + assertFalse(getElement(eqFile, "ConformLoadGroup", "ConformLoad_LG").contains("cim:LoadGroup.SubLoadArea")); + assertFalse(getElement(eqFile, "NonConformLoadGroup", "NonConformLoad_LG").contains("cim:LoadGroup.SubLoadArea")); + assertFalse(getElement(eqFile, "ControlArea", "Interchange").contains("cim:ControlArea.EnergyArea")); + assertEquals(0, getElementCount(sshFile, "StationSupply")); + assertEquals(0, getElementCount(sshFile, "GroundDisconnector")); + assertEquals(0, getElementCount(svFile, "SvStatus")); + } + } + + private void assertValidNodeBreakerElements(String eqFile, CgmesTopologyKind topologyKind, boolean cim100Export) { + if (topologyKind == CgmesTopologyKind.NODE_BREAKER) { + assertEquals(4, getElementCount(eqFile, "ConnectivityNode")); + assertEquals(1, getElementCount(eqFile, "Disconnector")); + } else { + // In case of a CIM100 bus-branch export, the buses from the BusBreakerView aren't exported as ConnectivityNode + int connectivityNodesCount = cim100Export ? 3 : 0; + assertEquals(connectivityNodesCount, getElementCount(eqFile, "ConnectivityNode")); + assertEquals(0, getElementCount(eqFile, "Disconnector")); // because it is non-retained + } + } + + private void assertValidTerminalCount(String eqFile, CgmesTopologyKind topologyKind, boolean cim100Export) { + // BusbarSection (2), Generator (1), ACLineSegment (2), ConformLoad (1), NonConformLoad (1) terminals are always exported + int terminalCount = 7; + if (topologyKind == CgmesTopologyKind.NODE_BREAKER || cim100Export) { + // StationSupply (1), GroundDisconnector (2) terminals are exported if not a CIM16 bus-branch export + terminalCount += 3; + } + if (topologyKind == CgmesTopologyKind.NODE_BREAKER) { + // non-retained Disconnector (2) terminals are exported if not a bus-branch export + terminalCount += 2; + } + + assertEquals(terminalCount, getElementCount(eqFile, "Terminal")); + } + + private Network mixedTopologyNetwork() { + Network network = NetworkFactory.findDefault().createNetwork("network", "test"); + + // VL_1: Bus-Breaker VL_2: Node-Breaker + // + // ________LN________ + // | | BBS_2A BBS_2B + // ______(BUS)______|__ _(1)____(0)____DIS____(3)________GRDIS__ + // | | | | | + // | | (2) (4) (5) + // GEN AUX LD_C LD_NC + + // Create Substation 1 with a Generator and a station supply Load + Substation substation1 = network.newSubstation() + .setId("ST_1") + .add(); + VoltageLevel voltageLevel1 = substation1.newVoltageLevel() + .setId("VL_1") + .setNominalV(400.0) + .setTopologyKind(TopologyKind.BUS_BREAKER) + .add(); + voltageLevel1.getBusBreakerView().newBus() + .setId("BUS") + .add(); + voltageLevel1.newGenerator() + .setId("GEN") + .setBus("BUS") + .setTargetP(1.0) + .setTargetQ(1.0) + .setMinP(0.0) + .setMaxP(2.0) + .setVoltageRegulatorOn(false) + .add(); + voltageLevel1.newLoad() + .setId("AUX") + .setBus("BUS") + .setP0(0.0) + .setQ0(0.0) + .setLoadType(LoadType.AUXILIARY) + .add() + .setProperty(Conversion.PROPERTY_CGMES_ORIGINAL_CLASS, CgmesNames.STATION_SUPPLY); + + // Create Substation 2 with a BusbarSection, a Load and a GroundDisconnector + Substation substation2 = network.newSubstation() + .setId("ST_2") + .add(); + VoltageLevel voltageLevel2 = substation2.newVoltageLevel() + .setId("VL_2") + .setNominalV(400.0) + .setTopologyKind(TopologyKind.NODE_BREAKER) + .add(); + voltageLevel2.getNodeBreakerView().newBusbarSection() + .setId("BBS_2A") + .setNode(0) + .add(); + voltageLevel2.getNodeBreakerView().newBusbarSection() + .setId("BBS_2B") + .setNode(3) + .add(); + voltageLevel2.getNodeBreakerView().newSwitch() + .setNode1(0) + .setNode2(3) + .setId("DIS") + .setKind(SwitchKind.DISCONNECTOR) + .setOpen(false) + .setRetained(false) + .add(); + voltageLevel2.newLoad() + .setId("LD_C") + .setNode(2) + .setP0(1.0) + .setQ0(0.0) + .add() + .setProperty(Conversion.PROPERTY_CGMES_ORIGINAL_CLASS, CgmesNames.CONFORM_LOAD); + voltageLevel2.newLoad() + .setId("LD_NC") + .setNode(4) + .setP0(0.0) + .setQ0(1.0) + .add() + .setProperty(Conversion.PROPERTY_CGMES_ORIGINAL_CLASS, CgmesNames.NONCONFORM_LOAD); + voltageLevel2.getNodeBreakerView().newSwitch() + .setId("GRDIS") + .setNode1(3) + .setNode2(5) + .setKind(SwitchKind.DISCONNECTOR) + .setOpen(false) + .setRetained(true) + .add() + .setProperty(Conversion.PROPERTY_CGMES_ORIGINAL_CLASS, "GroundDisconnector"); + + // Create a Line between substations 1 and 2 + Line line = network.newLine() + .setId("LN") + .setR(0.1) + .setX(1.0) + .setG1(0.0) + .setG2(0.0) + .setB1(0.0) + .setB2(0.0) + .setVoltageLevel1("VL_1") + .setVoltageLevel2("VL_2") + .setBus1("BUS") + .setNode2(1) + .add(); + + // Create ControlArea + network.newArea() + .setId("Interchange") + .setAreaType("ControlAreaTypeKind.Interchange") + .add(); + + // Create connectivity + voltageLevel2.getNodeBreakerView().newInternalConnection().setNode1(0).setNode2(1).add(); + voltageLevel2.getNodeBreakerView().newInternalConnection().setNode1(0).setNode2(2).add(); + voltageLevel2.getNodeBreakerView().newInternalConnection().setNode1(3).setNode2(4).add(); + + // Add limits + line.newCurrentLimits1().setPermanentLimit(100).add(); + line.newApparentPowerLimits1().setPermanentLimit(100).add(); + line.newActivePowerLimits2().setPermanentLimit(100).add(); + line.newApparentPowerLimits2().setPermanentLimit(100).add(); + + return network; + } +} From cca2fbdf4d7f89d8c923b30897d870974f5965e3 Mon Sep 17 00:00:00 2001 From: Romain Courtier Date: Tue, 18 Feb 2025 17:14:33 +0100 Subject: [PATCH 6/7] Update documentation Signed-off-by: Romain Courtier --- docs/grid_exchange_formats/cgmes/export.md | 52 ++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/grid_exchange_formats/cgmes/export.md b/docs/grid_exchange_formats/cgmes/export.md index 8178b8e6caa..6a90bc9754d 100644 --- a/docs/grid_exchange_formats/cgmes/export.md +++ b/docs/grid_exchange_formats/cgmes/export.md @@ -153,6 +153,53 @@ And the file `manualExampleBasename_SV.xml` will contain: Remember that, in addition to setting the info for metadata models in the IIDM extensions, you could also rely on parameters passed to the export methods. +## Topology kind + +The elements written in the exported files depend on the topology kind of the export and on the CIM version. +By default, the export topology kind is computed from the IIDM network's `VoltageLevel` [connectivity level](../../grid_model/network_subnetwork.md#voltage-level) as follows: +* If all `VoltageLevel` of the network are at `node/breaker` connectivity level, then the export topology kind is `NODE_BREAKER` +* If all `VoltageLevel` of the network are at `bus/breaker` connectivity level, then the export topology kind is `BUS_BRANCH` +* If some `VoltageLevel` of the network are at `node/breaker` and some other at `bus/breaker` connectivity level, then the export's topology kind depends on the CIM version for export: +it is `BUS_BRANCH` for CIM 16 and `NODE_BREAKER` for CIM 100 + +It is however possible to ignore the computed export topology kind and force it to be `NODE_BREAKER` or `BUS_BRANCH` by setting the parameter `iidm.export.cgmes.topology-kind`. + +The various configurations and the differences in what's written are summarized in the following table: + +| CIM version | Export
topology kind | Connectivity elements
are written | CIM 16 Equipment Operation
elements are written | +|-------------|--------------------------|---------------------------------------|-----------------------------------------------------| +| 16 | `NODE_BREAKER` | Yes (*) | Yes | +| 16 | `BUS_BRANCH` | No | No | +| 100 | `NODE_BREAKER` | Yes (*) | Yes | +| 100 | `BUS_BRANCH` | Yes (**) | Yes | + +### Connectivity elements +* non-retained `Switch` are always written in the case of a `NODE_BREAKER` export, and never written in the case of a `BUS_BRANCH` export +* `ConnectivityNode` are: + * Never exported in the case of a CIM 16 `BUS_BRANCH` export + * (*) Always exported in the case of a `NODE_BREAKER` export. If the VoltageLevel's connectivity level is `node/breaker`, +they are exported from nodes, and if the VoltageLevel's connectivity level is `bus/breaker`, they are exported from buses + * (**) Exported from buses of the BusBreakerView in case of a CIM 100 `BUS_BRANCH` export + +### CIM 16 Equipment Operation elements +If the version is CIM 16, a `BUS_BRANCH` export is intrinsically linked to not writing the _operation_ stereotype elements. + +This means the following classes are not written: +* `ConnectivityNode` +* `StationSupply` +* `GroundDisconnector` +* `ActivePowerLimit` +* `ApparentPowerLimit` +* `LoadArea` +* `SubLoadArea` +* `SvStatus` + +As well as the following attributes: +* `LoadGroup.SubLoadArea` +* `ControlArea.EnergyArea` + +In CIM 100 these elements have been integrated in the core equipment profile and can be written even if the export is `BUS_BRANCH`. + ## Conversion from PowSyBl grid model to CGMES The following sections describe in detail how each supported PowSyBl network model object is converted to CGMES network components. @@ -366,6 +413,11 @@ Optional property related to the naming strategy specified in `iidm.export.cgmes Optional property that determines which instance files will be exported. By default, it is a full CGMES export: the instance files for the profiles EQ, TP, SSH and SV are exported. +**iidm.export.cgmes.topology-kind** +Optional property that defines the topology kind of the export. Allowed values are: `NODE_BREAKER` and `BUS_BRANCH`. +By default, the export topology kind reflects the network's voltage levels connectivity level detail: node/breaker, bus/breaker, mixed (CIM 100/CGMES 3.0 only). +This property is used to bypass the natural export topology kind and force a desired one (e.g. export as bus/branch a node/breaker network). + **iidm.export.cgmes.modeling-authority-set** Optional property allowing to write a custom modeling authority set in the exported file headers. `powsybl.org` by default. If a Boundary set is given with the property `iidm.import.cgmes.boundary-location` and the network sourcing actor is found inside it, then the modeling authority set will be obtained from the boundary file without the need to set this property. From 0fe7cc70df0973bb915a350223e9983ed13fe925 Mon Sep 17 00:00:00 2001 From: Romain Courtier Date: Fri, 7 Mar 2025 12:51:11 +0100 Subject: [PATCH 7/7] Apply reviewers comments Signed-off-by: Romain Courtier --- .../powsybl/cgmes/conversion/CgmesExport.java | 2 +- .../conversion/export/CgmesExportContext.java | 4 ++-- .../cgmes/conversion/export/EquipmentExport.java | 2 +- .../test/export/CgmesTopologyKindTest.java | 5 +++-- docs/grid_exchange_formats/cgmes/export.md | 16 ++++++++-------- 5 files changed, 15 insertions(+), 14 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 f09e13e9a3b..1cccfd84eb4 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 @@ -706,7 +706,7 @@ public String getFormat() { private static final Parameter TOPOLOGY_KIND_PARAMETER = new Parameter( TOPOLOGY_KIND, ParameterType.STRING, - "The topology kind of the export", + "Force the topology kind for the export (disable automatic detection)", null, List.of(CgmesTopologyKind.NODE_BREAKER.name(), CgmesTopologyKind.BUS_BRANCH.name())); private static final Parameter CGM_EXPORT_PARAMETER = new 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 615fa99ff79..4659c48862e 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 @@ -141,7 +141,7 @@ public CgmesExportContext(Network network, ReferenceDataProvider referenceDataPr setCimVersion(cimCharacteristics.getCimVersion()); topologyKind = cimCharacteristics.getTopologyKind(); } else { - topologyKind = networkTopologyKind(network); + topologyKind = detectNetworkTopologyKind(network); } scenarioTime = network.getCaseDate(); addIidmMappings(network); @@ -158,7 +158,7 @@ public ReferenceDataProvider getReferenceDataProvider() { return referenceDataProvider; } - private CgmesTopologyKind networkTopologyKind(Network network) { + private CgmesTopologyKind detectNetworkTopologyKind(Network network) { long nodeBreakerVoltageLevelsCount = network.getVoltageLevelStream() .filter(vl -> vl.getTopologyKind() == TopologyKind.NODE_BREAKER) .count(); diff --git a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/EquipmentExport.java b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/EquipmentExport.java index f6d90c594c1..783307fe752 100644 --- a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/EquipmentExport.java +++ b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/EquipmentExport.java @@ -108,7 +108,7 @@ public static void write(Network network, XMLStreamWriter writer, CgmesExportCon private static void writeConnectivityNodes(Network network, Map mapNodeKey2NodeId, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { // ConnectivityNodes are: - // - always exported from nodes and buses in case of a node-breaker or CIM 100 export + // - always exported, preferably from nodes if present or buses otherwise in case of a node-breaker export // - exported from buses in case of a CIM 100 bus-branch export // - never exported in case of a CIM 16 bus-branch export if (!context.isCim16BusBranchExport()) { diff --git a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesTopologyKindTest.java b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesTopologyKindTest.java index 8567f9613aa..62b361d41fc 100644 --- a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesTopologyKindTest.java +++ b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/CgmesTopologyKindTest.java @@ -57,7 +57,7 @@ private void assertValidExport(Network network, CgmesTopologyKind topologyKind, // Assert the exports are valid assertValidProfileInHeader(eqFile, topologyKind, cim100Export); assertValidCim16EquipmentOperationElements(eqFile, sshFile, svFile, topologyKind, cim100Export); - assertValidNodeBreakerElements(eqFile, topologyKind, cim100Export); + assertValidConnectivityElements(eqFile, topologyKind, cim100Export); assertValidTerminalCount(eqFile, topologyKind, cim100Export); } @@ -65,6 +65,7 @@ private void assertValidProfileInHeader(String eqFile, CgmesTopologyKind topolog if (topologyKind == CgmesTopologyKind.NODE_BREAKER && !cim100Export) { assertTrue(eqFile.contains(CgmesNamespace.CIM_16_EQ_OPERATION_PROFILE)); } else { + assertFalse(eqFile.contains(CgmesNamespace.CIM_16_EQ_OPERATION_PROFILE)); assertFalse(eqFile.contains(CgmesNamespace.CIM_100_EQ_OPERATION_PROFILE)); } } @@ -101,7 +102,7 @@ private void assertValidCim16EquipmentOperationElements(String eqFile, String ss } } - private void assertValidNodeBreakerElements(String eqFile, CgmesTopologyKind topologyKind, boolean cim100Export) { + private void assertValidConnectivityElements(String eqFile, CgmesTopologyKind topologyKind, boolean cim100Export) { if (topologyKind == CgmesTopologyKind.NODE_BREAKER) { assertEquals(4, getElementCount(eqFile, "ConnectivityNode")); assertEquals(1, getElementCount(eqFile, "Disconnector")); diff --git a/docs/grid_exchange_formats/cgmes/export.md b/docs/grid_exchange_formats/cgmes/export.md index 6a90bc9754d..142796503b5 100644 --- a/docs/grid_exchange_formats/cgmes/export.md +++ b/docs/grid_exchange_formats/cgmes/export.md @@ -162,7 +162,7 @@ By default, the export topology kind is computed from the IIDM network's `Voltag * If some `VoltageLevel` of the network are at `node/breaker` and some other at `bus/breaker` connectivity level, then the export's topology kind depends on the CIM version for export: it is `BUS_BRANCH` for CIM 16 and `NODE_BREAKER` for CIM 100 -It is however possible to ignore the computed export topology kind and force it to be `NODE_BREAKER` or `BUS_BRANCH` by setting the parameter `iidm.export.cgmes.topology-kind`. +It is however possible to ignore the computed export topology kind and force it to be `NODE_BREAKER` or `BUS_BRANCH` by setting the parameter [`iidm.export.cgmes.topology-kind`](#options). The various configurations and the differences in what's written are summarized in the following table: @@ -174,7 +174,7 @@ The various configurations and the differences in what's written are summarized | 100 | `BUS_BRANCH` | Yes (**) | Yes | ### Connectivity elements -* non-retained `Switch` are always written in the case of a `NODE_BREAKER` export, and never written in the case of a `BUS_BRANCH` export +* Non-retained `Switch` are always written in the case of a `NODE_BREAKER` export, and never written in the case of a `BUS_BRANCH` export * `ConnectivityNode` are: * Never exported in the case of a CIM 16 `BUS_BRANCH` export * (*) Always exported in the case of a `NODE_BREAKER` export. If the VoltageLevel's connectivity level is `node/breaker`, @@ -413,9 +413,9 @@ Optional property related to the naming strategy specified in `iidm.export.cgmes Optional property that determines which instance files will be exported. By default, it is a full CGMES export: the instance files for the profiles EQ, TP, SSH and SV are exported. -**iidm.export.cgmes.topology-kind** +**iidm.export.cgmes.topology-kind** Optional property that defines the topology kind of the export. Allowed values are: `NODE_BREAKER` and `BUS_BRANCH`. -By default, the export topology kind reflects the network's voltage levels connectivity level detail: node/breaker, bus/breaker, mixed (CIM 100/CGMES 3.0 only). +By default, the export topology kind reflects the network's voltage levels connectivity level detail: node/breaker or bus/breaker. This property is used to bypass the natural export topology kind and force a desired one (e.g. export as bus/branch a node/breaker network). **iidm.export.cgmes.modeling-authority-set** @@ -440,11 +440,11 @@ the sums of active power and reactive power at the bus are higher than a thresho `iidm.export.cgmes.max-p-mismatch-converged` and `iidm.export.cgmes.max-q-mismatch-converged`. This property is set to `true` by default. -**iidm.export.cgmes.export-all-limits-group** +**iidm.export.cgmes.export-all-limits-group** Optional property that defines whether all OperationalLimitsGroup should be exported, or only the selected (active) ones. This property is set to `true` by default, which means all groups are exported (not only the active ones). -**iidm.export.cgmes.export-generators-in-local-regulation-mode** +**iidm.export.cgmes.export-generators-in-local-regulation-mode** Optional property that allows to export voltage regulating generators in local regulation mode. This doesn't concern reactive power regulating generators. If set to true, the generator's regulating terminal is set to the generator's own terminal and the target voltage is rescaled accordingly. This property is set to `false` by default. @@ -473,9 +473,9 @@ Its default value is 1. The business process in which the export takes place. This is used to generate unique UUIDs for the EQ, TP, SSH and SV file `FullModel`. Its default value is `1D`. -**iidm.export.cgmes.cgm_export** +**iidm.export.cgmes.cgm_export** Optional property to specify the export use-case: IGM (Individual Grid Model) or CGM (Common Grid Model). To export instance files of a CGM, set the value to `True`. The default value is `False` to export network as an IGM. -**iidm.export.cgmes.update-dependencies** +**iidm.export.cgmes.update-dependencies** Optional property to determine if dependencies in the exported instance files should be managed automatically. The default value is `True`.