diff --git a/docs/reference/network.rst b/docs/reference/network.rst index f3b9a58abd..6da7cc4058 100644 --- a/docs/reference/network.rst +++ b/docs/reference/network.rst @@ -79,6 +79,7 @@ All network elements are accessible as dataframes, using the following getters. Network.get_branches Network.get_busbar_sections Network.get_buses + Network.get_bus_breaker_view_buses Network.get_current_limits Network.get_dangling_lines Network.get_generators diff --git a/java/src/main/java/com/powsybl/dataframe/network/NetworkDataframes.java b/java/src/main/java/com/powsybl/dataframe/network/NetworkDataframes.java index 395094588e..5ed634269e 100644 --- a/java/src/main/java/com/powsybl/dataframe/network/NetworkDataframes.java +++ b/java/src/main/java/com/powsybl/dataframe/network/NetworkDataframes.java @@ -324,7 +324,7 @@ private static NetworkDataframeMapper areas() { } static NetworkDataframeMapper buses(boolean busBreakerView) { - return NetworkDataframeMapperBuilder.ofStream(n -> busBreakerView ? n.getBusBreakerView().getBusStream() : n.getBusView().getBusStream(), + var builder = NetworkDataframeMapperBuilder.ofStream(n -> busBreakerView ? n.getBusBreakerView().getBusStream() : n.getBusView().getBusStream(), getOrThrow((b, id) -> b.getBusView().getBus(id), "Bus")) .stringsIndex("id", Bus::getId) .strings("name", b -> b.getOptionalName().orElse(""), Identifiable::setName) @@ -333,8 +333,11 @@ static NetworkDataframeMapper buses(boolean busBreakerView) { .doubles("v_angle", (b, context) -> perUnitAngle(context, b.getAngle()), (b, vAngle, context) -> b.setAngle(unPerUnitAngle(context, vAngle))) .ints("connected_component", ifExistsInt(Bus::getConnectedComponent, Component::getNum)) .ints("synchronous_component", ifExistsInt(Bus::getSynchronousComponent, Component::getNum)) - .strings("voltage_level_id", b -> b.getVoltageLevel().getId()) - .booleans("fictitious", Identifiable::isFictitious, Identifiable::setFictitious, false) + .strings("voltage_level_id", b -> b.getVoltageLevel().getId()); + if (busBreakerView) { + builder.strings("bus_id", b -> NetworkUtil.getBusViewBus(b).map(Bus::getId).orElse("")); + } + return builder.booleans("fictitious", Identifiable::isFictitious, Identifiable::setFictitious, false) .addProperties() .build(); } diff --git a/java/src/main/java/com/powsybl/python/network/Dataframes.java b/java/src/main/java/com/powsybl/python/network/Dataframes.java index 45e326c4f3..1430a3124d 100644 --- a/java/src/main/java/com/powsybl/python/network/Dataframes.java +++ b/java/src/main/java/com/powsybl/python/network/Dataframes.java @@ -364,7 +364,7 @@ private static DataframeMapper createBusBreak private static List getBusBreakerViewBuses(VoltageLevel voltageLevel) { return voltageLevel.getBusBreakerView().getBusStream() - .map(bus -> new BusBreakerViewBusData(bus, NetworkUtil.getBusViewBus(bus))) + .map(bus -> new BusBreakerViewBusData(bus, NetworkUtil.getBusViewBus(bus).orElse(null))) .toList(); } diff --git a/java/src/main/java/com/powsybl/python/network/NetworkUtil.java b/java/src/main/java/com/powsybl/python/network/NetworkUtil.java index 4626f08130..150413d616 100644 --- a/java/src/main/java/com/powsybl/python/network/NetworkUtil.java +++ b/java/src/main/java/com/powsybl/python/network/NetworkUtil.java @@ -13,10 +13,7 @@ import com.powsybl.iidm.network.extensions.ConnectablePosition; import com.powsybl.python.commons.PyPowsyblApiHeader; -import java.util.Collection; -import java.util.List; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -250,21 +247,20 @@ public static String getRegulatedElementId(Supplier regulatingTerminal /** * @param b bus in Bus/Breaker view - * @return bus in bus view containing b if there is one, or null if none. + * @return bus in bus view containing b if there is one. */ - public static Bus getBusViewBus(Bus b) { + public static Optional getBusViewBus(Bus b) { VoltageLevel voltageLevel = b.getVoltageLevel(); if (voltageLevel.getTopologyKind() == TopologyKind.BUS_BREAKER) { // Bus/Breaker. There is an easy method directly available. - return voltageLevel.getBusView().getMergedBus(b.getId()); + return Optional.ofNullable(voltageLevel.getBusView().getMergedBus(b.getId())); } else { // Node/Breaker. // First we try the fast and easy way using connected terminals. Works for the vast majority of buses. - Bus busInBusView = b.getConnectedTerminalStream().map(t -> t.getBusView().getBus()) + Optional busInBusView = b.getConnectedTerminalStream().map(t -> t.getBusView().getBus()) .filter(Objects::nonNull) - .findFirst() - .orElse(null); - if (busInBusView != null) { + .findFirst(); + if (busInBusView.isPresent()) { return busInBusView; } // Didn't find using connected terminals. There is the possibility that the bus has zero connected terminal @@ -274,8 +270,7 @@ public static Bus getBusViewBus(Bus b) { return voltageLevel.getBusView().getBusStream() .filter(busViewBus -> voltageLevel.getBusBreakerView().getBusStreamFromBusViewBusId(busViewBus.getId()) .anyMatch(b2 -> b.getId().equals(b2.getId()))) - .findFirst() - .orElse(null); + .findFirst(); } } } diff --git a/java/src/test/java/com/powsybl/dataframe/network/NetworkDataframesTest.java b/java/src/test/java/com/powsybl/dataframe/network/NetworkDataframesTest.java index 4997d8a5a5..0260d38b89 100644 --- a/java/src/test/java/com/powsybl/dataframe/network/NetworkDataframesTest.java +++ b/java/src/test/java/com/powsybl/dataframe/network/NetworkDataframesTest.java @@ -21,6 +21,8 @@ import com.powsybl.iidm.network.extensions.*; import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory; import com.powsybl.iidm.network.test.HvdcTestNetwork; +import com.powsybl.iidm.network.test.TwoVoltageLevelNetworkFactory; +import com.powsybl.python.network.NetworkUtilTest; import com.powsybl.python.network.Networks; import org.junit.jupiter.api.Test; @@ -100,16 +102,64 @@ void buses() { .extracting(Series::getName) .containsExactly("id", "name", "v_mag", "v_angle", "connected_component", "synchronous_component", "voltage_level_id"); + assertThat(series.get(0).getStrings()) + .containsExactly("VLGEN_0", "VLHV1_0", "VLHV2_0", "VLLOAD_0"); assertThat(series.get(2).getDoubles()) .containsExactly(Double.NaN, Double.NaN, Double.NaN, Double.NaN); assertThat(series.get(4).getInts()) .containsExactly(0, 0, 0, 0); - assertThat(series.get(4).getInts()) + assertThat(series.get(5).getInts()) .containsExactly(0, 0, 0, 0); assertThat(series.get(6).getStrings()) .containsExactly("VLGEN", "VLHV1", "VLHV2", "VLLOAD"); } + @Test + void busBreakerViewBuses() { + // VL1 is Node/Breaker, VL2 is Bus/Breaker + Network network = TwoVoltageLevelNetworkFactory.create(); + List series = createDataFrame(BUS_FROM_BUS_BREAKER_VIEW, network); + assertThat(series) + .extracting(Series::getName) + .containsExactly("id", "name", "v_mag", "v_angle", "connected_component", "synchronous_component", + "voltage_level_id", "bus_id"); + assertThat(series.get(0).getStrings()) + .containsExactly("BUS1", "BUS2", "VL1_0", "VL1_3"); + assertThat(series.get(2).getDoubles()) + .containsExactly(Double.NaN, Double.NaN, Double.NaN, Double.NaN); + assertThat(series.get(4).getInts()) + .containsExactly(0, 0, 1, -99999); + assertThat(series.get(5).getInts()) + .containsExactly(0, 0, 1, -99999); + assertThat(series.get(6).getStrings()) + .containsExactly("VL2", "VL2", "VL1", "VL1"); + assertThat(series.get(7).getStrings()) + .containsExactly("VL2_0", "VL2_0", "VL1_0", ""); + } + + @Test + void busBreakerViewBusesNoConnectedTerminalOnBus() { + Network network = NetworkUtilTest.createTopologyTestNetwork(); + + List series = createDataFrame(BUS_FROM_BUS_BREAKER_VIEW, network); + assertThat(series) + .extracting(Series::getName) + .containsExactly("id", "name", "v_mag", "v_angle", "connected_component", "synchronous_component", + "voltage_level_id", "bus_id"); + assertThat(series.get(0).getStrings()) + .containsExactly("B1", "B2", "B3", "VL2_0", "VL2_1", "VL2_2"); + assertThat(series.get(2).getDoubles()) + .containsExactly(Double.NaN, Double.NaN, Double.NaN, Double.NaN, Double.NaN, Double.NaN); + assertThat(series.get(4).getInts()) + .containsExactly(0, 0, -99999, 1, 1, -99999); + assertThat(series.get(5).getInts()) + .containsExactly(0, 0, -99999, 1, 1, -99999); + assertThat(series.get(6).getStrings()) + .containsExactly("VL1", "VL1", "VL1", "VL2", "VL2", "VL2"); + assertThat(series.get(7).getStrings()) + .containsExactly("VL1_0", "VL1_0", "", "VL2_0", "VL2_0", ""); + } + @Test void generators() { Network network = EurostagTutorialExample1Factory.create(); diff --git a/java/src/test/java/com/powsybl/python/network/NetworkUtilTest.java b/java/src/test/java/com/powsybl/python/network/NetworkUtilTest.java index a79be4b257..59e9b48d3a 100644 --- a/java/src/test/java/com/powsybl/python/network/NetworkUtilTest.java +++ b/java/src/test/java/com/powsybl/python/network/NetworkUtilTest.java @@ -15,13 +15,14 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; /** * @author Geoffroy Jamgotchian {@literal } */ -class NetworkUtilTest { +public class NetworkUtilTest { @Test void test() { @@ -41,17 +42,17 @@ void testBusFromBusBreakerViewBus() { "VL2_1", "VL2_0", "VL2_2", ""); expected.forEach((busBreakerBusId, busIdExpected) -> { - Bus bus = NetworkUtil.getBusViewBus(network.getBusBreakerView().getBus(busBreakerBusId)); + Optional bus = NetworkUtil.getBusViewBus(network.getBusBreakerView().getBus(busBreakerBusId)); if (!busIdExpected.isEmpty()) { - assertNotNull(bus); - assertEquals(busIdExpected, bus.getId()); + assertTrue(bus.isPresent()); + assertEquals(busIdExpected, bus.orElseThrow().getId()); } else { - assertNull(bus); + assertTrue(bus.isEmpty()); } }); } - private static Network createTopologyTestNetwork() { + public static Network createTopologyTestNetwork() { Network network = NetworkFactory.findDefault().createNetwork("test", "code"); var vl1 = network.newVoltageLevel().setTopologyKind(TopologyKind.BUS_BREAKER).setId("VL1").setNominalV(400.).add(); diff --git a/pypowsybl/network/impl/network.py b/pypowsybl/network/impl/network.py index 8a38344268..dc0aa61d5e 100644 --- a/pypowsybl/network/impl/network.py +++ b/pypowsybl/network/impl/network.py @@ -538,11 +538,14 @@ def get_buses(self, all_attributes: bool = False, attributes: List[str] = None, - **v_mag**: Get the voltage magnitude of the bus (in kV) - **v_angle**: the voltage angle of the bus (in degree) - - **connected_component**: the number of terminals connected to this bus - - **synchronous_component**: the number of synchronous components that the bus is part of + - **connected_component**: The connected component to which the bus belongs + - **synchronous_component**: The synchronous component to which the bus belongs - **voltage_level_id**: at which substation the bus is connected - This dataframe is indexed on the bus ID. + This dataframe is indexed on the bus ID in the bus view. + + See Also: + :meth:`get_bus_breaker_view_buses` Examples: @@ -605,8 +608,75 @@ def get_buses(self, all_attributes: bool = False, attributes: List[str] = None, def get_bus_breaker_view_buses(self, all_attributes: bool = False, attributes: List[str] = None, **kwargs: ArrayLike) -> DataFrame: r""" - Get a dataframe of buses from the bus/breaker view. - See :meth:`get_buses` for documentation as attributes are the same. + Get a dataframe of buses from the bus/breaker view. + + Args: + all_attributes: flag for including all attributes in the dataframe, default is false + attributes: attributes to include in the dataframe. The 2 parameters are mutually exclusive. + If no parameter is specified, the dataframe will include the default attributes. + kwargs: the data to be selected, as named arguments. + + Returns: + A dataframe of buses from the bus/breaker view + + Notes: + The resulting dataframe, depending on the parameters, will include the following columns: + + - **v_mag**: Get the voltage magnitude of the bus (in kV) + - **v_angle**: the voltage angle of the bus (in degree) + - **connected_component**: The connected component to which the bus belongs + - **synchronous_component**: The synchronous component to which the bus belongs + - **voltage_level_id**: at which substation the bus is connected + - **bus_id**: the bus ID in the bus view + + This dataframe is indexed on the bus ID in the bus/breaker view. + + See Also: + :meth:`get_buses` + + Examples: + + .. code-block:: python + + net = pp.network.create_four_substations_node_breaker_network() + net.get_bus_breaker_view_buses() + + It outputs something like: + + ======== ==== ========= ======== ==================== ====================== ================ ======== + \ name v_mag v_angle connected_component synchronous_component voltage_level_id bus_id + ======== ==== ========= ======== ==================== ====================== ================ ======== + id + S1VL1_0 224.6139 2.2822 0 1 S1VL1 S1VL1_0 + S1VL1_2 224.6139 2.2822 0 1 S1VL1 S1VL1_0 + S1VL1_4 224.6139 2.2822 0 1 S1VL1 S1VL1_0 + S1VL2_0 400.0000 0.0000 0 1 S1VL2 S1VL2_0 + S1VL2_1 400.0000 0.0000 0 1 S1VL2 S1VL2_0 + S1VL2_3 400.0000 0.0000 0 1 S1VL2 S1VL2_0 + S1VL2_5 400.0000 0.0000 0 1 S1VL2 S1VL2_0 + S1VL2_7 400.0000 0.0000 0 1 S1VL2 S1VL2_0 + S1VL2_9 400.0000 0.0000 0 1 S1VL2 S1VL2_0 + S1VL2_11 400.0000 0.0000 0 1 S1VL2 S1VL2_0 + S1VL2_13 400.0000 0.0000 0 1 S1VL2 S1VL2_0 + S1VL2_15 400.0000 0.0000 0 1 S1VL2 S1VL2_0 + S1VL2_17 400.0000 0.0000 0 1 S1VL2 S1VL2_0 + S1VL2_19 400.0000 0.0000 0 1 S1VL2 S1VL2_0 + S1VL2_21 400.0000 0.0000 0 1 S1VL2 S1VL2_0 + S2VL1_0 408.8470 0.7347 0 0 S2VL1 S2VL1_0 + S2VL1_2 408.8470 0.7347 0 0 S2VL1 S2VL1_0 + S2VL1_4 408.8470 0.7347 0 0 S2VL1 S2VL1_0 + S2VL1_6 408.8470 0.7347 0 0 S2VL1 S2VL1_0 + S3VL1_0 400.0000 0.0000 0 0 S3VL1 S3VL1_0 + S3VL1_2 400.0000 0.0000 0 0 S3VL1 S3VL1_0 + S3VL1_4 400.0000 0.0000 0 0 S3VL1 S3VL1_0 + S3VL1_6 400.0000 0.0000 0 0 S3VL1 S3VL1_0 + S3VL1_8 400.0000 0.0000 0 0 S3VL1 S3VL1_0 + S3VL1_10 400.0000 0.0000 0 0 S3VL1 S3VL1_0 + S4VL1_0 400.0000 -1.1259 0 0 S4VL1 S4VL1_0 + S4VL1_6 400.0000 -1.1259 0 0 S4VL1 S4VL1_0 + S4VL1_2 400.0000 -1.1259 0 0 S4VL1 S4VL1_0 + S4VL1_4 400.0000 -1.1259 0 0 S4VL1 S4VL1_0 + ======== ==== ========= ======== ==================== ====================== ================ ======== """ return self.get_elements(ElementType.BUS_FROM_BUS_BREAKER_VIEW, all_attributes, attributes, **kwargs) diff --git a/tests/test_network.py b/tests/test_network.py index deffe0ab58..e489b9d1a3 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -1636,11 +1636,11 @@ def test_bus_breaker_view_buses(): expected_buses = pd.DataFrame( index=pd.Series(name='id', data=['NGEN', 'NHV1', 'NHV2', 'NLOAD']), columns=['name', 'v_mag', 'v_angle', 'connected_component', 'synchronous_component', - 'voltage_level_id'], - data=[['', nan, nan, 0, 0, 'VLGEN'], - ['', 380, nan, 0, 0, 'VLHV1'], - ['', 380, nan, 0, 0, 'VLHV2'], - ['', nan, nan, 0, 0, 'VLLOAD']]) + 'voltage_level_id', 'bus_id'], + data=[['', nan, nan, 0, 0, 'VLGEN', 'VLGEN_0'], + ['', 380, nan, 0, 0, 'VLHV1', 'VLHV1_0'], + ['', 380, nan, 0, 0, 'VLHV2', 'VLHV2_0'], + ['', nan, nan, 0, 0, 'VLLOAD', 'VLLOAD_0']]) pd.testing.assert_frame_equal(expected_buses, buses, check_dtype=False)