Skip to content

Commit aa0347c

Browse files
authored
nx-cugraph: add relabel_nodes and convert_node_labels_to_integers (#4531)
This was pretty tricky in places! This begins with `Graph` and `DiGraph`, which are probably tricker, because of the way edge data get merged when nodes are combined (and I wouldn't be surprised if there are other ways to do this operation). This is one of the most heavily used functions in networkx and also networkx dependents. Up next: multigraphs Authors: - Erik Welch (https://github.com/eriknw) - Ralph Liu (https://github.com/nv-rliu) Approvers: - Rick Ratzel (https://github.com/rlratzel) - Bradley Dice (https://github.com/bdice) URL: #4531
1 parent ac35be3 commit aa0347c

File tree

13 files changed

+536
-31
lines changed

13 files changed

+536
-31
lines changed

python/nx-cugraph/.flake8

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
# Copyright (c) 2023, NVIDIA CORPORATION.
1+
# Copyright (c) 2023-2024, NVIDIA CORPORATION.
22

33
[flake8]
44
max-line-length = 88
55
inline-quotes = "
66
extend-ignore =
7+
B020,
78
E203,
89
SIM105,
910
SIM401,

python/nx-cugraph/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,9 @@ Below is the list of algorithms that are currently supported in nx-cugraph.
267267
<a href="https://networkx.org/documentation/stable/reference/convert.html#module-networkx.convert_matrix">convert_matrix</a>
268268
├─ <a href="https://networkx.org/documentation/stable/reference/generated/networkx.convert_matrix.from_pandas_edgelist.html#networkx.convert_matrix.from_pandas_edgelist">from_pandas_edgelist</a>
269269
└─ <a href="https://networkx.org/documentation/stable/reference/generated/networkx.convert_matrix.from_scipy_sparse_array.html#networkx.convert_matrix.from_scipy_sparse_array">from_scipy_sparse_array</a>
270+
<a href="https://networkx.org/documentation/stable/reference/relabel.html#module-networkx.relabel">relabel</a>
271+
├─ <a href="https://networkx.org/documentation/stable/reference/generated/networkx.relabel.convert_node_labels_to_integers.html#networkx.relabel.convert_node_labels_to_integers">convert_node_labels_to_integers</a>
272+
└─ <a href="https://networkx.org/documentation/stable/reference/generated/networkx.relabel.relabel_nodes.html#networkx.relabel.relabel_nodes">relabel_nodes</a>
270273
</pre>
271274

272275
To request nx-cugraph backend support for a NetworkX API that is not listed

python/nx-cugraph/_nx_cugraph/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"complete_graph",
7070
"complete_multipartite_graph",
7171
"connected_components",
72+
"convert_node_labels_to_integers",
7273
"core_number",
7374
"cubical_graph",
7475
"cycle_graph",
@@ -130,6 +131,7 @@
130131
"path_graph",
131132
"petersen_graph",
132133
"reciprocity",
134+
"relabel_nodes",
133135
"reverse",
134136
"sedgewick_maze_graph",
135137
"shortest_path",

python/nx-cugraph/lint.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,15 @@ repos:
5050
- id: black
5151
# - id: black-jupyter
5252
- repo: https://github.com/astral-sh/ruff-pre-commit
53-
rev: v0.5.1
53+
rev: v0.5.4
5454
hooks:
5555
- id: ruff
5656
args: [--fix-only, --show-fixes] # --unsafe-fixes]
5757
- repo: https://github.com/PyCQA/flake8
5858
rev: 7.1.0
5959
hooks:
6060
- id: flake8
61-
args: ['--per-file-ignores=_nx_cugraph/__init__.py:E501', '--extend-ignore=SIM105'] # Why is this necessary?
61+
args: ['--per-file-ignores=_nx_cugraph/__init__.py:E501', '--extend-ignore=B020,SIM105'] # Why is this necessary?
6262
additional_dependencies: &flake8_dependencies
6363
# These versions need updated manually
6464
- flake8==7.1.0
@@ -77,7 +77,7 @@ repos:
7777
additional_dependencies: [tomli]
7878
files: ^(nx_cugraph|docs)/
7979
- repo: https://github.com/astral-sh/ruff-pre-commit
80-
rev: v0.5.1
80+
rev: v0.5.4
8181
hooks:
8282
- id: ruff
8383
- repo: https://github.com/pre-commit/pre-commit-hooks

python/nx-cugraph/nx_cugraph/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
from . import convert_matrix
2424
from .convert_matrix import *
2525

26+
from . import relabel
27+
from .relabel import *
28+
2629
from . import generators
2730
from .generators import *
2831

python/nx-cugraph/nx_cugraph/algorithms/operators/unary.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,10 @@ def reverse(G, copy=True):
5151
if not G.is_directed():
5252
raise nx.NetworkXError("Cannot reverse an undirected graph.")
5353
if isinstance(G, nx.Graph):
54+
if not copy:
55+
raise RuntimeError(
56+
"Using `copy=False` is invalid when using a NetworkX graph "
57+
"as input to `nx_cugraph.reverse`"
58+
)
5459
G = nxcg.from_networkx(G, preserve_all_attrs=True)
5560
return G.reverse(copy=copy)

python/nx-cugraph/nx_cugraph/algorithms/traversal/breadth_first_search.py

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -57,23 +57,41 @@ def _bfs(G, source, *, depth_limit=None, reverse=False):
5757
return distances[mask], predecessors[mask], node_ids[mask]
5858

5959

60-
@networkx_algorithm(is_incomplete=True, version_added="24.02", _plc="bfs")
61-
def generic_bfs_edges(G, source, neighbors=None, depth_limit=None, sort_neighbors=None):
62-
"""`neighbors` and `sort_neighbors` parameters are not yet supported."""
63-
if neighbors is not None:
64-
raise NotImplementedError(
65-
"neighbors argument in generic_bfs_edges is not currently supported"
66-
)
67-
if sort_neighbors is not None:
68-
raise NotImplementedError(
69-
"sort_neighbors argument in generic_bfs_edges is not currently supported"
70-
)
71-
return bfs_edges(G, source, depth_limit=depth_limit)
72-
73-
74-
@generic_bfs_edges._can_run
75-
def _(G, source, neighbors=None, depth_limit=None, sort_neighbors=None):
76-
return neighbors is None and sort_neighbors is None
60+
if nx.__version__[:3] <= "3.3":
61+
62+
@networkx_algorithm(is_incomplete=True, version_added="24.02", _plc="bfs")
63+
def generic_bfs_edges(
64+
G, source, neighbors=None, depth_limit=None, sort_neighbors=None
65+
):
66+
"""`neighbors` and `sort_neighbors` parameters are not yet supported."""
67+
if neighbors is not None:
68+
raise NotImplementedError(
69+
"neighbors argument in generic_bfs_edges is not currently supported"
70+
)
71+
if sort_neighbors is not None:
72+
raise NotImplementedError(
73+
"sort_neighbors argument in generic_bfs_edges is not supported"
74+
)
75+
return bfs_edges(G, source, depth_limit=depth_limit)
76+
77+
@generic_bfs_edges._can_run
78+
def _(G, source, neighbors=None, depth_limit=None, sort_neighbors=None):
79+
return neighbors is None and sort_neighbors is None
80+
81+
else:
82+
83+
@networkx_algorithm(is_incomplete=True, version_added="24.02", _plc="bfs")
84+
def generic_bfs_edges(G, source, neighbors=None, depth_limit=None):
85+
"""`neighbors` parameter is not yet supported."""
86+
if neighbors is not None:
87+
raise NotImplementedError(
88+
"neighbors argument in generic_bfs_edges is not currently supported"
89+
)
90+
return bfs_edges(G, source, depth_limit=depth_limit)
91+
92+
@generic_bfs_edges._can_run
93+
def _(G, source, neighbors=None, depth_limit=None):
94+
return neighbors is None
7795

7896

7997
@networkx_algorithm(is_incomplete=True, version_added="24.02", _plc="bfs")

python/nx-cugraph/nx_cugraph/convert.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -411,13 +411,21 @@ def func(it, edge_attr=edge_attr, dtype=dtype):
411411
# Node values may be numpy or cupy arrays (useful for str, object, etc).
412412
# Someday we'll let the user choose np or cp, and support edge values.
413413
node_mask = np.fromiter(iter_mask, bool)
414-
node_value = np.array(vals, dtype)
415414
try:
416-
node_value = cp.array(node_value)
415+
node_value = np.array(vals, dtype)
417416
except ValueError:
418-
pass
417+
# Handle e.g. list elements
418+
if dtype is None or dtype == object:
419+
node_value = np.fromiter(vals, object)
420+
else:
421+
raise
419422
else:
420-
node_mask = cp.array(node_mask)
423+
try:
424+
node_value = cp.array(node_value)
425+
except ValueError:
426+
pass
427+
else:
428+
node_mask = cp.array(node_mask)
421429
node_values[node_attr] = node_value
422430
node_masks[node_attr] = node_mask
423431
# if vals.ndim > 1: ...
@@ -431,7 +439,12 @@ def func(it, edge_attr=edge_attr, dtype=dtype):
431439
# Node values may be numpy or cupy arrays (useful for str, object, etc).
432440
# Someday we'll let the user choose np or cp, and support edge values.
433441
if dtype is None:
434-
node_value = np.array(list(iter_values))
442+
vals = list(iter_values)
443+
try:
444+
node_value = np.array(vals)
445+
except ValueError:
446+
# Handle e.g. list elements
447+
node_value = np.fromiter(vals, object)
435448
else:
436449
node_value = np.fromiter(iter_values, dtype)
437450
try:
@@ -477,6 +490,23 @@ def func(it, edge_attr=edge_attr, dtype=dtype):
477490
return rv
478491

479492

493+
def _to_tuples(ndim, L):
494+
if ndim > 2:
495+
L = list(map(_to_tuples.__get__(ndim - 1), L))
496+
return list(map(tuple, L))
497+
498+
499+
def _array_to_tuples(a):
500+
"""Like ``a.tolist()``, but nested structures are tuples instead of lists.
501+
502+
This is only different from ``a.tolist()`` if ``a.ndim > 1``. It is used to
503+
try to return tuples instead of lists for e.g. node values.
504+
"""
505+
if a.ndim > 1:
506+
return _to_tuples(a.ndim, a.tolist())
507+
return a.tolist()
508+
509+
480510
def _iter_attr_dicts(
481511
values: dict[AttrKey, any_ndarray[EdgeValue | NodeValue]],
482512
masks: dict[AttrKey, any_ndarray[bool]],
@@ -485,7 +515,7 @@ def _iter_attr_dicts(
485515
if full_attrs:
486516
full_dicts = (
487517
dict(zip(full_attrs, vals))
488-
for vals in zip(*(values[attr].tolist() for attr in full_attrs))
518+
for vals in zip(*(_array_to_tuples(values[attr]) for attr in full_attrs))
489519
)
490520
partial_attrs = list(values.keys() & masks.keys())
491521
if partial_attrs:

python/nx-cugraph/nx_cugraph/interface.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ def key(testpath):
6868
louvain_different = "Louvain may be different due to RNG"
6969
no_string_dtype = "string edge values not currently supported"
7070
sssp_path_different = "sssp may choose a different valid path"
71+
no_object_dtype_for_edges = (
72+
"Edges don't support object dtype (lists, strings, etc.)"
73+
)
74+
tuple_elements_preferred = "elements are tuples instead of lists"
75+
nx_cugraph_in_test_setup = (
76+
"nx-cugraph Graph is incompatible in test setup in nx versions < 3.3"
77+
)
7178

7279
xfail = {
7380
# This is removed while strongly_connected_components() is not
@@ -91,6 +98,81 @@ def key(testpath):
9198
"test_cycles.py:TestMinimumCycleBasis."
9299
"test_gh6787_and_edge_attribute_names"
93100
): sssp_path_different,
101+
key(
102+
"test_graph_hashing.py:test_isomorphic_edge_attr"
103+
): no_object_dtype_for_edges,
104+
key(
105+
"test_graph_hashing.py:test_isomorphic_edge_attr_and_node_attr"
106+
): no_object_dtype_for_edges,
107+
key(
108+
"test_graph_hashing.py:test_isomorphic_edge_attr_subgraph_hash"
109+
): no_object_dtype_for_edges,
110+
key(
111+
"test_graph_hashing.py:"
112+
"test_isomorphic_edge_attr_and_node_attr_subgraph_hash"
113+
): no_object_dtype_for_edges,
114+
key(
115+
"test_summarization.py:TestSNAPNoEdgeTypes.test_summary_graph"
116+
): no_object_dtype_for_edges,
117+
key(
118+
"test_summarization.py:TestSNAPUndirected.test_summary_graph"
119+
): no_object_dtype_for_edges,
120+
key(
121+
"test_summarization.py:TestSNAPDirected.test_summary_graph"
122+
): no_object_dtype_for_edges,
123+
key("test_gexf.py:TestGEXF.test_relabel"): no_object_dtype_for_edges,
124+
key(
125+
"test_gml.py:TestGraph.test_parse_gml_cytoscape_bug"
126+
): no_object_dtype_for_edges,
127+
key("test_gml.py:TestGraph.test_parse_gml"): no_object_dtype_for_edges,
128+
key("test_gml.py:TestGraph.test_read_gml"): no_object_dtype_for_edges,
129+
key("test_gml.py:TestGraph.test_data_types"): no_object_dtype_for_edges,
130+
key(
131+
"test_gml.py:TestPropertyLists.test_reading_graph_with_list_property"
132+
): no_object_dtype_for_edges,
133+
key(
134+
"test_relabel.py:"
135+
"test_relabel_preserve_node_order_partial_mapping_with_copy_false"
136+
): "Node order is preserved when relabeling with partial mapping",
137+
key(
138+
"test_gml.py:"
139+
"TestPropertyLists.test_reading_graph_with_single_element_list_property"
140+
): tuple_elements_preferred,
141+
key(
142+
"test_relabel.py:"
143+
"TestRelabel.test_relabel_multidigraph_inout_merge_nodes"
144+
): no_string_dtype,
145+
key(
146+
"test_relabel.py:TestRelabel.test_relabel_multigraph_merge_inplace"
147+
): no_string_dtype,
148+
key(
149+
"test_relabel.py:TestRelabel.test_relabel_multidigraph_merge_inplace"
150+
): no_string_dtype,
151+
key(
152+
"test_relabel.py:TestRelabel.test_relabel_multidigraph_inout_copy"
153+
): no_string_dtype,
154+
key(
155+
"test_relabel.py:TestRelabel.test_relabel_multigraph_merge_copy"
156+
): no_string_dtype,
157+
key(
158+
"test_relabel.py:TestRelabel.test_relabel_multidigraph_merge_copy"
159+
): no_string_dtype,
160+
key(
161+
"test_relabel.py:TestRelabel.test_relabel_multigraph_nonnumeric_key"
162+
): no_string_dtype,
163+
key("test_contraction.py:test_multigraph_path"): no_object_dtype_for_edges,
164+
key(
165+
"test_contraction.py:test_directed_multigraph_path"
166+
): no_object_dtype_for_edges,
167+
key(
168+
"test_contraction.py:test_multigraph_blockmodel"
169+
): no_object_dtype_for_edges,
170+
key(
171+
"test_summarization.py:TestSNAPUndirectedMulti.test_summary_graph"
172+
): no_string_dtype,
173+
key(
174+
"test_summarization.py:TestSNAPDirectedMulti.test_summary_graph"
175+
): no_string_dtype,
94176
}
95177

96178
from packaging.version import parse
@@ -118,6 +200,19 @@ def key(testpath):
118200
"test_strongly_connected.py:"
119201
"TestStronglyConnected.test_connected_raise"
120202
): "test is incompatible with pytest>=8",
203+
# NetworkX 3.3 introduced logic around functions that return graphs
204+
key(
205+
"test_vf2pp_helpers.py:TestGraphTinoutUpdating.test_updating"
206+
): nx_cugraph_in_test_setup,
207+
key(
208+
"test_vf2pp_helpers.py:TestGraphTinoutUpdating.test_restoring"
209+
): nx_cugraph_in_test_setup,
210+
key(
211+
"test_vf2pp_helpers.py:TestDiGraphTinoutUpdating.test_updating"
212+
): nx_cugraph_in_test_setup,
213+
key(
214+
"test_vf2pp_helpers.py:TestDiGraphTinoutUpdating.test_restoring"
215+
): nx_cugraph_in_test_setup,
121216
}
122217
)
123218

0 commit comments

Comments
 (0)