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 from_dict_of_lists and to_dict_of_lists #4537

Merged
merged 4 commits into from
Jul 30, 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
3 changes: 3 additions & 0 deletions python/nx-cugraph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ Below is the list of algorithms that are currently supported in nx-cugraph.
<a href="https://networkx.org/documentation/stable/reference/classes/index.html">classes</a>
└─ <a href="https://networkx.org/documentation/stable/reference/functions.html#module-networkx.classes.function">function</a>
└─ <a href="https://networkx.org/documentation/stable/reference/generated/networkx.classes.function.is_negatively_weighted.html#networkx.classes.function.is_negatively_weighted">is_negatively_weighted</a>
<a href="https://networkx.org/documentation/stable/reference/convert.html#module-networkx.convert">convert</a>
├─ <a href="https://networkx.org/documentation/stable/reference/generated/networkx.convert.from_dict_of_lists.html#networkx.convert.from_dict_of_lists">from_dict_of_lists</a>
└─ <a href="https://networkx.org/documentation/stable/reference/generated/networkx.convert.to_dict_of_lists.html#networkx.convert.to_dict_of_lists">to_dict_of_lists</a>
<a href="https://networkx.org/documentation/stable/reference/convert.html#module-networkx.convert_matrix">convert_matrix</a>
├─ <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>
└─ <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>
Expand Down
2 changes: 2 additions & 0 deletions python/nx-cugraph/_nx_cugraph/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"eigenvector_centrality",
"empty_graph",
"florentine_families_graph",
"from_dict_of_lists",
"from_pandas_edgelist",
"from_scipy_sparse_array",
"frucht_graph",
Expand Down Expand Up @@ -138,6 +139,7 @@
"star_graph",
"tadpole_graph",
"tetrahedral_graph",
"to_dict_of_lists",
"transitivity",
"triangles",
"trivial_graph",
Expand Down
102 changes: 100 additions & 2 deletions python/nx-cugraph/nx_cugraph/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import itertools
import operator as op
from collections import Counter
from collections import Counter, defaultdict
from collections.abc import Mapping
from typing import TYPE_CHECKING

Expand All @@ -24,14 +24,17 @@

import nx_cugraph as nxcg

from .utils import index_dtype
from .utils import index_dtype, networkx_algorithm
from .utils.misc import pairwise

if TYPE_CHECKING: # pragma: no cover
from nx_cugraph.typing import AttrKey, Dtype, EdgeValue, NodeValue, any_ndarray

__all__ = [
"from_networkx",
"to_networkx",
"from_dict_of_lists",
"to_dict_of_lists",
]

concat = itertools.chain.from_iterable
Expand Down Expand Up @@ -653,3 +656,98 @@ def _to_undirected_graph(
)
# TODO: handle cugraph.Graph
raise TypeError


@networkx_algorithm(version_added="24.08")
def from_dict_of_lists(d, create_using=None):
from .generators._utils import _create_using_class

graph_class, inplace = _create_using_class(create_using)
key_to_id = defaultdict(itertools.count().__next__)
src_indices = cp.array(
# cp.repeat is slow to use here, so use numpy instead
np.repeat(
np.fromiter(map(key_to_id.__getitem__, d), index_dtype),
np.fromiter(map(len, d.values()), index_dtype),
)
)
dst_indices = cp.fromiter(
map(key_to_id.__getitem__, concat(d.values())), index_dtype
)
# Initialize as directed first them symmetrize if undirected.
G = graph_class.to_directed_class().from_coo(
len(key_to_id),
src_indices,
dst_indices,
key_to_id=key_to_id,
)
if not graph_class.is_directed():
G = G.to_undirected()
if inplace:
return create_using._become(G)
return G


