diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index f54338e23..9471aefe8 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -102,6 +102,12 @@ jobs: cmake $GITHUB_WORKSPACE/cpp -DBUILD_PYPOWSYBL_JAVA=OFF -DPYPOWSYBL_JAVA_LIBRARY_DIR=$GITHUB_WORKSPACE/dist/lib -DPYPOWSYBL_JAVA_INCLUDE_DIR=$GITHUB_WORKSPACE/dist/include cmake --build . --config Release + - name: Upload wheel + uses: actions/upload-artifact@v3 + with: + name: pypowsybl-wheel-linux-${{ matrix.python.name }} + path: wheelhouse/*.whl + macos_windows_build: name: Build ${{ matrix.config.name }} ${{ matrix.python.name }} wheel runs-on: ${{ matrix.config.os }} @@ -184,7 +190,12 @@ jobs: unzip binaries.zip && mkdir build && cd build cmake $GITHUB_WORKSPACE/cpp -DBUILD_PYPOWSYBL_JAVA=OFF -DPYPOWSYBL_JAVA_LIBRARY_DIR=$GITHUB_WORKSPACE/dist/lib -DPYPOWSYBL_JAVA_INCLUDE_DIR=$GITHUB_WORKSPACE/dist/include cmake --build . --config Release - - + + - name: Upload wheel + uses: actions/upload-artifact@v3 + with: + name: pypowsybl-wheel-${{ matrix.config.name }}-${{ matrix.python.name }} + path: dist/*.whl + 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 76627f7c4..98ee18686 100644 --- a/java/src/main/java/com/powsybl/python/network/Dataframes.java +++ b/java/src/main/java/com/powsybl/python/network/Dataframes.java @@ -310,9 +310,9 @@ private static List getNodeBreakerViewNodes(VoltageLevel.NodeBreake return nodes.stream().map(node -> { Terminal terminal = nodeBreakerView.getTerminal(node); if (terminal == null) { - return new NodeContext(node, null); + return new NodeContext(node, null, null); } else { - return new NodeContext(node, terminal.getConnectable().getId()); + return new NodeContext(node, terminal.getConnectable().getId(), terminal.getConnectable().getType()); } }).collect(Collectors.toList()); } @@ -322,6 +322,7 @@ private static DataframeMapper createNodeBre .itemsProvider(Dataframes::getNodeBreakerViewNodes) .intsIndex("node", NodeContext::getNode) .strings("connectable_id", node -> Objects.toString(node.getConnectableId(), "")) + .strings("connectable_type", node -> node.getConnectableType() != null ? node.getConnectableType().name() : "") .build(); } diff --git a/java/src/main/java/com/powsybl/python/network/NodeContext.java b/java/src/main/java/com/powsybl/python/network/NodeContext.java index 42eeae1b4..e1061c989 100644 --- a/java/src/main/java/com/powsybl/python/network/NodeContext.java +++ b/java/src/main/java/com/powsybl/python/network/NodeContext.java @@ -7,8 +7,7 @@ */ package com.powsybl.python.network; -import javax.annotation.Nullable; -import java.util.Objects; +import com.powsybl.iidm.network.IdentifiableType; /** * @author Etienne Lesot @@ -16,10 +15,12 @@ public class NodeContext { private final int node; private final String connectableId; + private final IdentifiableType connectableType; - public NodeContext(int node, @Nullable String connectableId) { - this.node = Objects.requireNonNull(node); + public NodeContext(int node, String connectableId, IdentifiableType connectableType) { + this.node = node; this.connectableId = connectableId; + this.connectableType = connectableType; } public int getNode() { @@ -29,4 +30,8 @@ public int getNode() { public String getConnectableId() { return connectableId; } + + public IdentifiableType getConnectableType() { + return connectableType; + } } diff --git a/pypowsybl/network/impl/node_breaker_topology.py b/pypowsybl/network/impl/node_breaker_topology.py index 4f6386191..d32752c83 100644 --- a/pypowsybl/network/impl/node_breaker_topology.py +++ b/pypowsybl/network/impl/node_breaker_topology.py @@ -4,6 +4,11 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. # SPDX-License-Identifier: MPL-2.0 # +from os import PathLike +from pathlib import Path +from typing import Union + +import networkx as nx from pandas import DataFrame import networkx as _nx import pypowsybl._pypowsybl as _pp @@ -22,7 +27,7 @@ class NodeBreakerTopology: def __init__(self, network_handle: _pp.JavaHandle, voltage_level_id: str): self._internal_connections = create_data_frame_from_series_array( _pp.get_node_breaker_view_internal_connections(network_handle, voltage_level_id)) - self._switchs = create_data_frame_from_series_array( + self._switches = create_data_frame_from_series_array( _pp.get_node_breaker_view_switches(network_handle, voltage_level_id)) self._nodes = create_data_frame_from_series_array( _pp.get_node_breaker_view_nodes(network_handle, voltage_level_id)) @@ -32,7 +37,7 @@ def switches(self) -> DataFrame: """ The list of switches of the voltage level, together with their connection status, as a dataframe. """ - return self._switchs + return self._switches @property def nodes(self) -> DataFrame: @@ -55,6 +60,45 @@ def create_graph(self) -> _nx.Graph: """ graph = _nx.Graph() graph.add_nodes_from(self._nodes.index.tolist()) - graph.add_edges_from(self._switchs[['node1', 'node2']].values.tolist()) + graph.add_edges_from(self._switches[['node1', 'node2']].values.tolist()) graph.add_edges_from(self._internal_connections[['node1', 'node2']].values.tolist()) return graph + + def write_ampl(self, dir: Union[str, Path]) -> None: + """ + Write the node/breaker topology in a columned file format (with space as a separator) so that it is + easily readable by AMPL for running topology optimization. + + 3 files are being generated: + - topo_connections.txt: the list of node connections in the graph. + - topo_elements.txt: the elements ID and type associated to each of the nodes + - topo_valid.txt : the buses to which each of the nodes belong + + Args: + dir: directory path to generate the 3 files + """ + dir_path = Path(dir) + with open(dir_path / 'topo_connections.txt', 'w') as file: + file.write("# switch_id node1 node2 open\n") + for i, row in enumerate(self.switches.itertuples(index=False)): + file.write(f"{i} {row[4]} {row[5]} {1 if row[2] else 0}\n") + + with open(dir_path / 'topo_elements.txt', 'w') as file: + file.write("# node element_id element_type\n") + for node, row in self.nodes.iterrows(): + file.write(f"{node} '{row['connectable_id']}' {row['connectable_type']}\n") + + graph = nx.Graph() + graph.add_nodes_from(self.nodes.index.tolist()) + topo_switches = self.switches + closed_topo_switches = topo_switches[~topo_switches['open']] + graph.add_edges_from(closed_topo_switches[['node1', 'node2']].values.tolist()) + graph.add_edges_from(self.internal_connections[['node1', 'node2']].values.tolist()) + components = list(nx.connected_components(graph)) + + i_topo = 0 # TODO will be used later on to generate multiple topologies + with open(dir_path / 'topo_valid.txt', 'w') as file: + file.write("# topo_id bus_id node\n") + for i_component, component in enumerate(components): + for node in component: + file.write(f"{i_topo} {i_component} {node}\n") diff --git a/tests/test_network.py b/tests/test_network.py index 310eb3ac7..49ec2b90a 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -1317,6 +1317,7 @@ def test_node_breaker_view(): assert 0 == switches.loc['S4VL1_BBS_LINES3S4_DISCONNECTOR']['node1'] assert 5 == switches.loc['S4VL1_BBS_LINES3S4_DISCONNECTOR']['node2'] assert 7 == len(nodes) + assert 'BUSBAR_SECTION' == nodes.loc[0]['connectable_type'] assert topology.internal_connections.empty with pytest.raises(PyPowsyblError) as exc: