Skip to content

Commit 1465115

Browse files
dschultmartinfleis
andauthored
First part of SciPy sparse array migration from sparse matrices (#785)
* migration guide pass 1: make code work for both spmatrix and sparray switch from isspmatrix_csr to issparse and format='csr' * add conftest.txt to monkey-patch scipy sparse to fail on breaking * and ** * add test of row_st code to help coverage bot * workaround scipy1.8 limitation. Could be reverted for scipy1.12 * Clean up comments and remove conftest.txt -- PR ready to remove Draft status * bump scipy to 1.12 * cleanup --------- Co-authored-by: Martin Fleischmann <[email protected]>
1 parent 995b5d6 commit 1465115

File tree

10 files changed

+25
-38
lines changed

10 files changed

+25
-38
lines changed

ci/310-oldest.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ dependencies:
1010
- packaging=22
1111
- pandas=1.4
1212
- requests=2.27
13-
- scipy=1.8
13+
- scipy=1.12
1414
- shapely=2.0.1
1515
# testing
1616
- codecov

libpysal/graph/base.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33

44
import numpy as np
55
import pandas as pd
6-
from packaging.version import Version
7-
from scipy import __version__ as scipy_version
86
from scipy import sparse
97

108
from libpysal.weights import W
@@ -2000,9 +1998,6 @@ def higher_order(self, k=2, shortest_path=True, diagonal=False, lower_order=Fals
20001998
<Graph of 85 nodes and 1176 nonzero edges indexed by
20011999
[0, 1, 2, 3, 4, ...]>
20022000
"""
2003-
if not Version(scipy_version) >= Version("1.12.0"):
2004-
raise ImportError("Graph.higher_order() requires scipy>=1.12.0.")
2005-
20062001
binary = self.transform("B")
20072002
sp = binary.sparse
20082003

libpysal/graph/tests/test_base.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
import numpy as np
88
import pandas as pd
99
import pytest
10-
from packaging.version import Version
11-
from scipy import __version__ as scipy_version
1210
from scipy import sparse
1311

1412
from libpysal import graph, weights
@@ -1014,10 +1012,6 @@ def test_lag(self):
10141012
with pytest.raises(ValueError, match="The length of `y`"):
10151013
self.g_str.lag(list(range(1, 15)))
10161014

1017-
@pytest.mark.skipif(
1018-
Version(scipy_version) < Version("1.12.0"),
1019-
reason="sparse matrix power requires scipy>=1.12.0",
1020-
)
10211015
def test_higher_order(self):
10221016
cont = graph.Graph.build_contiguity(self.nybb)
10231017
k2 = cont.higher_order(2)
@@ -1091,10 +1085,6 @@ def test_higher_order(self):
10911085
lower = cont.higher_order(2, lower_order=True)
10921086
assert lower == expected
10931087

1094-
@pytest.mark.skipif(
1095-
Version(scipy_version) < Version("1.12.0"),
1096-
reason="sparse matrix power requires scipy>=1.12.0",
1097-
)
10981088
def test_higher_order_inclusive(self): # GH738
10991089
contig = graph.Graph.from_arrays(
11001090
[0, 1, 2, 3, 3, 4, 4], [0, 3, 4, 1, 4, 2, 3], [0, 1, 1, 1, 1, 1, 1]

libpysal/graph/tests/test_raster.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import numpy as np
22
import pandas as pd
33
import pytest
4-
from packaging.version import Version
5-
from scipy import __version__ as scipy_version
64

75
from libpysal import graph
86
from libpysal.weights.raster import testDataArray as dummy_array # noqa: N813
@@ -18,10 +16,6 @@ def setup_method(self):
1816
self.da4 = dummy_array((1, 1), missing_vals=False)
1917
self.da4.data = np.array([["test"]])
2018

21-
@pytest.mark.skipif(
22-
Version(scipy_version) < Version("1.12.0"),
23-
reason="sparse matrix power requires scipy>=1.12.0",
24-
)
2519
def test_queen(self):
2620
g1 = graph.Graph.build_raster_contiguity(self.da1, rook=False, k=2, n_jobs=-1)
2721
assert g1[(1, -30.0, -180.0)].to_dict() == {

libpysal/weights/raster.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ def da2WSP(
270270
# then eliminate zeros from the data. This changes the
271271
# sparsity of the csr_matrix !!
272272
if k > 1 and not include_nodata:
273-
sw = sum(sw**x for x in range(1, k + 1))
273+
sw = sum(sparse.linalg.matrix_power(sw, x) for x in range(1, k + 1))
274274
sw.setdiag(0)
275275
sw.eliminate_zeros()
276276
sw.data[:] = np.ones_like(sw.data, dtype=np.int8)

libpysal/weights/set_operations.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import copy
1313

1414
from numpy import ones
15-
from scipy.sparse import isspmatrix_csr
15+
from scipy.sparse import issparse
1616

1717
from .weights import WSP, W
1818

@@ -501,9 +501,9 @@ def w_clip(w1, w2, outSP=True, **kwargs): # noqa: N803
501501
if not w1.id_order:
502502
w1.id_order = None
503503
id_order = w1.id_order
504-
if not isspmatrix_csr(w1):
504+
if not issparse(w1):
505505
w1 = w1.sparse
506-
if not isspmatrix_csr(w2):
506+
if not issparse(w2):
507507
w2 = w2.sparse
508508
w2.data = ones(w2.data.shape)
509509
wc = w1.multiply(w2)

libpysal/weights/tests/test_util.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,21 @@ def test_lat2_w(self):
2525
assert w9[0] == {1: 1.0, 3: 1.0}
2626
assert w9[3] == {0: 1.0, 4: 1.0, 6: 1.0}
2727

28-
def test_lat2_sw(self):
29-
w9 = util.lat2SW(3, 3)
28+
@pytest.mark.parametrize("row_st", [True, False])
29+
def test_lat2_sw(self, row_st):
30+
w9 = util.lat2SW(3, 3, row_st=row_st)
3031
rows, cols = w9.shape
3132
n = rows * cols
3233
assert w9.nnz == 24
3334
pct_nonzero = w9.nnz / float(n)
3435
assert pct_nonzero == 0.29629629629629628
36+
if row_st:
37+
#### Can be this one-liner after scipy >=1.15 is assured
38+
# w9 = (w9.T.multiply(w9.tocsr().count_nonzero(axis=1))).T
39+
w9csr = w9.tocsr()
40+
nnz_by_axis1 = np.diff(w9csr.indptr)
41+
w9 = (w9csr.T.multiply(nnz_by_axis1)).T
42+
####
3543
data = w9.todense().tolist()
3644
assert data[0] == [0, 1, 0, 1, 0, 0, 0, 0, 0]
3745
assert data[1] == [1, 0, 1, 0, 1, 0, 0, 0, 0]

libpysal/weights/util.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ def higher_order_sp(
511511
w = w.sparse
512512
else:
513513
raise ValueError("Weights are not binary (0,1)")
514-
elif scipy.sparse.isspmatrix_csr(w):
514+
elif scipy.sparse.issparse(w) and w.format == "csr":
515515
if not np.unique(w.data) == np.array([1.0]):
516516
raise ValueError(
517517
"Sparse weights matrix is not binary (0,1) weights matrix."
@@ -523,17 +523,17 @@ def higher_order_sp(
523523
)
524524

525525
if lower_order:
526-
wk = sum(w**x for x in range(1, k + 1))
527526
shortest_path = False
527+
wk = sum(sparse.linalg.matrix_power(w, k) for k in range(1, k + 1))
528528
else:
529-
wk = w**k
529+
wk = sparse.linalg.matrix_power(w, k)
530530

531531
rk, ck = wk.nonzero()
532532
sk = set(zip(rk, ck, strict=True))
533533

534534
if shortest_path:
535535
for j in range(1, k):
536-
wj = w**j
536+
wj = sparse.linalg.matrix_power(w, j)
537537
rj, cj = wj.nonzero()
538538
sj = set(zip(rj, cj, strict=True))
539539
sk.difference_update(sj)
@@ -1225,7 +1225,7 @@ def lat2SW(nrows=3, ncols=5, criterion="rook", row_st=False):
12251225
m = sparse.dia_matrix((data, offsets), shape=(n, n), dtype=np.int8)
12261226
m = m + m.T
12271227
if row_st:
1228-
m = sparse.spdiags(1.0 / m.sum(1).T, 0, *m.shape) * m
1228+
m = sparse.dia_matrix(((1.0 / m.sum(1).T), [0]), shape=m.shape) @ m
12291229
m = m.tocsc()
12301230
m.eliminate_zeros()
12311231
return m

libpysal/weights/weights.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,7 @@ def diagW2(self):
721721
trcW2
722722
"""
723723
if "diagw2" not in self._cache:
724-
self._diagW2 = (self.sparse * self.sparse).diagonal()
724+
self._diagW2 = (self.sparse @ self.sparse).diagonal()
725725
self._cache["diagW2"] = self._diagW2
726726
return self._diagW2
727727

@@ -734,7 +734,7 @@ def diagWtW(self):
734734
trcWtW
735735
"""
736736
if "diagWtW" not in self._cache:
737-
self._diagWtW = (self.sparse.transpose() * self.sparse).diagonal()
737+
self._diagWtW = (self.sparse.transpose() @ self.sparse).diagonal()
738738
self._cache["diagWtW"] = self._diagWtW
739739
return self._diagWtW
740740

@@ -757,7 +757,7 @@ def diagWtW_WW(self):
757757
if "diagWtW_WW" not in self._cache:
758758
wt = self.sparse.transpose()
759759
w = self.sparse
760-
self._diagWtW_WW = (wt * w + w * w).diagonal()
760+
self._diagWtW_WW = (wt @ w + w @ w).diagonal()
761761
self._cache["diagWtW_WW"] = self._diagWtW_WW
762762
return self._diagWtW_WW
763763

@@ -1603,7 +1603,7 @@ def diagWtW_WW(self):
16031603
if "diagWtW_WW" not in self._cache:
16041604
wt = self.sparse.transpose()
16051605
w = self.sparse
1606-
self._diagWtW_WW = (wt * w + w * w).diagonal()
1606+
self._diagWtW_WW = (wt @ w + w @ w).diagonal()
16071607
self._cache["diagWtW_WW"] = self._diagWtW_WW
16081608
return self._diagWtW_WW
16091609

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ dependencies = [
3232
"pandas>=1.4",
3333
"platformdirs>=2.0.2",
3434
"requests>=2.27",
35-
"scipy>=1.8",
35+
"scipy>=1.12",
3636
"shapely>=2.0.1",
3737
"scikit-learn>=1.1",
3838
]

0 commit comments

Comments
 (0)