TopologyModificationUtils.java
/**
* Copyright (c) 2022, 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.modification.topology;
import com.google.common.collect.ImmutableList;
import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.iidm.modification.util.ModificationReports;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.extensions.BusbarSectionPosition;
import com.powsybl.iidm.network.extensions.ConnectablePosition;
import com.powsybl.math.graph.TraverseResult;
import org.apache.commons.lang3.Range;
import org.jgrapht.Graph;
import org.jgrapht.alg.util.Pair;
import org.jgrapht.graph.Pseudograph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.powsybl.iidm.modification.util.ModificationReports.*;
/**
* @author Miora Vedelago {@literal <miora.ralambotiana at rte-france.com>}
*/
public final class TopologyModificationUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(TopologyModificationUtils.class);
private TopologyModificationUtils() {
}
public static final class LoadingLimitsBags {
private final LoadingLimitsBag activePowerLimits;
private final LoadingLimitsBag apparentPowerLimits;
private final LoadingLimitsBag currentLimits;
public LoadingLimitsBags(Supplier<Optional<ActivePowerLimits>> activePowerLimitsGetter, Supplier<Optional<ApparentPowerLimits>> apparentPowerLimitsGetter,
Supplier<Optional<CurrentLimits>> currentLimitsGetter) {
activePowerLimits = activePowerLimitsGetter.get().map(LoadingLimitsBag::new).orElse(null);
apparentPowerLimits = apparentPowerLimitsGetter.get().map(LoadingLimitsBag::new).orElse(null);
currentLimits = currentLimitsGetter.get().map(LoadingLimitsBag::new).orElse(null);
}
LoadingLimitsBags(LoadingLimitsBag activePowerLimits, LoadingLimitsBag apparentPowerLimits, LoadingLimitsBag currentLimits) {
this.activePowerLimits = activePowerLimits;
this.apparentPowerLimits = apparentPowerLimits;
this.currentLimits = currentLimits;
}
Optional<LoadingLimitsBag> getActivePowerLimits() {
return Optional.ofNullable(activePowerLimits);
}
Optional<LoadingLimitsBag> getApparentPowerLimits() {
return Optional.ofNullable(apparentPowerLimits);
}
Optional<LoadingLimitsBag> getCurrentLimits() {
return Optional.ofNullable(currentLimits);
}
}
private static final class LoadingLimitsBag {
private final double permanentLimit;
private List<TemporaryLimitsBag> temporaryLimits = new ArrayList<>();
private LoadingLimitsBag(LoadingLimits limits) {
this.permanentLimit = limits.getPermanentLimit();
for (LoadingLimits.TemporaryLimit tl : limits.getTemporaryLimits()) {
temporaryLimits.add(new TemporaryLimitsBag(tl));
}
}
private LoadingLimitsBag(double permanentLimit, List<TemporaryLimitsBag> temporaryLimitsBags) {
this.permanentLimit = permanentLimit;
this.temporaryLimits = temporaryLimitsBags;
}
private double getPermanentLimit() {
return permanentLimit;
}
private List<TemporaryLimitsBag> getTemporaryLimits() {
return ImmutableList.copyOf(temporaryLimits);
}
}
private static final class TemporaryLimitsBag {
private final String name;
private final int acceptableDuration;
private final boolean fictitious;
private final double value;
TemporaryLimitsBag(LoadingLimits.TemporaryLimit temporaryLimit) {
this.name = temporaryLimit.getName();
this.acceptableDuration = temporaryLimit.getAcceptableDuration();
this.fictitious = temporaryLimit.isFictitious();
this.value = temporaryLimit.getValue();
}
private String getName() {
return name;
}
private int getAcceptableDuration() {
return acceptableDuration;
}
private boolean isFictitious() {
return fictitious;
}
private double getValue() {
return value;
}
}
static LineAdder createLineAdder(double percent, String id, String name, String voltageLevelId1, String voltageLevelId2, Network network, Line line) {
return network.newLine()
.setId(id)
.setName(name)
.setVoltageLevel1(voltageLevelId1)
.setVoltageLevel2(voltageLevelId2)
.setR(line.getR() * percent / 100)
.setX(line.getX() * percent / 100)
.setG1(line.getG1() * percent / 100)
.setB1(line.getB1() * percent / 100)
.setG2(line.getG2() * percent / 100)
.setB2(line.getB2() * percent / 100);
}
static LineAdder createLineAdder(String id, String name, String voltageLevelId1, String voltageLevelId2, Network network, Line line1, Line line2) {
return network.newLine()
.setId(id)
.setName(name)
.setVoltageLevel1(voltageLevelId1)
.setVoltageLevel2(voltageLevelId2)
.setR(line1.getR() + line2.getR())
.setX(line1.getX() + line2.getX())
.setG1(line1.getG1() + line2.getG1())
.setB1(line1.getB1() + line2.getB1())
.setG2(line1.getG2() + line2.getG2())
.setB2(line1.getB2() + line2.getB2());
}
static void attachLine(Terminal terminal, LineAdder adder, BiConsumer<Bus, LineAdder> connectableBusSetter,
BiConsumer<Bus, LineAdder> busSetter, BiConsumer<Integer, LineAdder> nodeSetter) {
if (terminal.getVoltageLevel().getTopologyKind() == TopologyKind.BUS_BREAKER) {
connectableBusSetter.accept(terminal.getBusBreakerView().getConnectableBus(), adder);
Bus bus = terminal.getBusBreakerView().getBus();
if (bus != null) {
busSetter.accept(bus, adder);
}
} else if (terminal.getVoltageLevel().getTopologyKind() == TopologyKind.NODE_BREAKER) {
int node = terminal.getNodeBreakerView().getNode();
nodeSetter.accept(node, adder);
} else {
throw new IllegalStateException();
}
}
public static void addLoadingLimits(Line created, LoadingLimitsBags limits, TwoSides side) {
if (side == TwoSides.ONE) {
limits.getActivePowerLimits().ifPresent(lim -> addLoadingLimits(created.newActivePowerLimits1(), lim));
limits.getApparentPowerLimits().ifPresent(lim -> addLoadingLimits(created.newApparentPowerLimits1(), lim));
limits.getCurrentLimits().ifPresent(lim -> addLoadingLimits(created.newCurrentLimits1(), lim));
} else {
limits.getActivePowerLimits().ifPresent(lim -> addLoadingLimits(created.newActivePowerLimits2(), lim));
limits.getApparentPowerLimits().ifPresent(lim -> addLoadingLimits(created.newApparentPowerLimits2(), lim));
limits.getCurrentLimits().ifPresent(lim -> addLoadingLimits(created.newCurrentLimits2(), lim));
}
}
private static <L extends LoadingLimits, A extends LoadingLimitsAdder<L, A>> void addLoadingLimits(A adder, LoadingLimitsBag limits) {
adder.setPermanentLimit(limits.getPermanentLimit());
for (TemporaryLimitsBag tl : limits.getTemporaryLimits()) {
adder.beginTemporaryLimit()
.setName(tl.getName())
.setAcceptableDuration(tl.getAcceptableDuration())
.setFictitious(tl.isFictitious())
.setValue(tl.getValue())
.endTemporaryLimit();
}
adder.add();
}
static void removeVoltageLevelAndSubstation(VoltageLevel voltageLevel, ReportNode reportNode) {
Optional<Substation> substation = voltageLevel.getSubstation();
String vlId = voltageLevel.getId();
boolean noMoreEquipments = voltageLevel.getConnectableStream().noneMatch(c -> c.getType() != IdentifiableType.BUSBAR_SECTION);
if (!noMoreEquipments) {
voltageLevelRemovingEquipmentsLeftReport(reportNode, vlId);
LOGGER.warn("Voltage level {} still contains equipments", vlId);
}
voltageLevel.remove();
voltageLevelRemovedReport(reportNode, vlId);
LOGGER.info("Voltage level {} removed", vlId);
substation.ifPresent(s -> {
if (s.getVoltageLevelStream().count() == 0) {
String substationId = s.getId();
s.remove();
substationRemovedReport(reportNode, substationId);
LOGGER.info("Substation {} removed", substationId);
}
});
}
static void createNBBreaker(int node1, int node2, String id, VoltageLevel.NodeBreakerView view, boolean open) {
view.newSwitch()
.setId(id)
.setEnsureIdUnicity(true)
.setKind(SwitchKind.BREAKER)
.setOpen(open)
.setRetained(true)
.setNode1(node1)
.setNode2(node2)
.add();
}
static void createNBDisconnector(int node1, int node2, String id, VoltageLevel.NodeBreakerView view, boolean open) {
view.newSwitch()
.setId(id)
.setEnsureIdUnicity(true)
.setKind(SwitchKind.DISCONNECTOR)
.setOpen(open)
.setNode1(node1)
.setNode2(node2)
.add();
}
static void createBusBreakerSwitch(String busId1, String busId2, String id, VoltageLevel.BusBreakerView view) {
view.newSwitch()
.setId(id)
.setEnsureIdUnicity(true)
.setOpen(false)
.setBus1(busId1)
.setBus2(busId2)
.add();
}
/**
* Utility method that associates a busbar section position index to the orders taken by all the connectables
* of the busbar sections of this index.
**/
static NavigableMap<Integer, List<Integer>> getSliceOrdersMap(VoltageLevel voltageLevel) {
// Compute the map of connectables by busbar sections
Map<BusbarSection, Set<Connectable<?>>> connectablesByBbs = new LinkedHashMap<>();
voltageLevel.getConnectableStream(BusbarSection.class)
.forEach(bbs -> fillConnectablesMap(bbs, connectablesByBbs));
// Merging the map by section index
Map<Integer, Set<Connectable<?>>> connectablesBySectionIndex = new LinkedHashMap<>();
connectablesByBbs.forEach((bbs, connectables) -> {
BusbarSectionPosition bbPosition = bbs.getExtension(BusbarSectionPosition.class);
if (bbPosition != null) {
connectablesBySectionIndex.merge(bbPosition.getSectionIndex(), connectables, (l1, l2) -> {
l1.addAll(l2);
return l1;
});
}
});
// Get the orders corresponding map
TreeMap<Integer, List<Integer>> ordersBySectionIndex = new TreeMap<>();
connectablesBySectionIndex.forEach((sectionIndex, connectables) -> {
List<Integer> orders = new ArrayList<>();
connectables.forEach(connectable -> addOrderPositions(connectable, voltageLevel, orders));
ordersBySectionIndex.put(sectionIndex, orders);
});
return ordersBySectionIndex;
}
/**
* Method that fills the map connectablesByBbs with all the connectables of a busbar section.
*/
static void fillConnectablesMap(BusbarSection bbs, Map<BusbarSection, Set<Connectable<?>>> connectablesByBbs) {
BusbarSectionPosition bbPosition = bbs.getExtension(BusbarSectionPosition.class);
int bbSection = bbPosition.getSectionIndex();
if (connectablesByBbs.containsKey(bbs)) {
return;
}
Set<Connectable<?>> connectables = connectablesByBbs.compute(bbs, (k, v) -> new LinkedHashSet<>());
bbs.getTerminal().traverse(new Terminal.TopologyTraverser() {
@Override
public TraverseResult traverse(Terminal terminal, boolean connected) {
if (terminal.getVoltageLevel() != bbs.getTerminal().getVoltageLevel()) {
return TraverseResult.TERMINATE_PATH;
}
Connectable<?> connectable = terminal.getConnectable();
if (connectable instanceof BusbarSection otherBbs) {
BusbarSectionPosition otherBbPosition = otherBbs.getExtension(BusbarSectionPosition.class);
if (otherBbPosition.getSectionIndex() == bbSection) {
connectablesByBbs.put(otherBbs, connectables);
} else {
return TraverseResult.TERMINATE_PATH;
}
}
connectables.add(connectable);
return TraverseResult.CONTINUE;
}
@Override
public TraverseResult traverse(Switch aSwitch) {
return TraverseResult.CONTINUE;
}
});
}
/**
* Get the list of parallel busbar sections on a given busbar section position
*
* @param voltageLevel Voltage level in which to find the busbar sections
* @param position busbar section position according to which busbar sections are found
* @return the list of busbar sections in the voltage level that have the same section position as the given position
*/
static List<BusbarSection> getParallelBusbarSections(VoltageLevel voltageLevel, BusbarSectionPosition position) {
// List of the bars for the second section
return voltageLevel.getNodeBreakerView().getBusbarSectionStream()
.filter(b -> b.getExtension(BusbarSectionPosition.class) != null)
.filter(b -> b.getExtension(BusbarSectionPosition.class).getSectionIndex() == position.getSectionIndex()).toList();
}
/**
* Creates a breaker and a disconnector between the connectable and the specified busbar
*/
static void createNodeBreakerSwitchesTopology(VoltageLevel voltageLevel, int connectableNode, int forkNode, NamingStrategy namingStrategy, String baseId, BusbarSection bbs) {
createNodeBreakerSwitchesTopology(voltageLevel, connectableNode, forkNode, namingStrategy, baseId, List.of(bbs), bbs);
}
/**
* Creates open disconnectors between the fork node and every busbar section of the list in a voltage level
*/
static void createNodeBreakerSwitchesTopology(VoltageLevel voltageLevel, int connectableNode, int forkNode, NamingStrategy namingStrategy, String baseId, List<BusbarSection> bbsList, BusbarSection bbs) {
// Closed breaker
createNBBreaker(connectableNode, forkNode, namingStrategy.getBreakerId(baseId), voltageLevel.getNodeBreakerView(), false);
// Disconnectors - only the one on the chosen busbarsection is closed
createDisconnectorTopology(voltageLevel, forkNode, namingStrategy, baseId, bbsList, bbs);
}
/**
* Creates disconnectors between the fork node and every busbar section of the list in a voltage level. Each disconnector will be closed if it is connected to the given bar, else opened
*/
static void createDisconnectorTopology(VoltageLevel voltageLevel, int forkNode, NamingStrategy namingStrategy, String baseId, List<BusbarSection> bbsList, BusbarSection bbs) {
createDisconnectorTopology(voltageLevel, forkNode, namingStrategy, baseId, bbsList, bbs, 0);
}
/**
* Creates disconnectors between the fork node and every busbar section of the list in a voltage level. Each disconnector will be closed if it is connected to the given bar, else opened
*/
static void createDisconnectorTopology(VoltageLevel voltageLevel, int forkNode, NamingStrategy namingStrategy, String baseId, List<BusbarSection> bbsList, BusbarSection bbs, int side) {
// Disconnectors - only the one on the chosen busbarsection is closed
bbsList.forEach(b -> {
int bbsNode = b.getTerminal().getNodeBreakerView().getNode();
createNBDisconnector(forkNode, bbsNode, namingStrategy.getDisconnectorId(b, baseId, forkNode, bbsNode, side), voltageLevel.getNodeBreakerView(), b != bbs);
});
}
/**
* Get all the unused positions before the lowest used position on the busbar section bbs.
* It is a range between the maximum used position on the busbar section with the highest section index lower than the section
* index of the given busbar section and the minimum position on the given busbar section.
* For two busbar sections with following indexes BBS1 with used orders 1,2,3 and BBS2 with used orders 7,8, this method
* applied to BBS2 will return a range from 4 to 6.
*/
public static Optional<Range<Integer>> getUnusedOrderPositionsBefore(BusbarSection bbs) {
BusbarSectionPosition busbarSectionPosition = bbs.getExtension(BusbarSectionPosition.class);
if (busbarSectionPosition == null) {
throw new PowsyblException("busbarSection has no BusbarSectionPosition extension");
}
VoltageLevel voltageLevel = bbs.getTerminal().getVoltageLevel();
NavigableMap<Integer, List<Integer>> allOrders = getSliceOrdersMap(voltageLevel);
int sectionIndex = busbarSectionPosition.getSectionIndex();
Optional<Integer> previousSliceMax = getMaxOrderUsedBefore(allOrders, sectionIndex);
Optional<Integer> sliceMin = allOrders.get(sectionIndex).stream().min(Comparator.naturalOrder());
int min = previousSliceMax.map(o -> o + 1).orElse(0);
int max = sliceMin.or(() -> getMinOrderUsedAfter(allOrders, sectionIndex)).map(o -> o - 1).orElse(Integer.MAX_VALUE);
return Optional.ofNullable(min <= max ? Range.of(min, max) : null);
}
/**
* Get all the unused positions after the highest used position on the busbar section bbs.
* It is a range between the minimum used position on the busbar section with the lowest section index higher than the section
* index of the given busbar section and the maximum position on the given busbar section.
* For two busbar sections with following indexes BBS1 with used orders 1,2,3 and BBS2 with used orders 7,8, this method
* applied to BBS1 will return a range from 4 to 6.
*/
public static Optional<Range<Integer>> getUnusedOrderPositionsAfter(BusbarSection bbs) {
BusbarSectionPosition busbarSectionPosition = bbs.getExtension(BusbarSectionPosition.class);
if (busbarSectionPosition == null) {
throw new PowsyblException("busbarSection has no BusbarSectionPosition extension");
}
VoltageLevel voltageLevel = bbs.getTerminal().getVoltageLevel();
NavigableMap<Integer, List<Integer>> allOrders = getSliceOrdersMap(voltageLevel);
int sectionIndex = busbarSectionPosition.getSectionIndex();
Optional<Integer> nextSliceMin = getMinOrderUsedAfter(allOrders, sectionIndex);
Optional<Integer> sliceMax = allOrders.get(sectionIndex).stream().max(Comparator.naturalOrder());
int min = sliceMax.or(() -> getMaxOrderUsedBefore(allOrders, sectionIndex)).map(o -> o + 1).orElse(0);
int max = nextSliceMin.map(o -> o - 1).orElse(Integer.MAX_VALUE);
return Optional.ofNullable(min <= max ? Range.of(min, max) : null);
}
/**
* Get the range of connectable positions delimited by neighbouring busbar sections, for a given busbar section.
* If the range is empty (for instance if positions max on left side is above position min on right side), the range returned is empty.
* Note that the connectable positions needs to be in ascending order in the voltage level for ascending busbar section index positions.
*/
public static Optional<Range<Integer>> getPositionRange(BusbarSection bbs) {
BusbarSectionPosition positionExtension = bbs.getExtension(BusbarSectionPosition.class);
if (positionExtension != null) {
VoltageLevel voltageLevel = bbs.getTerminal().getVoltageLevel();
NavigableMap<Integer, List<Integer>> allOrders = getSliceOrdersMap(voltageLevel);
int sectionIndex = positionExtension.getSectionIndex();
int max = getMinOrderUsedAfter(allOrders, sectionIndex).map(o -> o - 1).orElse(Integer.MAX_VALUE);
int min = getMaxOrderUsedBefore(allOrders, sectionIndex).map(o -> o + 1).orElse(0);
return Optional.ofNullable(min <= max ? Range.of(min, max) : null);
}
return Optional.of(Range.of(0, Integer.MAX_VALUE));
}
/**
* Method returning the maximum order in the slice with the highest section index lower to the given section.
* For two busbar sections with following indexes BBS1 with used orders 1,2,3 and BBS2 with used orders 7,8, this method
* applied to BBS2 will return 3.
*/
public static Optional<Integer> getMaxOrderUsedBefore(NavigableMap<Integer, List<Integer>> allOrders, int section) {
int s = section;
Map.Entry<Integer, List<Integer>> lowerEntry;
do {
lowerEntry = allOrders.lowerEntry(s);
if (lowerEntry == null) {
break;
}
s = lowerEntry.getKey();
} while (lowerEntry.getValue().isEmpty());
return Optional.ofNullable(lowerEntry)
.flatMap(entry -> entry.getValue().stream().max(Comparator.naturalOrder()));
}
/**
* Method returning the minimum order in the slice with the lowest section index higher to the given section.
* For two busbar sections with following indexes BBS1 with used orders 1,2,3 and BBS2 with used orders 7,8, this method
* applied to BBS1 will return 7.
*/
public static Optional<Integer> getMinOrderUsedAfter(NavigableMap<Integer, List<Integer>> allOrders, int section) {
int s = section;
Map.Entry<Integer, List<Integer>> higherEntry;
do {
higherEntry = allOrders.higherEntry(s);
if (higherEntry == null) {
break;
}
s = higherEntry.getKey();
} while (higherEntry.getValue().isEmpty());
return Optional.ofNullable(higherEntry)
.flatMap(entry -> entry.getValue().stream().min(Comparator.naturalOrder()));
}
/**
* Utility method to get all the taken feeder positions on a voltage level.
*/
public static Set<Integer> getFeederPositions(VoltageLevel voltageLevel) {
Set<Integer> feederPositionsOrders = new HashSet<>();
voltageLevel.getConnectables().forEach(connectable -> addOrderPositions(connectable, voltageLevel, feederPositionsOrders));
return feederPositionsOrders;
}
/**
* Utility method to get all the taken feeder positions on a voltage level by connectable.
*/
public static Map<String, List<Integer>> getFeederPositionsByConnectable(VoltageLevel voltageLevel) {
Map<String, List<Integer>> feederPositionsOrders = new HashMap<>();
getFeedersByConnectable(voltageLevel).forEach((k, v) -> {
List<Integer> orders = new ArrayList<>();
v.forEach(feeder -> feeder.getOrder().ifPresent(orders::add));
if (orders.size() > 1) {
Collections.sort(orders);
}
feederPositionsOrders.put(k, orders);
});
return feederPositionsOrders;
}
private static void addOrderPositions(Connectable<?> connectable, VoltageLevel voltageLevel, Collection<Integer> feederPositionsOrders) {
addOrderPositions(connectable, voltageLevel, feederPositionsOrders, false, ReportNode.NO_OP);
}
/**
* Method adding order position(s) of a connectable on a given voltage level to the given collection.
*/
private static void addOrderPositions(Connectable<?> connectable, VoltageLevel voltageLevel, Collection<Integer> feederPositionsOrders, boolean throwException, ReportNode reportNode) {
ConnectablePosition<?> position = (ConnectablePosition<?>) connectable.getExtension(ConnectablePosition.class);
if (position != null) {
List<Integer> orders = getOrderPositions(position, voltageLevel, connectable, throwException, reportNode);
feederPositionsOrders.addAll(orders);
}
}
/**
* Utility method to get all the feeders on a voltage level by connectable.
*/
public static Map<String, List<ConnectablePosition.Feeder>> getFeedersByConnectable(VoltageLevel voltageLevel) {
Map<String, List<ConnectablePosition.Feeder>> feedersByConnectable = new HashMap<>();
voltageLevel.getConnectables().forEach(connectable -> {
ConnectablePosition<?> position = (ConnectablePosition<?>) connectable.getExtension(ConnectablePosition.class);
if (position != null) {
List<ConnectablePosition.Feeder> feeder = getFeeders(position, voltageLevel, connectable, false, ReportNode.NO_OP);
feedersByConnectable.put(connectable.getId(), feeder);
}
});
return feedersByConnectable;
}
private static List<Integer> getOrderPositions(ConnectablePosition<?> position, VoltageLevel voltageLevel, Connectable<?> connectable, boolean throwException, ReportNode reportNode) {
List<ConnectablePosition.Feeder> feeders;
if (connectable instanceof Injection) {
feeders = getInjectionFeeder(position);
} else if (connectable instanceof Branch) {
feeders = getBranchFeeders(position, voltageLevel, (Branch<?>) connectable);
} else if (connectable instanceof ThreeWindingsTransformer twt) {
feeders = get3wtFeeders(position, voltageLevel, twt);
} else {
LOGGER.error("Given connectable not supported: {}", connectable.getClass().getName());
connectableNotSupported(reportNode, connectable);
if (throwException) {
throw new IllegalStateException("Given connectable not supported: " + connectable.getClass().getName());
}
return Collections.emptyList();
}
List<Integer> orders = new ArrayList<>();
feeders.forEach(feeder -> feeder.getOrder().ifPresent(orders::add));
if (orders.size() > 1) {
Collections.sort(orders);
}
return orders;
}
private static List<ConnectablePosition.Feeder> getFeeders(ConnectablePosition<?> position, VoltageLevel voltageLevel, Connectable<?> connectable, boolean throwException, ReportNode reportNode) {
if (connectable instanceof Injection) {
return getInjectionFeeder(position);
} else if (connectable instanceof Branch) {
return getBranchFeeders(position, voltageLevel, (Branch<?>) connectable);
} else if (connectable instanceof ThreeWindingsTransformer twt) {
return get3wtFeeders(position, voltageLevel, twt);
} else {
LOGGER.error("Given connectable not supported: {}", connectable.getClass().getName());
connectableNotSupported(reportNode, connectable);
if (throwException) {
throw new IllegalStateException("Given connectable not supported: " + connectable.getClass().getName());
}
}
return Collections.emptyList();
}
private static List<ConnectablePosition.Feeder> getInjectionFeeder(ConnectablePosition<?> position) {
return Optional.ofNullable(position.getFeeder()).map(List::of).orElse(Collections.emptyList());
}
private static List<ConnectablePosition.Feeder> getBranchFeeders(ConnectablePosition<?> position, VoltageLevel voltageLevel, Branch<?> branch) {
List<ConnectablePosition.Feeder> feeders = new ArrayList<>();
if (branch.getTerminal1().getVoltageLevel() == voltageLevel) {
Optional.ofNullable(position.getFeeder1()).ifPresent(feeders::add);
}
if (branch.getTerminal2().getVoltageLevel() == voltageLevel) {
Optional.ofNullable(position.getFeeder2()).ifPresent(feeders::add);
}
return feeders;
}
private static List<ConnectablePosition.Feeder> get3wtFeeders(ConnectablePosition<?> position, VoltageLevel voltageLevel, ThreeWindingsTransformer twt) {
List<ConnectablePosition.Feeder> feeders = new ArrayList<>();
if (twt.getLeg1().getTerminal().getVoltageLevel() == voltageLevel) {
Optional.ofNullable(position.getFeeder1()).ifPresent(feeders::add);
}
if (twt.getLeg2().getTerminal().getVoltageLevel() == voltageLevel) {
Optional.ofNullable(position.getFeeder2()).ifPresent(feeders::add);
}
if (twt.getLeg3().getTerminal().getVoltageLevel() == voltageLevel) {
Optional.ofNullable(position.getFeeder3()).ifPresent(feeders::add);
}
return feeders;
}
/**
* Method returning the first busbar section with the lowest BusbarSectionIndex if there are the BusbarSectionPosition extensions and the first busbar section found otherwise.
*/
public static BusbarSection getFirstBusbarSection(VoltageLevel voltageLevel) {
BusbarSection bbs;
if (voltageLevel.getNodeBreakerView().getBusbarSectionStream().anyMatch(b -> b.getExtension(BusbarSectionPosition.class) != null)) {
bbs = voltageLevel.getNodeBreakerView().getBusbarSectionStream()
.min(Comparator.comparingInt((BusbarSection b) -> {
BusbarSectionPosition position = b.getExtension(BusbarSectionPosition.class);
return position == null ? Integer.MAX_VALUE : position.getSectionIndex();
}).thenComparingInt((BusbarSection b) -> {
BusbarSectionPosition position = b.getExtension(BusbarSectionPosition.class);
return position == null ? Integer.MAX_VALUE : position.getBusbarIndex();
})).orElse(null);
} else {
bbs = voltageLevel.getNodeBreakerView().getBusbarSectionStream().findFirst().orElse(null);
}
if (bbs == null) {
throw new PowsyblException(String.format("Voltage level %s has no busbar section.", voltageLevel.getId()));
}
return bbs;
}
private static Optional<LoadingLimitsBag> mergeLimits(String lineId,
Optional<LoadingLimitsBag> limits1,
Optional<LoadingLimitsBag> limitsTeePointSide,
ReportNode reportNode) {
Optional<LoadingLimitsBag> limits;
double permanentLimit = limits1.map(LoadingLimitsBag::getPermanentLimit).orElse(Double.NaN);
List<TemporaryLimitsBag> temporaryLimits1 = limits1.map(LoadingLimitsBag::getTemporaryLimits).orElse(new ArrayList<>());
List<TemporaryLimitsBag> temporaryLimitsTeePointSide = limitsTeePointSide.map(LoadingLimitsBag::getTemporaryLimits).orElse(new ArrayList<>());
List<TemporaryLimitsBag> temporaryLimits = new ArrayList<>();
if (!limitsTeePointSide.isPresent()) { // no limits on tee point side : we keep limits on other side
limits = limits1;
} else {
// permanent limit : we keep the minimum permanent limit from both sides
if (Double.isNaN(permanentLimit)) {
permanentLimit = limitsTeePointSide.get().getPermanentLimit();
} else if (!Double.isNaN(limitsTeePointSide.get().getPermanentLimit())) {
permanentLimit = Math.min(permanentLimit, limitsTeePointSide.get().getPermanentLimit());
}
// temporary limits on both sides : they are ignored, otherwise, we keep temporary limits on side where they are defined
if (!temporaryLimits1.isEmpty() && !temporaryLimitsTeePointSide.isEmpty()) {
LOGGER.warn("Temporary limits on both sides for line {} : They are ignored", lineId);
ModificationReports.ignoreTemporaryLimitsOnBothLineSides(reportNode, lineId);
} else {
temporaryLimits = !temporaryLimits1.isEmpty() ? temporaryLimits1 : temporaryLimitsTeePointSide;
}
limits = Optional.of(new LoadingLimitsBag(permanentLimit, temporaryLimits));
}
return limits;
}
public static LoadingLimitsBags mergeLimits(String lineId, LoadingLimitsBags limits, LoadingLimitsBags limitsTeePointSide, ReportNode reportNode) {
Optional<LoadingLimitsBag> activePowerLimits = mergeLimits(lineId, limits.getActivePowerLimits(), limitsTeePointSide.getActivePowerLimits(), reportNode);
Optional<LoadingLimitsBag> apparentPowerLimits = mergeLimits(lineId, limits.getApparentPowerLimits(), limitsTeePointSide.getApparentPowerLimits(), reportNode);
Optional<LoadingLimitsBag> currentLimits = mergeLimits(lineId, limits.getCurrentLimits(), limitsTeePointSide.getCurrentLimits(), reportNode);
return new LoadingLimitsBags(activePowerLimits.orElse(null), apparentPowerLimits.orElse(null), currentLimits.orElse(null));
}
/**
* Find tee point connecting the 3 given lines, if any
* @return the tee point connecting the 3 given lines or null if none
*/
public static VoltageLevel findTeePoint(Line line1, Line line2, Line line3) {
Map<VoltageLevel, Long> countVoltageLevels = Stream.of(line1, line2, line3)
.map(Line::getTerminals)
.flatMap(List::stream)
.collect(Collectors.groupingBy(Terminal::getVoltageLevel, Collectors.counting()));
var commonVlMapEntry = Collections.max(countVoltageLevels.entrySet(), Map.Entry.comparingByValue());
// If the lines are connected by a tee point, there should be 4 distinct voltage levels and one of them should be found 3 times
if (countVoltageLevels.size() == 4 && commonVlMapEntry.getValue() == 3) {
return commonVlMapEntry.getKey();
} else {
return null;
}
}
/**
* Create topology and generate new connectable node and return it.
*/
public static int createTopologyAndGetConnectableNode(int side, String busOrBusbarSectionId, Network network, VoltageLevel voltageLevel, Connectable<?> connectable, NamingStrategy namingStrategy, ReportNode reportNode) {
int forkNode = voltageLevel.getNodeBreakerView().getMaximumNodeIndex() + 1;
int connectableNode = forkNode + 1;
buildTopology(side, busOrBusbarSectionId, network, voltageLevel, forkNode, connectableNode, connectable, namingStrategy, reportNode);
return connectableNode;
}
/**
* Create topology by using the provided connectable node (pre-determined connectable node)
*/
public static void createTopologyWithConnectableNode(int side, String busOrBusbarSectionId, Network network, VoltageLevel voltageLevel, int connectableNode, Connectable<?> connectable, NamingStrategy namingStrategy, ReportNode reportNode) {
int forkNode = voltageLevel.getNodeBreakerView().getMaximumNodeIndex() + 1;
buildTopology(side, busOrBusbarSectionId, network, voltageLevel, forkNode, connectableNode, connectable, namingStrategy, reportNode);
}
private static void buildTopology(int side, String busOrBusbarSectionId, Network network, VoltageLevel voltageLevel, int forkNode, int connectableNode, Connectable<?> connectable, NamingStrategy namingStrategy, ReportNode reportNode) {
// Information gathering
String baseId = namingStrategy.getSwitchBaseId(connectable, side);
BusbarSection bbs = network.getBusbarSection(busOrBusbarSectionId);
BusbarSectionPosition position = bbs.getExtension(BusbarSectionPosition.class);
// Topology creation
int parallelBbsNumber = 0;
if (position == null) {
// No position extension is present so only one disconnector is needed
createNodeBreakerSwitchesTopology(voltageLevel, connectableNode, forkNode, namingStrategy, baseId, bbs);
LOGGER.warn("No busbar section position extension found on {}, only one disconnector is created.", bbs.getId());
noBusbarSectionPositionExtensionReport(reportNode, bbs);
} else {
List<BusbarSection> bbsList = getParallelBusbarSections(voltageLevel, position);
parallelBbsNumber = bbsList.size() - 1;
createNodeBreakerSwitchesTopology(voltageLevel, connectableNode, forkNode, namingStrategy, baseId, bbsList, bbs);
}
LOGGER.info("New feeder bay associated to {} of type {} was created and connected to voltage level {} on busbar section {} with a closed disconnector " +
"and on {} parallel busbar sections with an open disconnector.", connectable.getId(), connectable.getType(), voltageLevel.getId(), busOrBusbarSectionId, parallelBbsNumber);
createdNodeBreakerFeederBay(reportNode, voltageLevel.getId(), busOrBusbarSectionId, connectable, parallelBbsNumber);
}
public static Integer getOppositeNode(Graph<Integer, Object> graph, int node, Object e) {
Integer edgeSource = graph.getEdgeSource(e);
return edgeSource == node ? graph.getEdgeTarget(e) : edgeSource;
}
/**
* Starting from the given node, traverse the graph and remove all the switches and/or internal connections until a
* fork node is encountered, for which special care is needed to clean the topology.
*/
public static void cleanTopology(VoltageLevel.NodeBreakerView nbv, Graph<Integer, Object> graph, int node, String connectableId, ReportNode reportNode) {
Set<Object> edges = graph.edgesOf(node);
if (edges.size() == 1) {
Object edge = edges.iterator().next();
Integer oppositeNode = getOppositeNode(graph, node, edge);
removeSwitchOrInternalConnection(nbv, graph, edge, reportNode);
cleanTopology(nbv, graph, oppositeNode, connectableId, reportNode);
} else if (edges.size() > 1) {
cleanFork(nbv, graph, node, edges, connectableId, reportNode);
}
}
/**
* Cleans up the topology for a node-breaker topology
*/
public static void cleanNodeBreakerTopology(Network network, String connectableId, ReportNode reportNode) {
Connectable<?> connectable = network.getConnectable(connectableId);
for (Terminal t : connectable.getTerminals()) {
if (t.getVoltageLevel().getTopologyKind() == TopologyKind.NODE_BREAKER) {
Graph<Integer, Object> graph = createGraphFromTerminal(t);
int node = t.getNodeBreakerView().getNode();
cleanTopology(t.getVoltageLevel().getNodeBreakerView(), graph, node, connectableId, reportNode);
}
}
}
public static void removeSwitchOrInternalConnection(VoltageLevel.NodeBreakerView nbv, Graph<Integer, Object> graph,
Object edge, ReportNode reportNode) {
if (edge instanceof Switch sw) {
String switchId = sw.getId();
nbv.removeSwitch(switchId);
removedSwitchReport(reportNode, switchId);
LOGGER.info("Switch {} removed", switchId);
} else {
Pair<Integer, Integer> ic = (Pair<Integer, Integer>) edge;
nbv.removeInternalConnections(ic.getFirst(), ic.getSecond());
removedInternalConnectionReport(reportNode, ic.getFirst(), ic.getSecond());
LOGGER.info("Internal connection between {} and {} removed", ic.getFirst(), ic.getSecond());
}
graph.removeEdge(edge);
}
/**
* Try to remove all edges of the given fork node
*/
public static void cleanFork(VoltageLevel.NodeBreakerView nbv, Graph<Integer, Object> graph, int node, Set<Object> edges, String connectableId, ReportNode reportNode) {
List<Object> toBusesOnly = new ArrayList<>();
List<Object> mixed = new ArrayList<>();
for (Object edge : edges) {
List<Connectable<?>> connectables = getLinkedConnectables(nbv, graph, node, edge);
if (connectables.stream().allMatch(BusbarSection.class::isInstance)) {
// the edge is only linked to busbarSections, or to no connectables, hence it's a good candidate for removal
toBusesOnly.add(edge);
} else if (connectables.stream().noneMatch(BusbarSection.class::isInstance)) {
// the edge is only linked to other non-busbarSection connectables, no further cleaning can be done
// Note that connectables cannot be empty because of previous if
String otherConnectableId = connectables.stream().map(Connectable::getId).findFirst().orElse("none");
removeFeederBayAborted(reportNode, connectableId, node, otherConnectableId);
LOGGER.info("Remove feeder bay of {} cannot go further node {}, as it is connected to {}", connectableId, node, otherConnectableId);
return;
} else {
// the edge is linked to busbarSections and non-busbarSection connectables, some further cleaning can be done if there's only one edge of that type
mixed.add(edge);
}
}
// We now know there are only edges which are
// - either only linked to busbarSections and no other connectables
// - or linked to busbarSections and connectables
// The former ones can be removed:
for (Object edge : toBusesOnly) {
removeAllSwitchesAndInternalConnections(nbv, graph, node, edge, reportNode);
}
// We don't remove the latter ones if more than one, as this would break the connection between them
if (mixed.size() == 1) {
// If only one, we're cleaning the dangling switches and/or internal connections
cleanMixedTopology(nbv, graph, node, reportNode);
}
}
private static List<Connectable<?>> getLinkedConnectables(VoltageLevel.NodeBreakerView nbv, Graph<Integer, Object> graph, Integer node, Object edge) {
Set<Integer> visitedNodes = new HashSet<>();
visitedNodes.add(node);
List<Connectable<?>> connectables = new ArrayList<>();
searchConnectables(nbv, graph, getOppositeNode(graph, node, edge), visitedNodes, connectables);
return connectables;
}
public static Graph<Integer, Object> createGraphFromTerminal(Terminal terminal) {
Graph<Integer, Object> graph = new Pseudograph<>(Object.class);
int node = terminal.getNodeBreakerView().getNode();
VoltageLevel.NodeBreakerView vlNbv = terminal.getVoltageLevel().getNodeBreakerView();
graph.addVertex(node);
vlNbv.traverse(node, (node1, sw, node2) -> {
TraverseResult result = vlNbv.getOptionalTerminal(node2)
.map(Terminal::getConnectable)
.filter(BusbarSection.class::isInstance)
.map(c -> TraverseResult.TERMINATE_PATH)
.orElse(TraverseResult.CONTINUE);
graph.addVertex(node2);
graph.addEdge(node1, node2, sw != null ? sw : Pair.of(node1, node2));
return result;
});
return graph;
}
/**
* Starting from the given node, traverse the graph and remove all the switches and/or internal connections until a
* fork node is encountered or a node on which a connectable is connected
*/
private static void cleanMixedTopology(VoltageLevel.NodeBreakerView nbv, Graph<Integer, Object> graph, int node, ReportNode reportNode) {
// Get the next edge and the opposite node
Set<Object> edges = graph.edgesOf(node);
Object edge = edges.iterator().next();
Integer oppositeNode = getOppositeNode(graph, node, edge);
// Remove the switch or internal connection on the current edge
removeSwitchOrInternalConnection(nbv, graph, edge, reportNode);
// List the connectables connected to the opposite node
List<Connectable<?>> connectables = new ArrayList<>();
nbv.getOptionalTerminal(oppositeNode).map(Terminal::getConnectable).ifPresent(connectables::add);
// If there is only one edge on the opposite node and no connectable, continue to remove the elements
if (graph.edgesOf(oppositeNode).size() == 1 && connectables.isEmpty()) {
cleanMixedTopology(nbv, graph, oppositeNode, reportNode);
}
}
private static void searchConnectables(VoltageLevel.NodeBreakerView nbv, Graph<Integer, Object> graph, Integer node,
Set<Integer> visitedNodes, List<Connectable<?>> connectables) {
if (visitedNodes.contains(node)) {
return;
}
nbv.getOptionalTerminal(node).map(Terminal::getConnectable).ifPresent(connectables::add);
if (!isBusbarSection(nbv, node)) {
visitedNodes.add(node);
for (Object e : graph.edgesOf(node)) {
searchConnectables(nbv, graph, getOppositeNode(graph, node, e), visitedNodes, connectables);
}
}
}
/**
* Traverse the graph and remove all switches and internal connections until encountering a {@link BusbarSection}.
*/
private static void removeAllSwitchesAndInternalConnections(VoltageLevel.NodeBreakerView nbv, Graph<Integer, Object> graph,
int originNode, Object edge, ReportNode reportNode) {
// in case of loops inside the traversed bay, the edge might have been already removed
if (!graph.containsEdge(edge)) {
return;
}
Integer oppositeNode = getOppositeNode(graph, originNode, edge);
removeSwitchOrInternalConnection(nbv, graph, edge, reportNode);
if (!isBusbarSection(nbv, oppositeNode)) {
for (Object otherEdge : new ArrayList<>(graph.edgesOf(oppositeNode))) {
removeAllSwitchesAndInternalConnections(nbv, graph, oppositeNode, otherEdge, reportNode);
}
}
}
private static boolean isBusbarSection(VoltageLevel.NodeBreakerView nbv, Integer node) {
Optional<Connectable<?>> c = nbv.getOptionalTerminal(node).map(Terminal::getConnectable);
return c.isPresent() && c.get() instanceof BusbarSection;
}
}