sparseqr 1.5__tar.gz → 1.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sparseqr-1.5 → sparseqr-1.5.1}/PKG-INFO +14 -1
- {sparseqr-1.5 → sparseqr-1.5.1}/README.md +13 -0
- {sparseqr-1.5 → sparseqr-1.5.1}/sparseqr/__init__.py +1 -1
- {sparseqr-1.5 → sparseqr-1.5.1}/sparseqr/sparseqr.py +26 -4
- {sparseqr-1.5 → sparseqr-1.5.1}/sparseqr.egg-info/PKG-INFO +14 -1
- sparseqr-1.5.1/test/test_complex.py +105 -0
- sparseqr-1.5/test/test_complex.py +0 -58
- {sparseqr-1.5 → sparseqr-1.5.1}/LICENSE.md +0 -0
- {sparseqr-1.5 → sparseqr-1.5.1}/pyproject.toml +0 -0
- {sparseqr-1.5 → sparseqr-1.5.1}/setup.cfg +0 -0
- {sparseqr-1.5 → sparseqr-1.5.1}/setup.py +0 -0
- {sparseqr-1.5 → sparseqr-1.5.1}/sparseqr/cffi_asarray.py +0 -0
- {sparseqr-1.5 → sparseqr-1.5.1}/sparseqr/sparseqr_gen.py +0 -0
- {sparseqr-1.5 → sparseqr-1.5.1}/sparseqr.egg-info/SOURCES.txt +0 -0
- {sparseqr-1.5 → sparseqr-1.5.1}/sparseqr.egg-info/dependency_links.txt +0 -0
- {sparseqr-1.5 → sparseqr-1.5.1}/sparseqr.egg-info/requires.txt +0 -0
- {sparseqr-1.5 → sparseqr-1.5.1}/sparseqr.egg-info/top_level.txt +0 -0
- {sparseqr-1.5 → sparseqr-1.5.1}/test/test_sparseqr.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sparseqr
|
|
3
|
-
Version: 1.5
|
|
3
|
+
Version: 1.5.1
|
|
4
4
|
Summary: Python wrapper for SuiteSparseQR
|
|
5
5
|
Author-email: Yotam Gingold <yotam@yotamgingold.com>
|
|
6
6
|
License-Expression: CC0-1.0
|
|
@@ -140,6 +140,13 @@ or leave them in their directory and call it as a module.
|
|
|
140
140
|
FLIT_USERNAME=__token__ uv tool run --with flit flit publish --format sdist
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
or
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
uv build --sdist
|
|
147
|
+
UV_PUBLISH_TOKEN=<token> uv publish
|
|
148
|
+
```
|
|
149
|
+
|
|
143
150
|
We don't publish binary wheels, because it must be compiled against suite-sparse as a system dependency. We could publish a `none-any` wheel, which would cause compilation to happen the first time the module is imported rather than when it is installed. Is there a point to that?
|
|
144
151
|
|
|
145
152
|
# Known issues
|
|
@@ -166,6 +173,12 @@ or
|
|
|
166
173
|
uv run --extra test pytest
|
|
167
174
|
```
|
|
168
175
|
|
|
176
|
+
or
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
uv run --with sparseqr,pytest path/to/test_sparseqr.py
|
|
180
|
+
```
|
|
181
|
+
|
|
169
182
|
# Dependencies
|
|
170
183
|
|
|
171
184
|
These are listed as dependencies and will be installed automatically:
|
|
@@ -121,6 +121,13 @@ or leave them in their directory and call it as a module.
|
|
|
121
121
|
FLIT_USERNAME=__token__ uv tool run --with flit flit publish --format sdist
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
or
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
uv build --sdist
|
|
128
|
+
UV_PUBLISH_TOKEN=<token> uv publish
|
|
129
|
+
```
|
|
130
|
+
|
|
124
131
|
We don't publish binary wheels, because it must be compiled against suite-sparse as a system dependency. We could publish a `none-any` wheel, which would cause compilation to happen the first time the module is imported rather than when it is installed. Is there a point to that?
|
|
125
132
|
|
|
126
133
|
# Known issues
|
|
@@ -147,6 +154,12 @@ or
|
|
|
147
154
|
uv run --extra test pytest
|
|
148
155
|
```
|
|
149
156
|
|
|
157
|
+
or
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
uv run --with sparseqr,pytest path/to/test_sparseqr.py
|
|
161
|
+
```
|
|
162
|
+
|
|
150
163
|
# Dependencies
|
|
151
164
|
|
|
152
165
|
These are listed as dependencies and will be installed automatically:
|
|
@@ -17,7 +17,7 @@ See the docstrings of the individual functions for details.
|
|
|
17
17
|
|
|
18
18
|
from __future__ import absolute_import
|
|
19
19
|
|
|
20
|
-
__version__ = '1.5'
|
|
20
|
+
__version__ = '1.5.1'
|
|
21
21
|
|
|
22
22
|
# import the important things into the package's top-level namespace.
|
|
23
23
|
from .sparseqr import qr, rz, solve, permutation_vector_to_matrix, qr_factorize,qmult
|
|
@@ -170,19 +170,41 @@ When no longer needed, the returned CHOLMOD dense matrix must be deallocated usi
|
|
|
170
170
|
nrow = numpy_A.shape[0]
|
|
171
171
|
ncol = numpy_A.shape[1]
|
|
172
172
|
lda = nrow # cholmod_dense is column-oriented
|
|
173
|
-
|
|
173
|
+
|
|
174
|
+
# Check if the array has complex entries and adapt cholmod type accordingly
|
|
175
|
+
is_complex = numpy.issubdtype(numpy_A.dtype, numpy.complexfloating)
|
|
176
|
+
cholmod_dtype = lib.CHOLMOD_COMPLEX if is_complex else lib.CHOLMOD_REAL
|
|
177
|
+
|
|
178
|
+
chol_A = lib.cholmod_l_allocate_dense( nrow, ncol, lda, cholmod_dtype, cc )
|
|
174
179
|
if chol_A == ffi.NULL:
|
|
175
180
|
raise RuntimeError("Failed to allocate chol_A")
|
|
176
181
|
Adata = ffi.cast( "double*", chol_A.x )
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
|
|
183
|
+
if is_complex:
|
|
184
|
+
# chol_A.x has size 2*nrow*ncol and real and imag parts are interleaved [real, imag, real, imag, ...]
|
|
185
|
+
array_element_size = ffi.sizeof(ffi.typeof(Adata).item)
|
|
186
|
+
Adata_view = numpy.frombuffer(ffi.buffer(Adata, 2*nrow*ncol * array_element_size), ctype2dtype["double"])
|
|
187
|
+
for j in range(ncol):
|
|
188
|
+
col_data = numpy_A[:,j]
|
|
189
|
+
Adata_view[(j*lda*2):((j+1)*lda*2):2] = col_data.real
|
|
190
|
+
Adata_view[(j*lda*2)+1:((j+1)*lda*2):2] = col_data.imag
|
|
191
|
+
else:
|
|
192
|
+
for j in range(ncol): # FIXME inefficient?
|
|
193
|
+
Adata[(j*lda):((j+1)*lda)] = numpy_A[:,j]
|
|
179
194
|
return chol_A
|
|
180
195
|
|
|
181
196
|
def cholmoddense2numpy( chol_A ):
|
|
182
197
|
'''Convert a CHOLMOD dense matrix to a NumPy array.'''
|
|
183
198
|
Adata = ffi.cast( "double*", chol_A.x )
|
|
184
199
|
|
|
185
|
-
|
|
200
|
+
if chol_A.xtype == lib.CHOLMOD_COMPLEX:
|
|
201
|
+
# read real and imag part from the buffer and create view as complex datatype
|
|
202
|
+
result = asarray(ffi, Adata, 2*chol_A.nrow*chol_A.ncol).copy()
|
|
203
|
+
complex_dtype = numpy.dtype(f"c{result.itemsize * 2}")
|
|
204
|
+
result = result.view(complex_dtype)
|
|
205
|
+
else:
|
|
206
|
+
result = asarray( ffi, Adata, chol_A.nrow*chol_A.ncol ).copy()
|
|
207
|
+
|
|
186
208
|
result = result.reshape( (chol_A.nrow, chol_A.ncol), order='F' )
|
|
187
209
|
return result
|
|
188
210
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sparseqr
|
|
3
|
-
Version: 1.5
|
|
3
|
+
Version: 1.5.1
|
|
4
4
|
Summary: Python wrapper for SuiteSparseQR
|
|
5
5
|
Author-email: Yotam Gingold <yotam@yotamgingold.com>
|
|
6
6
|
License-Expression: CC0-1.0
|
|
@@ -140,6 +140,13 @@ or leave them in their directory and call it as a module.
|
|
|
140
140
|
FLIT_USERNAME=__token__ uv tool run --with flit flit publish --format sdist
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
+
or
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
uv build --sdist
|
|
147
|
+
UV_PUBLISH_TOKEN=<token> uv publish
|
|
148
|
+
```
|
|
149
|
+
|
|
143
150
|
We don't publish binary wheels, because it must be compiled against suite-sparse as a system dependency. We could publish a `none-any` wheel, which would cause compilation to happen the first time the module is imported rather than when it is installed. Is there a point to that?
|
|
144
151
|
|
|
145
152
|
# Known issues
|
|
@@ -166,6 +173,12 @@ or
|
|
|
166
173
|
uv run --extra test pytest
|
|
167
174
|
```
|
|
168
175
|
|
|
176
|
+
or
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
uv run --with sparseqr,pytest path/to/test_sparseqr.py
|
|
180
|
+
```
|
|
181
|
+
|
|
169
182
|
# Dependencies
|
|
170
183
|
|
|
171
184
|
These are listed as dependencies and will be installed automatically:
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Tests for complex matrix support in sparseqr."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
import scipy.sparse
|
|
6
|
+
|
|
7
|
+
import sparseqr
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def adj(A):
|
|
11
|
+
"""Return the conjugate transpose (adjoint) of a matrix."""
|
|
12
|
+
return A.conj().T
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestComplexQR:
|
|
16
|
+
"""Tests for QR decomposition of complex matrices."""
|
|
17
|
+
|
|
18
|
+
ATOL = 1e-14 # close to machine precision
|
|
19
|
+
|
|
20
|
+
def test_complex_qr_unitary_q(self):
|
|
21
|
+
"""Test that Q is unitary for complex matrix (Q @ Q^H = I)."""
|
|
22
|
+
A = scipy.sparse.random(m=120, n=100, density=0.1, dtype=np.complex128, random_state=42)
|
|
23
|
+
Q, R, P, rank = sparseqr.qr(A)
|
|
24
|
+
|
|
25
|
+
QQH = (Q @ adj(Q)).toarray()
|
|
26
|
+
np.testing.assert_allclose(QQH, np.eye(Q.shape[0]), atol=self.ATOL)
|
|
27
|
+
|
|
28
|
+
def test_complex_qr_upper_triangular_r(self):
|
|
29
|
+
"""Test that R is upper triangular for complex matrix."""
|
|
30
|
+
A = scipy.sparse.random(m=120, n=100, density=0.1, dtype=np.complex128, random_state=42)
|
|
31
|
+
Q, R, P, rank = sparseqr.qr(A)
|
|
32
|
+
|
|
33
|
+
lower_triangle = np.tril(R.toarray(), k=-1)
|
|
34
|
+
np.testing.assert_allclose(lower_triangle, 0, atol=self.ATOL)
|
|
35
|
+
|
|
36
|
+
def test_complex_qr_reconstruction(self):
|
|
37
|
+
"""Test that A can be reconstructed from QR decomposition: A = (QR) @ P^T."""
|
|
38
|
+
A = scipy.sparse.random(m=120, n=100, density=0.1, dtype=np.complex128, random_state=42)
|
|
39
|
+
Q, R, P, rank = sparseqr.qr(A)
|
|
40
|
+
|
|
41
|
+
P_matrix = sparseqr.permutation_vector_to_matrix(P)
|
|
42
|
+
A_reconstructed = (Q @ R) @ P_matrix.T
|
|
43
|
+
np.testing.assert_allclose(A_reconstructed.toarray(), A.toarray(), atol=self.ATOL)
|
|
44
|
+
|
|
45
|
+
@pytest.mark.parametrize("shape", [(50, 50), (100, 50), (50, 100)])
|
|
46
|
+
def test_complex_qr_various_shapes(self, shape):
|
|
47
|
+
"""Test complex QR on square, tall, and wide matrices."""
|
|
48
|
+
m, n = shape
|
|
49
|
+
A = scipy.sparse.random(m=m, n=n, density=0.1, dtype=np.complex128, random_state=42)
|
|
50
|
+
Q, R, P, rank = sparseqr.qr(A)
|
|
51
|
+
|
|
52
|
+
P_matrix = sparseqr.permutation_vector_to_matrix(P)
|
|
53
|
+
A_reconstructed = (Q @ R) @ P_matrix.T
|
|
54
|
+
np.testing.assert_allclose(A_reconstructed.toarray(), A.toarray(), atol=self.ATOL)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestComplexSolve:
|
|
58
|
+
"""Tests for solve function with complex matrices."""
|
|
59
|
+
|
|
60
|
+
ATOL = 1e-10
|
|
61
|
+
|
|
62
|
+
def test_complex_solve_exact_system(self):
|
|
63
|
+
"""Test solving an exact complex system where Ax=b has exact solution."""
|
|
64
|
+
A = scipy.sparse.random(m=50, n=50, density=0.2, dtype=np.complex128, random_state=42)
|
|
65
|
+
# Add diagonal to make it well-conditioned
|
|
66
|
+
A = A + 5 * scipy.sparse.eye(50, dtype=np.complex128)
|
|
67
|
+
x_true = np.random.RandomState(42).random(50) + 1j * np.random.RandomState(43).random(50)
|
|
68
|
+
b = A @ x_true
|
|
69
|
+
|
|
70
|
+
x = sparseqr.solve(A, b, tolerance=0)
|
|
71
|
+
|
|
72
|
+
assert x is not None, "solve() returned None"
|
|
73
|
+
np.testing.assert_allclose(x, x_true, atol=self.ATOL)
|
|
74
|
+
|
|
75
|
+
def test_complex_solve_overdetermined_least_squares(self):
|
|
76
|
+
"""Test least-squares solution for overdetermined complex system."""
|
|
77
|
+
# 100 equations, 50 unknowns
|
|
78
|
+
A = scipy.sparse.random(m=100, n=50, density=0.2, dtype=np.complex128, random_state=42)
|
|
79
|
+
x_true = np.random.RandomState(42).random(50) + 1j * np.random.RandomState(43).random(50)
|
|
80
|
+
b = A @ x_true
|
|
81
|
+
|
|
82
|
+
x = sparseqr.solve(A, b, tolerance=0)
|
|
83
|
+
|
|
84
|
+
assert x is not None, "solve() returned None"
|
|
85
|
+
assert x.shape == (50,), f"Solution shape wrong: {x.shape}"
|
|
86
|
+
# For an exact system (b in range of A), solution should match
|
|
87
|
+
np.testing.assert_allclose(x, x_true, atol=self.ATOL)
|
|
88
|
+
|
|
89
|
+
def test_complex_solve_multiple_rhs(self):
|
|
90
|
+
"""Test solving AX=B with multiple complex RHS vectors."""
|
|
91
|
+
A = scipy.sparse.random(m=50, n=50, density=0.2, dtype=np.complex128, random_state=42)
|
|
92
|
+
A = A + 5 * scipy.sparse.eye(50, dtype=np.complex128)
|
|
93
|
+
X_true = (np.random.RandomState(42).random((50, 3)) +
|
|
94
|
+
1j * np.random.RandomState(43).random((50, 3)))
|
|
95
|
+
B = A @ X_true
|
|
96
|
+
|
|
97
|
+
X = sparseqr.solve(A, B, tolerance=0)
|
|
98
|
+
|
|
99
|
+
assert X is not None, "solve() returned None"
|
|
100
|
+
assert X.shape == (50, 3), f"Solution shape wrong: {X.shape}"
|
|
101
|
+
np.testing.assert_allclose(X, X_true, atol=self.ATOL)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == '__main__':
|
|
105
|
+
pytest.main([__file__, '-v'])
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
"""Tests for complex matrix support in sparseqr."""
|
|
2
|
-
|
|
3
|
-
import numpy as np
|
|
4
|
-
import pytest
|
|
5
|
-
from scipy.sparse import random as sparse_random
|
|
6
|
-
|
|
7
|
-
import sparseqr
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def adj(A):
|
|
11
|
-
"""Return the conjugate transpose (adjoint) of a matrix."""
|
|
12
|
-
return A.conj().T
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class TestComplexQR:
|
|
16
|
-
"""Tests for QR decomposition of complex matrices."""
|
|
17
|
-
|
|
18
|
-
ATOL = 1e-14 # close to machine precision
|
|
19
|
-
|
|
20
|
-
def test_complex_qr_unitary_q(self):
|
|
21
|
-
"""Test that Q is unitary for complex matrix (Q @ Q^H = I)."""
|
|
22
|
-
A = sparse_random(m=120, n=100, density=0.1, dtype=np.complex128, random_state=42)
|
|
23
|
-
Q, R, P, rank = sparseqr.qr(A)
|
|
24
|
-
|
|
25
|
-
QQH = (Q @ adj(Q)).toarray()
|
|
26
|
-
np.testing.assert_allclose(QQH, np.eye(Q.shape[0]), atol=self.ATOL)
|
|
27
|
-
|
|
28
|
-
def test_complex_qr_upper_triangular_r(self):
|
|
29
|
-
"""Test that R is upper triangular for complex matrix."""
|
|
30
|
-
A = sparse_random(m=120, n=100, density=0.1, dtype=np.complex128, random_state=42)
|
|
31
|
-
Q, R, P, rank = sparseqr.qr(A)
|
|
32
|
-
|
|
33
|
-
lower_triangle = np.tril(R.toarray(), k=-1)
|
|
34
|
-
np.testing.assert_allclose(lower_triangle, 0, atol=self.ATOL)
|
|
35
|
-
|
|
36
|
-
def test_complex_qr_reconstruction(self):
|
|
37
|
-
"""Test that A can be reconstructed from QR decomposition: A = (QR) @ P^T."""
|
|
38
|
-
A = sparse_random(m=120, n=100, density=0.1, dtype=np.complex128, random_state=42)
|
|
39
|
-
Q, R, P, rank = sparseqr.qr(A)
|
|
40
|
-
|
|
41
|
-
P_matrix = sparseqr.permutation_vector_to_matrix(P)
|
|
42
|
-
A_reconstructed = (Q @ R) @ P_matrix.T
|
|
43
|
-
np.testing.assert_allclose(A_reconstructed.toarray(), A.toarray(), atol=self.ATOL)
|
|
44
|
-
|
|
45
|
-
@pytest.mark.parametrize("shape", [(50, 50), (100, 50), (50, 100)])
|
|
46
|
-
def test_complex_qr_various_shapes(self, shape):
|
|
47
|
-
"""Test complex QR on square, tall, and wide matrices."""
|
|
48
|
-
m, n = shape
|
|
49
|
-
A = sparse_random(m=m, n=n, density=0.1, dtype=np.complex128, random_state=42)
|
|
50
|
-
Q, R, P, rank = sparseqr.qr(A)
|
|
51
|
-
|
|
52
|
-
P_matrix = sparseqr.permutation_vector_to_matrix(P)
|
|
53
|
-
A_reconstructed = (Q @ R) @ P_matrix.T
|
|
54
|
-
np.testing.assert_allclose(A_reconstructed.toarray(), A.toarray(), atol=self.ATOL)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if __name__ == '__main__':
|
|
58
|
-
pytest.main([__file__, '-v'])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|