Skip to content

Commit 78fa815

Browse files
authored
Merge pull request #887 from flying-sheep/pa/sparse-array
fix: support scipy.sparse.sparray
2 parents 2bdc042 + a9b7120 commit 78fa815

6 files changed

Lines changed: 35 additions & 26 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ jobs:
336336
run: |
337337
# We cannot install the test dependency group because many test dependencies cause
338338
# false positives in the sanitizer
339-
pip install --prefer-binary networkx pytest pytest-timeout
339+
pip install --prefer-binary networkx pytest pytest-timeout parameterized
340340
pip install -e .
341341
342342
# Only pytest, and nothing else should be run in this section due to the presence of LD_PRELOAD.

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,7 @@ def get_tag(self):
10111011
"networkx>=2.5",
10121012
"pytest>=7.0.1",
10131013
"pytest-timeout>=2.1.0",
1014+
"parameterized",
10141015
"numpy>=1.19.0; platform_python_implementation != 'PyPy'",
10151016
"pandas>=1.1.0; platform_python_implementation != 'PyPy'",
10161017
"scipy>=1.5.0; platform_python_implementation != 'PyPy'",
@@ -1039,6 +1040,7 @@ def get_tag(self):
10391040
"networkx>=2.5",
10401041
"pytest>=7.0.1",
10411042
"pytest-timeout>=2.1.0",
1043+
"parameterized",
10421044
],
10431045
# Dependencies needed for building the documentation
10441046
"doc": [

src/igraph/adjacency.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,16 @@ def _get_adjacency(
8686
return Matrix(data)
8787

8888

89-
def _get_adjacency_sparse(self, attribute=None):
90-
"""Returns the adjacency matrix of a graph as a SciPy CSR matrix.
89+
def _get_adjacency_sparse(self, attribute=None, *, container="matrix"):
90+
"""Returns the adjacency matrix of a graph as a SciPy CSR array or matrix.
9191
9292
@param attribute: if C{None}, returns the ordinary adjacency
9393
matrix. When the name of a valid edge attribute is given
9494
here, the matrix returned will contain the default value
9595
at the places where there is no edge or the value of the
9696
given attribute where there is an edge.
97-
@return: the adjacency matrix as a C{scipy.sparse.csr_matrix}.
97+
@param container: either C{"array"} or C{"matrix"}
98+
@return: the adjacency matrix as a C{scipy.sparse.csr_array} or C{scipy.sparse.csr_matrix}.
9899
"""
99100
try:
100101
from scipy import sparse
@@ -103,6 +104,10 @@ def _get_adjacency_sparse(self, attribute=None):
103104
"You should install scipy in order to use this function"
104105
) from None
105106

107+
if container not in {"array", "matrix"}:
108+
raise ValueError("container must be either 'array' or 'matrix'")
109+
cls = sparse.csr_array if container == "array" else sparse.csr_matrix
110+
106111
edges = self.get_edgelist()
107112
if attribute is None:
108113
weights = [1] * len(edges)
@@ -114,7 +119,7 @@ def _get_adjacency_sparse(self, attribute=None):
114119

115120
N = self.vcount()
116121
r, c = zip(*edges) if edges else ([], [])
117-
mtx = sparse.csr_matrix((weights, (r, c)), shape=(N, N))
122+
mtx = cls((weights, (r, c)), shape=(N, N))
118123

119124
if not self.is_directed():
120125
mtx = mtx + sparse.triu(mtx, 1).T + sparse.tril(mtx, -1).T

src/igraph/io/adjacency.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,23 @@
44
)
55

66

7+
def _sp_cls():
8+
try:
9+
from scipy import sparse
10+
except ImportError:
11+
return ()
12+
if not hasattr(sparse, "sparray"): # scipy < 1.11
13+
return sparse.spmatrix
14+
return (sparse.sparray, sparse.spmatrix)
15+
16+
717
def _construct_graph_from_adjacency(cls, matrix, mode="directed", loops="once"):
818
"""Generates a graph from its adjacency matrix.
919
1020
@param matrix: the adjacency matrix. Possible types are:
1121
- a list of lists
1222
- a numpy 2D array or matrix (will be converted to list of lists)
13-
- a scipy.sparse matrix (will be converted to a COO matrix, but not
23+
- a scipy.sparse array or matrix (will be converted to COO format, but not
1424
to a dense matrix)
1525
- a pandas.DataFrame (column/row names must match, and will be used
1626
as vertex names).
@@ -42,17 +52,12 @@ def _construct_graph_from_adjacency(cls, matrix, mode="directed", loops="once"):
4252
except ImportError:
4353
np = None
4454

45-
try:
46-
from scipy import sparse
47-
except ImportError:
48-
sparse = None
49-
5055
try:
5156
import pandas as pd
5257
except ImportError:
5358
pd = None
5459

55-
if (sparse is not None) and isinstance(matrix, sparse.spmatrix):
60+
if isinstance(matrix, _sp_cls()):
5661
return _graph_from_sparse_matrix(cls, matrix, mode=mode, loops=loops)
5762

5863
if (pd is not None) and isinstance(matrix, pd.DataFrame):
@@ -117,17 +122,12 @@ def _construct_graph_from_weighted_adjacency(
117122
except ImportError:
118123
np = None
119124

120-
try:
121-
from scipy import sparse
122-
except ImportError:
123-
sparse = None
124-
125125
try:
126126
import pandas as pd
127127
except ImportError:
128128
pd = None
129129

130-
if (sparse is not None) and isinstance(matrix, sparse.spmatrix):
130+
if isinstance(matrix, _sp_cls()):
131131
return _graph_from_weighted_sparse_matrix(
132132
cls,
133133
matrix,

src/igraph/sparse_matrix.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
# -*- coding: utf-8 -*-
33
"""Implementation of Python-level sparse matrix operations."""
44

5-
from __future__ import with_statement
6-
75
__all__ = ()
86
__docformat__ = "restructuredtext en"
97

@@ -65,7 +63,7 @@ def _maybe_halve_diagonal(m, condition):
6563
# Logic to get graph from scipy sparse matrix. This would be simple if there
6664
# weren't so many modes.
6765
def _graph_from_sparse_matrix(klass, matrix, mode="directed", loops="once"):
68-
"""Construct graph from sparse matrix, unweighted.
66+
"""Construct graph from sparse array or matrix, unweighted.
6967
7068
@param loops: specifies how the diagonal of the matrix should be handled:
7169
@@ -78,7 +76,7 @@ def _graph_from_sparse_matrix(klass, matrix, mode="directed", loops="once"):
7876
# matrix. The caller should make sure those conditions are met.
7977
from scipy import sparse
8078

81-
if not isinstance(matrix, sparse.coo_matrix):
79+
if not isinstance(matrix, (sparse.coo_matrix, *([sparse.coo_array] if hasattr(sparse, "coo_array") else []))):
8280
matrix = matrix.tocoo()
8381

8482
nvert = max(matrix.shape)
@@ -150,7 +148,7 @@ def _graph_from_sparse_matrix(klass, matrix, mode="directed", loops="once"):
150148
def _graph_from_weighted_sparse_matrix(
151149
klass, matrix, mode=ADJ_DIRECTED, attr="weight", loops="once"
152150
):
153-
"""Construct graph from sparse matrix, weighted
151+
"""Construct graph from sparse array or matrix, weighted
154152
155153
NOTE: Of course, you cannot emcompass a fully general weighted multigraph
156154
with a single adjacency matrix, so we don't try to do it here either.
@@ -165,7 +163,7 @@ def _graph_from_weighted_sparse_matrix(
165163
# matrix. The caller should make sure those conditions are met.
166164
from scipy import sparse
167165

168-
if not isinstance(matrix, sparse.coo_matrix):
166+
if not isinstance(matrix, (sparse.coo_matrix, *([sparse.coo_array] if hasattr(sparse, "coo_array") else []))):
169167
matrix = matrix.tocoo()
170168

171169
nvert = max(matrix.shape)

tests/test_generators.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import unittest
22

3+
from parameterized import parameterized
4+
35
from igraph import Graph, InternalError
46

57

@@ -747,11 +749,13 @@ def testWeightedAdjacencyNumPy(self):
747749
self.assertListEqual(el, [(1, 0), (0, 1), (3, 1), (0, 2), (2, 2)])
748750
self.assertListEqual(g.es["w0"], [2, 1, 1, 2, 2.5])
749751

752+
# not testing csc_* here, as the edge order comes out different
753+
@parameterized.expand(["coo_array", "coo_matrix", "csr_matrix", "csr_array"])
750754
@unittest.skipIf(
751755
(sparse is None) or (np is None), "test case depends on NumPy/SciPy"
752756
)
753-
def testSparseWeightedAdjacency(self):
754-
mat = sparse.coo_matrix(
757+
def testSparseWeightedAdjacency(self, cls):
758+
mat = getattr(sparse, cls)(
755759
[[0, 1, 2, 0], [2, 0, 0, 0], [0, 0, 2.5, 0], [0, 1, 0, 0]]
756760
)
757761

0 commit comments

Comments
 (0)