TieLineUtil.java
/**
* Copyright (c) 2021, 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.iidm.network.util;
import com.google.common.collect.Sets;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.iidm.network.*;
import org.apache.commons.math3.complex.Complex;
import com.powsybl.iidm.network.util.LinkData.BranchAdmittanceMatrix;
import org.apache.commons.math3.complex.ComplexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author Luma Zamarre��o {@literal <zamarrenolm at aia.es>}
* @author Jos�� Antonio Marqu��s {@literal <marquesja at aia.es>}
*/
public final class TieLineUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(TieLineUtil.class);
public static final String NO_TIE_LINE_MESSAGE = "No tie line automatically created, tie lines must be created by hand.";
private TieLineUtil() {
}
public static String buildMergedId(String id1, String id2) {
if (id1.compareTo(id2) < 0) {
return id1 + " + " + id2;
}
if (id1.compareTo(id2) > 0) {
return id2 + " + " + id1;
}
return id1;
}
public static String buildMergedName(String id1, String id2, String name1, String name2) {
if (name1 == null) {
return name2;
}
if (name2 == null) {
return name1;
}
if (name1.compareTo(name2) == 0) {
return name1;
}
if (id1.compareTo(id2) < 0) {
return name1 + " + " + name2;
}
if (id1.compareTo(id2) > 0) {
return name2 + " + " + name1;
}
if (name1.compareTo(name2) < 0) {
return name1 + " + " + name2;
}
return name2 + " + " + name1;
}
public static void mergeProperties(DanglingLine dl1, DanglingLine dl2, Properties properties) {
mergeProperties(dl1, dl2, properties, ReportNode.NO_OP);
}
public static void mergeProperties(DanglingLine dl1, DanglingLine dl2, Properties properties, ReportNode reportNode) {
Set<String> dl1Properties = dl1.getPropertyNames();
Set<String> dl2Properties = dl2.getPropertyNames();
Set<String> commonProperties = Sets.intersection(dl1Properties, dl2Properties);
Sets.difference(dl1Properties, commonProperties).forEach(prop -> properties.setProperty(prop, dl1.getProperty(prop)));
Sets.difference(dl2Properties, commonProperties).forEach(prop -> properties.setProperty(prop, dl2.getProperty(prop)));
commonProperties.forEach(prop -> {
if (dl1.getProperty(prop).equals(dl2.getProperty(prop))) {
properties.setProperty(prop, dl1.getProperty(prop));
} else if (dl1.getProperty(prop).isEmpty()) {
LOGGER.debug("Inconsistencies of property '{}' between both sides of merged line. Side 1 is empty, keeping side 2 value '{}'", prop, dl2.getProperty(prop));
NetworkReports.propertyOnlyOnOneSide(reportNode, prop, dl2.getProperty(prop), 1, dl1.getId(), dl2.getId());
properties.setProperty(prop, dl2.getProperty(prop));
} else if (dl2.getProperty(prop).isEmpty()) {
LOGGER.debug("Inconsistencies of property '{}' between both sides of merged line. Side 2 is empty, keeping side 1 value '{}'", prop, dl1.getProperty(prop));
NetworkReports.propertyOnlyOnOneSide(reportNode, prop, dl1.getProperty(prop), 2, dl1.getId(), dl2.getId());
properties.setProperty(prop, dl1.getProperty(prop));
} else {
LOGGER.debug("Inconsistencies of property '{}' between both sides of merged line. '{}' on side 1 and '{}' on side 2. Removing the property of merged line", prop, dl1.getProperty(prop), dl2.getProperty(prop));
NetworkReports.inconsistentPropertyValues(reportNode, prop, dl1.getProperty(prop), dl2.getProperty(prop), dl1.getId(), dl2.getId());
}
});
dl1Properties.forEach(prop -> properties.setProperty(prop + "_1", dl1.getProperty(prop)));
dl2Properties.forEach(prop -> properties.setProperty(prop + "_2", dl2.getProperty(prop)));
}
public static void mergeIdenticalAliases(DanglingLine dl1, DanglingLine dl2, Map<String, String> aliases) {
mergeIdenticalAliases(dl1, dl2, aliases, ReportNode.NO_OP);
}
public static void mergeIdenticalAliases(DanglingLine dl1, DanglingLine dl2, Map<String, String> aliases, ReportNode reportNode) {
for (String alias : dl1.getAliases()) {
if (dl2.getAliases().contains(alias)) {
LOGGER.debug("Alias '{}' is found in dangling lines '{}' and '{}'. It is moved to their new tie line.", alias, dl1.getId(), dl2.getId());
NetworkReports.moveCommonAliases(reportNode, alias, dl1.getId(), dl2.getId());
String type1 = dl1.getAliasType(alias).orElse("");
String type2 = dl2.getAliasType(alias).orElse("");
if (type1.equals(type2)) {
aliases.put(alias, type1);
} else {
LOGGER.warn("Inconsistencies found for alias '{}' type in dangling lines '{}' and '{}'. Type is lost.", alias, dl1.getId(), dl2.getId());
NetworkReports.inconsistentAliasTypes(reportNode, alias, type1, type2, dl1.getId(), dl2.getId());
aliases.put(alias, "");
}
}
}
aliases.keySet().forEach(alias -> {
dl1.removeAlias(alias);
dl2.removeAlias(alias);
});
}
public static void mergeDifferentAliases(DanglingLine dl1, DanglingLine dl2, Map<String, String> aliases, ReportNode reportNode) {
for (String alias : dl1.getAliases()) {
if (!dl2.getAliases().contains(alias)) {
aliases.put(alias, dl1.getAliasType(alias).orElse(""));
}
}
for (String alias : dl2.getAliases()) {
if (!dl1.getAliases().contains(alias)) {
String type = dl2.getAliasType(alias).orElse("");
if (!type.isEmpty() && aliases.containsValue(type)) {
String tmpType = type;
String alias1 = aliases.entrySet().stream().filter(e -> tmpType.equals(e.getValue())).map(Map.Entry::getKey).findFirst().orElseThrow(IllegalStateException::new);
aliases.put(alias1, type + "_1");
LOGGER.warn("Inconsistencies found for alias type '{}'('{}' for '{}' and '{}' for '{}'). " +
"Types are respectively renamed as '{}_1' and '{}_2'.", type, alias1, dl1.getId(), alias, dl2.getId(), type, type);
NetworkReports.inconsistentAliasValues(reportNode, alias1, alias, type, dl1.getId(), dl2.getId());
type += "_2";
}
aliases.put(alias, type);
}
}
aliases.keySet().forEach(alias -> {
if (dl1.getAliases().contains(alias)) {
dl1.removeAlias(alias);
}
if (dl2.getAliases().contains(alias)) {
dl2.removeAlias(alias);
}
});
}
/**
* <b>Analyze a network and return its dangling lines which are candidate to become tie lines when merging the network with another.</b>
* <b>Is candidate for a pairing key 'k':
* <li>the only connected dangling line of pairing key 'k', if disconnected dangling lines of pairing key 'k' exist;</li>
* <li>the only disconnected dangling line of pairing key 'k', if no connected dangling line of pairing key 'k' exists.</li>
* <li>no dangling line at all</li>
* </b>
* @param network a network
* @param logPairingKey a Predicate indicating if we want to log a warning when several dangling lines are found for the same pairing key.
* @return The list of the dangling lines which are candidate to become tie lines (one or zero by pairing key)
*/
public static List<DanglingLine> findCandidateDanglingLines(Network network, Predicate<String> logPairingKey) {
Objects.requireNonNull(network);
Objects.requireNonNull(logPairingKey);
List<DanglingLine> candidates = new ArrayList<>();
Set<String> pairingKeys = new HashSet<>();
Map<String, List<DanglingLine>> connectedByPairingKey = new HashMap<>();
Map<String, List<DanglingLine>> disconnectedByPairingKey = new HashMap<>();
network.getDanglingLines(DanglingLineFilter.UNPAIRED).forEach(dl -> {
String pairingKey = dl.getPairingKey();
Map<String, List<DanglingLine>> mapToUpdate = dl.getTerminal().isConnected() ? connectedByPairingKey : disconnectedByPairingKey;
mapToUpdate.computeIfAbsent(pairingKey, k -> new ArrayList<>()).add(dl);
pairingKeys.add(pairingKey);
});
for (String pairingKey : pairingKeys) {
boolean doLog = logPairingKey.test(pairingKey);
List<DanglingLine> connected = Optional.ofNullable(connectedByPairingKey.get(pairingKey)).orElse(Collections.emptyList());
List<DanglingLine> disconnected = Optional.ofNullable(disconnectedByPairingKey.get(pairingKey)).orElse(Collections.emptyList());
if (connected.isEmpty()) {
DanglingLine dl = disconnected.get(0); // Cannot be empty here: we always have at least 1 connected or disconnected dangling line
if (disconnected.size() == 1) {
candidates.add(dl);
} else if (doLog) {
LOGGER.warn("Several disconnected dangling lines {} (and no connected one) of the same subnetwork are candidate for merging for pairing key '{}'. " + NO_TIE_LINE_MESSAGE,
disconnected.stream().map(DanglingLine::getId).toList(), pairingKey);
}
} else if (connected.size() == 1) {
DanglingLine dl = connected.get(0);
candidates.add(dl);
if (!disconnected.isEmpty() && doLog) {
LOGGER.warn("Several dangling lines {} of the same subnetwork are candidate for merging for pairing key '{}'. " +
"Only '{}' is considered (the only connected one)",
Stream.concat(Stream.of(dl.getId()), disconnected.stream().map(DanglingLine::getId)).collect(Collectors.toList()),
pairingKey, dl.getId());
}
} else if (doLog) {
LOGGER.warn("Several connected dangling lines {} of the same subnetwork are candidate for merging for pairing key '{}'. " + NO_TIE_LINE_MESSAGE,
connected.stream().map(DanglingLine::getId).toList(), pairingKey);
}
}
return candidates;
}
/**
* If it exists, find the dangling line in the merging network that should be associated to a candidate dangling line in the network to be merged.
* Two dangling lines in different IGM should be associated if:
* - they have the same non-null pairing key and are the only dangling lines to have this pairing key in their respective networks
* OR
* - they have the same non-null pairing key and are the only connected dangling lines to have this pairing key in their respective networks
*
* @param candidateDanglingLine candidate dangling line in the network to be merged
* @param getDanglingLinesByPairingKey function to retrieve dangling lines with a given pairing key in the merging network.
* @param associateDanglingLines function associating two dangling lines
*/
public static void findAndAssociateDanglingLines(DanglingLine candidateDanglingLine, Function<String, List<DanglingLine>> getDanglingLinesByPairingKey,
BiConsumer<DanglingLine, DanglingLine> associateDanglingLines) {
//TODO This method is quite complicated. It would be better to call `findCandidateDanglingLines` on both networks to merge
// and to only process the retrieved candidate lists together.
Objects.requireNonNull(candidateDanglingLine);
Objects.requireNonNull(getDanglingLinesByPairingKey);
Objects.requireNonNull(associateDanglingLines);
// mapping by pairing key
if (candidateDanglingLine.getPairingKey() != null) { // if pairing key null: no associated dangling line
// If we call this method on the results of "findCandidateDanglingLines", the following test is useless
if (candidateDanglingLine.getNetwork().getDanglingLineStream(DanglingLineFilter.UNPAIRED)
.filter(d -> d != candidateDanglingLine)
.filter(d -> candidateDanglingLine.getPairingKey().equals(d.getPairingKey()))
.anyMatch(d -> d.getTerminal().isConnected())) { // check that there is no connected dangling line with same pairing key in the network to be merged
return; // in that case, do nothing
}
List<DanglingLine> dls = getDanglingLinesByPairingKey.apply(candidateDanglingLine.getPairingKey());
if (dls != null) {
if (dls.size() == 1) { // if there is exactly one dangling line in the merging network, merge it
associateDanglingLines.accept(dls.get(0), candidateDanglingLine);
}
if (dls.size() > 1) { // if more than one dangling line in the merging network, check how many are connected
associateConnectedDanglingLine(candidateDanglingLine, dls, associateDanglingLines);
}
}
}
}
private static void associateConnectedDanglingLine(DanglingLine candidateDanglingLine, List<DanglingLine> dls,
BiConsumer<DanglingLine, DanglingLine> associateDanglingLines) {
// Connected DanglingLines
List<DanglingLine> connectedDls = dls.stream().filter(dl -> dl.getTerminal().isConnected()).toList();
// If there is exactly one connected dangling line in the merging network, merge it. Otherwise, do nothing
if (connectedDls.size() == 1) {
LOGGER.warn("Several dangling lines {} of the same subnetwork are candidate for merging for pairing key '{}'. " +
"Tie line automatically created using the only connected one '{}'.",
dls.stream().map(DanglingLine::getId).toList(), connectedDls.get(0).getPairingKey(),
connectedDls.get(0).getId());
associateDanglingLines.accept(connectedDls.get(0), candidateDanglingLine);
} else {
String status = connectedDls.size() > 1 ? "connected" : "disconnected";
LOGGER.warn("Several {} dangling lines {} of the same subnetwork are candidate for merging for pairing key '{}'. " + NO_TIE_LINE_MESSAGE,
status, connectedDls.stream().map(DanglingLine::getId).toList(),
connectedDls.get(0).getPairingKey());
}
}
public static double getR(DanglingLine dl1, DanglingLine dl2) {
LinkData.BranchAdmittanceMatrix adm = TieLineUtil.equivalentBranchAdmittanceMatrix(dl1, dl2);
// Add 0.0 to avoid negative zero, tests where the R value is compared as text, fail
return zeroImpedanceLine(adm) ? 0.0 : adm.y12().negate().reciprocal().getReal() + 0.0;
}
public static double getX(DanglingLine dl1, DanglingLine dl2) {
LinkData.BranchAdmittanceMatrix adm = TieLineUtil.equivalentBranchAdmittanceMatrix(dl1, dl2);
// Add 0.0 to avoid negative zero, tests where the X value is compared as text, fail
return zeroImpedanceLine(adm) ? 0.0 : adm.y12().negate().reciprocal().getImaginary() + 0.0;
}
public static double getG1(DanglingLine dl1, DanglingLine dl2) {
LinkData.BranchAdmittanceMatrix adm = TieLineUtil.equivalentBranchAdmittanceMatrix(dl1, dl2);
return adm.y11().add(adm.y12()).getReal();
}
public static double getB1(DanglingLine dl1, DanglingLine dl2) {
LinkData.BranchAdmittanceMatrix adm = TieLineUtil.equivalentBranchAdmittanceMatrix(dl1, dl2);
return adm.y11().add(adm.y12()).getImaginary();
}
public static double getG2(DanglingLine dl1, DanglingLine dl2) {
LinkData.BranchAdmittanceMatrix adm = TieLineUtil.equivalentBranchAdmittanceMatrix(dl1, dl2);
return adm.y22().add(adm.y21()).getReal();
}
public static double getB2(DanglingLine dl1, DanglingLine dl2) {
LinkData.BranchAdmittanceMatrix adm = TieLineUtil.equivalentBranchAdmittanceMatrix(dl1, dl2);
return adm.y22().add(adm.y21()).getImaginary();
}
public static double getBoundaryV(DanglingLine dl1, DanglingLine dl2) {
Complex boundaryV = voltageAtTheBoundaryNode(dl1, dl2);
return boundaryV.abs();
}
public static double getBoundaryAngle(DanglingLine dl1, DanglingLine dl2) {
Complex boundaryV = voltageAtTheBoundaryNode(dl1, dl2);
return Math.toDegrees(Math.atan2(boundaryV.getImaginary(), boundaryV.getReal()));
}
private static LinkData.BranchAdmittanceMatrix equivalentBranchAdmittanceMatrix(DanglingLine dl1,
DanglingLine dl2) {
// zero impedance dangling lines should be supported
BranchAdmittanceMatrix adm1 = LinkData.calculateBranchAdmittance(dl1.getR(), dl1.getX(), 1.0, 0.0, 1.0, 0.0,
new Complex(dl1.getG(), dl1.getB()), new Complex(0.0, 0.0));
BranchAdmittanceMatrix adm2 = LinkData.calculateBranchAdmittance(dl2.getR(), dl2.getX(), 1.0, 0.0, 1.0, 0.0,
new Complex(0.0, 0.0), new Complex(dl2.getG(), dl2.getB()));
if (zeroImpedanceLine(adm1) && zeroImpedanceLine(adm2)) {
return adm1;
} else if (zeroImpedanceLine(adm1)) {
return adm2;
} else if (zeroImpedanceLine(adm2)) {
return adm1;
} else {
return LinkData.kronChain(adm1, TwoSides.TWO, adm2, TwoSides.ONE);
}
}
private static boolean zeroImpedanceLine(BranchAdmittanceMatrix adm) {
if (adm.y12().getReal() == 0.0 && adm.y12().getImaginary() == 0.0) {
return true;
} else {
return adm.y21().getReal() == 0.0 && adm.y22().getImaginary() == 0.0;
}
}
private static Complex voltageAtTheBoundaryNode(DanglingLine dl1, DanglingLine dl2) {
Complex v1 = ComplexUtils.polar2Complex(DanglingLineData.getV(dl1), DanglingLineData.getTheta(dl1));
Complex v2 = ComplexUtils.polar2Complex(DanglingLineData.getV(dl2), DanglingLineData.getTheta(dl2));
BranchAdmittanceMatrix adm1 = LinkData.calculateBranchAdmittance(dl1.getR(), dl1.getX(), 1.0, 0.0, 1.0, 0.0,
new Complex(dl1.getG(), dl1.getB()), new Complex(0.0, 0.0));
BranchAdmittanceMatrix adm2 = LinkData.calculateBranchAdmittance(dl2.getR(), dl2.getX(), 1.0, 0.0, 1.0, 0.0,
new Complex(0.0, 0.0), new Complex(dl2.getG(), dl2.getB()));
if (zeroImpedanceLine(adm1) && zeroImpedanceLine(adm2)) {
return v1;
} else if (zeroImpedanceLine(adm1)) {
return v1;
} else if (zeroImpedanceLine(adm2)) {
return v2;
} else {
return adm1.y21().multiply(v1).add(adm2.y12().multiply(v2)).negate().divide(adm1.y22().add(adm2.y11()));
}
}
/**
* <p>Retrieve, if it exists, the dangling line paired to the given one.</p>
*
* @param danglingLine a dangling line
* @return an Optional containing the dangling line paired to the given one
*/
public static Optional<DanglingLine> getPairedDanglingLine(DanglingLine danglingLine) {
return danglingLine.getTieLine().map(t ->
t.getDanglingLine1() == danglingLine ? t.getDanglingLine2() : t.getDanglingLine1());
}
}