@networkx_algorithm(version_added="24.08")
def to_dict_of_lists(G, nodelist=None):
G = _to_graph(G)
src_indices = G.src_indices
dst_indices = G.dst_indices
if nodelist is not None:
try:
node_ids = G._nodekeys_to_nodearray(nodelist)
except KeyError as exc:
gname = "digraph" if G.is_directed() else "graph"
raise nx.NetworkXError(
f"The node {exc.args[0]} is not in the {gname}."
) from exc
mask = cp.isin(src_indices, node_ids) & cp.isin(dst_indices, node_ids)
src_indices = src_indices[mask]
dst_indices = dst_indices[mask]
# Sort indices so we can use `cp.unique` to determine boundaries.
# This is like exporting to DCSR.
if G.is_multigraph():
stacked = cp.unique(cp.vstack((src_indices, dst_indices)), axis=1)
src_indices = stacked[0]
dst_indices = stacked[1]
else:
stacked = cp.vstack((dst_indices, src_indices))
indices = cp.lexsort(stacked)
src_indices = src_indices[indices]
dst_indices = dst_indices[indices]
compressed_srcs, left_bounds = cp.unique(src_indices, return_index=True)
# Ensure we include isolate nodes in the result (and in proper order)
rv = None
if nodelist is not None:
if compressed_srcs.size != len(nodelist):
if G.key_to_id is None:
# `G._nodekeys_to_nodearray` does not check for valid node keys.
container = range(G._N)
for key in nodelist:
if key not in container:
gname = "digraph" if G.is_directed() else "graph"
raise nx.NetworkXError(f"The node {key} is not in the {gname}.")
rv = {key: [] for key in nodelist}
elif compressed_srcs.size != G._N:
rv = {key: [] for key in G}
# We use `boundaries` like this in `_groupby` too
boundaries = pairwise(itertools.chain(left_bounds.tolist(), [src_indices.size]))
dst_indices = dst_indices.tolist()
if G.key_to_id is None:
it = zip(compressed_srcs.tolist(), boundaries)
if rv is None:
return {src: dst_indices[start:end] for src, (start, end) in it}
rv.update((src, dst_indices[start:end]) for src, (start, end) in it)
return rv
to_key = G.id_to_key.__getitem__
it = zip(compressed_srcs.tolist(), boundaries)
if rv is None:
return {
to_key(src): list(map(to_key, dst_indices[start:end]))
for src, (start, end) in it
}
rv.update(
(to_key(src), list(map(to_key, dst_indices[start:end])))
for src, (start, end) in it
)
return rv
50 changes: 49 additions & 1 deletion python/nx-cugraph/nx_cugraph/tests/test_convert.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,10 +13,13 @@
import cupy as cp
import networkx as nx
import pytest
from packaging.version import parse

import nx_cugraph as nxcg
from nx_cugraph import interface

nxver = parse(nx.__version__)


@pytest.mark.parametrize(
"graph_class", [nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph]
Expand Down Expand Up @@ -224,3 +227,48 @@ def test_multigraph(graph_class):
H = nxcg.to_networkx(Gcg)
assert type(G) is type(H)
assert nx.utils.graphs_equal(G, H)


def test_to_dict_of_lists():
G = nx.MultiGraph()
G.add_edge("a", "b")
G.add_edge("a", "c")
G.add_edge("a", "b")
expected = nx.to_dict_of_lists(G)
result = nxcg.to_dict_of_lists(G)
assert expected == result
expected = nx.to_dict_of_lists(G, nodelist=["a", "b"])
result = nxcg.to_dict_of_lists(G, nodelist=["a", "b"])
assert expected == result
with pytest.raises(nx.NetworkXError, match="The node d is not in the graph"):
nx.to_dict_of_lists(G, nodelist=["a", "d"])
with pytest.raises(nx.NetworkXError, match="The node d is not in the graph"):
nxcg.to_dict_of_lists(G, nodelist=["a", "d"])
G.add_node("d") # No edges
expected = nx.to_dict_of_lists(G)
result = nxcg.to_dict_of_lists(G)
assert expected == result
expected = nx.to_dict_of_lists(G, nodelist=["a", "d"])
result = nxcg.to_dict_of_lists(G, nodelist=["a", "d"])
assert expected == result
# Now try with default node ids
G = nx.DiGraph()
G.add_edge(0, 1)
G.add_edge(0, 2)
expected = nx.to_dict_of_lists(G)
result = nxcg.to_dict_of_lists(G)
assert expected == result
expected = nx.to_dict_of_lists(G, nodelist=[0, 1])
result = nxcg.to_dict_of_lists(G, nodelist=[0, 1])
assert expected == result
with pytest.raises(nx.NetworkXError, match="The node 3 is not in the digraph"):
nx.to_dict_of_lists(G, nodelist=[0, 3])
with pytest.raises(nx.NetworkXError, match="The node 3 is not in the digraph"):
nxcg.to_dict_of_lists(G, nodelist=[0, 3])
G.add_node(3) # No edges
expected = nx.to_dict_of_lists(G)
result = nxcg.to_dict_of_lists(G)
assert expected == result
expected = nx.to_dict_of_lists(G, nodelist=[0, 3])
result = nxcg.to_dict_of_lists(G, nodelist=[0, 3])
assert expected == result
Loading