From 4748ca1a940dfa9a59ba821514a1df8b132be3cf Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 16 Jan 2024 09:39:31 -0600 Subject: [PATCH 1/2] nx-cugraph: PLC now handles isolated nodes; clean up our workarounds (#4092) Hooray for removing and cleaning code! Tests also added (we already tested isolated nodes for Louvain). nx-cugraph was updated to handle isolated nodes by passing the node set to PLC in #4077 Authors: - Erik Welch (https://github.com/eriknw) Approvers: - Rick Ratzel (https://github.com/rlratzel) URL: https://github.com/rapidsai/cugraph/pull/4092 --- python/nx-cugraph/_nx_cugraph/__init__.py | 10 +-- python/nx-cugraph/nx_cugraph/_version.py | 6 +- .../algorithms/community/louvain.py | 12 +--- .../algorithms/components/connected.py | 64 +++++++------------ .../nx-cugraph/nx_cugraph/tests/__init__.py | 3 +- .../nx_cugraph/tests/test_connected.py | 30 +++++++++ 6 files changed, 61 insertions(+), 64 deletions(-) create mode 100644 python/nx-cugraph/nx_cugraph/tests/test_connected.py diff --git a/python/nx-cugraph/_nx_cugraph/__init__.py b/python/nx-cugraph/_nx_cugraph/__init__.py index d02c9c3e940..4e869c76b7a 100644 --- a/python/nx-cugraph/_nx_cugraph/__init__.py +++ b/python/nx-cugraph/_nx_cugraph/__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 @@ -107,21 +107,21 @@ }, "extra_docstrings": { # BEGIN: extra_docstrings - "betweenness_centrality": "`weight` parameter is not yet supported.", + "betweenness_centrality": "`weight` parameter is not yet supported, and RNG with seed may be different.", "bfs_edges": "`sort_neighbors` parameter is not yet supported.", "bfs_predecessors": "`sort_neighbors` parameter is not yet supported.", "bfs_successors": "`sort_neighbors` parameter is not yet supported.", "bfs_tree": "`sort_neighbors` parameter is not yet supported.", - "edge_betweenness_centrality": "`weight` parameter is not yet supported.", + "edge_betweenness_centrality": "`weight` parameter is not yet supported, and RNG with seed may be different.", "eigenvector_centrality": "`nstart` parameter is not used, but it is checked for validity.", - "from_pandas_edgelist": "cudf.DataFrame inputs also supported.", + "from_pandas_edgelist": "cudf.DataFrame inputs also supported; value columns with str is unsuppported.", "generic_bfs_edges": "`neighbors` and `sort_neighbors` parameters are not yet supported.", "k_truss": ( "Currently raises `NotImplementedError` for graphs with more than one connected\n" "component when k >= 3. We expect to fix this soon." ), "katz_centrality": "`nstart` isn't used (but is checked), and `normalized=False` is not supported.", - "louvain_communities": "`seed` parameter is currently ignored.", + "louvain_communities": "`seed` parameter is currently ignored, and self-loops are not yet supported.", "pagerank": "`dangling` parameter is not supported, but it is checked for validity.", # END: extra_docstrings }, diff --git a/python/nx-cugraph/nx_cugraph/_version.py b/python/nx-cugraph/nx_cugraph/_version.py index 868a2e19475..a528a3bfe1b 100644 --- a/python/nx-cugraph/nx_cugraph/_version.py +++ b/python/nx-cugraph/nx_cugraph/_version.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. @@ -11,12 +11,10 @@ # 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 importlib.resources # Read VERSION file from the module that is symlinked to VERSION file -# in the root of the repo at build time or copied to the moudle at +# in the root of the repo at build time or copied to the module at # installation. VERSION is a separate file that allows CI build-time scripts # to update version info (including commit hashes) without modifying # source files. diff --git a/python/nx-cugraph/nx_cugraph/algorithms/community/louvain.py b/python/nx-cugraph/nx_cugraph/algorithms/community/louvain.py index d023bab1a47..413ff9ca5e3 100644 --- a/python/nx-cugraph/nx_cugraph/algorithms/community/louvain.py +++ b/python/nx-cugraph/nx_cugraph/algorithms/community/louvain.py @@ -23,8 +23,6 @@ not_implemented_for, ) -from ..isolate import _isolates - __all__ = ["louvain_communities"] @@ -56,7 +54,6 @@ def louvain_communities( seed = _seed_to_int(seed) # Unused, but ensure it's valid for future compatibility G = _to_undirected_graph(G, weight) if G.src_indices.size == 0: - # TODO: PLC doesn't handle empty graphs gracefully! return [{key} for key in G._nodeiter_to_iter(range(len(G)))] if max_level is None: max_level = 500 @@ -76,14 +73,7 @@ def louvain_communities( do_expensive_check=False, ) groups = _groupby(clusters, node_ids, groups_are_canonical=True) - rv = [set(G._nodearray_to_list(ids)) for ids in groups.values()] - # TODO: PLC doesn't handle isolated node_ids yet, so this is a temporary fix - isolates = _isolates(G) - if isolates.size > 0: - isolates = isolates[isolates > node_ids.max()] - if isolates.size > 0: - rv.extend({node} for node in G._nodearray_to_list(isolates)) - return rv + return [set(G._nodearray_to_list(ids)) for ids in groups.values()] @louvain_communities._can_run diff --git a/python/nx-cugraph/nx_cugraph/algorithms/components/connected.py b/python/nx-cugraph/nx_cugraph/algorithms/components/connected.py index cb12aed1d39..95cc907a82b 100644 --- a/python/nx-cugraph/nx_cugraph/algorithms/components/connected.py +++ b/python/nx-cugraph/nx_cugraph/algorithms/components/connected.py @@ -10,8 +10,6 @@ # 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 itertools - import cupy as cp import networkx as nx import pylibcugraph as plc @@ -19,8 +17,6 @@ from nx_cugraph.convert import _to_undirected_graph from nx_cugraph.utils import _groupby, networkx_algorithm, not_implemented_for -from ..isolate import _isolates - __all__ = [ "number_connected_components", "connected_components", @@ -32,19 +28,17 @@ @not_implemented_for("directed") @networkx_algorithm(plc="weakly_connected_components", version_added="23.12") def number_connected_components(G): - return sum(1 for _ in connected_components(G)) - # PREFERRED IMPLEMENTATION, BUT PLC DOES NOT HANDLE ISOLATED VERTICES WELL - # G = _to_undirected_graph(G) - # unused_node_ids, labels = plc.weakly_connected_components( - # resource_handle=plc.ResourceHandle(), - # graph=G._get_plc_graph(), - # offsets=None, - # indices=None, - # weights=None, - # labels=None, - # do_expensive_check=False, - # ) - # return cp.unique(labels).size + G = _to_undirected_graph(G) + unused_node_ids, labels = plc.weakly_connected_components( + resource_handle=plc.ResourceHandle(), + graph=G._get_plc_graph(), + offsets=None, + indices=None, + weights=None, + labels=None, + do_expensive_check=False, + ) + return cp.unique(labels).size @number_connected_components._can_run @@ -61,7 +55,6 @@ def _(G): def connected_components(G): G = _to_undirected_graph(G) if G.src_indices.size == 0: - # TODO: PLC doesn't handle empty graphs (or isolated nodes) gracefully! return [{key} for key in G._nodeiter_to_iter(range(len(G)))] node_ids, labels = plc.weakly_connected_components( resource_handle=plc.ResourceHandle(), @@ -73,16 +66,7 @@ def connected_components(G): do_expensive_check=False, ) groups = _groupby(labels, node_ids) - it = (G._nodearray_to_set(connected_ids) for connected_ids in groups.values()) - # TODO: PLC doesn't handle isolated vertices yet, so this is a temporary fix - isolates = _isolates(G) - if isolates.size > 0: - isolates = isolates[isolates > node_ids.max()] - if isolates.size > 0: - it = itertools.chain( - it, ({node} for node in G._nodearray_to_list(isolates)) - ) - return it + return (G._nodearray_to_set(connected_ids) for connected_ids in groups.values()) @not_implemented_for("directed") @@ -93,20 +77,16 @@ def is_connected(G): raise nx.NetworkXPointlessConcept( "Connectivity is undefined for the null graph." ) - for community in connected_components(G): - return len(community) == len(G) - raise RuntimeError # pragma: no cover - # PREFERRED IMPLEMENTATION, BUT PLC DOES NOT HANDLE ISOLATED VERTICES WELL - # unused_node_ids, labels = plc.weakly_connected_components( - # resource_handle=plc.ResourceHandle(), - # graph=G._get_plc_graph(), - # offsets=None, - # indices=None, - # weights=None, - # labels=None, - # do_expensive_check=False, - # ) - # return labels.size == len(G) and cp.unique(labels).size == 1 + unused_node_ids, labels = plc.weakly_connected_components( + resource_handle=plc.ResourceHandle(), + graph=G._get_plc_graph(), + offsets=None, + indices=None, + weights=None, + labels=None, + do_expensive_check=False, + ) + return bool((labels == labels[0]).all()) @not_implemented_for("directed") diff --git a/python/nx-cugraph/nx_cugraph/tests/__init__.py b/python/nx-cugraph/nx_cugraph/tests/__init__.py index ce94db52fa2..c2002fd3fb9 100644 --- a/python/nx-cugraph/nx_cugraph/tests/__init__.py +++ b/python/nx-cugraph/nx_cugraph/tests/__init__.py @@ -1,5 +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 diff --git a/python/nx-cugraph/nx_cugraph/tests/test_connected.py b/python/nx-cugraph/nx_cugraph/tests/test_connected.py new file mode 100644 index 00000000000..fa9f283abc0 --- /dev/null +++ b/python/nx-cugraph/nx_cugraph/tests/test_connected.py @@ -0,0 +1,30 @@ +# 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 networkx as nx + +import nx_cugraph as nxcg + + +def test_connected_isolated_nodes(): + G = nx.complete_graph(4) + G.add_node(max(G) + 1) + assert nx.is_connected(G) is False + assert nxcg.is_connected(G) is False + assert nx.number_connected_components(G) == 2 + assert nxcg.number_connected_components(G) == 2 + assert sorted(nx.connected_components(G)) == [{0, 1, 2, 3}, {4}] + assert sorted(nxcg.connected_components(G)) == [{0, 1, 2, 3}, {4}] + assert nx.node_connected_component(G, 0) == {0, 1, 2, 3} + assert nxcg.node_connected_component(G, 0) == {0, 1, 2, 3} + assert nx.node_connected_component(G, 4) == {4} + assert nxcg.node_connected_component(G, 4) == {4} From 8672534b667838c13c2818f7d8bded6da8155dff Mon Sep 17 00:00:00 2001 From: Erik Welch Date: Tue, 16 Jan 2024 19:39:48 -0600 Subject: [PATCH 2/2] nx-cugraph: add weakly connected components (#4071) This doesn't currently work, because `plc.weakly_connected_components` only works on symmetric graphs (so it's not actually performing wcc now is it?): > RuntimeError: non-success value returned from cugraph_weakly_connected_components: CUGRAPH_UNKNOWN_ERROR cuGraph failure at file=[...]/cugraph/cpp/src/components/weakly_connected_components_impl.cuh line=283: Invalid input argument: input graph should be symmetric for weakly connected components. _These are high-priority algorithms for `nx-cugraph`, because they are widely used by networkx dependents._ Authors: - Erik Welch (https://github.com/eriknw) Approvers: - Rick Ratzel (https://github.com/rlratzel) URL: https://github.com/rapidsai/cugraph/pull/4071 --- python/nx-cugraph/_nx_cugraph/__init__.py | 6 ++ .../algorithms/components/__init__.py | 4 +- .../algorithms/components/connected.py | 22 ++++- .../components/strongly_connected.py | 85 +++++++++++++++++++ .../algorithms/components/weakly_connected.py | 47 ++++++++++ .../nx_cugraph/algorithms/isolate.py | 24 ++++-- python/nx-cugraph/nx_cugraph/classes/graph.py | 23 ++++- python/nx-cugraph/nx_cugraph/interface.py | 7 +- 8 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 python/nx-cugraph/nx_cugraph/algorithms/components/strongly_connected.py create mode 100644 python/nx-cugraph/nx_cugraph/algorithms/components/weakly_connected.py diff --git a/python/nx-cugraph/_nx_cugraph/__init__.py b/python/nx-cugraph/_nx_cugraph/__init__.py index 4e869c76b7a..d9b997411ae 100644 --- a/python/nx-cugraph/_nx_cugraph/__init__.py +++ b/python/nx-cugraph/_nx_cugraph/__init__.py @@ -70,6 +70,8 @@ "in_degree_centrality", "is_connected", "is_isolate", + "is_strongly_connected", + "is_weakly_connected", "isolates", "k_truss", "karate_club_graph", @@ -85,6 +87,8 @@ "number_connected_components", "number_of_isolates", "number_of_selfloops", + "number_strongly_connected_components", + "number_weakly_connected_components", "octahedral_graph", "out_degree_centrality", "pagerank", @@ -95,6 +99,7 @@ "single_source_shortest_path_length", "single_target_shortest_path_length", "star_graph", + "strongly_connected_components", "tadpole_graph", "tetrahedral_graph", "trivial_graph", @@ -102,6 +107,7 @@ "truncated_tetrahedron_graph", "turan_graph", "tutte_graph", + "weakly_connected_components", "wheel_graph", # END: functions }, diff --git a/python/nx-cugraph/nx_cugraph/algorithms/components/__init__.py b/python/nx-cugraph/nx_cugraph/algorithms/components/__init__.py index 26816ef3692..12a09b535c0 100644 --- a/python/nx-cugraph/nx_cugraph/algorithms/components/__init__.py +++ b/python/nx-cugraph/nx_cugraph/algorithms/components/__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 @@ -11,3 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. from .connected import * +from .strongly_connected import * +from .weakly_connected import * diff --git a/python/nx-cugraph/nx_cugraph/algorithms/components/connected.py b/python/nx-cugraph/nx_cugraph/algorithms/components/connected.py index 95cc907a82b..cdb9f54f6c4 100644 --- a/python/nx-cugraph/nx_cugraph/algorithms/components/connected.py +++ b/python/nx-cugraph/nx_cugraph/algorithms/components/connected.py @@ -29,9 +29,15 @@ @networkx_algorithm(plc="weakly_connected_components", version_added="23.12") def number_connected_components(G): G = _to_undirected_graph(G) + return _number_connected_components(G) + + +def _number_connected_components(G, symmetrize=None): + if G.src_indices.size == 0: + return len(G) unused_node_ids, labels = plc.weakly_connected_components( resource_handle=plc.ResourceHandle(), - graph=G._get_plc_graph(), + graph=G._get_plc_graph(symmetrize=symmetrize), offsets=None, indices=None, weights=None, @@ -54,11 +60,15 @@ def _(G): @networkx_algorithm(plc="weakly_connected_components", version_added="23.12") def connected_components(G): G = _to_undirected_graph(G) + return _connected_components(G) + + +def _connected_components(G, symmetrize=None): if G.src_indices.size == 0: return [{key} for key in G._nodeiter_to_iter(range(len(G)))] node_ids, labels = plc.weakly_connected_components( resource_handle=plc.ResourceHandle(), - graph=G._get_plc_graph(), + graph=G._get_plc_graph(symmetrize=symmetrize), offsets=None, indices=None, weights=None, @@ -73,13 +83,19 @@ def connected_components(G): @networkx_algorithm(plc="weakly_connected_components", version_added="23.12") def is_connected(G): G = _to_undirected_graph(G) + return _is_connected(G) + + +def _is_connected(G, symmetrize=None): if len(G) == 0: raise nx.NetworkXPointlessConcept( "Connectivity is undefined for the null graph." ) + if G.src_indices.size == 0: + return len(G) == 1 unused_node_ids, labels = plc.weakly_connected_components( resource_handle=plc.ResourceHandle(), - graph=G._get_plc_graph(), + graph=G._get_plc_graph(symmetrize=symmetrize), offsets=None, indices=None, weights=None, diff --git a/python/nx-cugraph/nx_cugraph/algorithms/components/strongly_connected.py b/python/nx-cugraph/nx_cugraph/algorithms/components/strongly_connected.py new file mode 100644 index 00000000000..8fdf99ed5ea --- /dev/null +++ b/python/nx-cugraph/nx_cugraph/algorithms/components/strongly_connected.py @@ -0,0 +1,85 @@ +# 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 pylibcugraph as plc + +from nx_cugraph.convert import _to_directed_graph +from nx_cugraph.utils import ( + _groupby, + index_dtype, + networkx_algorithm, + not_implemented_for, +) + +__all__ = [ + "number_strongly_connected_components", + "strongly_connected_components", + "is_strongly_connected", +] + + +def _strongly_connected_components(G): + # TODO: create utility function to convert just the indices to CSR + # TODO: this uses a legacy PLC function (strongly_connected_components) + N = len(G) + indices = cp.lexsort(cp.vstack((G.dst_indices, G.src_indices))) + dst_indices = G.dst_indices[indices] + offsets = cp.searchsorted( + G.src_indices, cp.arange(N + 1, dtype=index_dtype), sorter=indices + ).astype(index_dtype) + labels = cp.zeros(N, dtype=index_dtype) + plc.strongly_connected_components( + offsets=offsets, + indices=dst_indices, + weights=None, + num_verts=N, + num_edges=dst_indices.size, + labels=labels, + ) + return labels + + +@not_implemented_for("undirected") +@networkx_algorithm(version_added="24.02", plc="strongly_connected_components") +def strongly_connected_components(G): + G = _to_directed_graph(G) + if G.src_indices.size == 0: + return [{key} for key in G._nodeiter_to_iter(range(len(G)))] + labels = _strongly_connected_components(G) + groups = _groupby(labels, cp.arange(len(G), dtype=index_dtype)) + return (G._nodearray_to_set(connected_ids) for connected_ids in groups.values()) + + +@not_implemented_for("undirected") +@networkx_algorithm(version_added="24.02", plc="strongly_connected_components") +def number_strongly_connected_components(G): + G = _to_directed_graph(G) + if G.src_indices.size == 0: + return len(G) + labels = _strongly_connected_components(G) + return cp.unique(labels).size + + +@not_implemented_for("undirected") +@networkx_algorithm(version_added="24.02", plc="strongly_connected_components") +def is_strongly_connected(G): + G = _to_directed_graph(G) + if len(G) == 0: + raise nx.NetworkXPointlessConcept( + "Connectivity is undefined for the null graph." + ) + if G.src_indices.size == 0: + return len(G) == 1 + labels = _strongly_connected_components(G) + return bool((labels == labels[0]).all()) diff --git a/python/nx-cugraph/nx_cugraph/algorithms/components/weakly_connected.py b/python/nx-cugraph/nx_cugraph/algorithms/components/weakly_connected.py new file mode 100644 index 00000000000..5b797b39118 --- /dev/null +++ b/python/nx-cugraph/nx_cugraph/algorithms/components/weakly_connected.py @@ -0,0 +1,47 @@ +# 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 nx_cugraph.convert import _to_directed_graph +from nx_cugraph.utils import networkx_algorithm, not_implemented_for + +from .connected import ( + _connected_components, + _is_connected, + _number_connected_components, +) + +__all__ = [ + "number_weakly_connected_components", + "weakly_connected_components", + "is_weakly_connected", +] + + +@not_implemented_for("undirected") +@networkx_algorithm(plc="weakly_connected_components", version_added="24.02") +def weakly_connected_components(G): + G = _to_directed_graph(G) + return _connected_components(G, symmetrize="union") + + +@not_implemented_for("undirected") +@networkx_algorithm(plc="weakly_connected_components", version_added="24.02") +def number_weakly_connected_components(G): + G = _to_directed_graph(G) + return _number_connected_components(G, symmetrize="union") + + +@not_implemented_for("undirected") +@networkx_algorithm(plc="weakly_connected_components", version_added="24.02") +def is_weakly_connected(G): + G = _to_directed_graph(G) + return _is_connected(G, symmetrize="union") diff --git a/python/nx-cugraph/nx_cugraph/algorithms/isolate.py b/python/nx-cugraph/nx_cugraph/algorithms/isolate.py index c7e5d7113de..62b47a9b354 100644 --- a/python/nx-cugraph/nx_cugraph/algorithms/isolate.py +++ b/python/nx-cugraph/nx_cugraph/algorithms/isolate.py @@ -15,9 +15,10 @@ from typing import TYPE_CHECKING import cupy as cp +import numpy as np from nx_cugraph.convert import _to_graph -from nx_cugraph.utils import networkx_algorithm +from nx_cugraph.utils import index_dtype, networkx_algorithm if TYPE_CHECKING: # pragma: no cover from nx_cugraph.typing import IndexValue @@ -36,19 +37,28 @@ def is_isolate(G, n): ) -def _mark_isolates(G) -> cp.ndarray[bool]: +def _mark_isolates(G, symmetrize=None) -> cp.ndarray[bool]: """Return a boolean mask array indicating indices of isolated nodes.""" mark_isolates = cp.ones(len(G), bool) - mark_isolates[G.src_indices] = False - if G.is_directed(): - mark_isolates[G.dst_indices] = False + if G.is_directed() and symmetrize == "intersection": + N = G._N + # Upcast to int64 so indices don't overflow + src_dst = N * G.src_indices.astype(np.int64) + G.dst_indices + src_dst_T = G.src_indices + N * G.dst_indices.astype(np.int64) + src_dst_new = cp.intersect1d(src_dst, src_dst_T) + new_indices = cp.floor_divide(src_dst_new, N, dtype=index_dtype) + mark_isolates[new_indices] = False + else: + mark_isolates[G.src_indices] = False + if G.is_directed(): + mark_isolates[G.dst_indices] = False return mark_isolates -def _isolates(G) -> cp.ndarray[IndexValue]: +def _isolates(G, symmetrize=None) -> cp.ndarray[IndexValue]: """Like isolates, but return an array of indices instead of an iterator of nodes.""" G = _to_graph(G) - return cp.nonzero(_mark_isolates(G))[0] + return cp.nonzero(_mark_isolates(G, symmetrize=symmetrize))[0] @networkx_algorithm(version_added="23.10") diff --git a/python/nx-cugraph/nx_cugraph/classes/graph.py b/python/nx-cugraph/nx_cugraph/classes/graph.py index cb6b4e7ae42..251e92b70ec 100644 --- a/python/nx-cugraph/nx_cugraph/classes/graph.py +++ b/python/nx-cugraph/nx_cugraph/classes/graph.py @@ -592,6 +592,7 @@ def _get_plc_graph( store_transposed: bool = False, switch_indices: bool = False, edge_array: cp.ndarray[EdgeValue] | None = None, + symmetrize: str | None = None, ): if edge_array is not None or edge_attr is None: pass @@ -650,12 +651,30 @@ def _get_plc_graph( dst_indices = self.dst_indices if switch_indices: src_indices, dst_indices = dst_indices, src_indices + if symmetrize is not None: + if edge_array is not None: + raise NotImplementedError( + "edge_array must be None when symmetrizing the graph" + ) + N = self._N + # Upcast to int64 so indices don't overflow + src_dst = N * src_indices.astype(np.int64) + dst_indices + src_dst_T = src_indices + N * dst_indices.astype(np.int64) + if symmetrize == "union": + src_dst_new = cp.union1d(src_dst, src_dst_T) + elif symmetrize == "intersection": + src_dst_new = cp.intersect1d(src_dst, src_dst_T) + else: + raise ValueError( + f'symmetrize must be "union" or "intersection"; got "{symmetrize}"' + ) + src_indices, dst_indices = cp.divmod(src_dst_new, N, dtype=index_dtype) return plc.SGGraph( resource_handle=plc.ResourceHandle(), graph_properties=plc.GraphProperties( - is_multigraph=self.is_multigraph(), - is_symmetric=not self.is_directed(), + is_multigraph=self.is_multigraph() and symmetrize is None, + is_symmetric=not self.is_directed() or symmetrize is not None, ), src_or_offset_array=src_indices, dst_or_index_array=dst_indices, diff --git a/python/nx-cugraph/nx_cugraph/interface.py b/python/nx-cugraph/nx_cugraph/interface.py index 34eb5969869..04591c0e9e3 100644 --- a/python/nx-cugraph/nx_cugraph/interface.py +++ b/python/nx-cugraph/nx_cugraph/interface.py @@ -68,7 +68,12 @@ def key(testpath): louvain_different = "Louvain may be different due to RNG" no_string_dtype = "string edge values not currently supported" - xfail = {} + xfail = { + key( + "test_strongly_connected.py:" + "TestStronglyConnected.test_condensation_mapping_and_members" + ): "Strongly connected groups in different iteration order", + } from packaging.version import parse