Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nx-cugraph: add is_tree, etc. #4097

Merged
merged 4 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions python/nx-cugraph/_nx_cugraph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,14 @@
"house_x_graph",
"icosahedral_graph",
"in_degree_centrality",
"is_arborescence",
"is_bipartite",
"is_branching",
"is_connected",
"is_forest",
"is_isolate",
"is_strongly_connected",
"is_tree",
"is_weakly_connected",
"isolates",
"k_truss",
Expand Down
2 changes: 2 additions & 0 deletions python/nx-cugraph/nx_cugraph/algorithms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
link_analysis,
shortest_paths,
traversal,
tree,
)
from .bipartite import complete_bipartite_graph, is_bipartite
from .centrality import *
Expand All @@ -31,3 +32,4 @@
from .reciprocity import *
from .shortest_paths import *
from .traversal import *
from .tree.recognition import *
2 changes: 1 addition & 1 deletion python/nx-cugraph/nx_cugraph/algorithms/isolate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
rlratzel marked this conversation as resolved.
Show resolved Hide resolved
return int(cp.count_nonzero(_mark_isolates(G)))
13 changes: 13 additions & 0 deletions python/nx-cugraph/nx_cugraph/algorithms/tree/__init__.py
Original file line number Diff line number Diff line change
@@ -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 *
74 changes: 74 additions & 0 deletions python/nx-cugraph/nx_cugraph/algorithms/tree/recognition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# 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)
# A tree must have an edge count equal to the number of nodes minus the
# tree's root node.
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
# A tree must have an edge count equal to the number of nodes minus the
# tree's root node.
return len(G) - 1 == G.number_of_edges() and is_connected(G)
12 changes: 7 additions & 5 deletions python/nx-cugraph/nx_cugraph/classes/digraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from .graph import Graph

if TYPE_CHECKING: # pragma: no cover
from nx_cugraph.typing import NodeKey
from nx_cugraph.typing import AttrKey

__all__ = ["DiGraph"]

Expand All @@ -47,10 +47,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

Expand Down Expand Up @@ -182,11 +180,15 @@ def _in_degrees_array(self, *, ignore_selfloops=False):
if ignore_selfloops:
not_selfloops = self.src_indices != dst_indices
dst_indices = dst_indices[not_selfloops]
if dst_indices.size == 0:
return cp.zeros(self._N, dtype=np.int64)
return cp.bincount(dst_indices, minlength=self._N)

def _out_degrees_array(self, *, ignore_selfloops=False):
src_indices = self.src_indices
if ignore_selfloops:
not_selfloops = src_indices != self.dst_indices
src_indices = src_indices[not_selfloops]
if src_indices.size == 0:
return cp.zeros(self._N, dtype=np.int64)
return cp.bincount(src_indices, minlength=self._N)
4 changes: 3 additions & 1 deletion python/nx-cugraph/nx_cugraph/classes/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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))
4 changes: 3 additions & 1 deletion python/nx-cugraph/nx_cugraph/classes/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -740,6 +740,8 @@ def _degrees_array(self, *, ignore_selfloops=False):
src_indices = src_indices[not_selfloops]
if self.is_directed():
dst_indices = dst_indices[not_selfloops]
if src_indices.size == 0:
return cp.zeros(self._N, dtype=np.int64)
Comment on lines +743 to +744
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi: I added this check b/c cupy/cupy#8116

degrees = cp.bincount(src_indices, minlength=self._N)
if self.is_directed():
degrees += cp.bincount(dst_indices, minlength=self._N)
Expand Down
Loading