From 591fd2c0afe62f1df1d916260c0725f4ad0d3441 Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Wed, 17 Jan 2024 07:01:11 -0800 Subject: [PATCH] nx-cugraph: add `is_tree`, etc. --- python/nx-cugraph/_nx_cugraph/__init__.py | 4 ++ .../nx_cugraph/algorithms/__init__.py | 4 +- .../nx_cugraph/algorithms/isolate.py | 2 +- .../nx_cugraph/algorithms/tree/__init__.py | 13 ++++ .../nx_cugraph/algorithms/tree/recognition.py | 70 +++++++++++++++++++ .../nx-cugraph/nx_cugraph/classes/digraph.py | 15 ++-- .../nx-cugraph/nx_cugraph/classes/function.py | 4 +- python/nx-cugraph/nx_cugraph/classes/graph.py | 4 +- 8 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 python/nx-cugraph/nx_cugraph/algorithms/tree/__init__.py create mode 100644 python/nx-cugraph/nx_cugraph/algorithms/tree/recognition.py diff --git a/python/nx-cugraph/_nx_cugraph/__init__.py b/python/nx-cugraph/_nx_cugraph/__init__.py index d9b997411ae..48a3ad4654d 100644 --- a/python/nx-cugraph/_nx_cugraph/__init__.py +++ b/python/nx-cugraph/_nx_cugraph/__init__.py @@ -68,9 +68,13 @@ "house_x_graph", "icosahedral_graph", "in_degree_centrality", + "is_arborescence", + "is_branching", "is_connected", + "is_forest", "is_isolate", "is_strongly_connected", + "is_tree", "is_weakly_connected", "isolates", "k_truss", diff --git a/python/nx-cugraph/nx_cugraph/algorithms/__init__.py b/python/nx-cugraph/nx_cugraph/algorithms/__init__.py index d28a629fe63..ed783ff0d18 100644 --- a/python/nx-cugraph/nx_cugraph/algorithms/__init__.py +++ b/python/nx-cugraph/nx_cugraph/algorithms/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023, NVIDIA CORPORATION. +# Copyright (c) 2023-2024, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -18,6 +18,7 @@ link_analysis, shortest_paths, traversal, + tree, ) from .bipartite import complete_bipartite_graph from .centrality import * @@ -28,3 +29,4 @@ from .link_analysis import * from .shortest_paths import * from .traversal import * +from .tree.recognition import * diff --git a/python/nx-cugraph/nx_cugraph/algorithms/isolate.py b/python/nx-cugraph/nx_cugraph/algorithms/isolate.py index 62b47a9b354..9621fbeaa9d 100644 --- a/python/nx-cugraph/nx_cugraph/algorithms/isolate.py +++ b/python/nx-cugraph/nx_cugraph/algorithms/isolate.py @@ -70,4 +70,4 @@ def isolates(G): @networkx_algorithm(version_added="23.10") def number_of_isolates(G): G = _to_graph(G) - return _mark_isolates(G).sum().tolist() + return int(cp.count_nonzero(_mark_isolates(G))) diff --git a/python/nx-cugraph/nx_cugraph/algorithms/tree/__init__.py b/python/nx-cugraph/nx_cugraph/algorithms/tree/__init__.py new file mode 100644 index 00000000000..91bf72417be --- /dev/null +++ b/python/nx-cugraph/nx_cugraph/algorithms/tree/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from .recognition import * diff --git a/python/nx-cugraph/nx_cugraph/algorithms/tree/recognition.py b/python/nx-cugraph/nx_cugraph/algorithms/tree/recognition.py new file mode 100644 index 00000000000..3a47d30a91d --- /dev/null +++ b/python/nx-cugraph/nx_cugraph/algorithms/tree/recognition.py @@ -0,0 +1,70 @@ +# Copyright (c) 2024, NVIDIA CORPORATION. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import cupy as cp +import networkx as nx + +import nx_cugraph as nxcg +from nx_cugraph.convert import _to_directed_graph, _to_graph +from nx_cugraph.utils import networkx_algorithm, not_implemented_for + +__all__ = ["is_arborescence", "is_branching", "is_forest", "is_tree"] + + +@not_implemented_for("undirected") +@networkx_algorithm(plc="weakly_connected_components", version_added="24.02") +def is_arborescence(G): + G = _to_directed_graph(G) + return is_tree(G) and int(G._in_degrees_array().max()) <= 1 + + +@not_implemented_for("undirected") +@networkx_algorithm(plc="weakly_connected_components", version_added="24.02") +def is_branching(G): + G = _to_directed_graph(G) + return is_forest(G) and int(G._in_degrees_array().max()) <= 1 + + +@networkx_algorithm(plc="weakly_connected_components", version_added="24.02") +def is_forest(G): + G = _to_graph(G) + if len(G) == 0: + raise nx.NetworkXPointlessConcept("G has no nodes.") + if is_directed := G.is_directed(): + connected_components = nxcg.weakly_connected_components + else: + connected_components = nxcg.connected_components + for components in connected_components(G): + node_ids = G._list_to_nodearray(list(components)) + # TODO: create utilities for creating subgraphs + mask = cp.isin(G.src_indices, node_ids) & cp.isin(G.dst_indices, node_ids) + if is_directed: + if int(cp.count_nonzero(mask)) != len(components) - 1: + return False + else: + src_indices = G.src_indices[mask] + dst_indices = G.dst_indices[mask] + if int(cp.count_nonzero(src_indices <= dst_indices)) != len(components) - 1: + return False + return True + + +@networkx_algorithm(plc="weakly_connected_components", version_added="24.02") +def is_tree(G): + G = _to_graph(G) + if len(G) == 0: + raise nx.NetworkXPointlessConcept("G has no nodes.") + if G.is_directed(): + is_connected = nxcg.is_weakly_connected + else: + is_connected = nxcg.is_connected + return len(G) - 1 == G.number_of_edges() and is_connected(G) diff --git a/python/nx-cugraph/nx_cugraph/classes/digraph.py b/python/nx-cugraph/nx_cugraph/classes/digraph.py index 52ea2334c85..eb96f49685e 100644 --- a/python/nx-cugraph/nx_cugraph/classes/digraph.py +++ b/python/nx-cugraph/nx_cugraph/classes/digraph.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023, NVIDIA CORPORATION. +# Copyright (c) 2023-2024, NVIDIA CORPORATION. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -16,13 +16,14 @@ import cupy as cp import networkx as nx +import numpy as np import nx_cugraph as nxcg from .graph import Graph if TYPE_CHECKING: # pragma: no cover - from nx_cugraph.typing import NodeKey + from nx_cugraph.typing import AttrKey __all__ = ["DiGraph"] @@ -44,10 +45,8 @@ def to_networkx_class(cls) -> type[nx.DiGraph]: return nx.DiGraph @networkx_api - def number_of_edges( - self, u: NodeKey | None = None, v: NodeKey | None = None - ) -> int: - if u is not None or v is not None: + def size(self, weight: AttrKey | None = None) -> int: + if weight is not None: raise NotImplementedError return self.src_indices.size @@ -66,7 +65,11 @@ def reverse(self, copy: bool = True) -> DiGraph: ################### def _in_degrees_array(self): + if self.dst_indices.size == 0: + return cp.zeros(self._N, dtype=np.int64) return cp.bincount(self.dst_indices, minlength=self._N) def _out_degrees_array(self): + if self.src_indices.size == 0: + return cp.zeros(self._N, dtype=np.int64) return cp.bincount(self.src_indices, minlength=self._N) diff --git a/python/nx-cugraph/nx_cugraph/classes/function.py b/python/nx-cugraph/nx_cugraph/classes/function.py index 435dfe37239..7212a4d2da9 100644 --- a/python/nx-cugraph/nx_cugraph/classes/function.py +++ b/python/nx-cugraph/nx_cugraph/classes/function.py @@ -10,6 +10,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import cupy as cp + from nx_cugraph.convert import _to_graph from nx_cugraph.utils import networkx_algorithm @@ -20,4 +22,4 @@ def number_of_selfloops(G): G = _to_graph(G) is_selfloop = G.src_indices == G.dst_indices - return is_selfloop.sum().tolist() + return int(cp.count_nonzero(is_selfloop)) diff --git a/python/nx-cugraph/nx_cugraph/classes/graph.py b/python/nx-cugraph/nx_cugraph/classes/graph.py index 251e92b70ec..b7e25d6d66b 100644 --- a/python/nx-cugraph/nx_cugraph/classes/graph.py +++ b/python/nx-cugraph/nx_cugraph/classes/graph.py @@ -522,7 +522,7 @@ def size(self, weight: AttrKey | None = None) -> int: if weight is not None: raise NotImplementedError # If no self-edges, then `self.src_indices.size // 2` - return int((self.src_indices <= self.dst_indices).sum()) + return int(cp.count_nonzero(self.src_indices <= self.dst_indices)) @networkx_api def to_directed(self, as_view: bool = False) -> nxcg.DiGraph: @@ -733,6 +733,8 @@ def _become(self, other: Graph): return self def _degrees_array(self): + if self.src_indices.size == 0: + return cp.zeros(self._N, dtype=np.int64) degrees = cp.bincount(self.src_indices, minlength=self._N) if self.is_directed(): degrees += cp.bincount(self.dst_indices, minlength=self._N)