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 88f037cfca1..f40640f275f 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 @@ -42,6 +42,7 @@ import static com.powsybl.cgmes.conversion.export.elements.LoadingLimitEq.loadingLimitClassName; import static com.powsybl.cgmes.model.CgmesNames.DC_TERMINAL1; import static com.powsybl.cgmes.model.CgmesNames.DC_TERMINAL2; +import static com.powsybl.cgmes.model.CgmesNames.NONCONFORM_LOAD; import static com.powsybl.cgmes.model.CgmesNamespace.RDF_NAMESPACE; import static com.powsybl.cgmes.conversion.naming.CgmesObjectReference.Part.*; import static com.powsybl.cgmes.conversion.naming.CgmesObjectReference.ref; @@ -93,6 +94,7 @@ public static void write(Network network, XMLStreamWriter writer, CgmesExportCon writeVoltageLevels(network, cimNamespace, writer, context, exportedBaseVoltagesByNominalV); writeBusbarSections(network, cimNamespace, writer, context); writeLoads(network, loadGroups, cimNamespace, writer, context); + writeFictitiousInjections(network, loadGroups, mapNodeKey2NodeId, cimNamespace, writer, context); String loadAreaId = writeLoadGroups(network, loadGroups.found(), cimNamespace, writer, context); writeGenerators(network, mapTerminal2Id, regulatingControlsWritten, cimNamespace, writer, context); writeBatteries(network, cimNamespace, writer, context); @@ -281,6 +283,64 @@ private static String writeLoadGroups(Network network, Collection fou return loadAreaId; } + private static void writeFictitiousInjections(Network network, LoadGroups loadGroups, Map mapNodeKey2NodeId, + String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { + for (VoltageLevel vl : network.getVoltageLevels()) { + if (vl.getTopologyKind() == TopologyKind.NODE_BREAKER && !context.isBusBranchExport()) { + writeNodeBreakerFictitiousInjections(vl, loadGroups, mapNodeKey2NodeId, cimNamespace, writer, context); + } else { + writeBusBranchFictitiousInjections(vl, loadGroups, mapNodeKey2NodeId, cimNamespace, writer, context); + } + } + } + + private static void writeNodeBreakerFictitiousInjections(VoltageLevel vl, LoadGroups loadGroups, Map mapNodeKey2NodeId, + String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { + VoltageLevel.NodeBreakerView nb = vl.getNodeBreakerView(); + for (int node : nb.getNodes()) { + double p = nb.getFictitiousP0(node); + double q = nb.getFictitiousQ0(node); + if (p != 0.0 || q != 0.0) { + String loadId = context.getNamingStrategy().getCgmesId(refTyped(vl), FICTITIOUS, ref("NCL"), ref(node)); + String loadName = vl.getNameOrId() + "_FICT_NCL_" + node; + String terminalId = context.getNamingStrategy().getCgmesId(refTyped(vl), FICTITIOUS, TERMINAL, ref(node)); + String cnId = mapNodeKey2NodeId.get(buildNodeKey(vl, node)); + + writeFictitiousInjection(p, loadId, loadName, terminalId, cnId, vl, loadGroups, cimNamespace, writer, context); + } + } + } + + private static void writeBusBranchFictitiousInjections(VoltageLevel vl, LoadGroups loadGroups, Map mapNodeKey2NodeId, + String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { + for (Bus b : vl.getBusBreakerView().getBuses()) { + double p = b.getFictitiousP0(); + double q = b.getFictitiousQ0(); + if (p != 0.0 || q != 0.0) { + String loadId = context.getNamingStrategy().getCgmesId(refTyped(b), FICTITIOUS, ref("NCL")); + String loadName = b.getNameOrId() + "_FICT_NCL"; + String terminalId = context.getNamingStrategy().getCgmesId(refTyped(b), FICTITIOUS, TERMINAL); + String cnId = mapNodeKey2NodeId.get(buildNodeKey(b)); + + writeFictitiousInjection(p, loadId, loadName, terminalId, cnId, vl, loadGroups, cimNamespace, writer, context); + } + } + } + + private static void writeFictitiousInjection(double p, String loadId, String loadName, String terminalId, String cnId, + VoltageLevel vl, LoadGroups loadGroups, String cimNamespace, + XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { + String equipmentContainerId = context.getNamingStrategy().getCgmesId(vl); + if (p <= 0) { + writeEnergySource(loadId, loadName, equipmentContainerId, cimNamespace, writer, context); + } else { + String loadGroup = loadGroups.groupFor(NONCONFORM_LOAD, context); + EnergyConsumerEq.write(NONCONFORM_LOAD, loadId, loadName, loadGroup, equipmentContainerId, null, cimNamespace, writer, context); + } + + TerminalEq.write(terminalId, loadId, cnId, 1, cimNamespace, writer, context); + } + private static void writeLoads(Network network, LoadGroups loadGroups, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { for (Load load : network.getLoads()) { if (context.isExportedEquipment(load)) { @@ -290,7 +350,7 @@ private static void writeLoads(Network network, LoadGroups loadGroups, String ci case CgmesNames.ASYNCHRONOUS_MACHINE -> writeAsynchronousMachine(loadId, load.getNameOrId(), cimNamespace, writer, context); case CgmesNames.ENERGY_SOURCE -> writeEnergySource(loadId, load.getNameOrId(), context.getNamingStrategy().getCgmesId(load.getTerminal().getVoltageLevel()), cimNamespace, writer, context); - case CgmesNames.ENERGY_CONSUMER, CgmesNames.CONFORM_LOAD, CgmesNames.NONCONFORM_LOAD, CgmesNames.STATION_SUPPLY -> { + case CgmesNames.ENERGY_CONSUMER, CgmesNames.CONFORM_LOAD, NONCONFORM_LOAD, CgmesNames.STATION_SUPPLY -> { String loadGroup = loadGroups.groupFor(className, context); String loadResponseCharacteristicId = writeLoadResponseCharacteristic(load, cimNamespace, writer, context); EnergyConsumerEq.write(className, loadId, load.getNameOrId(), loadGroup, context.getNamingStrategy().getCgmesId(load.getTerminal().getVoltageLevel()), loadResponseCharacteristicId, cimNamespace, writer, context); 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 3e247469122..633b7617f75 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 @@ -33,6 +33,7 @@ import static com.powsybl.cgmes.conversion.naming.CgmesObjectReference.Part.TOPOLOGICAL_ISLAND; import static com.powsybl.cgmes.conversion.naming.CgmesObjectReference.ref; +import static com.powsybl.cgmes.conversion.naming.CgmesObjectReference.refTyped; /** * @author Miora Ralambotiana {@literal } @@ -271,9 +272,11 @@ boolean isInAccordanceWithKirchhoffsFirstLaw(Bus bus) { } boolean isInAccordance; - if (BusTools.hasAnyFinite(busViewBus, Terminal::getP) && BusTools.hasAnyFinite(busViewBus, Terminal::getQ)) { - double sumP = BusTools.sum(busViewBus, Terminal::getP); - double sumQ = BusTools.sum(busViewBus, Terminal::getQ); + if (bus.getFictitiousP0() != 0.0 + || bus.getFictitiousQ0() != 0.0 + || BusTools.hasAnyFinite(busViewBus, Terminal::getP) && BusTools.hasAnyFinite(busViewBus, Terminal::getQ)) { + double sumP = BusTools.sum(busViewBus, Terminal::getP) + bus.getFictitiousP0(); + double sumQ = BusTools.sum(busViewBus, Terminal::getQ) + bus.getFictitiousQ0(); isInAccordance = Math.abs(sumP) <= maxPMismatchConverged && Math.abs(sumQ) <= maxQMismatchConverged; if (!isInAccordance && LOG.isInfoEnabled()) { LOG.info("Bus {} is not in accordance with Kirchhoff's first law. Mismatch = {}", bus, String.format("(%.4f, %.4f)", sumP, sumQ)); @@ -390,6 +393,9 @@ private static void writePowerFlows(Network network, String cimNamespace, XMLStr } } + // Write SvPowerFlow for synthetic fictitious injections exported as NonConformLoad + writeFictitiousInjectionsPowerFlows(network, cimNamespace, writer, context); + Map equivalentInjectionTerminalP = new HashMap<>(); Map equivalentInjectionTerminalQ = new HashMap<>(); network.getDanglingLines(DanglingLineFilter.ALL).forEach(dl -> { @@ -418,6 +424,39 @@ private static void writePowerFlows(Network network, String cimNamespace, XMLStr } } + private static void writeFictitiousInjectionsPowerFlows(Network network, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) { + for (VoltageLevel vl : network.getVoltageLevels()) { + if (vl.getTopologyKind() == TopologyKind.NODE_BREAKER && !context.isBusBranchExport()) { + writeNodeBreakerFictitiousInjections(vl, cimNamespace, writer, context); + } else { + writeBusBranchFictitiousInjections(vl, cimNamespace, writer, context); + } + } + } + + private static void writeNodeBreakerFictitiousInjections(VoltageLevel vl, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) { + VoltageLevel.NodeBreakerView nb = vl.getNodeBreakerView(); + for (int node : nb.getNodes()) { + double p = nb.getFictitiousP0(node); + double q = nb.getFictitiousQ0(node); + if (p != 0.0 || q != 0.0) { + String terminalId = context.getNamingStrategy().getCgmesId(refTyped(vl), com.powsybl.cgmes.conversion.naming.CgmesObjectReference.Part.FICTITIOUS, com.powsybl.cgmes.conversion.naming.CgmesObjectReference.Part.TERMINAL, ref(node)); + writePowerFlow(terminalId, p, q, cimNamespace, writer, context); + } + } + } + + private static void writeBusBranchFictitiousInjections(VoltageLevel vl, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) { + for (Bus b : vl.getBusBreakerView().getBuses()) { + double p = b.getFictitiousP0(); + double q = b.getFictitiousQ0(); + if (p != 0.0 || q != 0.0) { + String terminalId = context.getNamingStrategy().getCgmesId(refTyped(b), com.powsybl.cgmes.conversion.naming.CgmesObjectReference.Part.FICTITIOUS, com.powsybl.cgmes.conversion.naming.CgmesObjectReference.Part.TERMINAL); + writePowerFlow(terminalId, p, q, cimNamespace, writer, context); + } + } + } + private static void writePowerFlowForSwitchesInVoltageLevel(VoltageLevel vl, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) { SlackTerminal st = vl.getExtension(SlackTerminal.class); Terminal slackTerminal = st != null ? st.getTerminal() : null; 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 e25c83a9e60..ba79b3c5edd 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 @@ -76,6 +76,7 @@ public static void write(Network network, XMLStreamWriter writer, CgmesExportCon } writeLoads(network, cimNamespace, writer, context); + writeFictitiousInjections(network, cimNamespace, writer, context); writeEquivalentInjections(network, cimNamespace, writer, context); writeTapChangers(network, cimNamespace, regulatingControlViews, writer, context); writeGenerators(network, cimNamespace, regulatingControlViews, writer, context); @@ -171,6 +172,53 @@ private static void writeTerminals(Network network, String cimNamespace, XMLStre } } + private static void writeFictitiousInjections(Network network, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { + for (VoltageLevel vl : network.getVoltageLevels()) { + if (vl.getTopologyKind() == TopologyKind.NODE_BREAKER && !context.isBusBranchExport()) { + writeNodeBreakerFictitiousInjections(vl, cimNamespace, writer, context); + } else { + writeBusBranchFictitiousInjections(vl, cimNamespace, writer, context); + } + } + } + + private static void writeNodeBreakerFictitiousInjections(VoltageLevel vl, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { + VoltageLevel.NodeBreakerView nb = vl.getNodeBreakerView(); + for (int node : nb.getNodes()) { + double p = nb.getFictitiousP0(node); + double q = nb.getFictitiousQ0(node); + if (p != 0.0 || q != 0.0) { + String loadId = context.getNamingStrategy().getCgmesId(refTyped(vl), com.powsybl.cgmes.conversion.naming.CgmesObjectReference.Part.FICTITIOUS, ref("NCL"), ref(node)); + String terminalId = context.getNamingStrategy().getCgmesId(refTyped(vl), com.powsybl.cgmes.conversion.naming.CgmesObjectReference.Part.FICTITIOUS, com.powsybl.cgmes.conversion.naming.CgmesObjectReference.Part.TERMINAL, ref(node)); + writeFictitiousInjection(loadId, terminalId, p, q, cimNamespace, writer, context); + } + } + } + + private static void writeBusBranchFictitiousInjections(VoltageLevel vl, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { + for (Bus b : vl.getBusBreakerView().getBuses()) { + double p = b.getFictitiousP0(); + double q = b.getFictitiousQ0(); + if (p != 0.0 || q != 0.0) { + String loadId = context.getNamingStrategy().getCgmesId(refTyped(b), com.powsybl.cgmes.conversion.naming.CgmesObjectReference.Part.FICTITIOUS, ref("NCL")); + String terminalId = context.getNamingStrategy().getCgmesId(refTyped(b), com.powsybl.cgmes.conversion.naming.CgmesObjectReference.Part.FICTITIOUS, com.powsybl.cgmes.conversion.naming.CgmesObjectReference.Part.TERMINAL); + writeFictitiousInjection(loadId, terminalId, p, q, cimNamespace, writer, context); + } + } + } + + private static void writeFictitiousInjection(String loadId, String terminalId, double p, double q, + String cimNamespace, XMLStreamWriter writer, + CgmesExportContext context) throws XMLStreamException { + if (p <= 0) { + writeEnergySource(loadId, p, q, cimNamespace, writer, context); + } else { + writeSshEnergyConsumer(loadId, CgmesNames.NONCONFORM_LOAD, p, q, cimNamespace, writer, context); + } + // Terminal connected state (always connected in SSH for fictitious terminals) + writeTerminal(terminalId, true, cimNamespace, writer, context); + } + private static void writeEquivalentInjections(Network network, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { // One equivalent injection for every dangling line List exported = new ArrayList<>(); diff --git a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/TopologyExport.java b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/TopologyExport.java index f6b15c0b728..a7008ed0a06 100644 --- a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/TopologyExport.java +++ b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/TopologyExport.java @@ -69,12 +69,53 @@ private static void writeTerminals(Network network, String cimNamespace, XMLStre writeBoundaryTerminals(network, cimNamespace, writer, context); writeSwitchesTerminals(network, cimNamespace, writer, context); writeDcTerminals(network, cimNamespace, writer, context); + // Fictitious injections as NonConformLoad in Bus/Breaker topology: link terminals to TopologicalNode (bus) + writeFictitiousInjectionTerminals(network, cimNamespace, writer, context); // Only if it is an updated export if (!context.isExportEquipment()) { writeBusbarSectionTerminalsFromBusBranchCgmesModel(network, cimNamespace, writer, context); } } + private static void writeFictitiousInjectionTerminals(Network network, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { + for (VoltageLevel vl : network.getVoltageLevels()) { + if (vl.getTopologyKind() == TopologyKind.NODE_BREAKER && !context.isBusBranchExport()) { + writeNodeBreakerFictitiousTerminals(vl, cimNamespace, writer, context); + } else { + writeBusBranchFictitiousTerminals(vl, cimNamespace, writer, context); + } + } + } + + private static void writeNodeBreakerFictitiousTerminals(VoltageLevel vl, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { + VoltageLevel.NodeBreakerView nodeBreakerView = vl.getNodeBreakerView(); + + for (int node : nodeBreakerView.getNodes()) { + double p = nodeBreakerView.getFictitiousP0(node); + double q = nodeBreakerView.getFictitiousQ0(node); + + if (p != 0.0 || q != 0.0) { + String terminalId = context.getNamingStrategy().getCgmesId(refTyped(vl), FICTITIOUS, TERMINAL, ref(node)); + Bus bus = getBusForBusBreakerViewBus(vl, node); + String topologicalNodeId = context.getNamingStrategy().getCgmesId(bus); + writeTerminal(terminalId, topologicalNodeId, cimNamespace, writer, context); + } + } + } + + private static void writeBusBranchFictitiousTerminals(VoltageLevel vl, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { + for (Bus bus : vl.getBusBreakerView().getBuses()) { + double p = bus.getFictitiousP0(); + double q = bus.getFictitiousQ0(); + + if (p != 0.0 || q != 0.0) { + String terminalId = context.getNamingStrategy().getCgmesId(refTyped(bus), FICTITIOUS, TERMINAL); + String topologicalNodeId = context.getNamingStrategy().getCgmesId(bus); + writeTerminal(terminalId, topologicalNodeId, cimNamespace, writer, context); + } + } + } + private static void writeBusbarSectionTerminalsFromBusBranchCgmesModel(Network network, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { for (Bus b : network.getBusBreakerView().getBuses()) { String topologicalNodeId = context.getNamingStrategy().getCgmesId(b); diff --git a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/FictitiousInjectionsExportTest.java b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/FictitiousInjectionsExportTest.java new file mode 100644 index 00000000000..8abaf12c46f --- /dev/null +++ b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/FictitiousInjectionsExportTest.java @@ -0,0 +1,164 @@ +/** + * 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.test.ConversionUtil; +import com.powsybl.commons.test.AbstractSerDeTest; +import com.powsybl.iidm.network.*; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Properties; + +import static com.powsybl.cgmes.conversion.test.ConversionUtil.getAttribute; +import static com.powsybl.cgmes.conversion.test.ConversionUtil.getElement; +import static com.powsybl.cgmes.conversion.test.ConversionUtil.getResource; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author Coline Piloquet {@literal } + */ +class FictitiousInjectionsExportTest extends AbstractSerDeTest { + + @Test + void nodeBreakerExport() throws IOException { + Network network = NetworkFactory.findDefault().createNetwork("TestNetworkNodeBreaker", "test"); + Substation s = network.newSubstation().setId("S").add(); + VoltageLevel vl = s.newVoltageLevel() + .setId("VL") + .setNominalV(225.0) + .setTopologyKind(TopologyKind.NODE_BREAKER) + .add(); + + vl.getNodeBreakerView().newBusbarSection().setId("BBS").setNode(0).add(); + + // Set fictitious injection on node 0 + vl.getNodeBreakerView().setFictitiousP0(0, 1.1).setFictitiousQ0(0, 2.2); + + Properties params = new Properties(); + + String eqXml = ConversionUtil.writeCgmesProfile(network, "EQ", tmpDir, params); + String sshXml = ConversionUtil.writeCgmesProfile(network, "SSH", tmpDir, params); + String tpXml = ConversionUtil.writeCgmesProfile(network, "TP", tmpDir, params); + String svXml = ConversionUtil.writeCgmesProfile(network, "SV", tmpDir, params); + + String loadId = "VL_VL_FICT_NCL_0"; + String terminalId = "VL_VL_FICT_T_0"; + + // EQ: NonConformLoad exists and Terminal references ConnectivityNode + assertNotNull(getElement(eqXml, "NonConformLoad", loadId)); + String terminalEq = getElement(eqXml, "Terminal", terminalId); + assertNotNull(terminalEq); + assertTrue(terminalEq.contains("TODO details +(cgmes-fictitious-injections-export)= +### Fictitious injections (fictitiousP0/fictitiousQ0) + +The fictitious injections on buses (bus-branch topology) or on nodes (node-breaker topology) are created using: +- Bus topology: `Bus.setFictitiousP0(double)` and `Bus.setFictitiousQ0(double)` +- Node-breaker: `VoltageLevel.getNodeBreakerView().setFictitiousP0(int node, double)` and `setFictitiousQ0(int node, double)` + +These fictitious injections are exported in CGMES as a `NonConformLoad` or an `EnergySource` depending on `fictitiousP0`, with values written to SSH and connectivity/topology bindings set according to the network topology and CIM version. A corresponding `SvPowerFlow` is written in SV for the terminal. +In case of a node-breaker or CIM100 export, the terminal will refer to a `ConnectivityNode`. + (cgmes-shunt-compensator-export)= ### Shunt compensator