From 8996bc28ffc5f795214a32964f1e29f5b2ce24c4 Mon Sep 17 00:00:00 2001 From: Romain Courtier Date: Thu, 20 Nov 2025 10:49:23 +0100 Subject: [PATCH 1/7] Fix EQ export Signed-off-by: Romain Courtier --- .../conversion/export/EquipmentExport.java | 34 ++++++++++++++----- .../export/elements/LoadingLimitEq.java | 7 ++-- 2 files changed, 29 insertions(+), 12 deletions(-) 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 1b99eb44a0d..12c6bba5edd 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 @@ -660,7 +660,7 @@ private static void writeTwoWindingsTransformers(Network network, Map> void writePhaseTapChanger(C eq, PhaseTapChanger ptc, String twtName, int endNumber, String endId, double neutralU, Set regulatingControlsWritten, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { + private static > void writePhaseTapChanger(C eq, PhaseTapChanger ptc, String twtName, int endNumber, String endId, double neutralU, Set regulatingControlsWritten, String cimNamespace, String euNamespace, Set exportedLimitTypes, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { if (ptc != null) { String aliasType = Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.PHASE_TAP_CHANGER + endNumber; String tapChangerId = eq.getAliasFromType(aliasType).orElseThrow(); @@ -848,11 +848,29 @@ private static > void writePhaseTapChanger(C eq, PhaseT Optional regulatingControlId = getTapChangerControlId(eq, tapChangerId); String cgmesRegulatingControlId = null; if (regulatingControlId.isPresent() && CgmesExportUtil.regulatingControlIsDefined(ptc)) { - String mode = CgmesExportUtil.getPhaseTapChangerRegulationMode(ptc); - String controlName = twtName + "_PTC_RC"; - String terminalId = CgmesExportUtil.getTerminalId(ptc.getRegulationTerminal(), context); cgmesRegulatingControlId = context.getNamingStrategy().getCgmesId(regulatingControlId.get()); if (!regulatingControlsWritten.contains(cgmesRegulatingControlId)) { + String mode = RegulatingControlEq.REGULATING_CONTROL_ACTIVE_POWER; + String controlName = twtName + "_PTC_RC"; + String terminalId = CgmesExportUtil.getTerminalId(ptc.getRegulationTerminal(), context); + if (ptc.getRegulationMode() == PhaseTapChanger.RegulationMode.CURRENT_LIMITER) { + //TODO log not supported regulation mode + + // Add a CurrentLimit with the PhaseTapChanger current limiter regulation value to the regulated terminal. + String operationalLimitSetId = context.getNamingStrategy().getCgmesId(ref(terminalId), ref(PhaseTapChanger.RegulationMode.CURRENT_LIMITER.name()), OPERATIONAL_LIMIT_SET); + String operationalLimitSetName = twtName + "_PTC_" + PhaseTapChanger.RegulationMode.CURRENT_LIMITER; + OperationalLimitSetEq.write(operationalLimitSetId, operationalLimitSetName, terminalId, cimNamespace, writer, context); + + String className = "CurrentLimit"; + String operationalLimitId = context.getNamingStrategy().getCgmesId(ref(operationalLimitSetId), ref(className), PATL, OPERATIONAL_LIMIT_VALUE); + String operationalLimitTypeId = context.getNamingStrategy().getCgmesId(PATL, OPERATIONAL_LIMIT_TYPE); + LoadingLimitEq.write(operationalLimitId, className, "PATL", ptc.getRegulationValue(), operationalLimitTypeId, operationalLimitSetId, cimNamespace, writer, context); + + if (!exportedLimitTypes.contains(operationalLimitTypeId)) { + OperationalLimitTypeEq.writePatl(operationalLimitTypeId, cimNamespace, euNamespace, writer, context); + exportedLimitTypes.add(operationalLimitTypeId); + } + } TapChangerEq.writeControl(cgmesRegulatingControlId, controlName, mode, terminalId, cimNamespace, writer, context); regulatingControlsWritten.add(cgmesRegulatingControlId); } @@ -1225,7 +1243,7 @@ private static void writeLoadingLimits(LoadingLimits limits, String cimNamespace // Write the permanent limit String className = loadingLimitClassName(limits); String operationalLimitId = context.getNamingStrategy().getCgmesId(ref(operationalLimitSetId), ref(className), PATL, OPERATIONAL_LIMIT_VALUE); - LoadingLimitEq.write(operationalLimitId, limits, "PATL", limits.getPermanentLimit(), operationalLimitTypeId, operationalLimitSetId, cimNamespace, writer, context); + LoadingLimitEq.write(operationalLimitId, className, "PATL", limits.getPermanentLimit(), operationalLimitTypeId, operationalLimitSetId, cimNamespace, writer, context); if (!limits.getTemporaryLimits().isEmpty()) { for (LoadingLimits.TemporaryLimit temporaryLimit : limits.getTemporaryLimits()) { @@ -1241,7 +1259,7 @@ private static void writeLoadingLimits(LoadingLimits limits, String cimNamespace // Write the temporary limit operationalLimitId = context.getNamingStrategy().getCgmesId(ref(operationalLimitSetId), ref(className), TATL, ref(acceptableDuration), OPERATIONAL_LIMIT_VALUE); String temporaryLimitName = temporaryLimit.getName().isEmpty() ? "TATL " + temporaryLimit.getAcceptableDuration() : temporaryLimit.getName(); // If the temporary limit name is empty, write TATL and the acceptable duration - LoadingLimitEq.write(operationalLimitId, limits, temporaryLimitName, temporaryLimit.getValue(), operationalLimitTypeId, operationalLimitSetId, cimNamespace, writer, context); + LoadingLimitEq.write(operationalLimitId, className, temporaryLimitName, temporaryLimit.getValue(), operationalLimitTypeId, operationalLimitSetId, cimNamespace, writer, context); } } } diff --git a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/elements/LoadingLimitEq.java b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/elements/LoadingLimitEq.java index 9a9086ef5e1..631cfe1d8cb 100644 --- a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/elements/LoadingLimitEq.java +++ b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/elements/LoadingLimitEq.java @@ -23,11 +23,10 @@ */ public final class LoadingLimitEq { - public static void write(String id, LoadingLimits loadingLimits, String name, double value, + public static void write(String id, String className, String name, double value, String operationalLimitTypeId, String operationalLimitSetId, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { - String cgmesClass = loadingLimitClassName(loadingLimits); - CgmesExportUtil.writeStartIdName(cgmesClass, id, name, cimNamespace, writer, context); - writer.writeStartElement(cimNamespace, cgmesClass + "." + getLimitValueAttributeName(context)); + CgmesExportUtil.writeStartIdName(className, id, name, cimNamespace, writer, context); + writer.writeStartElement(cimNamespace, className + "." + getLimitValueAttributeName(context)); writer.writeCharacters(CgmesExportUtil.format(value)); writer.writeEndElement(); CgmesExportUtil.writeReference("OperationalLimit.OperationalLimitSet", operationalLimitSetId, cimNamespace, writer, context); From 8bcf0cd92d4652f681e9c41f67a69d40bfa446f7 Mon Sep 17 00:00:00 2001 From: Romain Courtier Date: Thu, 20 Nov 2025 10:51:17 +0100 Subject: [PATCH 2/7] Fix SSH export Signed-off-by: Romain Courtier --- .../cgmes/conversion/export/CgmesExportUtil.java | 7 ------- .../export/SteadyStateHypothesisExport.java | 12 ++++++++---- .../export/elements/RegulatingControlEq.java | 1 - 3 files changed, 8 insertions(+), 12 deletions(-) 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 8fec46d49bb..86f4fd41dd8 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 @@ -559,13 +559,6 @@ public static String getTcMode(RatioTapChanger rtc) { }; } - public static String getPhaseTapChangerRegulationMode(PhaseTapChanger ptc) { - return switch (ptc.getRegulationMode()) { - case CURRENT_LIMITER -> RegulatingControlEq.REGULATING_CONTROL_CURRENT_FLOW; - case ACTIVE_POWER_CONTROL -> RegulatingControlEq.REGULATING_CONTROL_ACTIVE_POWER; - }; - } - public static boolean isMinusOrMaxValue(double value) { return value == -Double.MAX_VALUE || value == Double.MAX_VALUE; } 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 4f67d0c5dd2..3a9f127334c 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 @@ -488,19 +488,23 @@ private static void addRegulatingControlView(TapChanger tc, CgmesTap } else if (tc instanceof PhaseTapChanger phaseTapChanger && CgmesExportUtil.regulatingControlIsDefined(phaseTapChanger)) { boolean valid; - String unitMultiplier = switch (CgmesExportUtil.getPhaseTapChangerRegulationMode(phaseTapChanger)) { - case RegulatingControlEq.REGULATING_CONTROL_CURRENT_FLOW -> { + boolean regulating; + String unitMultiplier = switch (phaseTapChanger.getRegulationMode()) { + case PhaseTapChanger.RegulationMode.CURRENT_LIMITER -> { // Unit multiplier is none (multiply by 1), regulation value is a current in Amperes valid = true; + regulating = false; yield "none"; } - case RegulatingControlEq.REGULATING_CONTROL_ACTIVE_POWER -> { + case PhaseTapChanger.RegulationMode.ACTIVE_POWER_CONTROL -> { // Unit multiplier is M, regulation value is an active power flow in MW valid = true; + regulating = phaseTapChanger.isRegulating(); yield "M"; } default -> { valid = false; + regulating = false; yield "none"; } }; @@ -508,7 +512,7 @@ private static void addRegulatingControlView(TapChanger tc, CgmesTap rcv = new RegulatingControlView(controlId, RegulatingControlType.TAP_CHANGER_CONTROL, true, - phaseTapChanger.isRegulating(), + regulating, phaseTapChanger.getTargetDeadband(), phaseTapChanger.getRegulationValue(), unitMultiplier); diff --git a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/elements/RegulatingControlEq.java b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/elements/RegulatingControlEq.java index 83e478b97a8..4fd4f09da92 100644 --- a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/elements/RegulatingControlEq.java +++ b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/export/elements/RegulatingControlEq.java @@ -27,7 +27,6 @@ public final class RegulatingControlEq { public static final String REGULATING_CONTROL_VOLTAGE = "RegulatingControlModeKind.voltage"; public static final String REGULATING_CONTROL_REACTIVE_POWER = "RegulatingControlModeKind.reactivePower"; public static final String REGULATING_CONTROL_ACTIVE_POWER = "RegulatingControlModeKind.activePower"; - public static final String REGULATING_CONTROL_CURRENT_FLOW = "RegulatingControlModeKind.currentFlow"; public static String writeRegulatingControlEq(Connectable c, String terminalId, Set regulatingControlsWritten, String mode, String cimNamespace, XMLStreamWriter writer, CgmesExportContext context) throws XMLStreamException { String regulatingControlId = context.getNamingStrategy().getCgmesIdFromProperty(c, Conversion.PROPERTY_REGULATING_CONTROL); From e420acb41af2856863ce09deebca7b08ec69092b Mon Sep 17 00:00:00 2001 From: Romain Courtier Date: Thu, 20 Nov 2025 10:55:35 +0100 Subject: [PATCH 3/7] Add unit test Signed-off-by: Romain Courtier --- .../cgmes/conversion/test/ConversionUtil.java | 12 +++++++ .../test/TapChangerConversionTest.java | 34 +++++++++++++++++++ .../test/export/EquipmentExportTest.java | 24 ++----------- 3 files changed, 49 insertions(+), 21 deletions(-) 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 9e68c684ea4..f3a6391d6c4 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 @@ -156,6 +156,18 @@ public static String getElement(String xmlFile, String className, String rdfId) return getFirstMatch(xmlFile, pattern); } + public static String getAttribute(String element, String attributeName) { + String regex = "(.*?)"; + Pattern pattern = Pattern.compile(regex); + return getFirstMatch(element, pattern); + } + + public static String getResource(String element, String attributeName) { + String regex = ""; + Pattern pattern = Pattern.compile(regex); + return getFirstMatch(element, pattern); + } + public static long getElementCount(String xmlFile, String className) { String regex = "("; Pattern pattern = Pattern.compile(regex); diff --git a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/TapChangerConversionTest.java b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/TapChangerConversionTest.java index d4963249275..e326570bf23 100644 --- a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/TapChangerConversionTest.java +++ b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/TapChangerConversionTest.java @@ -13,6 +13,7 @@ import com.powsybl.commons.test.AbstractSerDeTest; import com.powsybl.commons.test.PowsyblTestReportResourceBundle; import com.powsybl.iidm.network.*; +import com.powsybl.iidm.network.test.PhaseShifterTestCaseFactory; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -64,4 +65,37 @@ void invalidLtcFlagTest() throws IOException { assertTrue(logs.contains("TapChanger PTC has regulation enabled but has no load tap changing capability. Fixed ltcFlag to true.")); } + @Test + void currentLimiterExportTest() throws IOException { + // IIDM network: + // A PhaseShifter with current limiter regulation mode. + // CGMES network: + // A PhaseShifter with active power regulation mode, off regulation and the current limit saved as an operational limit. + Network network = PhaseShifterTestCaseFactory.createRegulatingWithoutMode(); + + // PhaseShifter PS1 has a PhaseTapChanger in current limiter regulation mode, + // with a current limit value of 200A at PS1 secondary terminal. + PhaseTapChanger ptc = network.getTwoWindingsTransformer("PS1").getPhaseTapChanger(); + assertEquals(PhaseTapChanger.RegulationMode.CURRENT_LIMITER, ptc.getRegulationMode()); + assertEquals(200, ptc.getRegulationValue()); + assertTrue(ptc.isRegulating()); + assertEquals("PS1", ptc.getRegulationTerminal().getConnectable().getId()); + assertEquals(2, ptc.getRegulationTerminal().getSide().getNum()); + assertTrue(network.getTwoWindingsTransformer("PS1").getOperationalLimitsGroups2().isEmpty()); + + // PhaseTapChanger is in active power regulation mode and the regulation is disabled. + // A CurentLimit with the value 200A has been created. + String eqFile = writeCgmesProfile(network, "EQ", tmpDir); + String tapChangerControl = getElement(eqFile, "TapChangerControl", "PS1_PTC_RC"); + assertEquals("http://iec.ch/TC57/2013/CIM-schema-cim16#RegulatingControlModeKind.activePower", getResource(tapChangerControl, "RegulatingControl.mode")); + String limitSet = getElement(eqFile, "OperationalLimitSet", "PS1_PT_T_2_CURRENT_LIMITER_OLS"); + assertEquals("PS1_PT_T_2", getResource(limitSet, "OperationalLimitSet.Terminal")); + String currentLimit = getElement(eqFile, "CurrentLimit", "PS1_PT_T_2_CURRENT_LIMITER_OLS_CurrentLimit_PATL_OLV"); + assertEquals("200", getAttribute(currentLimit, "CurrentLimit.value")); + + String sshFile = writeCgmesProfile(network, "SSH", tmpDir); + tapChangerControl = getElement(sshFile, "TapChangerControl", "PS1_PTC_RC"); + assertEquals("false", getAttribute(tapChangerControl, "RegulatingControl.enabled")); + } + } diff --git a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/EquipmentExportTest.java b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/EquipmentExportTest.java index 31e92cad1a3..3aabcdba3f4 100644 --- a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/EquipmentExportTest.java +++ b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/EquipmentExportTest.java @@ -1021,14 +1021,14 @@ void phaseTapChangerTapChangerControlEQTest() throws IOException { Properties exportParams = new Properties(); exportParams.put(CgmesExport.PROFILES, "EQ"); - // PST with no regulation mode but regulating true => set to CURRENT_LIMITER mode + // PST with no regulation mode but regulating true => set to ACTIVE_POWER_CONTROL mode network = PhaseShifterTestCaseFactory.createRegulatingWithoutMode(); assertTrue(network.getTwoWindingsTransformer("PS1").getPhaseTapChanger().isRegulating()); eq = getEQ(network, baseName, tmpDir, exportParams); - testTcTccWithAttribute(eq, "_PS1_PTC_RC", "_PS1_PT_T_2", "currentFlow"); + testTcTccWithAttribute(eq, "_PS1_PTC_RC", "_PS1_PT_T_2", "activePower"); network.getTwoWindingsTransformer("PS1").getPhaseTapChanger().setRegulating(false); eq = getEQ(network, baseName, tmpDir, exportParams); - testTcTccWithAttribute(eq, "_PS1_PTC_RC", "_PS1_PT_T_2", "currentFlow"); + testTcTccWithAttribute(eq, "_PS1_PTC_RC", "_PS1_PT_T_2", "activePower"); // PST local with ACTIVE_POWER_CONTROL network = PhaseShifterTestCaseFactory.createLocalActivePowerWithTargetDeadband(); @@ -1039,24 +1039,6 @@ void phaseTapChangerTapChangerControlEQTest() throws IOException { eq = getEQ(network, baseName, tmpDir, exportParams); testTcTccWithAttribute(eq, "_PS1_PTC_RC", "_PS1_PT_T_2", "activePower"); - // PST local with CURRENT_LIMITER - network = PhaseShifterTestCaseFactory.createLocalCurrentLimiterWithTargetDeadband(); - assertFalse(network.getTwoWindingsTransformer("PS1").getPhaseTapChanger().isRegulating()); - eq = getEQ(network, baseName, tmpDir, exportParams); - testTcTccWithAttribute(eq, "_PS1_PTC_RC", "_PS1_PT_T_2", "currentFlow"); - network.getTwoWindingsTransformer("PS1").getPhaseTapChanger().setRegulating(true); - eq = getEQ(network, baseName, tmpDir, exportParams); - testTcTccWithAttribute(eq, "_PS1_PTC_RC", "_PS1_PT_T_2", "currentFlow"); - - // PST remote with CURRENT_LIMITER - network = PhaseShifterTestCaseFactory.createRemoteCurrentLimiterWithTargetDeadband(); - assertFalse(network.getTwoWindingsTransformer("PS1").getPhaseTapChanger().isRegulating()); - eq = getEQ(network, baseName, tmpDir, exportParams); - testTcTccWithAttribute(eq, "_PS1_PTC_RC", "_LD2_EC_T_1", "currentFlow"); - network.getTwoWindingsTransformer("PS1").getPhaseTapChanger().setRegulating(true); - eq = getEQ(network, baseName, tmpDir, exportParams); - testTcTccWithAttribute(eq, "_PS1_PTC_RC", "_LD2_EC_T_1", "currentFlow"); - // PST remote with ACTIVE_POWER_CONTROL network = PhaseShifterTestCaseFactory.createRemoteActivePowerWithTargetDeadband(); assertFalse(network.getTwoWindingsTransformer("PS1").getPhaseTapChanger().isRegulating()); From 6a7c2ccb2458e36e956ab9451b001b803e622abf Mon Sep 17 00:00:00 2001 From: Romain Courtier Date: Fri, 21 Nov 2025 00:03:55 +0100 Subject: [PATCH 4/7] Add log in report node Signed-off-by: Romain Courtier --- .../java/com/powsybl/cgmes/conversion/CgmesReports.java | 8 ++++++++ .../powsybl/cgmes/conversion/export/EquipmentExport.java | 4 +++- .../main/resources/com/powsybl/commons/reports.properties | 1 + .../resources/com/powsybl/commons/reports_fr.properties | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesReports.java b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesReports.java index bf539fdb3e9..38f5fc845d0 100644 --- a/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesReports.java +++ b/cgmes/cgmes-conversion/src/main/java/com/powsybl/cgmes/conversion/CgmesReports.java @@ -285,4 +285,12 @@ public static void unexpectedPointToPointDcConfigurationReport(ReportNode report .withSeverity(TypedValue.WARN_SEVERITY) .add(); } + + public static void phaseTapChangerCurrentLimiterModeNotSupportedReport(ReportNode reportNode, String phaseTapChangerId) { + reportNode.newReportNode() + .withMessageTemplate("core.cgmes.conversion.phaseTapChangerCurrentLimiterModeNotSupported") + .withUntypedValue("phaseTapChangerId", phaseTapChangerId) + .withSeverity(TypedValue.WARN_SEVERITY) + .add(); + } } 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 12c6bba5edd..3f247437e5a 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 @@ -8,6 +8,7 @@ package com.powsybl.cgmes.conversion.export; import com.powsybl.cgmes.conversion.CgmesExport; +import com.powsybl.cgmes.conversion.CgmesReports; import com.powsybl.cgmes.conversion.Conversion; import com.powsybl.cgmes.conversion.naming.NamingStrategy; import com.powsybl.cgmes.conversion.export.elements.*; @@ -854,7 +855,8 @@ private static > void writePhaseTapChanger(C eq, PhaseT String controlName = twtName + "_PTC_RC"; String terminalId = CgmesExportUtil.getTerminalId(ptc.getRegulationTerminal(), context); if (ptc.getRegulationMode() == PhaseTapChanger.RegulationMode.CURRENT_LIMITER) { - //TODO log not supported regulation mode + // Log not supported regulation mode + CgmesReports.phaseTapChangerCurrentLimiterModeNotSupportedReport(context.getReportNode(), cgmesTapChangerId); // Add a CurrentLimit with the PhaseTapChanger current limiter regulation value to the regulated terminal. String operationalLimitSetId = context.getNamingStrategy().getCgmesId(ref(terminalId), ref(PhaseTapChanger.RegulationMode.CURRENT_LIMITER.name()), OPERATIONAL_LIMIT_SET); diff --git a/commons/src/main/resources/com/powsybl/commons/reports.properties b/commons/src/main/resources/com/powsybl/commons/reports.properties index 72085f9965f..b9daef79f8b 100644 --- a/commons/src/main/resources/com/powsybl/commons/reports.properties +++ b/commons/src/main/resources/com/powsybl/commons/reports.properties @@ -21,6 +21,7 @@ core.cgmes.conversion.missingMandatoryAttribute = Couldn't retrieve mandatory at core.cgmes.conversion.multipleUnpairedDanglingLinesAtSameBoundary = Multiple unpaired DanglingLines were connected at the same boundary side. Adjusted original injection from (${p0}, ${q0}) to (${p0Adjusted}, ${q0Adjusted}) for dangling line ${danglingLineId}. core.cgmes.conversion.nominalVoltageIsZero = Ignoring VoltageLevel: ${voltageLevelId} for its nominal voltage is equal to 0. core.cgmes.conversion.notVisitedDcEquipment = DCEquipment ${dcEquipmentId} is discarded as it couldn't be attached to any DCIsland. +core.cgmes.conversion.phaseTapChangerCurrentLimiterModeNotSupported = PhaseTapChanger ${phaseTapChangerId}: current limiter regulation mode isn't supported in CGMES. PhaseTapChanger regulation disabled and mode changed to active power flow. core.cgmes.conversion.removingUnattachedHvdcConverterStation = HVDC Converter Station ${converterId} will be removed since it has no attached HVDC line. core.cgmes.conversion.settingVoltagesAndAngles = Setting voltages and angles. core.cgmes.conversion.substationMapping = Original ${substationMappingSize} Substation container(s) connected by transformers have been merged in IIDM. Map of original Substation to IIDM: ${mapAsString}. diff --git a/commons/src/main/resources/com/powsybl/commons/reports_fr.properties b/commons/src/main/resources/com/powsybl/commons/reports_fr.properties index eb68376b5b1..846266d7e57 100644 --- a/commons/src/main/resources/com/powsybl/commons/reports_fr.properties +++ b/commons/src/main/resources/com/powsybl/commons/reports_fr.properties @@ -21,6 +21,7 @@ core.cgmes.conversion.missingMandatoryAttribute = Impossible de r core.cgmes.conversion.multipleUnpairedDanglingLinesAtSameBoundary = Plusieurs lignes frontières non appairées ont été connectées du même côté de la frontière. Ajustement de l'injection originale de (${p0}, ${q0}) à (${p0Adjusted}, ${q0Adjusted}) pour la ligne frontière ${danglingLineId}. core.cgmes.conversion.nominalVoltageIsZero = Poste ignoré : ${voltageLevelId} car sa tension nominale est égale à 0. core.cgmes.conversion.notVisitedDcEquipment = DCEquipment ${dcEquipmentId} est enlevé car il ne peut être rattaché à aucun DCIsland. +core.cgmes.conversion.phaseTapChangerCurrentLimiterModeNotSupported = Déphaseur ${phaseTapChangerId}: le mode de régulation en limitation de courant n'est pas supporté en CGMES. Déphaseur passé hors-régulation avec un mode de régulation en transit de puissance active. core.cgmes.conversion.removingUnattachedHvdcConverterStation = La station de conversion HVDC ${converterId} est supprimée car elle n'a pas de ligne HVDC de rattachement. core.cgmes.conversion.settingVoltagesAndAngles = Configuration des tensions et des angles. core.cgmes.conversion.substationMapping = ${substationMappingSize} sites connectés par des transformateurs ont été fusionnés dans l'IIDM. Map du site d'origine en IIDM : ${mapAsString}. From c26efe4e01e032336cef02d037f23f760bfb77f0 Mon Sep 17 00:00:00 2001 From: Romain Courtier Date: Fri, 21 Nov 2025 00:09:14 +0100 Subject: [PATCH 5/7] Fix documentation Signed-off-by: Romain Courtier --- docs/grid_exchange_formats/cgmes/export.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/grid_exchange_formats/cgmes/export.md b/docs/grid_exchange_formats/cgmes/export.md index b409574988e..68e74a9f8bf 100644 --- a/docs/grid_exchange_formats/cgmes/export.md +++ b/docs/grid_exchange_formats/cgmes/export.md @@ -357,16 +357,15 @@ If the transformer has a `TapChanger`, the CGMES SSH `step` is written from the If the network comes from a CIM-CGMES model and the tap changer has initially a `TapChangerControl`, it always has at export too. Otherwise, a `TapChangerControl` is exported for the tap changer if it is considered as defined. A `RatioTapChanger` is considered as defined if it has a valid regulation value, a valid target deadband and a non-null regulating terminal. -A `PhaseTapChanger` is considered as defined if it has a valid regulation value, -a valid target deadband, and a non-null regulating terminal. By default its regulation mode is set to `CURRENT_LIMITER`. +A `PhaseTapChanger` is considered as defined if it has a valid regulation value, a valid target deadband, and a non-null regulating terminal. In a `RatioTapChanger`, the `TapChangerControl` is exported with `RegulatingControl.mode` set to `RegulatingControlModeKind.reactivePower` when `RatioTapChanger` `regulationMode` is set to `REACTIVE_POWER`, and with `RegulatingControl.mode` set to `RegulatingControlModeKind.voltage` when `RatioTapChanger` `regulationMode` is set to `VOLTAGE`. -In a `PhaseTapChanger`, the `TapChangerControl` is exported with `RegulatingControl.mode` set to `RegulatingControlModeKind.activePower` when -`PhaseTapChanger` `regulationMode` is set to `ACTIVE_POWER_CONTROL`, and with `RegulatingControl.mode` set to `RegulatingControlModeKind.currentFlow` -when `PhaseTapChanger` `regulationMode` is set to `CURRENT_LIMITER`. +In a `PhaseTapChanger`, the `TapChangerControl` is always exported with `RegulatingControl.mode` set to `RegulatingControlModeKind.activePower`. +If the original `PhaseTapChanger` `regulationMode` is `CURRENT_LIMITER`, the `TapChangerControl` is put off regulation +and a `OperationalLimitSet` with a `CurrentLimit` is created at the regulated terminal with the regulation value. (cgmes-two-winding-transformer-export)= ### TwoWindingsTransformer From 0009d8eb77d429d9e7666535e10d597d89828803 Mon Sep 17 00:00:00 2001 From: Romain Courtier Date: Tue, 25 Nov 2025 16:27:19 +0100 Subject: [PATCH 6/7] Apply reviewer's comments Signed-off-by: Romain Courtier --- docs/grid_exchange_formats/cgmes/export.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/grid_exchange_formats/cgmes/export.md b/docs/grid_exchange_formats/cgmes/export.md index 68e74a9f8bf..564704e7db2 100644 --- a/docs/grid_exchange_formats/cgmes/export.md +++ b/docs/grid_exchange_formats/cgmes/export.md @@ -364,8 +364,8 @@ In a `RatioTapChanger`, the `TapChangerControl` is exported with `RegulatingCont `RatioTapChanger` `regulationMode` is set to `VOLTAGE`. In a `PhaseTapChanger`, the `TapChangerControl` is always exported with `RegulatingControl.mode` set to `RegulatingControlModeKind.activePower`. -If the original `PhaseTapChanger` `regulationMode` is `CURRENT_LIMITER`, the `TapChangerControl` is put off regulation -and a `OperationalLimitSet` with a `CurrentLimit` is created at the regulated terminal with the regulation value. +If the original `PhaseTapChanger` `regulationMode` is `CURRENT_LIMITER`, the `TapChangerControl` regulation is disabled +and an `OperationalLimitSet` with a `CurrentLimit` is created at the regulated terminal with the regulation value. (cgmes-two-winding-transformer-export)= ### TwoWindingsTransformer From 1fd599a7e1d74d7d19fd2abeba3fc00d65865d36 Mon Sep 17 00:00:00 2001 From: Romain Courtier Date: Tue, 25 Nov 2025 17:43:16 +0100 Subject: [PATCH 7/7] Apply reviewer's comment Signed-off-by: Romain Courtier --- .../export/SteadyStateHypothesisExport.java | 11 ++++------- .../conversion/test/TapChangerConversionTest.java | 2 ++ .../test/export/SteadyStateHypothesisExportTest.java | 12 ++++++------ docs/grid_exchange_formats/cgmes/export.md | 5 +++-- 4 files changed, 15 insertions(+), 15 deletions(-) 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 4ed1dd03b62..91c387e19f6 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 @@ -488,33 +488,30 @@ private static void addRegulatingControlView(TapChanger tc, CgmesTap } else if (tc instanceof PhaseTapChanger phaseTapChanger && CgmesExportUtil.regulatingControlIsDefined(phaseTapChanger)) { boolean valid; - boolean regulating; String unitMultiplier = switch (phaseTapChanger.getRegulationMode()) { case PhaseTapChanger.RegulationMode.CURRENT_LIMITER -> { // Unit multiplier is none (multiply by 1), regulation value is a current in Amperes valid = true; - regulating = false; yield "none"; } case PhaseTapChanger.RegulationMode.ACTIVE_POWER_CONTROL -> { // Unit multiplier is M, regulation value is an active power flow in MW valid = true; - regulating = phaseTapChanger.isRegulating(); yield "M"; } default -> { valid = false; - regulating = false; yield "none"; } }; if (valid) { + boolean isActivePowerControlMode = phaseTapChanger.getRegulationMode() == PhaseTapChanger.RegulationMode.ACTIVE_POWER_CONTROL; rcv = new RegulatingControlView(controlId, RegulatingControlType.TAP_CHANGER_CONTROL, true, - regulating, - phaseTapChanger.getTargetDeadband(), - phaseTapChanger.getRegulationValue(), + isActivePowerControlMode && phaseTapChanger.isRegulating(), + isActivePowerControlMode ? phaseTapChanger.getTargetDeadband() : 0.0, + isActivePowerControlMode ? phaseTapChanger.getRegulationValue() : 0.0, unitMultiplier); } } diff --git a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/TapChangerConversionTest.java b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/TapChangerConversionTest.java index e326570bf23..473ddfaf7c7 100644 --- a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/TapChangerConversionTest.java +++ b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/TapChangerConversionTest.java @@ -96,6 +96,8 @@ void currentLimiterExportTest() throws IOException { String sshFile = writeCgmesProfile(network, "SSH", tmpDir); tapChangerControl = getElement(sshFile, "TapChangerControl", "PS1_PTC_RC"); assertEquals("false", getAttribute(tapChangerControl, "RegulatingControl.enabled")); + assertEquals("0", getAttribute(tapChangerControl, "RegulatingControl.targetDeadband")); + assertEquals("0", getAttribute(tapChangerControl, "RegulatingControl.targetValue")); } } diff --git a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/SteadyStateHypothesisExportTest.java b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/SteadyStateHypothesisExportTest.java index 07081e75af9..fd44cbb5e3a 100644 --- a/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/SteadyStateHypothesisExportTest.java +++ b/cgmes/cgmes-conversion/src/test/java/com/powsybl/cgmes/conversion/test/export/SteadyStateHypothesisExportTest.java @@ -401,10 +401,10 @@ void phaseTapChangerTapChangerControlSSHTest() throws IOException { // PST with no regulation mode but regulating true => set to CURRENT_LIMITER mode network = PhaseShifterTestCaseFactory.createWithTargetDeadband(); ssh = getSSH(network, baseName, tmpDir, exportParams); - testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "true", "10", "200", "none"); + testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "true", "0", "0", "none"); network.getTwoWindingsTransformer("PS1").getPhaseTapChanger().setRegulating(false); ssh = getSSH(network, baseName, tmpDir, exportParams); - testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "false", "10", "200", "none"); + testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "false", "0", "0", "none"); // PST local with ACTIVE_POWER_CONTROL network = PhaseShifterTestCaseFactory.createLocalActivePowerWithTargetDeadband(); @@ -417,18 +417,18 @@ void phaseTapChangerTapChangerControlSSHTest() throws IOException { // PST local with CURRENT_LIMITER network = PhaseShifterTestCaseFactory.createLocalCurrentLimiterWithTargetDeadband(); ssh = getSSH(network, baseName, tmpDir, exportParams); - testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "true", "10", "200", "none"); + testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "true", "0", "0", "none"); network.getTwoWindingsTransformer("PS1").getPhaseTapChanger().setRegulating(false); ssh = getSSH(network, baseName, tmpDir, exportParams); - testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "false", "10", "200", "none"); + testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "false", "0", "0", "none"); // PST remote with CURRENT_LIMITER network = PhaseShifterTestCaseFactory.createRemoteCurrentLimiterWithTargetDeadband(); ssh = getSSH(network, baseName, tmpDir, exportParams); - testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "true", "10", "200", "none"); + testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "true", "0", "0", "none"); network.getTwoWindingsTransformer("PS1").getPhaseTapChanger().setRegulating(false); ssh = getSSH(network, baseName, tmpDir, exportParams); - testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "false", "10", "200", "none"); + testTcTccWithAttribute(ssh, "_PS1_PTC_RC", "true", "false", "0", "0", "none"); // PST remote with ACTIVE_POWER_CONTROL network = PhaseShifterTestCaseFactory.createRemoteActivePowerWithTargetDeadband(); diff --git a/docs/grid_exchange_formats/cgmes/export.md b/docs/grid_exchange_formats/cgmes/export.md index fc4777aa96e..7315a31a584 100644 --- a/docs/grid_exchange_formats/cgmes/export.md +++ b/docs/grid_exchange_formats/cgmes/export.md @@ -417,8 +417,9 @@ In a `RatioTapChanger`, the `TapChangerControl` is exported with `RegulatingCont `RatioTapChanger` `regulationMode` is set to `VOLTAGE`. In a `PhaseTapChanger`, the `TapChangerControl` is always exported with `RegulatingControl.mode` set to `RegulatingControlModeKind.activePower`. -If the original `PhaseTapChanger` `regulationMode` is `CURRENT_LIMITER`, the `TapChangerControl` regulation is disabled -and an `OperationalLimitSet` with a `CurrentLimit` is created at the regulated terminal with the regulation value. +If the original `PhaseTapChanger` `regulationMode` is `CURRENT_LIMITER`, the `TapChangerControl` regulation is disabled, +the regulation target value and deadband are set to 0, and an `OperationalLimitSet` with a `CurrentLimit` is created +at the regulated terminal with the regulation value. (cgmes-two-winding-transformer-export)= ### TwoWindingsTransformer