NodeContainerMapping.java
/**
* Copyright (c) 2017-2018, 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;
import com.powsybl.cgmes.model.CgmesContainer;
import com.powsybl.cgmes.model.CgmesNames;
import com.powsybl.cgmes.model.CgmesTerminal;
import com.powsybl.triplestore.api.PropertyBag;
import com.powsybl.triplestore.api.PropertyBags;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.stream.Collectors;
/**
* CGMES standard: <br>
* A PowerTransformer is contained in one Substation, but it can connect a Terminal to another different Substation <br>
* A Switch can connect to different voltageLevels
* <p>
* IIDM Model: <br>
* Ends of transformers need to be in the same substation<br>
* Ends of switches need to be in the same voltageLevel
* <p>
* Solution: <br>
* CGMES substations that are connected by transformers will be mapped to a single IIDM substation <br>
* CGMES voltageLevels that are connected by switches will be mapped to a single IIDM voltageLevel
* <p>
* Example: <br>
* We suppose that VL1, VL2, VL3, VL4, VL5, VL6 and VL7 are CGMES voltageLevels, <br>
* Sw23 is a switch connecting voltageLevels VL2 and VL3, <br>
* Sw34 is a switch connecting voltageLevels VL3 and VL4 and <br>
* Sw67 is a switch connecting voltageLevels VL6 and VL7
* <p>
* Steps: <br>
* Fill voltageLevelAdjacency Map <br>
* Two voltageLevels are adjacent if they are connected by a switch <br>
* The voltageLevelAdjacency Map will include the following records <br>
* (VL1, []) <br>
* (VL2, [VL2, VL3]) <br>
* (VL3, [VL2, VL3, VL4]) <br>
* (VL4, [VL3, VL4]) <br>
* (VL5, []) <br>
* (VL6, [VL6, VL7]) <br>
* (VL7, [VL6, VL7]) <br>
* <p>
* For each non-visited VoltageLevel-key of the voltageLevelAdjacency Map all connected voltageLevels will be calculated <br>
* Two voltageLevels are connected if they are adjacent <br>
* (allConnected method) <br>
* All connected VoltageLevels to VL1 will be [VL1] <br>
* All connected VoltageLevels to VL2 will be [VL2, VL3, VL4] <br>
* All connected VoltageLevels to VL5 will be [VL5] <br>
* All connected VoltageLevels to VL6 will be [VL6, VL7]
* <p>
* So the following voltageLevels should be merged <br>
* [VL2, VL3, VL4] and the representative (IIDM voltageLevel) will be VL2 <br>
* [VL6, VL7] and the representative (IIDM voltageLevel) will be VL6
* <p>
* And finally previous data is recorded in the voltageLevelMapping Map as <br>
* (For each merged voltageLevel a record (merged voltageLevel, representative voltageLevel) is added) <br>
* (VL3, VL2) <br>
* (VL4, VL2) <br>
* (VL7, VL6) <br>
* <p>
* The voltageLevelMapping Map will be used to assign the IIDM voltageLevel during the conversion process
* <p>
* The same algorithm is used to identify the substations that should be merged but: <br>
* Two substations are adjacent if there is a transformer between them. <br>
* The two substations associated with two adjacent voltageLevels, are adjacent if they are different substations.
* <p>
* @author Luma Zamarre��o {@literal <zamarrenolm at aia.es>}
* @author Jos�� Antonio Marqu��s {@literal <marquesja at aia.es>}
*/
public class NodeContainerMapping {
public NodeContainerMapping(Context context) {
this.context = context;
this.substationMapping = new HashMap<>();
this.voltageLevelMapping = new HashMap<>();
this.fictitiousVoltageLevels = new HashMap<>();
this.referenceVoltageLevels = new HashMap<>();
}
public boolean substationIsMapped(String cgmesIdentifier) {
String sid = context.namingStrategy().getIidmId(CgmesNames.SUBSTATION, cgmesIdentifier);
return substationMapping.containsKey(sid);
}
public String substationIidm(String cgmesIdentifier) {
String sid = context.namingStrategy().getIidmId(CgmesNames.SUBSTATION, cgmesIdentifier);
if (substationMapping.containsKey(sid)) {
return substationMapping.get(sid);
}
return sid;
}
// All the keys for a given value, all the merged substations that have cgmesIdentifier as representative
public Set<String> mergedSubstations(String cgmesIdentifier) {
String sid = context.namingStrategy().getIidmId(CgmesNames.SUBSTATION, cgmesIdentifier);
return substationMapping.entrySet().stream().filter(r -> r.getValue().equals(sid))
.map(Map.Entry::getKey).collect(Collectors.toSet());
}
public boolean voltageLevelIsMapped(String cgmesIdentifier) {
String vlid = context.namingStrategy().getIidmId(CgmesNames.VOLTAGE_LEVEL, cgmesIdentifier);
return voltageLevelMapping.containsKey(vlid);
}
public String voltageLevelIidm(String cgmesIdentifier) {
String vlid = context.namingStrategy().getIidmId(CgmesNames.VOLTAGE_LEVEL, cgmesIdentifier);
if (voltageLevelMapping.containsKey(vlid)) {
return voltageLevelMapping.get(vlid);
}
return vlid;
}
// All the keys for a given value, all the merged voltageLevels that have cgmesIdentifier as representative
public Set<String> mergedVoltageLevels(String cgmesIdentifier) {
String vlid = context.namingStrategy().getIidmId(CgmesNames.VOLTAGE_LEVEL, cgmesIdentifier);
return voltageLevelMapping.entrySet().stream().filter(r -> r.getValue().equals(vlid))
.map(Map.Entry::getKey).collect(Collectors.toSet());
}
public void build() {
Map<String, Set<String>> voltageLevelAdjacency = new HashMap<>();
Map<String, Set<String>> substationAdjacency = new HashMap<>();
Map<String, Set<String>> fictitiousVoltageLevelAdjacency = new HashMap<>();
buildAdjacency(voltageLevelAdjacency, substationAdjacency, fictitiousVoltageLevelAdjacency);
buildVoltageLevelMapping(voltageLevelAdjacency);
buildSubstationMapping(substationAdjacency);
buildReferenceVoltageLevels(fictitiousVoltageLevelAdjacency);
// substation containers including connectivityNodes must be connected using switches to other containers (voltageLevel, Line)
// then there is no need for a new container
fictitiousVoltageLevels.keySet().stream()
.filter(this::isSubstationContainer)
.map(this::voltageLevelIidm) // map the representative voltageLevel
.filter(voltageLevelRepresentative -> fictitiousVoltageLevels.containsKey(voltageLevelRepresentative) && isSubstationContainer(voltageLevelRepresentative)) // representative must be fictitious
.findFirst().ifPresent(fictitiousVoltageLevelForSubstationContainer -> {
String containerId = getContainerId(fictitiousVoltageLevelForSubstationContainer).orElseThrow();
throw new ConversionException("Substation container directly associated with connectivity or topological nodes. It is not expected to create a fictitious voltage level: " + containerId);
});
}
private void buildAdjacency(Map<String, Set<String>> voltageLevelAdjacency, Map<String, Set<String>> substationAdjacency,
Map<String, Set<String>> fictitiousVoltageLevelAdjacency) {
boolean fictitiousVoltageLevelForEveryNode = context.config().getCreateFictitiousVoltageLevelsForEveryNode();
context.cgmes().switches().forEach(sw -> addAdjacencyThroughSwitch(voltageLevelAdjacency, substationAdjacency, sw, fictitiousVoltageLevelForEveryNode));
context.cgmes().transformers().stream()
.map(t -> context.transformerEnds(t.getId("PowerTransformer")))
.forEach(tends -> addAdjacencyThroughTransformerEnds(substationAdjacency, tends));
context.cgmes().acLineSegments().forEach(ac -> addAdjacencyThroughBranch(fictitiousVoltageLevelAdjacency, ac, fictitiousVoltageLevelForEveryNode));
context.cgmes().seriesCompensators().forEach(sc -> addAdjacencyThroughBranch(fictitiousVoltageLevelAdjacency, sc, fictitiousVoltageLevelForEveryNode));
}
// Two different voltageLevels are adjacent if they are connected by a switch
// If the corresponding substations are different they are also adjacent
private void addAdjacencyThroughSwitch(Map<String, Set<String>> voltageLevelAdjacency,
Map<String, Set<String>> substationAdjacency,
PropertyBag sw, boolean fictitiousVoltageLevelForEveryNode) {
CgmesTerminal t1 = context.cgmes().terminal(sw.getId(CgmesNames.TERMINAL + 1));
CgmesTerminal t2 = context.cgmes().terminal(sw.getId(CgmesNames.TERMINAL + 2));
Optional<String> nodeId1 = context.cgmes().node(t1, context.nodeBreaker());
Optional<String> nodeId2 = context.cgmes().node(t2, context.nodeBreaker());
if (nodeId1.isPresent() && !context.boundary().containsNode(nodeId1.get()) && nodeId2.isPresent() && !context.boundary().containsNode(nodeId2.get())) {
Optional<CgmesContainer> cgmesContainer1 = context.cgmes().nodeContainer(nodeId1.get());
Optional<CgmesContainer> cgmesContainer2 = context.cgmes().nodeContainer(nodeId2.get());
if (cgmesContainer1.isPresent() && cgmesContainer2.isPresent()) {
String voltageLevelId1 = findVoltageLevelAndRecordItIfItIsFictitious(cgmesContainer1.get(), nodeId1.get(), fictitiousVoltageLevelForEveryNode);
String voltageLevelId2 = findVoltageLevelAndRecordItIfItIsFictitious(cgmesContainer2.get(), nodeId2.get(), fictitiousVoltageLevelForEveryNode);
addAdjacency(voltageLevelAdjacency, voltageLevelId1, voltageLevelId2);
addAdjacency(substationAdjacency, cgmesContainer1.get().substation(), cgmesContainer2.get().substation());
}
}
}
private String findVoltageLevelAndRecordItIfItIsFictitious(CgmesContainer cgmesContainer, String nodeId, boolean fictitiousVoltageLevelForEveryNode) {
if (cgmesContainer.isVoltageLevel()) {
return cgmesContainer.voltageLevel();
} else {
String fictitiousVoltageLevelId = getFictitiousVoltageLevelForContainer(cgmesContainer.id(), nodeId, fictitiousVoltageLevelForEveryNode);
recordFictitiousVoltageLevel(fictitiousVoltageLevels, fictitiousVoltageLevelId, cgmesContainer.id(), nodeId);
return fictitiousVoltageLevelId;
}
}
private static void recordFictitiousVoltageLevel(Map<String, ContainerR> fictitiousVoltageLevels, String fictitiousVoltageLevelId, String cgmesContainerId, String nodeId) {
if (fictitiousVoltageLevels.containsKey(fictitiousVoltageLevelId)) {
if (fictitiousVoltageLevels.get(fictitiousVoltageLevelId).containerId().equals(cgmesContainerId)) {
fictitiousVoltageLevels.get(fictitiousVoltageLevelId).nodeIdSet().add(nodeId);
} else {
throw new ConversionException("Unexpected cgmesContainerId: " + cgmesContainerId);
}
} else {
Set<String> nodeIdSet = new HashSet<>();
nodeIdSet.add(nodeId);
fictitiousVoltageLevels.put(fictitiousVoltageLevelId, new ContainerR(cgmesContainerId, nodeIdSet));
}
}
// Two different substations are adjacent if they are connected by a transformer
private void addAdjacencyThroughTransformerEnds(Map<String, Set<String>> substationAdjacency, PropertyBags tends) {
List<String> substationsIds = substationsIds(tends);
if (substationsIds.size() <= 1) {
return;
}
String sub0 = substationsIds.get(0);
for (int i = 1; i < substationsIds.size(); i++) {
addAdjacency(substationAdjacency, sub0, substationsIds.get(i));
}
}
private void addAdjacencyThroughBranch(Map<String, Set<String>> fictitiousVoltageLevelAdjacency,
PropertyBag equipment, boolean fictitiousVoltageLevelByNode) {
CgmesTerminal t1 = context.cgmes().terminal(equipment.getId(CgmesNames.TERMINAL + 1));
CgmesTerminal t2 = context.cgmes().terminal(equipment.getId(CgmesNames.TERMINAL + 2));
Optional<String> nodeId1 = context.cgmes().node(t1, context.nodeBreaker());
Optional<String> nodeId2 = context.cgmes().node(t2, context.nodeBreaker());
if (nodeId1.isPresent() && !context.boundary().containsNode(nodeId1.get()) && nodeId2.isPresent() && !context.boundary().containsNode(nodeId2.get())) {
Optional<CgmesContainer> cgmesContainer1 = context.cgmes().nodeContainer(nodeId1.get());
Optional<CgmesContainer> cgmesContainer2 = context.cgmes().nodeContainer(nodeId2.get());
if (cgmesContainer1.isPresent() && cgmesContainer2.isPresent() && isValidReference(cgmesContainer1.get(), cgmesContainer2.get())) {
String voltageLevelId1 = findVoltageLevelAndRecordItIfItIsFictitious(cgmesContainer1.get(), nodeId1.get(), fictitiousVoltageLevelByNode);
String voltageLevelId2 = findVoltageLevelAndRecordItIfItIsFictitious(cgmesContainer2.get(), nodeId2.get(), fictitiousVoltageLevelByNode);
addAdjacency(fictitiousVoltageLevelAdjacency, voltageLevelId1, voltageLevelId2);
}
}
}
// one or both of them must be LineContainers
private static boolean isValidReference(CgmesContainer cgmesContainer1, CgmesContainer cgmesContainer2) {
return !(cgmesContainer1.isVoltageLevel() && cgmesContainer2.isVoltageLevel());
}
private static void addAdjacency(Map<String, Set<String>> adjacency, String id1, String id2) {
if (isValidAdjacency(id1, id2)) {
adjacency.computeIfAbsent(id1, k -> new HashSet<>()).add(id2);
adjacency.computeIfAbsent(id2, k -> new HashSet<>()).add(id1);
}
}
private static boolean isValidAdjacency(String ad1, String ad2) {
return ad1 != null && ad2 != null && !ad1.equals(ad2);
}
private void buildVoltageLevelMapping(Map<String, Set<String>> voltageLevelAdjacency) {
Set<String> visitedVoltageLevels = new HashSet<>();
for (String vl : voltageLevelAdjacency.keySet()) {
if (!visitedVoltageLevels.contains(vl)) {
Set<String> vlAds = allConnected(voltageLevelAdjacency, visitedVoltageLevels, vl);
String selectedVoltageLevelId = representativeVoltageLevelId(vlAds);
recordMergedIds(voltageLevelMapping, vlAds, selectedVoltageLevelId);
}
}
if (!voltageLevelMapping.isEmpty()) {
CgmesReports.voltageLevelMappingReport(context.getReportNode(), voltageLevelMapping.size(), voltageLevelMapping.toString());
LOG.warn("Original {} VoltageLevel container(s) connected by switches have been merged in IIDM. Map of original VoltageLevel to IIDM: {}",
voltageLevelMapping.size(), voltageLevelMapping);
}
}
private void buildSubstationMapping(Map<String, Set<String>> substationAdjacency) {
Set<String> visitedSubstations = new HashSet<>();
for (String sub : substationAdjacency.keySet()) {
if (!visitedSubstations.contains(sub)) {
Set<String> subAds = allConnected(substationAdjacency, visitedSubstations, sub);
String selectedSubstationId = representativeSubstationId(subAds);
recordMergedIds(substationMapping, subAds, selectedSubstationId);
}
}
if (!substationMapping.isEmpty()) {
CgmesReports.substationMappingReport(context.getReportNode(), substationMapping.size(), substationMapping.toString());
LOG.warn("Original {} Substation container(s) connected by transformers have been merged in IIDM. Map of original Substation to IIDM: {}",
substationMapping.size(), substationMapping);
}
}
private void buildReferenceVoltageLevels(Map<String, Set<String>> fictitiousVoltageLevelAdjacency) {
// Find a representative for fictitious voltage levels of substation containers
voltageLevelMapping.forEach((key, value) -> {
if (fictitiousVoltageLevels.containsKey(key) && !fictitiousVoltageLevels.containsKey(value)) {
referenceVoltageLevels.put(key, value);
}
});
// Find a representative for fictitious voltage levels of line containers
Set<String> visited = new HashSet<>();
for (String fictitiousVoltageLevelId : fictitiousVoltageLevelAdjacency.keySet()) {
if (!visited.contains(fictitiousVoltageLevelId)) {
Set<String>vlAds = allConnected(fictitiousVoltageLevelAdjacency, visited, fictitiousVoltageLevelId);
referenceVoltageLevel(vlAds).ifPresent(referenceId -> recordReferenceVoltageLevel(vlAds, referenceId));
}
}
// All fictitious voltage levels need a reference voltage level
fictitiousVoltageLevels.keySet().stream()
.filter(fictitiousVoltageLevel -> !referenceVoltageLevels.containsKey(fictitiousVoltageLevel))
.findFirst()
.ifPresent(fictitiousVoltageLevelIdWithoutReference -> {
throw new ConversionException("Fictitious voltage level without reference: " + fictitiousVoltageLevelIdWithoutReference);
});
}
private Optional<String> referenceVoltageLevel(Set<String> voltageLevelIds) {
return voltageLevelIds.stream()
.filter(vl -> !fictitiousVoltageLevels.containsKey(vl))
.min(Comparator.naturalOrder());
}
private void recordReferenceVoltageLevel(Set<String> voltageLevelIds, String referenceId) {
voltageLevelIds.stream()
.filter(fictitiousVoltageLevels::containsKey) // only for the fictitious voltage levels
.forEach(vl -> referenceVoltageLevels.put(vl, referenceId));
}
// Given an id (substation / voltageLevel) returns all connected ids (substations / voltageLevels)
// Two ids are connected if they are adjacent in the adjacency Map
private static Set<String> allConnected(Map<String, Set<String>> adjacency, Set<String> visited, String id) {
ArrayList<String> allConnected = new ArrayList<>();
// Insert id in the allConnected list and record it as visited
allConnected.add(id);
visited.add(id);
// Expand, adding in each step all non-visited adjacent ids
int k = 0;
while (k < allConnected.size()) {
String vl0 = allConnected.get(k);
if (adjacency.containsKey(vl0)) {
adjacency.get(vl0).forEach(ad -> {
if (visited.contains(ad)) {
return;
}
allConnected.add(ad);
visited.add(ad);
});
}
k++;
}
return new HashSet<>(allConnected);
}
private String representativeVoltageLevelId(Collection<String> voltageLevelIds) {
Optional<String> existingVoltageLevelId = voltageLevelIds.stream()
.filter(voltageLevelId -> !fictitiousVoltageLevels.containsKey(voltageLevelId))
.min(Comparator.naturalOrder());
if (existingVoltageLevelId.isPresent()) {
return existingVoltageLevelId.get();
}
Optional<String> fictitiousVoltageLevelIdForLineContainer = voltageLevelIds.stream()
.filter(voltageLevelId -> !isSubstationContainer(voltageLevelId)) // Fictitious for LineContainer
.min(Comparator.naturalOrder());
return fictitiousVoltageLevelIdForLineContainer.orElseGet(() -> voltageLevelIds.stream().min(Comparator.naturalOrder())
.orElseThrow(() -> new IllegalStateException("Unexpected: voltageLevelIds list is empty")));
}
private String representativeSubstationId(Collection<String> substationIds) {
return substationIds.stream()
.filter(substationId -> context.config().substationIdsExcludedFromMapping()
.stream()
.noneMatch(substationId::matches))
.min(Comparator.naturalOrder())
.orElse(substationIds.iterator().next());
}
// For each merged substation a record (merged substation, representative substation) is added
// For each merged voltageLevel a record (merged voltageLevel, representative voltageLevel) is added
private static void recordMergedIds(Map<String, String> mapping, Collection<String> mergedIds, String representativeId) {
for (String id : mergedIds) {
if (!id.equals(representativeId)) {
mapping.put(id, representativeId);
}
}
}
private List<String> substationsIds(PropertyBags tends) {
List<String> substationsIds = new ArrayList<>();
for (PropertyBag end : tends) {
CgmesTerminal t = context.cgmes().terminal(end.getId(CgmesNames.TERMINAL));
Optional<String> nodeId = context.cgmes().node(t, context.nodeBreaker());
if (nodeId.isPresent() && !context.boundary().containsNode(nodeId.get())) {
Optional<CgmesContainer> cgmesContainer = context.cgmes().nodeContainer(nodeId.get());
if (cgmesContainer.isPresent()) {
String sid = cgmesContainer.get().substation();
if (sid != null) {
substationsIds.add(context.namingStrategy().getIidmId(CgmesNames.SUBSTATION, sid));
}
}
}
}
return substationsIds;
}
// Only iidm voltage levels that are fictitious
Set<String> getFictitiousVoltageLevelsForLineContainersToBeCreated() {
return fictitiousVoltageLevels.keySet().stream()
.filter(fictitiousVoltageLevelId -> !isSubstationContainer(fictitiousVoltageLevelId))
.map(this::voltageLevelIidm) // map the representative voltageLevel
.filter(fictitiousVoltageLevels::containsKey) // representative must be fictitious
.collect(Collectors.toSet());
}
Optional<String> getContainerId(String fictitiousVoltageLevelId) {
return fictitiousVoltageLevels.containsKey(fictitiousVoltageLevelId) ? Optional.of(fictitiousVoltageLevels.get(fictitiousVoltageLevelId).containerId()) : Optional.empty();
}
Optional<String> getContainerName(String fictitiousVoltageLevelId) {
Optional<String> containerId = getContainerId(fictitiousVoltageLevelId);
if (containerId.isPresent()) {
CgmesContainer cgmesContainer = context.cgmes().container(containerId.get());
return cgmesContainer != null ? Optional.of(cgmesContainer.name()) : Optional.empty();
} else {
return Optional.empty();
}
}
private boolean isSubstationContainer(String fictitiousVoltageLevelId) {
Optional<String> containerId = getContainerId(fictitiousVoltageLevelId);
if (containerId.isPresent()) {
CgmesContainer cgmesContainer = context.cgmes().container(containerId.get());
return cgmesContainer != null && cgmesContainer.isSubstation();
} else {
return false;
}
}
Optional<String> getReferenceVoltageLevelId(String fictitiousVoltageLevelId) {
if (referenceVoltageLevels.containsKey(fictitiousVoltageLevelId)) {
String referenceVoltageLevelId = referenceVoltageLevels.get(fictitiousVoltageLevelId);
return Optional.ofNullable(voltageLevelIidm(referenceVoltageLevelId));
} else {
return Optional.empty();
}
}
public String getFictitiousVoltageLevelForContainer(String containerId, String nodeId) {
return getFictitiousVoltageLevelForContainer(containerId, nodeId, context.config().getCreateFictitiousVoltageLevelsForEveryNode());
}
private static String getFictitiousVoltageLevelForContainer(String containerId, String nodeId, boolean fictitiousVoltageLevelForEveryNode) {
// ? one voltage level for each node : one voltage level for each container
return fictitiousVoltageLevelForEveryNode ? nodeId + "_VL" : containerId + "_VL";
}
private final Context context;
private final Map<String, String> substationMapping;
private final Map<String, String> voltageLevelMapping;
private final Map<String, ContainerR> fictitiousVoltageLevels;
private final Map<String, String> referenceVoltageLevels;
private static final Logger LOG = LoggerFactory.getLogger(NodeContainerMapping.class);
private record ContainerR(String containerId, Set<String> nodeIdSet) {
}
}