Skip to content
Open
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
20 changes: 15 additions & 5 deletions graphblas/io/_networkx.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ def from_networkx(G, nodelist=None, dtype=None, weight="weight", name=None):
Graph to convert
nodelist : list, optional
List of nodes in the nx.Graph. If not provided, all nodes will be used.
dtype :
Data type
dtype : optional
A built-in data type (bool, int, float or complex). If not provided, it
is inferred from the weights.
weight : str, default="weight"
Weight attribute
name : str, optional
Expand All @@ -29,13 +30,22 @@ def from_networkx(G, nodelist=None, dtype=None, weight="weight", name=None):
If the graph has no nodes, or ``nodelist`` is empty, repeats a node, or
names one the graph does not have (as ``nx.to_scipy_sparse_array`` does).
ValueError
If a weight is not a bool, int (at most 64 bits), float or complex scalar.
If a weight is not a bool, int (at most 64 bits), float or complex scalar,
or ``dtype`` is a user-defined type.

"""
import networkx as nx

if dtype is not None:
dtype = lookup_dtype(dtype).np_type
dtype = lookup_dtype(dtype)
if dtype._is_udt:
# numpy reads the weights as the fields or elements of one value of
# the type, which built a matrix of the type's base dtype for some edge
# counts and raised unrelated errors for others (through scipy too).
raise ValueError(
f"dtype must be a built-in bool, int, float or complex type, not the UDT {dtype}"
)
dtype = dtype.np_type

# The node selection below mirrors nx.to_scipy_sparse_array so that empty
# graphs, nodelist subsets, missing nodes, and duplicate nodes raise the
Expand Down Expand Up @@ -68,7 +78,7 @@ def from_networkx(G, nodelist=None, dtype=None, weight="weight", name=None):
G = G.subgraph(nodelist)

index = dict(zip(nodelist, range(nlen), strict=True))
if weight is None and not G.is_multigraph() and (dtype is None or dtype.kind in "biufc"):
if weight is None and not G.is_multigraph():
# Every entry is 1, so read only the coordinates: row by row in nodelist
# order, straight from the adjacency, which for an undirected graph already
# holds both directions and each self-loop once. No tuple and no value per
Expand Down
18 changes: 18 additions & 0 deletions graphblas/tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,24 @@ def test_from_networkx_structure_matches_the_general_path(graph_cls, dtype):
gb.io.from_networkx(G, nodelist=["a", "missing"], weight=None)


@pytest.mark.skipif("not nx")
@pytest.mark.parametrize("weight", [None, "weight"])
@pytest.mark.parametrize("nedges", [1, 2, 3])
@pytest.mark.parametrize("dtype", ["record", "FP64[2]", "INT64[3]"])
def test_from_networkx_rejects_udt_dtype(weight, nedges, dtype):
# numpy read the weights as the fields or elements of one UDT value, so a type
# whose element count matched the number of weights came back as its base
# dtype (FP64[2] with two edges gave FP64), and other counts raised unrelated
# numpy errors.
if dtype == "record":
dtype = dtypes.register_anonymous(
np.dtype([("from_networkx_x", np.int64), ("from_networkx_y", np.float64)])
)
G = nx.path_graph(nedges + 1)
with pytest.raises(ValueError, match="not the UDT"):
gb.io.from_networkx(G, weight=weight, dtype=dtype)


@pytest.mark.skipif("not nx")
@pytest.mark.parametrize("wt", [np.inf, -np.inf, 1e308])
def test_from_networkx_undirected_selfloop_keeps_its_weight(wt):
Expand Down
Loading