sparseqr 1.4.1__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,3 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: sparseqr
3
+ Version: 1.5.1
4
+ Summary: Python wrapper for SuiteSparseQR
5
+ Author-email: Yotam Gingold <yotam@yotamgingold.com>
6
+ License-Expression: CC0-1.0
7
+ Project-URL: homepage, https://github.com/yig/PySPQR
8
+ Project-URL: source, https://github.com/yig/PySPQR
9
+ Keywords: suitesparse,bindings,wrapper,scipy,numpy,qr-decomposition,qr-factorisation,sparse-matrix,sparse-linear-system,sparse-linear-solver
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE.md
13
+ Requires-Dist: numpy>1.2
14
+ Requires-Dist: scipy>=1.0
15
+ Requires-Dist: cffi>=1.0
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest>=7.0; extra == "test"
18
+ Dynamic: license-file
19
+
1
20
  # Python wrapper for SuiteSparseQR
2
21
 
3
22
  This module wraps the [SuiteSparseQR](http://faculty.cse.tamu.edu/davis/suitesparse.html)
@@ -103,19 +122,29 @@ or leave them in their directory and call it as a module.
103
122
 
104
123
  # Deploy
105
124
 
106
- 1. Change the version in:
125
+ 1. Change the version in `sparseqr/__init__.py`
126
+
127
+ 2. Update `CHANGELOG.md`
128
+
129
+ 3. Commit to git. Push to GitHub.
130
+
131
+ 4. Run (in a clean repos, e.g., `git clone . clean; cd clean`):
107
132
 
108
133
  ```
109
- sparseqr/__init__.py
110
- pyproject.toml
134
+ flit publish --format sdist
111
135
  ```
112
136
 
113
- 2. Update `CHANGELOG.md`
137
+ Using [uv](https://docs.astral.sh/uv/) and [PyPI API tokens](https://pypi.org/help/#apitoken):
138
+
139
+ ```
140
+ FLIT_USERNAME=__token__ uv tool run --with flit flit publish --format sdist
141
+ ```
114
142
 
115
- 3. Run:
143
+ or
116
144
 
117
145
  ```
118
- flit publish --format sdist
146
+ uv build --sdist
147
+ UV_PUBLISH_TOKEN=<token> uv publish
119
148
  ```
120
149
 
121
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?
@@ -124,17 +153,35 @@ We don't publish binary wheels, because it must be compiled against suite-sparse
124
153
 
125
154
  `pip uninstall sparseqr` won't remove the generated libraries. It will list them with a warning.
126
155
 
127
- # Tested on
156
+ # Tested
157
+
158
+ GitHub Continuous Integration (CI) tests:
128
159
 
129
- - Python 3.9, 3.13.
130
- - Conda and not conda.
131
- - macOS, Ubuntu Linux, and Linux Mint.
160
+ - Python 3.9, 3.10, 3.11, 3.12, 3.13, 3.14.
161
+ - macOS, Ubuntu Linux, and Windows.
162
+ - conda (Windows) and not conda (Linux/macOS). I tested conda on macOS manually at one point.
132
163
 
133
- PYTHONPATH='.:$PYTHONPATH' python3 test/test.py
164
+ Test manually with:
165
+
166
+ ```
167
+ python -m pytest
168
+ ```
169
+
170
+ or
171
+
172
+ ```
173
+ uv run --extra test pytest
174
+ ```
175
+
176
+ or
177
+
178
+ ```
179
+ uv run --with sparseqr,pytest path/to/test_sparseqr.py
180
+ ```
134
181
 
135
182
  # Dependencies
136
183
 
137
- These are installed via pip:
184
+ These are listed as dependencies and will be installed automatically:
138
185
 
139
186
  * [SciPy/NumPy](http://www.scipy.org)
140
187
  * [cffi](http://cffi.readthedocs.io/)
@@ -1,18 +1,3 @@
1
- Metadata-Version: 2.3
2
- Name: sparseqr
3
- Version: 1.4.1
4
- Summary: Python wrapper for SuiteSparseQR
5
- Keywords: suitesparse,bindings,wrapper,scipy,numpy,qr-decomposition,qr-factorisation,sparse-matrix,sparse-linear-system,sparse-linear-solver
6
- Author-email: Yotam Gingold <yotam@yotamgingold.com>
7
- Requires-Python: >= 3.8
8
- Description-Content-Type: text/markdown
9
- Requires-Dist: numpy >1.2
10
- Requires-Dist: scipy >= 1.0
11
- Requires-Dist: cffi >= 1.0
12
- Requires-Dist: setuptools >35
13
- Project-URL: homepage, https://github.com/yig/PySPQR
14
- Project-URL: source, https://github.com/yig/PySPQR
15
-
16
1
  # Python wrapper for SuiteSparseQR
17
2
 
18
3
  This module wraps the [SuiteSparseQR](http://faculty.cse.tamu.edu/davis/suitesparse.html)
@@ -118,19 +103,29 @@ or leave them in their directory and call it as a module.
118
103
 
119
104
  # Deploy
120
105
 
121
- 1. Change the version in:
106
+ 1. Change the version in `sparseqr/__init__.py`
107
+
108
+ 2. Update `CHANGELOG.md`
109
+
110
+ 3. Commit to git. Push to GitHub.
111
+
112
+ 4. Run (in a clean repos, e.g., `git clone . clean; cd clean`):
122
113
 
123
114
  ```
124
- sparseqr/__init__.py
125
- pyproject.toml
115
+ flit publish --format sdist
126
116
  ```
127
117
 
128
- 2. Update `CHANGELOG.md`
118
+ Using [uv](https://docs.astral.sh/uv/) and [PyPI API tokens](https://pypi.org/help/#apitoken):
119
+
120
+ ```
121
+ FLIT_USERNAME=__token__ uv tool run --with flit flit publish --format sdist
122
+ ```
129
123
 
130
- 3. Run:
124
+ or
131
125
 
132
126
  ```
133
- flit publish --format sdist
127
+ uv build --sdist
128
+ UV_PUBLISH_TOKEN=<token> uv publish
134
129
  ```
135
130
 
136
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?
@@ -139,17 +134,35 @@ We don't publish binary wheels, because it must be compiled against suite-sparse
139
134
 
140
135
  `pip uninstall sparseqr` won't remove the generated libraries. It will list them with a warning.
141
136
 
142
- # Tested on
137
+ # Tested
138
+
139
+ GitHub Continuous Integration (CI) tests:
143
140
 
144
- - Python 3.9, 3.13.
145
- - Conda and not conda.
146
- - macOS, Ubuntu Linux, and Linux Mint.
141
+ - Python 3.9, 3.10, 3.11, 3.12, 3.13, 3.14.
142
+ - macOS, Ubuntu Linux, and Windows.
143
+ - conda (Windows) and not conda (Linux/macOS). I tested conda on macOS manually at one point.
147
144
 
148
- PYTHONPATH='.:$PYTHONPATH' python3 test/test.py
145
+ Test manually with:
146
+
147
+ ```
148
+ python -m pytest
149
+ ```
150
+
151
+ or
152
+
153
+ ```
154
+ uv run --extra test pytest
155
+ ```
156
+
157
+ or
158
+
159
+ ```
160
+ uv run --with sparseqr,pytest path/to/test_sparseqr.py
161
+ ```
149
162
 
150
163
  # Dependencies
151
164
 
152
- These are installed via pip:
165
+ These are listed as dependencies and will be installed automatically:
153
166
 
154
167
  * [SciPy/NumPy](http://www.scipy.org)
155
168
  * [cffi](http://cffi.readthedocs.io/)
@@ -161,4 +174,3 @@ These must be installed manually:
161
174
  # License
162
175
 
163
176
  Public Domain [CC0](http://creativecommons.org/publicdomain/zero/1.0/)
164
-
@@ -1,9 +1,9 @@
1
1
  [project]
2
2
  name = "sparseqr"
3
- version = "1.4.1"
3
+ dynamic = ["version"]
4
4
  description = "Python wrapper for SuiteSparseQR"
5
5
  authors = [{name = "Yotam Gingold", email = "yotam@yotamgingold.com"}]
6
- license = {text = "Public Domain CC0"}
6
+ license = "CC0-1.0"
7
7
  readme = "README.md"
8
8
  keywords = ["suitesparse", "bindings", "wrapper", "scipy", "numpy", "qr-decomposition", "qr-factorisation", "sparse-matrix", "sparse-linear-system", "sparse-linear-solver"]
9
9
 
@@ -12,18 +12,29 @@ requires-python = ">= 3.8"
12
12
  dependencies = [
13
13
  "numpy >1.2",
14
14
  "scipy >= 1.0",
15
- "cffi >= 1.0",
16
- "setuptools >35",
15
+ "cffi >= 1.0"
17
16
  ]
18
17
 
18
+ [project.optional-dependencies]
19
+ test = ["pytest>=7.0"]
20
+
19
21
  [project.urls]
20
22
  homepage = "https://github.com/yig/PySPQR"
21
23
  source = "https://github.com/yig/PySPQR"
22
24
 
25
+ [tool.pytest.ini_options]
26
+ testpaths = ["test"]
27
+ python_files = ["test_*.py"]
28
+ python_functions = ["test_*"]
29
+ addopts = "-v"
30
+
23
31
  [build-system]
24
32
  requires = ["setuptools>=61"]
25
33
  build-backend = "setuptools.build_meta"
26
34
 
35
+ [tool.setuptools.dynamic]
36
+ version = {attr = "sparseqr.__version__"}
37
+
27
38
  #[tool.setuptools.packages.find]
28
39
  # include = ["test/*.py", "README.md", "LICENSE.md"]
29
40
  #exclude = ["sparseqr/_sparseqr*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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.4.1'
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
@@ -35,7 +35,7 @@ import _cffi_backend
35
35
  import scipy.sparse
36
36
  import numpy
37
37
 
38
- from .cffi_asarray import asarray
38
+ from .cffi_asarray import asarray, ctype2dtype
39
39
 
40
40
  '''
41
41
  Helpful links for developers:
@@ -72,17 +72,30 @@ When no longer needed, the returned CHOLMOD sparse matrix must be deallocated us
72
72
 
73
73
  nnz = scipy_A.nnz
74
74
 
75
+ # Check if the sparse matrix has complex entries and adapt cholmod type accordingly
76
+ is_complex = numpy.issubdtype(scipy_A, complex)
77
+ cholmod_dtype = lib.CHOLMOD_COMPLEX if is_complex else lib.CHOLMOD_REAL
78
+
75
79
  ## There is a potential performance win if we know A is symmetric and we
76
80
  ## can get only the upper or lower triangular elements.
77
- chol_A = lib.cholmod_l_allocate_triplet( scipy_A.shape[0], scipy_A.shape[1], nnz, 0, lib.CHOLMOD_REAL, cc )
81
+ chol_A = lib.cholmod_l_allocate_triplet( scipy_A.shape[0], scipy_A.shape[1], nnz, 0, cholmod_dtype, cc )
78
82
 
83
+ # Cast and copy indices
79
84
  Ai = ffi.cast( "SuiteSparse_long*", chol_A.i )
80
85
  Aj = ffi.cast( "SuiteSparse_long*", chol_A.j )
81
- Avals = ffi.cast( "double*", chol_A.x )
82
-
83
86
  Ai[0:nnz] = scipy_A.row
84
87
  Aj[0:nnz] = scipy_A.col
85
- Avals[0:nnz] = scipy_A.data
88
+
89
+ # Handle values based on dtype
90
+ Avals = ffi.cast( "double*", chol_A.x )
91
+ if is_complex:
92
+ # chol_A.x has size 2*nnz and real and imag parts are interleaved [real, imag, real, imag, ...]
93
+ array_element_size = ffi.sizeof(ffi.typeof(Avals).item)
94
+ Avals_view = numpy.frombuffer(ffi.buffer(Avals, 2*nnz * array_element_size), ctype2dtype["double"])
95
+ Avals_view[0::2] = scipy_A.data.real
96
+ Avals_view[1::2] = scipy_A.data.imag
97
+ else:
98
+ Avals[0:nnz] = scipy_A.data
86
99
 
87
100
  chol_A.nnz = nnz
88
101
 
@@ -125,7 +138,14 @@ def cholmodsparse2scipy( chol_A ):
125
138
  ## doesn't and the cholmod memory fill get freed.
126
139
  i = asarray( ffi, Ai, nnz ).copy()
127
140
  j = asarray( ffi, Aj, nnz ).copy()
128
- data = asarray( ffi, Adata, nnz ).copy()
141
+
142
+ if chol_A.xtype == lib.CHOLMOD_COMPLEX:
143
+ # read real and imag part from the buffer and create view as complex datatype
144
+ data = asarray(ffi, Adata, 2*nnz).copy()
145
+ complex_dtype = numpy.dtype(f"c{data.itemsize * 2}")
146
+ data = data.view(complex_dtype)
147
+ else:
148
+ data = asarray(ffi, Adata, nnz).copy()
129
149
 
130
150
  scipy_A = scipy.sparse.coo_matrix(
131
151
  ( data, ( i, j ) ),
@@ -150,19 +170,41 @@ When no longer needed, the returned CHOLMOD dense matrix must be deallocated usi
150
170
  nrow = numpy_A.shape[0]
151
171
  ncol = numpy_A.shape[1]
152
172
  lda = nrow # cholmod_dense is column-oriented
153
- 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 )
154
179
  if chol_A == ffi.NULL:
155
180
  raise RuntimeError("Failed to allocate chol_A")
156
181
  Adata = ffi.cast( "double*", chol_A.x )
157
- for j in range(ncol): # FIXME inefficient?
158
- 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]
159
194
  return chol_A
160
195
 
161
196
  def cholmoddense2numpy( chol_A ):
162
197
  '''Convert a CHOLMOD dense matrix to a NumPy array.'''
163
198
  Adata = ffi.cast( "double*", chol_A.x )
164
199
 
165
- 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
+
166
208
  result = result.reshape( (chol_A.nrow, chol_A.ncol), order='F' )
167
209
  return result
168
210
 
@@ -17,6 +17,9 @@ libraries = ['spqr']
17
17
  if 'CONDA_PREFIX' in os.environ:
18
18
  include_dirs.append( os.path.join(os.environ['CONDA_PREFIX'], 'include', 'suitesparse') )
19
19
  library_dirs.append( os.path.join(os.environ['CONDA_PREFIX'], 'lib') )
20
+ ## For Windows:
21
+ include_dirs.append( os.path.join(os.environ['CONDA_PREFIX'], 'Library', 'include', 'suitesparse') )
22
+ library_dirs.append( os.path.join(os.environ['CONDA_PREFIX'], 'Library', 'lib') )
20
23
 
21
24
  ## Otherwise, add common system-wide directories
22
25
  else:
@@ -31,8 +34,9 @@ else:
31
34
 
32
35
  if platform.system() == 'Windows':
33
36
  # https://github.com/yig/PySPQR/issues/6
34
- libraries.extend( ['amd','btf','camd','ccolamd','cholmod','colamd','cxsparse'
35
- 'klu','lapack','ldl','lumfpack','metis','suitesparseconfig','libblas'] )
37
+ ## Update: This list fails for `windows-latest` on GitHub. Some are missing. The only needed library is `cholmod`.
38
+ # libraries.extend( ['amd','btf','camd','ccolamd','cholmod','colamd','cxsparse', 'klu','lapack','ldl','lumfpack','metis','suitesparseconfig','libblas'] )
39
+ libraries.extend( ['cholmod'] )
36
40
 
37
41
  ffibuilder = FFI()
38
42
 
@@ -50,7 +54,7 @@ ffibuilder.set_source( "sparseqr._sparseqr",
50
54
  library_dirs = library_dirs,
51
55
  libraries = libraries )
52
56
 
53
- ffibuilder.cdef("""
57
+ ffibuilder.cdef(r"""
54
58
  // The int... is a magic thing which tells the compiler to figure out what the right
55
59
  // integer type is.
56
60
  typedef int... SuiteSparse_long;
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: sparseqr
3
+ Version: 1.5.1
4
+ Summary: Python wrapper for SuiteSparseQR
5
+ Author-email: Yotam Gingold <yotam@yotamgingold.com>
6
+ License-Expression: CC0-1.0
7
+ Project-URL: homepage, https://github.com/yig/PySPQR
8
+ Project-URL: source, https://github.com/yig/PySPQR
9
+ Keywords: suitesparse,bindings,wrapper,scipy,numpy,qr-decomposition,qr-factorisation,sparse-matrix,sparse-linear-system,sparse-linear-solver
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE.md
13
+ Requires-Dist: numpy>1.2
14
+ Requires-Dist: scipy>=1.0
15
+ Requires-Dist: cffi>=1.0
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest>=7.0; extra == "test"
18
+ Dynamic: license-file
19
+
20
+ # Python wrapper for SuiteSparseQR
21
+
22
+ This module wraps the [SuiteSparseQR](http://faculty.cse.tamu.edu/davis/suitesparse.html)
23
+ decomposition function for use with [SciPy](http://www.scipy.org).
24
+ This is Matlab's sparse `[Q,R,E] = qr()`.
25
+ For some reason, no one ever wrapped that function of SuiteSparseQR for Python.
26
+
27
+ Also wrapped are the SuiteSparseQR solvers for ``A x = b`` for the cases with sparse `A` and dense or sparse `b`.
28
+ This is especially useful for solving sparse overdetermined linear systems in the least-squares sense.
29
+ Here `A` is of size m-by-n and `b` is m-by-k (storing `k` different right-hand side vectors, each considered separately).
30
+
31
+ # Usage
32
+
33
+ ```python
34
+ import numpy
35
+ import scipy.sparse.linalg
36
+ import sparseqr
37
+
38
+ # QR decompose a sparse matrix M such that Q R = M E
39
+ #
40
+ M = scipy.sparse.rand( 10, 10, density = 0.1 )
41
+ Q, R, E, rank = sparseqr.qr( M )
42
+ print( "Should be approximately zero:", abs( Q*R - M*sparseqr.permutation_vector_to_matrix(E) ).sum() )
43
+
44
+ # Solve many linear systems "M x = b for b in columns(B)"
45
+ #
46
+ B = scipy.sparse.rand( 10, 5, density = 0.1 ) # many RHS, sparse (could also have just one RHS with shape (10,))
47
+ x = sparseqr.solve( M, B, tolerance = 0 )
48
+
49
+ # Solve an overdetermined linear system A x = b in the least-squares sense
50
+ #
51
+ # The same routine also works for the usual non-overdetermined case.
52
+ #
53
+ A = scipy.sparse.rand( 20, 10, density = 0.1 ) # 20 equations, 10 unknowns
54
+ b = numpy.random.random(20) # one RHS, dense, but could also have many (in shape (20,k))
55
+ x = sparseqr.solve( A, b, tolerance = 0 )
56
+ ## Call `rz()`:
57
+ sparseqr.rz( A, b, tolerance = 0 )
58
+
59
+ # Solve a linear system M x = B via QR decomposition
60
+ #
61
+ # This approach is slow due to the explicit construction of Q, but may be
62
+ # useful if a large number of systems need to be solved with the same M.
63
+ #
64
+ M = scipy.sparse.rand( 10, 10, density = 0.1 )
65
+ Q, R, E, rank = sparseqr.qr( M )
66
+ r = rank # r could be min(M.shape) if M is full-rank
67
+
68
+ # The system is only solvable if the lower part of Q.T @ B is all zero:
69
+ print( "System is solvable if this is zero (unlikely for a random matrix):", abs( (( Q.tocsc()[:,r:] ).T ).dot( B ) ).sum() )
70
+
71
+ # Systems with large non-square matrices can benefit from "economy" decomposition.
72
+ M = scipy.sparse.rand( 20, 5, density=0.1 )
73
+ B = scipy.sparse.rand( 20, 5, density = 0.1 )
74
+ Q, R, E, rank = sparseqr.qr( M )
75
+ print("Q shape (should be 20x20):", Q.shape)
76
+ print("R shape (should be 20x5):", R.shape)
77
+ Q, R, E, rank = sparseqr.qr( M, economy=True )
78
+ print("Q shape (should be 20x5):", Q.shape)
79
+ print("R shape (should be 5x5):", R.shape)
80
+
81
+
82
+ R = R.tocsr()[:r,:r] #for best performance, spsolve_triangular() wants the Matrix to be in CSR format.
83
+ Q = Q.tocsc()[:,:r] # Use CSC format for fast indexing of columns.
84
+ QB = (Q.T).dot(B).todense() # spsolve_triangular() need the RHS in array format.
85
+ result = scipy.sparse.linalg.spsolve_triangular(R, QB, lower=False)
86
+
87
+ # Recover a solution (as a dense array):
88
+ x = numpy.zeros( ( M.shape[1], B.shape[1] ), dtype = result.dtype )
89
+ x[:r] = result
90
+ x[E] = x.copy()
91
+
92
+ # Recover a solution (as a sparse matrix):
93
+ x = scipy.sparse.vstack( ( result, scipy.sparse.coo_matrix( ( M.shape[1] - rank, B.shape[1] ), dtype = result.dtype ) ) )
94
+ x.row = E[ x.row ]
95
+ ```
96
+
97
+ # Installation
98
+
99
+ Before installing this module, you must first install [SuiteSparseQR](http://faculty.cse.tamu.edu/davis/suitesparse.html). You can do that via conda (`conda install suitesparse`) or your system's package manager (macOS: `brew install suitesparse`; debian/ubuntu linux: `apt-get install libsuitesparse-dev`).
100
+
101
+ Now you are ready to install this module.
102
+
103
+ ## Via `pip`
104
+
105
+ From PyPI:
106
+
107
+ ```bash
108
+ pip install sparseqr
109
+ ```
110
+
111
+ From GitHub:
112
+
113
+ ```bash
114
+ pip install git+https://github.com/yig/PySPQR.git
115
+ ```
116
+
117
+ ## Directly
118
+
119
+ Copy the three `sparseqr/*.py` files next to your source code,
120
+ or leave them in their directory and call it as a module.
121
+
122
+
123
+ # Deploy
124
+
125
+ 1. Change the version in `sparseqr/__init__.py`
126
+
127
+ 2. Update `CHANGELOG.md`
128
+
129
+ 3. Commit to git. Push to GitHub.
130
+
131
+ 4. Run (in a clean repos, e.g., `git clone . clean; cd clean`):
132
+
133
+ ```
134
+ flit publish --format sdist
135
+ ```
136
+
137
+ Using [uv](https://docs.astral.sh/uv/) and [PyPI API tokens](https://pypi.org/help/#apitoken):
138
+
139
+ ```
140
+ FLIT_USERNAME=__token__ uv tool run --with flit flit publish --format sdist
141
+ ```
142
+
143
+ or
144
+
145
+ ```
146
+ uv build --sdist
147
+ UV_PUBLISH_TOKEN=<token> uv publish
148
+ ```
149
+
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?
151
+
152
+ # Known issues
153
+
154
+ `pip uninstall sparseqr` won't remove the generated libraries. It will list them with a warning.
155
+
156
+ # Tested
157
+
158
+ GitHub Continuous Integration (CI) tests:
159
+
160
+ - Python 3.9, 3.10, 3.11, 3.12, 3.13, 3.14.
161
+ - macOS, Ubuntu Linux, and Windows.
162
+ - conda (Windows) and not conda (Linux/macOS). I tested conda on macOS manually at one point.
163
+
164
+ Test manually with:
165
+
166
+ ```
167
+ python -m pytest
168
+ ```
169
+
170
+ or
171
+
172
+ ```
173
+ uv run --extra test pytest
174
+ ```
175
+
176
+ or
177
+
178
+ ```
179
+ uv run --with sparseqr,pytest path/to/test_sparseqr.py
180
+ ```
181
+
182
+ # Dependencies
183
+
184
+ These are listed as dependencies and will be installed automatically:
185
+
186
+ * [SciPy/NumPy](http://www.scipy.org)
187
+ * [cffi](http://cffi.readthedocs.io/)
188
+
189
+ These must be installed manually:
190
+
191
+ * [SuiteSparseQR](http://faculty.cse.tamu.edu/davis/suitesparse.html) (macOS: `brew install suitesparse`; debian/ubuntu linux: `apt-get install libsuitesparse-dev`)
192
+
193
+ # License
194
+
195
+ Public Domain [CC0](http://creativecommons.org/publicdomain/zero/1.0/)
@@ -0,0 +1,15 @@
1
+ LICENSE.md
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ sparseqr/__init__.py
6
+ sparseqr/cffi_asarray.py
7
+ sparseqr/sparseqr.py
8
+ sparseqr/sparseqr_gen.py
9
+ sparseqr.egg-info/PKG-INFO
10
+ sparseqr.egg-info/SOURCES.txt
11
+ sparseqr.egg-info/dependency_links.txt
12
+ sparseqr.egg-info/requires.txt
13
+ sparseqr.egg-info/top_level.txt
14
+ test/test_complex.py
15
+ test/test_sparseqr.py
@@ -0,0 +1,6 @@
1
+ numpy>1.2
2
+ scipy>=1.0
3
+ cffi>=1.0
4
+
5
+ [test]
6
+ pytest>=7.0
@@ -0,0 +1 @@
1
+ sparseqr
@@ -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'])
@@ -0,0 +1,313 @@
1
+ """Tests for sparseqr module.
2
+
3
+ Run with: pytest test/ -v
4
+ Or simply: python -m pytest
5
+ """
6
+
7
+ import numpy as np
8
+ import scipy.sparse
9
+ import pytest
10
+
11
+ import sparseqr
12
+
13
+
14
+ class TestQR:
15
+ """Tests for sparseqr.qr() function."""
16
+
17
+ def test_qr_basic_decomposition(self):
18
+ """Test that Q*R = M*E for a random sparse matrix."""
19
+ M = scipy.sparse.rand(10, 10, density=0.1, random_state=42)
20
+ Q, R, E, rank = sparseqr.qr(M)
21
+
22
+ # Q*R should equal M*E
23
+ E_matrix = sparseqr.permutation_vector_to_matrix(E)
24
+ residual = abs(Q @ R - M @ E_matrix).sum()
25
+ assert residual < 1e-10, f"QR decomposition residual too large: {residual}"
26
+
27
+ def test_qr_rectangular_tall(self):
28
+ """Test QR on a tall (overdetermined) matrix."""
29
+ M = scipy.sparse.rand(20, 10, density=0.1, random_state=42)
30
+ Q, R, E, rank = sparseqr.qr(M)
31
+
32
+ assert Q.shape == (20, 20), f"Q shape wrong: {Q.shape}"
33
+ assert R.shape == (20, 10), f"R shape wrong: {R.shape}"
34
+
35
+ E_matrix = sparseqr.permutation_vector_to_matrix(E)
36
+ residual = abs(Q @ R - M @ E_matrix).sum()
37
+ assert residual < 1e-10
38
+
39
+ def test_qr_rectangular_wide(self):
40
+ """Test QR on a wide (underdetermined) matrix."""
41
+ M = scipy.sparse.rand(10, 20, density=0.1, random_state=42)
42
+ Q, R, E, rank = sparseqr.qr(M)
43
+
44
+ assert Q.shape == (10, 10), f"Q shape wrong: {Q.shape}"
45
+ assert R.shape == (10, 20), f"R shape wrong: {R.shape}"
46
+
47
+ def test_qr_economy_mode(self):
48
+ """Test economy=True produces smaller Q and R."""
49
+ M = scipy.sparse.rand(20, 5, density=0.1, random_state=42)
50
+
51
+ # Full QR
52
+ Q_full, R_full, E, rank = sparseqr.qr(M, economy=False)
53
+ assert Q_full.shape == (20, 20)
54
+ assert R_full.shape == (20, 5)
55
+
56
+ # Economy QR
57
+ Q_econ, R_econ, E, rank = sparseqr.qr(M, economy=True)
58
+ assert Q_econ.shape == (20, 5), f"Economy Q shape wrong: {Q_econ.shape}"
59
+ assert R_econ.shape == (5, 5), f"Economy R shape wrong: {R_econ.shape}"
60
+
61
+ def test_qr_identity_matrix(self):
62
+ """Test QR decomposition of identity matrix."""
63
+ I = scipy.sparse.eye(5, format='coo')
64
+ Q, R, E, rank = sparseqr.qr(I)
65
+
66
+ assert rank == 5, f"Rank should be 5, got {rank}"
67
+
68
+ # Q and R should both be identity (up to permutation)
69
+ E_matrix = sparseqr.permutation_vector_to_matrix(E)
70
+ residual = abs(Q @ R - I @ E_matrix).sum()
71
+ assert residual < 1e-10
72
+
73
+ def test_qr_with_tolerance(self):
74
+ """Test QR with explicit tolerance parameter."""
75
+ M = scipy.sparse.rand(10, 10, density=0.2, random_state=42)
76
+ Q, R, E, rank = sparseqr.qr(M, tolerance=0)
77
+
78
+ E_matrix = sparseqr.permutation_vector_to_matrix(E)
79
+ residual = abs(Q @ R - M @ E_matrix).sum()
80
+ assert residual < 1e-10
81
+
82
+
83
+ class TestSolve:
84
+ """Tests for sparseqr.solve() function."""
85
+
86
+ def test_solve_dense_rhs_single(self):
87
+ """Test solving Ax=b with single dense RHS vector."""
88
+ # Create a well-conditioned matrix
89
+ A = scipy.sparse.diags([1, 2, 3, 4, 5], format='coo', dtype=np.float64) + scipy.sparse.rand(5, 5, density=0.1, random_state=42)
90
+ b = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
91
+
92
+ x = sparseqr.solve(A, b, tolerance=0)
93
+
94
+ assert x is not None, "solve() returned None"
95
+ assert x.shape == (5,), f"Solution shape wrong: {x.shape}"
96
+
97
+ def test_solve_dense_rhs_multiple(self):
98
+ """Test solving AX=B with multiple dense RHS vectors."""
99
+ A = scipy.sparse.diags([1, 2, 3, 4, 5], format='coo', dtype=np.float64) + scipy.sparse.rand(5, 5, density=0.1, random_state=42)
100
+ B = np.random.RandomState(42).random((5, 3))
101
+
102
+ X = sparseqr.solve(A, B, tolerance=0)
103
+
104
+ assert X is not None, "solve() returned None"
105
+ assert X.shape == (5, 3), f"Solution shape wrong: {X.shape}"
106
+
107
+ def test_solve_sparse_rhs(self):
108
+ """Test solving AX=B with sparse RHS."""
109
+ A = scipy.sparse.diags([1, 2, 3, 4, 5], format='coo', dtype=np.float64) + scipy.sparse.rand(5, 5, density=0.1, random_state=42)
110
+ B = scipy.sparse.rand(5, 3, density=0.3, random_state=42)
111
+
112
+ X = sparseqr.solve(A, B, tolerance=0)
113
+
114
+ assert X is not None, "solve() returned None"
115
+ assert scipy.sparse.issparse(X), "Solution should be sparse for sparse RHS"
116
+ assert X.shape == (5, 3), f"Solution shape wrong: {X.shape}"
117
+
118
+ def test_solve_overdetermined_least_squares(self):
119
+ """Test least-squares solution for overdetermined system."""
120
+ # 20 equations, 10 unknowns
121
+ A = scipy.sparse.rand(20, 10, density=0.2, random_state=42)
122
+ b = np.random.RandomState(42).random(20)
123
+
124
+ x = sparseqr.solve(A, b, tolerance=0)
125
+
126
+ assert x is not None, "solve() returned None"
127
+ assert x.shape == (10,), f"Solution shape wrong: {x.shape}"
128
+
129
+ def test_solve_exact_system(self):
130
+ """Test solving an exact system where Ax=b has exact solution."""
131
+ # Create a system with known solution
132
+ A = scipy.sparse.eye(5, format='coo')
133
+ x_true = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
134
+ b = A @ x_true
135
+
136
+ x = sparseqr.solve(A, b, tolerance=0)
137
+
138
+ assert x is not None
139
+ np.testing.assert_allclose(x, x_true, rtol=1e-10)
140
+
141
+
142
+ class TestRZ:
143
+ """Tests for sparseqr.rz() function."""
144
+
145
+ def test_rz_basic(self):
146
+ """Test rz() returns expected outputs."""
147
+ A = scipy.sparse.rand(20, 10, density=0.1, random_state=42)
148
+ b = np.random.RandomState(42).random(20)
149
+
150
+ Z, R, E, rank = sparseqr.rz(A, b, tolerance=0)
151
+
152
+ assert Z is not None
153
+ assert R is not None
154
+ assert isinstance(rank, (int, np.integer))
155
+
156
+ def test_rz_output_shapes(self):
157
+ """Test rz() returns correct shapes."""
158
+ m, n = 15, 8
159
+ A = scipy.sparse.rand(m, n, density=0.2, random_state=42)
160
+ b = np.random.RandomState(42).random(m)
161
+
162
+ Z, R, E, rank = sparseqr.rz(A, b, tolerance=0)
163
+
164
+ # Z should have n rows (one per unknown)
165
+ assert Z.shape[0] == n, f"Z rows should be {n}, got {Z.shape[0]}"
166
+
167
+
168
+ class TestQRFactorize:
169
+ """Tests for sparseqr.qr_factorize() and qmult() functions."""
170
+
171
+ def test_qr_factorize_basic(self):
172
+ """Test qr_factorize() returns a factorization object."""
173
+ M = scipy.sparse.rand(100, 100, density=0.05, random_state=42)
174
+ QR = sparseqr.qr_factorize(M)
175
+
176
+ assert QR is not None
177
+
178
+ def test_qmult_basic(self):
179
+ """Test qmult() with Householder form QR."""
180
+ M = scipy.sparse.rand(100, 100, density=0.05, random_state=42)
181
+ QR = sparseqr.qr_factorize(M)
182
+
183
+ X = np.zeros((M.shape[0], 1))
184
+ X[-1, 0] = 1
185
+
186
+ Y = sparseqr.qmult(QR, X)
187
+
188
+ assert Y.shape == (100, 1), f"Y shape wrong: {Y.shape}"
189
+
190
+ def test_qmult_methods(self):
191
+ """Test different qmult methods (Q'X, QX, XQ', XQ)."""
192
+ M = scipy.sparse.rand(20, 20, density=0.2, random_state=42)
193
+ QR = sparseqr.qr_factorize(M)
194
+
195
+ X = np.random.RandomState(42).random((20, 1))
196
+
197
+ # method=0: Q'X
198
+ Y0 = sparseqr.qmult(QR, X, method=0)
199
+ assert Y0.shape == (20, 1)
200
+
201
+ # method=1: QX (default)
202
+ Y1 = sparseqr.qmult(QR, X, method=1)
203
+ assert Y1.shape == (20, 1)
204
+
205
+
206
+ class TestPermutationVectorToMatrix:
207
+ """Tests for sparseqr.permutation_vector_to_matrix() function."""
208
+
209
+ def test_identity_permutation(self):
210
+ """Test identity permutation [0,1,2,3,4]."""
211
+ E = np.array([0, 1, 2, 3, 4])
212
+ P = sparseqr.permutation_vector_to_matrix(E)
213
+
214
+ expected = scipy.sparse.eye(5)
215
+ diff = abs(P - expected).sum()
216
+ assert diff < 1e-10, "Identity permutation should produce identity matrix"
217
+
218
+ def test_swap_permutation(self):
219
+ """Test a permutation that swaps first and last."""
220
+ E = np.array([4, 1, 2, 3, 0])
221
+ P = sparseqr.permutation_vector_to_matrix(E)
222
+
223
+ # P[E[k], k] = 1, so P[4,0]=1, P[1,1]=1, P[2,2]=1, P[3,3]=1, P[0,4]=1
224
+ assert P.shape == (5, 5)
225
+ assert P.nnz == 5 # exactly 5 non-zeros
226
+
227
+ # Convert to dense for easier checking
228
+ P_dense = P.toarray()
229
+ assert P_dense[4, 0] == 1
230
+ assert P_dense[0, 4] == 1
231
+
232
+ def test_permutation_orthogonality(self):
233
+ """Test that permutation matrix is orthogonal (P @ P.T = I)."""
234
+ E = np.array([2, 0, 4, 1, 3])
235
+ P = sparseqr.permutation_vector_to_matrix(E)
236
+
237
+ result = P @ P.T
238
+ expected = scipy.sparse.eye(5)
239
+ diff = abs(result - expected).sum()
240
+ assert diff < 1e-10
241
+
242
+
243
+ class TestEdgeCases:
244
+ """Tests for edge cases and special matrices."""
245
+
246
+ def test_very_sparse_matrix(self):
247
+ """Test with very sparse matrix."""
248
+ M = scipy.sparse.rand(50, 50, density=0.01, random_state=42)
249
+ Q, R, E, rank = sparseqr.qr(M)
250
+
251
+ E_matrix = sparseqr.permutation_vector_to_matrix(E)
252
+ residual = abs(Q @ R - M @ E_matrix).sum()
253
+ assert residual < 1e-10
254
+
255
+ def test_diagonal_matrix(self):
256
+ """Test with diagonal matrix."""
257
+ M = scipy.sparse.diags([1, 2, 3, 4, 5], format='coo', dtype=np.float64)
258
+ Q, R, E, rank = sparseqr.qr(M)
259
+
260
+ assert rank == 5
261
+ E_matrix = sparseqr.permutation_vector_to_matrix(E)
262
+ residual = abs(Q @ R - M @ E_matrix).sum()
263
+ assert residual < 1e-10
264
+
265
+ def test_single_element_matrix(self):
266
+ """Test with 1x1 matrix."""
267
+ M = scipy.sparse.coo_matrix([[3.0]])
268
+ Q, R, E, rank = sparseqr.qr(M)
269
+
270
+ assert rank == 1
271
+ assert Q.shape == (1, 1)
272
+ assert R.shape == (1, 1)
273
+
274
+ def test_different_sparse_formats(self):
275
+ """Test that different input formats work (coo, csr, csc)."""
276
+ base = scipy.sparse.rand(10, 10, density=0.2, random_state=42)
277
+
278
+ for fmt in ['coo', 'csr', 'csc', 'lil']:
279
+ M = base.asformat(fmt)
280
+ Q, R, E, rank = sparseqr.qr(M)
281
+ E_matrix = sparseqr.permutation_vector_to_matrix(E)
282
+ residual = abs(Q @ R - M @ E_matrix).sum()
283
+ assert residual < 1e-10, f"Failed for format {fmt}"
284
+
285
+
286
+ class TestNumericalAccuracy:
287
+ """Tests for numerical accuracy."""
288
+
289
+ def test_orthogonality_of_q(self):
290
+ """Test that Q is orthogonal (Q.T @ Q ≈ I for the relevant columns)."""
291
+ M = scipy.sparse.rand(10, 10, density=0.3, random_state=42)
292
+ Q, R, E, rank = sparseqr.qr(M)
293
+
294
+ # For a square matrix with full rank, Q should be orthogonal
295
+ if rank == 10:
296
+ QtQ = (Q.T @ Q).toarray()
297
+ I = np.eye(10)
298
+ np.testing.assert_allclose(QtQ, I, atol=1e-10)
299
+
300
+ def test_upper_triangular_r(self):
301
+ """Test that R is upper triangular."""
302
+ M = scipy.sparse.rand(10, 10, density=0.3, random_state=42)
303
+ Q, R, E, rank = sparseqr.qr(M)
304
+
305
+ R_dense = R.toarray()
306
+ # Check lower triangle is zero (below diagonal)
307
+ for i in range(R_dense.shape[0]):
308
+ for j in range(min(i, R_dense.shape[1])):
309
+ assert abs(R_dense[i, j]) < 1e-10, f"R[{i},{j}] = {R_dense[i,j]} should be zero"
310
+
311
+
312
+ if __name__ == '__main__':
313
+ pytest.main([__file__, '-v'])
sparseqr-1.4.1/.gitignore DELETED
@@ -1,3 +0,0 @@
1
- *.pyc
2
- *.pyo
3
- _spqr.*
@@ -1,57 +0,0 @@
1
- # Changelog
2
- All notable changes to this project will be documented in this file.
3
-
4
- The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
- and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
-
7
- ## [Unreleased]
8
-
9
- ## [v1.4.1] - 2025-02-10
10
- ### Fixed
11
- - An import statement got lost in the 1.4 update.
12
-
13
- ## [v1.4] - 2025-01-28
14
- ### Fixed
15
- - Modernized the build system (`pyproject.toml`).
16
- - Changed the way suite-sparse is found to be more robust.
17
-
18
- ## [v1.3] - 2025-01-09
19
- ### Added
20
- - Bindings for `qr_factorize` and `qmult` (thanks to jkrokowski)
21
-
22
- ### Fixed
23
- - Compatibility with more environments (more search paths, newer numpy, setuptools dependency)
24
- - Readme example uses `spsolve_triangular`.
25
-
26
- ## [v1.2.1] - 2023-04-12
27
- ### Fixed
28
- - Fixed a memory leak in `qr()` and `rz()`.
29
- ### Changed
30
- - Bumped minimal Python version to 3.8.
31
- - `rz()` is called by the test script. Its output is ignored.
32
-
33
- ## [v1.2] - 2022-05-27
34
- ### Added
35
- - Added support for partial "economy" decompositions. (Christoph Hansknecht <c.hansknecht@tu-braunschweig.de>): 'The "economy" option can be used in SPQR to compute a QR factorization of a (m x n) matrix with m < n consisting of blocks Q_1, and Q_2, where Q_1 has as shape of (m x n) and Q_2 of (m x k - n). For k = n we get the reduced form, for k = m the full one. For k in between m and n, SPQR yields a block that spans part of the kernel of A. This patch adds this functionality to PySPQR.'
36
- - Added support for macOS on arm64.
37
-
38
- ## [v1.1.2] - 2021-08-09
39
- ### Added
40
- - Added rz recomposition (thanks to Ben Smith <bsmith@apl.washington.edu>)
41
- - Added support for "economy" decomposition. (Jeffrey Bouas <ignirtoq@gmail.com>)
42
- ### Changed
43
- - Supports conda environments (thanks to Ben Smith <bsmith@apl.washington.edu> and Sterling Baird <sterling.baird@icloud.com>)
44
-
45
- ## [v1.0.0] - 2017-08-31
46
- ### Added
47
- - Installation and packaging using `setuptools`
48
- ### Changed
49
- - Rename module `spqr` to `sparseqr`
50
- - Clean up public API: `qr`, `solve`, `permutation_vector_to_matrix`
51
-
52
- ## [v1.0.0] - 2017-08-31
53
- ### Added
54
- - Installation and packaging using `setuptools` (thanks to Juha Jeronen <juha.jeronen@tut.fi>)
55
- ### Changed
56
- - Rename module `spqr` to `sparseqr`
57
- - Clean up public API: `qr`, `solve`, `permutation_vector_to_matrix`
@@ -1,79 +0,0 @@
1
- #!/usr/bin/python
2
- # -*- coding: utf-8 -*-
3
-
4
- from __future__ import division, print_function, absolute_import
5
-
6
- import numpy
7
- import scipy.sparse.linalg
8
- import sparseqr
9
-
10
- # QR decompose a sparse matrix M such that Q R = M E
11
- #
12
- M = scipy.sparse.rand( 10, 10, density = 0.1 )
13
- Q, R, E, rank = sparseqr.qr( M )
14
- print( "Should be approximately zero:", abs( Q*R - M*sparseqr.permutation_vector_to_matrix(E) ).sum() )
15
-
16
- # Solve many linear systems "M x = b for b in columns(B)"
17
- #
18
- B = scipy.sparse.rand( 10, 5, density = 0.1 ) # many RHS, sparse (could also have just one RHS with shape (10,))
19
- x = sparseqr.solve( M, B, tolerance = 0 )
20
-
21
- # Solve an overdetermined linear system A x = b in the least-squares sense
22
- #
23
- # The same routine also works for the usual non-overdetermined case.
24
- #
25
- A = scipy.sparse.rand( 20, 10, density = 0.1 ) # 20 equations, 10 unknowns
26
- b = numpy.random.random(20) # one RHS, dense, but could also have many (in shape (20,k))
27
- x = sparseqr.solve( A, b, tolerance = 0 )
28
- ## Call `rz()`:
29
- sparseqr.rz( A, b, tolerance = 0 )
30
-
31
- # Solve a linear system M x = B via QR decomposition
32
- #
33
- # This approach is slow due to the explicit construction of Q, but may be
34
- # useful if a large number of systems need to be solved with the same M.
35
- #
36
- M = scipy.sparse.rand( 10, 10, density = 0.1 )
37
- Q, R, E, rank = sparseqr.qr( M )
38
- r = rank # r could be min(M.shape) if M is full-rank
39
-
40
- # The system is only solvable if the lower part of Q.T @ B is all zero:
41
- print( "System is solvable if this is zero (unlikely for a random matrix):", abs( (( Q.tocsc()[:,r:] ).T ).dot( B ) ).sum() )
42
-
43
- # Systems with large non-square matrices can benefit from "economy" decomposition.
44
- M = scipy.sparse.rand( 20, 5, density=0.1 )
45
- B = scipy.sparse.rand( 20, 5, density = 0.1 )
46
- Q, R, E, rank = sparseqr.qr( M )
47
- print("Q shape (should be 20x20):", Q.shape)
48
- print("R shape (should be 20x5):", R.shape)
49
- Q, R, E, rank = sparseqr.qr( M, economy=True )
50
- print("Q shape (should be 20x5):", Q.shape)
51
- print("R shape (should be 5x5):", R.shape)
52
-
53
- # Use CSC format for fast indexing of columns.
54
- R = R.tocsc()[:r,:r]
55
- Q = Q.tocsc()[:,:r]
56
- QB = (Q.T).dot(B).tocsc() # for best performance, spsolve() wants the RHS to be in CSC format.
57
- result = scipy.sparse.linalg.spsolve(R, QB)
58
-
59
- # Recover a solution (as a dense array):
60
- x = numpy.zeros( ( M.shape[1], B.shape[1] ), dtype = result.dtype )
61
- x[:r] = result.todense()
62
- x[E] = x.copy()
63
-
64
- # Recover a solution (as a sparse matrix):
65
- x = scipy.sparse.vstack( ( result.tocoo(), scipy.sparse.coo_matrix( ( M.shape[1] - rank, B.shape[1] ), dtype = result.dtype ) ) )
66
- x.row = E[ x.row ]
67
-
68
- #initialize QR Factorization object
69
- M = scipy.sparse.rand( 100, 100, density=0.05 )
70
-
71
- #perform QR factorization, but store in Householder form
72
- QR= sparseqr.qr_factorize( M )
73
- X = numpy.zeros((M.shape[0],1))
74
- #change last entry of the first column to a 1
75
- # this allows us to construct only the first column of Q
76
- X[-1,0]=1
77
-
78
- Y = sparseqr.qmult(QR,X)
79
- print("Y shape (should be 100x1):",Y.shape)
File without changes
File without changes