-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add backward compatibility for CgmesControlAreas extension deserializ…
…ation (#3298) * Add a backward compatibility SerDe for removed "CgmesControlAreas" extension * Skip CgmesControlArea if an Area with the same id already exists * Improve test coverage: Boundary on HVDC + side * Code review * Make CgmesControlAreasSerDe non versionable * Adaptation after rebase: TreeDataReader.skipChildNodes => skipNode Signed-off-by: Olivier Perrin <olivier.perrin@rte-france.com>
- Loading branch information
Showing
3 changed files
with
297 additions
and
0 deletions.
There are no files selected for viewing
153 changes: 153 additions & 0 deletions
153
...ions/src/main/java/com/powsybl/cgmes/extensions/compatibility/CgmesControlAreasSerDe.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
/** | ||
* 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.extensions.compatibility; | ||
|
||
import com.google.auto.service.AutoService; | ||
import com.google.common.base.Strings; | ||
import com.powsybl.cgmes.model.CgmesNames; | ||
import com.powsybl.commons.PowsyblException; | ||
import com.powsybl.commons.extensions.AbstractExtensionSerDe; | ||
import com.powsybl.commons.extensions.Extension; | ||
import com.powsybl.commons.extensions.ExtensionSerDe; | ||
import com.powsybl.commons.io.DeserializerContext; | ||
import com.powsybl.commons.io.SerializerContext; | ||
import com.powsybl.commons.io.TreeDataReader; | ||
import com.powsybl.iidm.network.*; | ||
import com.powsybl.iidm.serde.NetworkDeserializerContext; | ||
import com.powsybl.iidm.serde.TerminalRefSerDe; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.util.Map; | ||
import java.util.OptionalDouble; | ||
|
||
/** | ||
* @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>} | ||
*/ | ||
@AutoService(ExtensionSerDe.class) | ||
public class CgmesControlAreasSerDe extends AbstractExtensionSerDe<Network, CgmesControlAreasSerDe.DummyExt> { | ||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(CgmesControlAreasSerDe.class); | ||
|
||
private static final String CONTROL_AREA_ROOT_ELEMENT = "controlArea"; | ||
private static final String CONTROL_AREA_ARRAY_ELEMENT = "controlAreas"; | ||
public static final String TERMINAL_ROOT_ELEMENT = "terminal"; | ||
public static final String TERMINAL_ARRAY_ELEMENT = "terminals"; | ||
public static final String BOUNDARY_ROOT_ELEMENT = "boundary"; | ||
public static final String BOUNDARY_ARRAY_ELEMENT = "boundaries"; | ||
|
||
public CgmesControlAreasSerDe() { | ||
super("cgmesControlAreas", "network", CgmesControlAreasSerDe.DummyExt.class, "cgmesControlAreas.xsd", | ||
"http://www.powsybl.org/schema/iidm/ext/cgmes_control_areas/1_0", "cca"); | ||
} | ||
|
||
@Override | ||
public Map<String, String> getArrayNameToSingleNameMap() { | ||
return Map.of(CONTROL_AREA_ARRAY_ELEMENT, CONTROL_AREA_ROOT_ELEMENT, | ||
TERMINAL_ARRAY_ELEMENT, TERMINAL_ROOT_ELEMENT, | ||
BOUNDARY_ARRAY_ELEMENT, BOUNDARY_ROOT_ELEMENT); | ||
} | ||
|
||
@Override | ||
public void write(DummyExt extension, SerializerContext context) { | ||
throw new IllegalStateException("Should not happen"); | ||
} | ||
|
||
@Override | ||
public DummyExt read(Network extendable, DeserializerContext context) { | ||
NetworkDeserializerContext networkContext = (NetworkDeserializerContext) context; | ||
TreeDataReader reader = networkContext.getReader(); | ||
reader.readChildNodes(elementName -> { | ||
if (elementName.equals(CONTROL_AREA_ROOT_ELEMENT)) { | ||
String id = reader.readStringAttribute("id"); | ||
if (extendable.getArea(id) == null) { | ||
Area area = extendable.newArea() | ||
.setAreaType(CgmesNames.CONTROL_AREA_TYPE_KIND_INTERCHANGE) | ||
.setId(id) | ||
.setName(reader.readStringAttribute("name")) | ||
.setInterchangeTarget(reader.readDoubleAttribute("netInterchange")) | ||
.add(); | ||
|
||
OptionalDouble pTolerance = reader.readOptionalDoubleAttribute("pTolerance"); | ||
pTolerance.ifPresent(t -> area.setProperty(CgmesNames.P_TOLERANCE, Double.toString(t))); | ||
|
||
String energyIdentificationCodeEic = reader.readStringAttribute("energyIdentificationCodeEic"); | ||
if (!Strings.isNullOrEmpty(energyIdentificationCodeEic)) { | ||
area.addAlias(energyIdentificationCodeEic, CgmesNames.ENERGY_IDENT_CODE_EIC); | ||
} | ||
readBoundariesAndTerminals(networkContext, area, extendable); | ||
} else { | ||
LOGGER.warn("Area with id {} already exists. Skipping this CgmesControlArea.", id); | ||
reader.skipNode(); | ||
} | ||
} else { | ||
throw new PowsyblException("Unknown element name '" + elementName + "' in 'cgmesControlArea'"); | ||
} | ||
}); | ||
return null; | ||
} | ||
|
||
private void readBoundariesAndTerminals(NetworkDeserializerContext networkContext, Area area, Network network) { | ||
TreeDataReader reader = networkContext.getReader(); | ||
reader.readChildNodes(elementName -> { | ||
switch (elementName) { | ||
case BOUNDARY_ROOT_ELEMENT -> { | ||
String id = networkContext.getAnonymizer().deanonymizeString(reader.readStringAttribute("id")); | ||
Identifiable<?> identifiable = network.getIdentifiable(id); | ||
boolean isAc = true; // Set to "true" because this piece of data is not available | ||
if (identifiable instanceof DanglingLine dl) { | ||
area.newAreaBoundary() | ||
.setAc(isAc) | ||
.setBoundary(dl.getBoundary()) | ||
.add(); | ||
} else if (identifiable instanceof TieLine tl) { | ||
TwoSides side = reader.readEnumAttribute("side", TwoSides.class); | ||
area.newAreaBoundary() | ||
.setAc(isAc) | ||
.setBoundary(tl.getDanglingLine(side).getBoundary()) | ||
.add(); | ||
} else { | ||
throw new PowsyblException("Unexpected Identifiable instance: " + identifiable.getClass()); | ||
} | ||
reader.readEndNode(); | ||
} | ||
case TERMINAL_ROOT_ELEMENT -> { | ||
Terminal terminal = TerminalRefSerDe.readTerminal(networkContext, network); | ||
area.newAreaBoundary() | ||
.setAc(terminal.getConnectable().getType() != IdentifiableType.HVDC_CONVERTER_STATION) | ||
.setTerminal(terminal) | ||
.add(); | ||
} | ||
default -> throw new PowsyblException("Unknown element name '" + elementName + "' in 'controlArea'"); | ||
} | ||
}); | ||
} | ||
|
||
@Override | ||
public boolean isSerializable(CgmesControlAreasSerDe.DummyExt ext) { | ||
return false; // Backward writing compatibility is not yet supported. | ||
} | ||
|
||
// An extension is needed to define the SerDe. But it shouldn't be used otherwise. | ||
public static class DummyExt implements Extension<Network> { | ||
@Override | ||
public String getName() { | ||
return "cgmesControlAreas"; | ||
} | ||
|
||
@Override | ||
public Network getExtendable() { | ||
throw new UnsupportedOperationException("This extension should not be used."); | ||
} | ||
|
||
@Override | ||
public void setExtendable(Network extendable) { | ||
throw new UnsupportedOperationException("This extension should not be used."); | ||
} | ||
} | ||
} |
125 changes: 125 additions & 0 deletions
125
.../src/test/java/com/powsybl/cgmes/extensions/compatibility/CgmesControlAreasSerDeTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
/** | ||
* 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.extensions.compatibility; | ||
|
||
import com.powsybl.cgmes.model.CgmesNames; | ||
import com.powsybl.iidm.network.Area; | ||
import com.powsybl.iidm.network.AreaBoundary; | ||
import com.powsybl.iidm.network.Network; | ||
import com.powsybl.iidm.network.ThreeSides; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import java.util.Iterator; | ||
|
||
import static org.junit.jupiter.api.Assertions.*; | ||
|
||
/** | ||
* @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>} | ||
*/ | ||
class CgmesControlAreasSerDeTest { | ||
|
||
private static final double EPSILON = 0.0001; | ||
|
||
@Test | ||
void readingCompatibilityTest() { | ||
Network network = Network.read("eurostag_cgmes_control_area.xml", | ||
getClass().getResourceAsStream("/eurostag_cgmes_control_area.xml")); | ||
|
||
assertEquals(4, network.getAreaCount()); | ||
|
||
{ | ||
Area area1 = network.getArea("cgmesControlAreaId"); | ||
assertNotNull(area1); | ||
assertAll( | ||
() -> assertEquals(CgmesNames.CONTROL_AREA_TYPE_KIND_INTERCHANGE, area1.getAreaType()), | ||
() -> assertEquals("cgmesControlAreaName", area1.getNameOrId()), | ||
() -> assertEquals(100.0, area1.getInterchangeTarget().orElse(Double.NaN), EPSILON), | ||
() -> assertNull(area1.getProperty(CgmesNames.P_TOLERANCE)), | ||
() -> assertEquals("energyIdentCodeEic", area1.getAliasFromType(CgmesNames.ENERGY_IDENT_CODE_EIC).orElse("")), | ||
() -> assertEquals(2, area1.getAreaBoundaryStream().count()) | ||
); | ||
Iterator<AreaBoundary> boundaryIterator = area1.getAreaBoundaries().iterator(); | ||
AreaBoundary b1 = boundaryIterator.next(); | ||
AreaBoundary b2 = boundaryIterator.next(); | ||
AreaBoundary boundaryOnTerminal; | ||
AreaBoundary boundaryOnBoundary; | ||
if (b1.getTerminal().isPresent()) { | ||
boundaryOnTerminal = b1; | ||
boundaryOnBoundary = b2; | ||
} else { | ||
boundaryOnBoundary = b1; | ||
boundaryOnTerminal = b2; | ||
} | ||
assertAll( | ||
() -> assertTrue(boundaryOnTerminal.isAc()), | ||
() -> assertEquals("NHV1_NHV2_1", boundaryOnTerminal.getTerminal().orElseThrow().getConnectable().getId()), | ||
() -> assertEquals(ThreeSides.ONE, boundaryOnTerminal.getTerminal().orElseThrow().getSide()), | ||
() -> assertFalse(boundaryOnTerminal.getBoundary().isPresent()), | ||
() -> assertTrue(boundaryOnBoundary.isAc()), | ||
() -> assertFalse(boundaryOnBoundary.getTerminal().isPresent()), | ||
() -> assertEquals("DL", boundaryOnBoundary.getBoundary().orElseThrow().getDanglingLine().getId()) | ||
); | ||
} | ||
|
||
{ | ||
Area area = network.getArea("areaId-2"); | ||
assertNotNull(area); | ||
assertAll( | ||
() -> assertEquals(CgmesNames.CONTROL_AREA_TYPE_KIND_INTERCHANGE, area.getAreaType()), | ||
() -> assertEquals("area-2", area.getNameOrId()), | ||
() -> assertEquals(90.0, area.getInterchangeTarget().orElse(Double.NaN), EPSILON), | ||
() -> assertEquals(0.024, Double.parseDouble(area.getProperty(CgmesNames.P_TOLERANCE, "NaN")), EPSILON), | ||
() -> assertFalse(area.getAliasFromType(CgmesNames.ENERGY_IDENT_CODE_EIC).isPresent()), | ||
() -> assertEquals(1, area.getAreaBoundaryStream().count()) | ||
); | ||
AreaBoundary b = area.getAreaBoundaries().iterator().next(); | ||
assertAll( | ||
() -> assertTrue(b.isAc()), | ||
() -> assertEquals("NHV1_NHV2_2", b.getTerminal().orElseThrow().getConnectable().getId()), | ||
() -> assertEquals(ThreeSides.TWO, b.getTerminal().orElseThrow().getSide()), | ||
() -> assertFalse(b.getBoundary().isPresent()) | ||
); | ||
} | ||
|
||
{ | ||
Area area = network.getArea("areaId-3"); | ||
assertNotNull(area); | ||
assertAll( | ||
() -> assertEquals(CgmesNames.CONTROL_AREA_TYPE_KIND_INTERCHANGE, area.getAreaType()), | ||
() -> assertEquals("area-3", area.getNameOrId()) | ||
); | ||
AreaBoundary b = area.getAreaBoundaries().iterator().next(); | ||
assertAll( | ||
() -> assertTrue(b.isAc()), | ||
() -> assertFalse(b.getTerminal().isPresent()), | ||
() -> assertTrue(b.getBoundary().isPresent()), | ||
() -> assertEquals("DL8", b.getBoundary().orElseThrow().getDanglingLine().getId()) | ||
); | ||
} | ||
|
||
// When an Area with the same ID already exists, the CgmesControlArea is not imported (and the Area is not updated) | ||
{ | ||
Area area = network.getArea("alreadyExistingArea"); | ||
assertNotNull(area); | ||
assertAll( | ||
() -> assertEquals(CgmesNames.CONTROL_AREA_TYPE_KIND_INTERCHANGE, area.getAreaType()), | ||
() -> assertEquals("area", area.getNameOrId()), | ||
() -> assertEquals(Double.NaN, area.getInterchangeTarget().orElse(Double.NaN)), | ||
() -> assertEquals(Double.NaN, Double.parseDouble(area.getProperty(CgmesNames.P_TOLERANCE, "NaN"))), | ||
() -> assertFalse(area.getAliasFromType(CgmesNames.ENERGY_IDENT_CODE_EIC).isPresent()), | ||
() -> assertEquals(1, area.getAreaBoundaryStream().count()) | ||
); | ||
AreaBoundary b = area.getAreaBoundaries().iterator().next(); | ||
assertAll( | ||
() -> assertTrue(b.isAc()), | ||
() -> assertEquals("DL0", b.getBoundary().orElseThrow().getDanglingLine().getId()), | ||
() -> assertFalse(b.getTerminal().isPresent()) | ||
); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters