Skip to content

Commit

Permalink
Add backward compatibility for CgmesControlAreas extension deserializ…
Browse files Browse the repository at this point in the history
…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
olperr1 authored Feb 25, 2025
1 parent d11b044 commit c5ed5a7
Show file tree
Hide file tree
Showing 3 changed files with 297 additions and 0 deletions.
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.");
}
}
}
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())
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,31 @@
<iidm:voltageLevel id="VLGEN" nominalV="24.0" topologyKind="BUS_BREAKER">
<iidm:busBreakerTopology>
<iidm:bus id="NGEN"/>
<iidm:bus id="NDL0"/>
<iidm:bus id="NDL"/>
</iidm:busBreakerTopology>
<iidm:generator id="GEN" energySource="OTHER" minP="-9999.99" maxP="9999.99" voltageRegulatorOn="true" targetP="607.0" targetV="24.5" targetQ="301.0" bus="NGEN" connectableBus="NGEN">
<iidm:minMaxReactiveLimits minQ="-9999.99" maxQ="9999.99"/>
</iidm:generator>
<iidm:danglingLine id="DL0" p0="0.0" q0="0.0" r="1.0" x="1.0" g="0.0" b="0.0" bus="NDL0" connectableBus="NDL0"/>
<iidm:danglingLine id="DL" p0="0.0" q0="0.0" r="1.0" x="1.0" g="0.0" b="0.0" bus="NDL" connectableBus="NDL"/>
</iidm:voltageLevel>
<iidm:voltageLevel id="VLHV1" nominalV="380.0" topologyKind="BUS_BREAKER">
<iidm:busBreakerTopology>
<iidm:bus id="NHV1"/>
<iidm:bus id="NDL-9"/>
</iidm:busBreakerTopology>
<iidm:danglingLine id="DL9" p0="0.0" q0="0.0" r="1.0" x="1.0" g="0.0" b="0.0" bus="NDL-9" connectableBus="NDL-9"/>
</iidm:voltageLevel>
<iidm:twoWindingsTransformer id="NGEN_NHV1" r="0.26658461538461536" x="11.104492831516762" g="0.0" b="0.0" ratedU1="24.0" ratedU2="400.0" bus1="NGEN" connectableBus1="NGEN" voltageLevelId1="VLGEN" bus2="NHV1" connectableBus2="NHV1" voltageLevelId2="VLHV1"/>
</iidm:substation>
<iidm:substation id="P2" country="FR" tso="RTE" geographicalTags="B">
<iidm:voltageLevel id="VLHV2" nominalV="380.0" topologyKind="BUS_BREAKER">
<iidm:busBreakerTopology>
<iidm:bus id="NHV2"/>
<iidm:bus id="NDL-8"/>
</iidm:busBreakerTopology>
<iidm:danglingLine id="DL8" p0="0.0" q0="0.0" r="1.0" x="1.0" g="0.0" b="0.0" bus="NDL-8" connectableBus="NDL-8"/>
</iidm:voltageLevel>
<iidm:voltageLevel id="VLLOAD" nominalV="150.0" topologyKind="BUS_BREAKER">
<iidm:busBreakerTopology>
Expand All @@ -41,12 +47,25 @@
</iidm:substation>
<iidm:line id="NHV1_NHV2_1" r="3.0" x="33.0" g1="0.0" b1="1.93E-4" g2="0.0" b2="1.93E-4" bus1="NHV1" connectableBus1="NHV1" voltageLevelId1="VLHV1" bus2="NHV2" connectableBus2="NHV2" voltageLevelId2="VLHV2"/>
<iidm:line id="NHV1_NHV2_2" r="3.0" x="33.0" g1="0.0" b1="1.93E-4" g2="0.0" b2="1.93E-4" bus1="NHV1" connectableBus1="NHV1" voltageLevelId1="VLHV1" bus2="NHV2" connectableBus2="NHV2" voltageLevelId2="VLHV2"/>
<iidm:tieLine id="TIELINE" danglingLineId1="DL9" danglingLineId2="DL8"/>
<iidm:area id="alreadyExistingArea" name="area" areaType="ControlAreaTypeKind.Interchange">
<iidm:areaBoundary ac="true" type="boundaryRef" id="DL0"/>
</iidm:area>
<iidm:extension id="sim1">
<cca:cgmesControlAreas>
<cca:controlArea id="cgmesControlAreaId" name="cgmesControlAreaName" energyIdentificationCodeEic="energyIdentCodeEic" netInterchange="100.0">
<cca:terminal id="NHV1_NHV2_1" side="ONE"/>
<cca:boundary id="DL"/>
</cca:controlArea>
<cca:controlArea id="areaId-2" name="area-2" pTolerance="0.024" netInterchange="90.0">
<cca:terminal id="NHV1_NHV2_2" side="TWO"/>
</cca:controlArea>
<cca:controlArea id="areaId-3" name="area-3" netInterchange="97.0">
<cca:boundary id="TIELINE" side="TWO"/>
</cca:controlArea>
<cca:controlArea id="alreadyExistingArea" name="area0" netInterchange="90.0">
<cca:terminal id="NHV1_NHV2_2" side="ONE"/>
</cca:controlArea>
</cca:cgmesControlAreas>
</iidm:extension>
</iidm:network>

0 comments on commit c5ed5a7

Please sign in to comment.