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.
@@ -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
- chol_A = lib.cholmod_l_allocate_dense( nrow, ncol, lda, lib.CHOLMOD_REAL, cc )
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
- for j in range(ncol): # FIXME inefficient?
178
- Adata[(j*lda):((j+1)*lda)] = numpy_A[:,j]
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
- result = asarray( ffi, Adata, chol_A.nrow*chol_A.ncol ).copy()
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