├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── pyproject.toml ├── setup.py ├── sparseqr ├── __init__.py ├── cffi_asarray.py ├── sparseqr.py └── sparseqr_gen.py └── test └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | _spqr.* 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 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 ): '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 ) 41 | - Added support for "economy" decomposition. (Jeffrey Bouas ) 42 | ### Changed 43 | - Supports conda environments (thanks to Ben Smith and Sterling Baird ) 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 ) 55 | ### Changed 56 | - Rename module `spqr` to `sparseqr` 57 | - Clean up public API: `qr`, `solve`, `permutation_vector_to_matrix` 58 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python wrapper for SuiteSparseQR 2 | 3 | This module wraps the [SuiteSparseQR](http://faculty.cse.tamu.edu/davis/suitesparse.html) 4 | decomposition function for use with [SciPy](http://www.scipy.org). 5 | This is Matlab's sparse `[Q,R,E] = qr()`. 6 | For some reason, no one ever wrapped that function of SuiteSparseQR for Python. 7 | 8 | Also wrapped are the SuiteSparseQR solvers for ``A x = b`` for the cases with sparse `A` and dense or sparse `b`. 9 | This is especially useful for solving sparse overdetermined linear systems in the least-squares sense. 10 | Here `A` is of size m-by-n and `b` is m-by-k (storing `k` different right-hand side vectors, each considered separately). 11 | 12 | # Usage 13 | 14 | ```python 15 | import numpy 16 | import scipy.sparse.linalg 17 | import sparseqr 18 | 19 | # QR decompose a sparse matrix M such that Q R = M E 20 | # 21 | M = scipy.sparse.rand( 10, 10, density = 0.1 ) 22 | Q, R, E, rank = sparseqr.qr( M ) 23 | print( "Should be approximately zero:", abs( Q*R - M*sparseqr.permutation_vector_to_matrix(E) ).sum() ) 24 | 25 | # Solve many linear systems "M x = b for b in columns(B)" 26 | # 27 | B = scipy.sparse.rand( 10, 5, density = 0.1 ) # many RHS, sparse (could also have just one RHS with shape (10,)) 28 | x = sparseqr.solve( M, B, tolerance = 0 ) 29 | 30 | # Solve an overdetermined linear system A x = b in the least-squares sense 31 | # 32 | # The same routine also works for the usual non-overdetermined case. 33 | # 34 | A = scipy.sparse.rand( 20, 10, density = 0.1 ) # 20 equations, 10 unknowns 35 | b = numpy.random.random(20) # one RHS, dense, but could also have many (in shape (20,k)) 36 | x = sparseqr.solve( A, b, tolerance = 0 ) 37 | ## Call `rz()`: 38 | sparseqr.rz( A, b, tolerance = 0 ) 39 | 40 | # Solve a linear system M x = B via QR decomposition 41 | # 42 | # This approach is slow due to the explicit construction of Q, but may be 43 | # useful if a large number of systems need to be solved with the same M. 44 | # 45 | M = scipy.sparse.rand( 10, 10, density = 0.1 ) 46 | Q, R, E, rank = sparseqr.qr( M ) 47 | r = rank # r could be min(M.shape) if M is full-rank 48 | 49 | # The system is only solvable if the lower part of Q.T @ B is all zero: 50 | print( "System is solvable if this is zero (unlikely for a random matrix):", abs( (( Q.tocsc()[:,r:] ).T ).dot( B ) ).sum() ) 51 | 52 | # Systems with large non-square matrices can benefit from "economy" decomposition. 53 | M = scipy.sparse.rand( 20, 5, density=0.1 ) 54 | B = scipy.sparse.rand( 20, 5, density = 0.1 ) 55 | Q, R, E, rank = sparseqr.qr( M ) 56 | print("Q shape (should be 20x20):", Q.shape) 57 | print("R shape (should be 20x5):", R.shape) 58 | Q, R, E, rank = sparseqr.qr( M, economy=True ) 59 | print("Q shape (should be 20x5):", Q.shape) 60 | print("R shape (should be 5x5):", R.shape) 61 | 62 | 63 | R = R.tocsr()[:r,:r] #for best performance, spsolve_triangular() wants the Matrix to be in CSR format. 64 | Q = Q.tocsc()[:,:r] # Use CSC format for fast indexing of columns. 65 | QB = (Q.T).dot(B).todense() # spsolve_triangular() need the RHS in array format. 66 | result = scipy.sparse.linalg.spsolve_triangular(R, QB, lower=False) 67 | 68 | # Recover a solution (as a dense array): 69 | x = numpy.zeros( ( M.shape[1], B.shape[1] ), dtype = result.dtype ) 70 | x[:r] = result 71 | x[E] = x.copy() 72 | 73 | # Recover a solution (as a sparse matrix): 74 | x = scipy.sparse.vstack( ( result, scipy.sparse.coo_matrix( ( M.shape[1] - rank, B.shape[1] ), dtype = result.dtype ) ) ) 75 | x.row = E[ x.row ] 76 | ``` 77 | 78 | # Installation 79 | 80 | 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`). 81 | 82 | Now you are ready to install this module. 83 | 84 | ## Via `pip` 85 | 86 | From PyPI: 87 | 88 | ```bash 89 | pip install sparseqr 90 | ``` 91 | 92 | From GitHub: 93 | 94 | ```bash 95 | pip install git+https://github.com/yig/PySPQR.git 96 | ``` 97 | 98 | ## Directly 99 | 100 | Copy the three `sparseqr/*.py` files next to your source code, 101 | or leave them in their directory and call it as a module. 102 | 103 | 104 | # Deploy 105 | 106 | 1. Change the version in: 107 | 108 | ``` 109 | sparseqr/__init__.py 110 | pyproject.toml 111 | ``` 112 | 113 | 2. Update `CHANGELOG.md` 114 | 115 | 3. Commit to git. Push to GitHub. 116 | 117 | 4. Run (in a clean repos, e.g., `git clone . clean; cd clean`): 118 | 119 | ``` 120 | flit publish --format sdist 121 | ``` 122 | 123 | Using [uv](https://docs.astral.sh/uv/) and [PyPI API tokens](https://pypi.org/help/#apitoken): 124 | 125 | ``` 126 | FLIT_USERNAME=__token__ uv tool run --with flit flit publish --format sdist 127 | ``` 128 | 129 | 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? 130 | 131 | # Known issues 132 | 133 | `pip uninstall sparseqr` won't remove the generated libraries. It will list them with a warning. 134 | 135 | # Tested on 136 | 137 | - Python 3.9, 3.13. 138 | - Conda and not conda. 139 | - macOS, Ubuntu Linux, and Linux Mint. 140 | 141 | PYTHONPATH='.:$PYTHONPATH' python3 test/test.py 142 | 143 | # Dependencies 144 | 145 | These are installed via pip: 146 | 147 | * [SciPy/NumPy](http://www.scipy.org) 148 | * [cffi](http://cffi.readthedocs.io/) 149 | 150 | These must be installed manually: 151 | 152 | * [SuiteSparseQR](http://faculty.cse.tamu.edu/davis/suitesparse.html) (macOS: `brew install suitesparse`; debian/ubuntu linux: `apt-get install libsuitesparse-dev`) 153 | 154 | # License 155 | 156 | Public Domain [CC0](http://creativecommons.org/publicdomain/zero/1.0/) 157 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "sparseqr" 3 | version = "1.4.1" 4 | description = "Python wrapper for SuiteSparseQR" 5 | authors = [{name = "Yotam Gingold", email = "yotam@yotamgingold.com"}] 6 | license = {text = "Public Domain CC0"} 7 | readme = "README.md" 8 | keywords = ["suitesparse", "bindings", "wrapper", "scipy", "numpy", "qr-decomposition", "qr-factorisation", "sparse-matrix", "sparse-linear-system", "sparse-linear-solver"] 9 | 10 | requires-python = ">= 3.8" 11 | 12 | dependencies = [ 13 | "numpy >1.2", 14 | "scipy >= 1.0", 15 | "cffi >= 1.0", 16 | "setuptools >35", 17 | ] 18 | 19 | [project.urls] 20 | homepage = "https://github.com/yig/PySPQR" 21 | source = "https://github.com/yig/PySPQR" 22 | 23 | [build-system] 24 | requires = ["setuptools>=61"] 25 | build-backend = "setuptools.build_meta" 26 | 27 | #[tool.setuptools.packages.find] 28 | # include = ["test/*.py", "README.md", "LICENSE.md"] 29 | #exclude = ["sparseqr/_sparseqr*"] 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | from __future__ import division, print_function, absolute_import 4 | 5 | from setuptools import setup #, dist 6 | import os 7 | 8 | setup( 9 | # See 10 | # http://setuptools.readthedocs.io/en/latest/setuptools.html 11 | # 12 | setup_requires = ["cffi>=1.0.0"], 13 | cffi_modules = ["sparseqr/sparseqr_gen.py:ffibuilder"], 14 | install_requires = ["cffi>=1.0.0"], 15 | ) 16 | -------------------------------------------------------------------------------- /sparseqr/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | """Python bindings for SuiteSparse QR routines. 4 | 5 | Exported functions: 6 | sparseqr.qr QR decompose sparse matrix 7 | sparseqr.solve solve linear system, LHS sparse 8 | sparseqr.permutation_vector_to_matrix utility for conversion 9 | 10 | The solver works also for overdetermined linear systems, 11 | making it useful for solving linear least-squares problems. 12 | 13 | In solve(), the RHS can be dense or sparse. 14 | 15 | See the docstrings of the individual functions for details. 16 | """ 17 | 18 | from __future__ import absolute_import 19 | 20 | __version__ = '1.4.1' 21 | 22 | # import the important things into the package's top-level namespace. 23 | from .sparseqr import qr, rz, solve, permutation_vector_to_matrix, qr_factorize,qmult 24 | 25 | -------------------------------------------------------------------------------- /sparseqr/cffi_asarray.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Yotam Gingold 3 | License: Public Domain [CC0](http://creativecommons.org/publicdomain/zero/1.0/) 4 | Description: An `asarray` function that wraps a cffi pointer in a numpy.array. 5 | URL: https://gist.github.com/yig/77667e676163bbfc6c44af02657618a6 6 | ''' 7 | 8 | from __future__ import print_function, division, absolute_import 9 | 10 | import numpy 11 | 12 | ## Create the dictionary mapping ctypes to numpy dtypes. 13 | ctype2dtype = {} 14 | 15 | ## Integer types 16 | for prefix in ( 'int', 'uint' ): 17 | for log_bytes in range( 4 ): 18 | ctype = '%s%d_t' % ( prefix, 8*(2**log_bytes) ) 19 | dtype = '%s%d' % ( prefix[0], 2**log_bytes ) 20 | # print( ctype ) 21 | # print( dtype ) 22 | ctype2dtype[ ctype ] = numpy.dtype( dtype ) 23 | 24 | ## Floating point types 25 | ctype2dtype[ 'float' ] = numpy.dtype( 'f4' ) 26 | ctype2dtype[ 'double' ] = numpy.dtype( 'f8' ) 27 | 28 | # print( ctype2dtype ) 29 | 30 | def asarray( ffi, ptr, length ): 31 | ## Get the canonical C type of the elements of ptr as a string. 32 | T = ffi.getctype( ffi.typeof( ptr ).item ) 33 | # print( T ) 34 | # print( ffi.sizeof( T ) ) 35 | 36 | if T not in ctype2dtype: 37 | raise RuntimeError( "Cannot create an array for element type: %s" % T ) 38 | 39 | return numpy.frombuffer( ffi.buffer( ptr, length*ffi.sizeof( T ) ), ctype2dtype[T] ) 40 | 41 | def test(): 42 | from cffi import FFI 43 | ffi = FFI() 44 | 45 | N = 10 46 | ptr = ffi.new( "float[]", N ) 47 | 48 | arr = asarray( ffi, ptr, N ) 49 | arr[:] = numpy.arange( N ) 50 | 51 | for i in range( N ): 52 | print( arr[i], ptr[i] ) 53 | 54 | if __name__ == '__main__': 55 | test() 56 | -------------------------------------------------------------------------------- /sparseqr/sparseqr.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Yotam Gingold 3 | License: Public Domain [CC0](http://creativecommons.org/publicdomain/zero/1.0/) 4 | Description: Wrapper for SuiteSparse qr() and solve() functions. Matlab and Julia have it, Python should have it, too. 5 | ''' 6 | 7 | from __future__ import print_function, division, absolute_import 8 | 9 | # The compilation here works only if the files have been copied locally into the project, 10 | # as the compile requires write access into the directory the files reside in. 11 | # 12 | # In an installed copy of PySPQR, the compile step has already been run by setup.py at packaging time. 13 | # 14 | try: 15 | from ._sparseqr import ffi, lib 16 | except ImportError: 17 | print( "=== Wrapper module not compiled; compiling..." ) 18 | from .sparseqr_gen import main 19 | main() 20 | print( "=== ...compiled." ) 21 | 22 | from ._sparseqr import ffi, lib 23 | 24 | # Packaging note: 25 | # 26 | # http://cffi.readthedocs.io/en/latest/cdef.html says that: 27 | # 28 | # Note that some bundler tools that try to find all modules used by a project, like PyInstaller, 29 | # will miss _cffi_backend in the out-of-line mode because your program contains no explicit 30 | # import cffi or import _cffi_backend. You need to add _cffi_backend explicitly (as a "hidden import" 31 | # in PyInstaller, but it can also be done more generally by adding the line import _cffi_backend in your main program). 32 | # 33 | import _cffi_backend 34 | 35 | import scipy.sparse 36 | import numpy 37 | 38 | from .cffi_asarray import asarray 39 | 40 | ''' 41 | Helpful links for developers: 42 | The primary docs: 43 | http://cffi.readthedocs.io/en/latest/overview.html 44 | http://cffi.readthedocs.io/en/latest/using.html 45 | 46 | Some helpful examples for #define and NumPy: 47 | https://kogs-www.informatik.uni-hamburg.de/~seppke/content/teaching/wise1314/20131107_pridoehl-cffi.pdf 48 | ''' 49 | 50 | ## Initialize cholmod 51 | cc = ffi.new("cholmod_common*") 52 | lib.cholmod_l_start( cc ) 53 | 54 | ## Set up cholmod deinit to run when Python exits 55 | def _deinit(): 56 | '''Deinitialize the CHOLMOD library.''' 57 | lib.cholmod_l_finish( cc ) 58 | import atexit 59 | atexit.register(_deinit) 60 | 61 | 62 | ## Data format conversion 63 | 64 | def scipy2cholmodsparse( scipy_A ): 65 | '''Convert a SciPy sparse matrix to a CHOLMOD sparse matrix. 66 | 67 | The input is first internally converted to scipy.sparse.coo_matrix format. 68 | 69 | When no longer needed, the returned CHOLMOD sparse matrix must be deallocated using cholmod_free_sparse(). 70 | ''' 71 | scipy_A = scipy_A.tocoo() 72 | 73 | nnz = scipy_A.nnz 74 | 75 | ## There is a potential performance win if we know A is symmetric and we 76 | ## 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 ) 78 | 79 | Ai = ffi.cast( "SuiteSparse_long*", chol_A.i ) 80 | Aj = ffi.cast( "SuiteSparse_long*", chol_A.j ) 81 | Avals = ffi.cast( "double*", chol_A.x ) 82 | 83 | Ai[0:nnz] = scipy_A.row 84 | Aj[0:nnz] = scipy_A.col 85 | Avals[0:nnz] = scipy_A.data 86 | 87 | chol_A.nnz = nnz 88 | 89 | ## Print what cholmod sees as the matrix. 90 | # lib.cholmod_l_print_triplet( chol_A, "A".encode('utf-8'), cc ) 91 | assert lib.cholmod_l_check_triplet( chol_A, cc ) == 1 92 | 93 | ## Convert to a cholmod_sparse matrix. 94 | result = lib.cholmod_l_triplet_to_sparse( chol_A, nnz, cc ) 95 | ## Free the space used by the cholmod triplet matrix. 96 | _cholmod_free_triplet( chol_A ) 97 | 98 | return result 99 | 100 | def cholmodsparse2scipy( chol_A ): 101 | '''Convert a CHOLMOD sparse matrix to a scipy.sparse.coo_matrix.''' 102 | ## Convert to a cholmod_triplet matrix. 103 | chol_A = lib.cholmod_l_sparse_to_triplet( chol_A, cc ) 104 | 105 | nnz = chol_A.nnz 106 | 107 | Ai = ffi.cast( "SuiteSparse_long*", chol_A.i ) 108 | Aj = ffi.cast( "SuiteSparse_long*", chol_A.j ) 109 | Adata = ffi.cast( "double*", chol_A.x ) 110 | 111 | ## Have to pass through list(). 112 | ## https://bitbucket.org/cffi/cffi/issues/292/cant-copy-data-to-a-numpy-array 113 | ## http://stackoverflow.com/questions/16276268/how-to-pass-a-numpy-array-into-a-cffi-function-and-how-to-get-one-back-out 114 | ''' 115 | i = numpy.zeros( nnz, dtype = numpy.int64 ) 116 | j = numpy.zeros( nnz, dtype = numpy.int64 ) 117 | data = numpy.zeros( nnz ) 118 | 119 | i[0:nnz] = list( Ai[0:nnz] ) 120 | j[0:nnz] = list( Aj[0:nnz] ) 121 | data[0:nnz] = list( Adata[0:nnz] ) 122 | ''' 123 | ## UPDATE: I can do this without going through list() or making two extra copies. 124 | ## NOTE: Create a copy() of the array data, because the coo_matrix() constructor 125 | ## doesn't and the cholmod memory fill get freed. 126 | i = asarray( ffi, Ai, nnz ).copy() 127 | j = asarray( ffi, Aj, nnz ).copy() 128 | data = asarray( ffi, Adata, nnz ).copy() 129 | 130 | scipy_A = scipy.sparse.coo_matrix( 131 | ( data, ( i, j ) ), 132 | shape = ( chol_A.nrow, chol_A.ncol ) 133 | ) 134 | 135 | ## Free the space used by the cholmod triplet matrix. 136 | _cholmod_free_triplet( chol_A ) 137 | 138 | return scipy_A 139 | 140 | def numpy2cholmoddense( numpy_A ): 141 | '''Convert a NumPy array (rank-1 or rank-2) to a CHOLMOD dense matrix. 142 | 143 | Rank-1 arrays are converted to column vectors. 144 | 145 | When no longer needed, the returned CHOLMOD dense matrix must be deallocated using cholmod_free_dense(). 146 | ''' 147 | numpy_A = numpy.atleast_2d( numpy_A ) 148 | if numpy_A.shape[0] == 1 and numpy_A.shape[1] > 1: # prefer column vector 149 | numpy_A = numpy_A.T 150 | nrow = numpy_A.shape[0] 151 | ncol = numpy_A.shape[1] 152 | lda = nrow # cholmod_dense is column-oriented 153 | chol_A = lib.cholmod_l_allocate_dense( nrow, ncol, lda, lib.CHOLMOD_REAL, cc ) 154 | if chol_A == ffi.NULL: 155 | raise RuntimeError("Failed to allocate chol_A") 156 | 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] 159 | return chol_A 160 | 161 | def cholmoddense2numpy( chol_A ): 162 | '''Convert a CHOLMOD dense matrix to a NumPy array.''' 163 | Adata = ffi.cast( "double*", chol_A.x ) 164 | 165 | result = asarray( ffi, Adata, chol_A.nrow*chol_A.ncol ).copy() 166 | result = result.reshape( (chol_A.nrow, chol_A.ncol), order='F' ) 167 | return result 168 | 169 | def permutation_vector_to_matrix( E ): 170 | '''Convert a permutation vector E (list or rank-1 array, length n) to a permutation matrix (n by n). 171 | 172 | The result is returned as a scipy.sparse.coo_matrix, where the entries at (E[k], k) are 1. 173 | ''' 174 | n = len( E ) 175 | j = numpy.arange( n ) 176 | return scipy.sparse.coo_matrix( ( numpy.ones(n), ( E, j ) ), shape = ( n, n ) ) 177 | 178 | 179 | ## Memory management 180 | 181 | # Used only internally by this module (the user sees only sparse and dense formats). 182 | def _cholmod_free_triplet( A ): 183 | '''Deallocate a CHOLMOD triplet format matrix.''' 184 | A_ptr = ffi.new("cholmod_triplet**") 185 | A_ptr[0] = A 186 | lib.cholmod_l_free_triplet( A_ptr, cc ) 187 | 188 | def cholmod_free_sparse( A ): 189 | '''Deallocate a CHOLMOD sparse matrix.''' 190 | A_ptr = ffi.new("cholmod_sparse**") 191 | A_ptr[0] = A 192 | lib.cholmod_l_free_sparse( A_ptr, cc ) 193 | 194 | def cholmod_free_dense( A ): 195 | '''Deallocate a CHOLMOD dense matrix.''' 196 | A_ptr = ffi.new("cholmod_dense**") 197 | A_ptr[0] = A 198 | lib.cholmod_l_free_dense( A_ptr, cc ) 199 | 200 | 201 | ## Solvers 202 | def rz(A, B, tolerance = None): 203 | getCTX=int(0) 204 | chol_A = scipy2cholmodsparse( A ) 205 | chol_b = numpy2cholmoddense( B ) 206 | chol_Z = ffi.new("cholmod_dense**") 207 | chol_R = ffi.new("cholmod_sparse**") 208 | chol_E = ffi.new("SuiteSparse_long**") 209 | if tolerance is None: 210 | tolerance=0. 211 | 212 | rank = lib.SuiteSparseQR_C( 213 | ## Input 214 | lib.SPQR_ORDERING_DEFAULT, 215 | tolerance, 216 | A.shape[1], 217 | getCTX, 218 | chol_A, 219 | ffi.NULL, 220 | chol_b, 221 | ## Output 222 | ffi.NULL, 223 | chol_Z, 224 | chol_R, 225 | chol_E, 226 | ffi.NULL, 227 | ffi.NULL, 228 | ffi.NULL, 229 | cc 230 | ) 231 | scipy_Z = cholmoddense2numpy( chol_Z[0] ) 232 | scipy_R = cholmodsparse2scipy( chol_R[0] ) 233 | 234 | ## If chol_E is null, there was no permutation. 235 | if chol_E == ffi.NULL: 236 | E = None 237 | else: 238 | E = asarray( ffi, chol_E[0], A.shape[1] ).copy() 239 | 240 | ## Free cholmod stuff 241 | cholmod_free_dense( chol_Z[0] ) 242 | cholmod_free_sparse( chol_R[0] ) 243 | cholmod_free_sparse( chol_A ) 244 | cholmod_free_dense( chol_b ) 245 | 246 | return scipy_Z, scipy_R, E, rank 247 | 248 | 249 | def qr( A, tolerance = None, economy = None ): 250 | ''' 251 | Given a sparse matrix A, 252 | returns Q, R, E, rank such that: 253 | Q*R = A*permutation_vector_to_matrix(E) 254 | rank is the estimated rank of A. 255 | 256 | If optional `tolerance` parameter is negative, it has the following meanings: 257 | #define SPQR_DEFAULT_TOL ... /* if tol <= -2, the default tol is used */ 258 | #define SPQR_NO_TOL ... /* if -2 < tol < 0, then no tol is used */ 259 | 260 | For A an m-by-n matrix, Q will be m-by-m and R will be m-by-n. 261 | 262 | If optional `economy` parameter is truthy, Q will be m-by-k and R will be 263 | k-by-n, where k = min(m, n). 264 | 265 | The performance-optimal format for A is scipy.sparse.coo_matrix. 266 | 267 | For solving systems of the form A x = b, see solve(). 268 | 269 | qr() can also be used to solve systems, as follows: 270 | 271 | # inputs: scipy.sparse.coo_matrix A, rank-1 numpy.array b (RHS) 272 | import numpy 273 | import scipy 274 | 275 | Q, R, E, rank = sparseqr.qr( A ) 276 | r = rank # r could be min(A.shape) if A is full-rank 277 | 278 | # The system is only solvable if the lower part of Q.T @ B is all zero: 279 | print( "System is solvable if this is zero:", abs( (( Q.tocsc()[:,r:] ).T ).dot( B ) ).sum() ) 280 | 281 | # Use CSC format for fast indexing of columns. 282 | R = R.tocsc()[:r,:r] 283 | Q = Q.tocsc()[:,:r] 284 | QB = (Q.T).dot(B).tocsc() # for best performance, spsolve() wants the RHS to be in CSC format. 285 | result = scipy.sparse.linalg.spsolve(R, QB) 286 | 287 | # Recover a solution (as a dense array): 288 | x = numpy.zeros( ( A.shape[1], B.shape[1] ), dtype = result.dtype ) 289 | x[:r] = result.todense() 290 | x[E] = x.copy() 291 | 292 | # Recover a solution (as a sparse matrix): 293 | x = scipy.sparse.vstack( ( result.tocoo(), scipy.sparse.coo_matrix( ( A.shape[1] - rank, B.shape[1] ), dtype = result.dtype ) ) ) 294 | x.row = E[ x.row ] 295 | 296 | Be aware that this approach is slow and takes a lot of memory, because qr() explicitly constructs Q. 297 | Unless you have a large number of systems to solve with the same A, solve() is much faster. 298 | ''' 299 | 300 | chol_A = scipy2cholmodsparse( A ) 301 | 302 | chol_Q = ffi.new("cholmod_sparse**") 303 | chol_R = ffi.new("cholmod_sparse**") 304 | chol_E = ffi.new("SuiteSparse_long**") 305 | 306 | if tolerance is None: tolerance = lib.SPQR_DEFAULT_TOL 307 | 308 | if economy is None: economy = False 309 | 310 | if isinstance(economy, bool): 311 | econ = A.shape[1] if economy else A.shape[0] 312 | else: 313 | # Treat as a number 314 | econ = int(economy) 315 | 316 | rank = lib.SuiteSparseQR_C_QR( 317 | ## Input 318 | lib.SPQR_ORDERING_DEFAULT, 319 | tolerance, 320 | econ, 321 | chol_A, 322 | ## Output 323 | chol_Q, 324 | chol_R, 325 | chol_E, 326 | cc 327 | ) 328 | 329 | scipy_Q = cholmodsparse2scipy( chol_Q[0] ) 330 | scipy_R = cholmodsparse2scipy( chol_R[0] ) 331 | 332 | ## If chol_E is null, there was no permutation. 333 | if chol_E == ffi.NULL: 334 | E = None 335 | else: 336 | ## Have to pass through list(). 337 | ## https://bitbucket.org/cffi/cffi/issues/292/cant-copy-data-to-a-numpy-array 338 | ## http://stackoverflow.com/questions/16276268/how-to-pass-a-numpy-array-into-a-cffi-function-and-how-to-get-one-back-out 339 | # E = numpy.zeros( A.shape[1], dtype = int ) 340 | # E[0:A.shape[1]] = list( chol_E[0][0:A.shape[1]] ) 341 | ## UPDATE: I can do this without going through list() or making two extra copies. 342 | E = asarray( ffi, chol_E[0], A.shape[1] ).copy() 343 | 344 | ## Free cholmod stuff 345 | cholmod_free_sparse( chol_Q[0] ) 346 | cholmod_free_sparse( chol_R[0] ) 347 | cholmod_free_sparse( chol_A ) 348 | ## Apparently we don't need to do this. (I get a malloc error.) 349 | # lib.cholmod_l_free( A.shape[1], ffi.sizeof("SuiteSparse_long"), chol_E, cc ) 350 | 351 | return scipy_Q, scipy_R, E, rank 352 | 353 | 354 | def qr_factorize( A, tolerance = None): 355 | ''' 356 | Given a sparse matrix A, 357 | returns a QR factorization in householder form 358 | 359 | If optional `tolerance` parameter is negative, it has the following meanings: 360 | #define SPQR_DEFAULT_TOL ... /* if tol <= -2, the default tol is used */ 361 | #define SPQR_NO_TOL ... /* if -2 < tol < 0, then no tol is used */ 362 | 363 | For A an m-by-n matrix, Q will be m-by-m and R will be m-by-n. 364 | 365 | The performance-optimal format for A is scipy.sparse.coo_matrix. 366 | 367 | ''' 368 | 369 | chol_A = scipy2cholmodsparse( A ) 370 | 371 | if tolerance is None: tolerance = lib.SPQR_DEFAULT_TOL 372 | 373 | QR = lib.SuiteSparseQR_C_factorize( 374 | ## Input 375 | lib.SPQR_ORDERING_DEFAULT, 376 | tolerance, 377 | chol_A, 378 | cc 379 | ) 380 | 381 | cholmod_free_sparse( chol_A ) 382 | ## Apparently we don't need to do this. (I get a malloc error.) 383 | # lib.cholmod_l_free( A.shape[1], ffi.sizeof("SuiteSparse_long"), chol_E, cc ) 384 | 385 | return QR 386 | 387 | 388 | def qmult( QR, X, method=1): 389 | ''' 390 | Given a QR factorization struct 391 | a dense matrix 392 | returns Q applied to X in a dense matrix 393 | 394 | From the suitesparse documentation: 395 | /* 396 | Applies Q in Householder form (as stored in the QR factorization object 397 | returned by SuiteSparseQR_C_factorize) to a dense matrix X. 398 | 399 | method SPQR_QTX (0): Y = Q'*X 400 | method SPQR_QX (1): Y = Q*X 401 | method SPQR_XQT (2): Y = X*Q' 402 | method SPQR_XQ (3): Y = X*Q 403 | */ 404 | 405 | ''' 406 | 407 | chol_X = numpy2cholmoddense( X ) 408 | 409 | chol_Y = lib.SuiteSparseQR_C_qmult( 410 | ## Input 411 | method, 412 | QR, 413 | chol_X, 414 | cc 415 | ) 416 | numpy_Y = cholmoddense2numpy( chol_Y ) 417 | 418 | ## Free cholmod stuff 419 | cholmod_free_dense( chol_X ) 420 | cholmod_free_dense( chol_Y ) 421 | 422 | return numpy_Y 423 | 424 | 425 | def solve( A, b, tolerance = None ): 426 | ''' 427 | Given a sparse m-by-n matrix A, and dense or sparse m-by-k matrix (storing k RHS vectors) b, 428 | solve A x = b in the least-squares sense. 429 | 430 | This is much faster than using qr() to solve the system, since Q is not explicitly constructed. 431 | 432 | Returns x on success, None on failure. 433 | 434 | The format of the returned x (on success) is either dense or sparse, corresponding to 435 | the format of the b that was supplied. 436 | 437 | The performance-optimal format for A is scipy.sparse.coo_matrix. 438 | 439 | If optional `tolerance` parameter is negative, it has the following meanings: 440 | #define SPQR_DEFAULT_TOL ... /* if tol <= -2, the default tol is used */ 441 | #define SPQR_NO_TOL ... /* if -2 < tol < 0, then no tol is used */ 442 | ''' 443 | if isinstance( b, scipy.sparse.spmatrix ): 444 | return _solve_with_sparse_rhs( A, b, tolerance ) 445 | else: 446 | return _solve_with_dense_rhs( A, b, tolerance ) 447 | 448 | def _solve_with_dense_rhs( A, b, tolerance = None ): 449 | ''' 450 | Given a sparse m-by-n matrix A, and dense m-by-k matrix (storing k RHS vectors) b, 451 | solve A x = b in the least-squares sense. 452 | 453 | This is much faster than using qr() to solve the system, since Q is not explicitly constructed. 454 | 455 | Returns x (dense) on success, None on failure. 456 | 457 | The performance-optimal format for A is scipy.sparse.coo_matrix. 458 | 459 | If optional `tolerance` parameter is negative, it has the following meanings: 460 | #define SPQR_DEFAULT_TOL ... /* if tol <= -2, the default tol is used */ 461 | #define SPQR_NO_TOL ... /* if -2 < tol < 0, then no tol is used */ 462 | ''' 463 | 464 | chol_A = scipy2cholmodsparse( A ) 465 | chol_b = numpy2cholmoddense( b ) 466 | 467 | if tolerance is None: tolerance = lib.SPQR_DEFAULT_TOL 468 | 469 | chol_x = lib.SuiteSparseQR_C_backslash( 470 | ## Input 471 | lib.SPQR_ORDERING_DEFAULT, 472 | tolerance, 473 | chol_A, 474 | chol_b, 475 | cc ) 476 | 477 | if chol_x == ffi.NULL: 478 | return None # failed 479 | 480 | ## Return x with the same shape as b. 481 | x_shape = list( b.shape ) 482 | x_shape[0] = A.shape[1] 483 | numpy_x = cholmoddense2numpy( chol_x ).reshape( x_shape ) 484 | 485 | ## Free cholmod stuff 486 | cholmod_free_sparse( chol_A ) 487 | cholmod_free_dense( chol_b ) 488 | cholmod_free_dense( chol_x ) 489 | return numpy_x 490 | 491 | def _solve_with_sparse_rhs( A, b, tolerance = None ): 492 | ''' 493 | Given a sparse m-by-n matrix A, and sparse m-by-k matrix (storing k RHS vectors) b, 494 | solve A x = b in the least-squares sense. 495 | 496 | This is much faster than using qr() to solve the system, since Q is not explicitly constructed. 497 | 498 | Returns x (sparse) on success, None on failure. 499 | 500 | The performance-optimal format for A and b is scipy.sparse.coo_matrix. 501 | 502 | If optional `tolerance` parameter is negative, it has the following meanings: 503 | #define SPQR_DEFAULT_TOL ... /* if tol <= -2, the default tol is used */ 504 | #define SPQR_NO_TOL ... /* if -2 < tol < 0, then no tol is used */ 505 | ''' 506 | 507 | chol_A = scipy2cholmodsparse( A ) 508 | chol_b = scipy2cholmodsparse( b ) 509 | 510 | if tolerance is None: tolerance = lib.SPQR_DEFAULT_TOL 511 | 512 | chol_x = lib.SuiteSparseQR_C_backslash_sparse( 513 | ## Input 514 | lib.SPQR_ORDERING_DEFAULT, 515 | tolerance, 516 | chol_A, 517 | chol_b, 518 | cc ) 519 | 520 | if chol_x == ffi.NULL: 521 | return None # failed 522 | 523 | scipy_x = cholmodsparse2scipy( chol_x ) 524 | 525 | ## Free cholmod stuff 526 | cholmod_free_sparse( chol_A ) 527 | cholmod_free_sparse( chol_b ) 528 | cholmod_free_sparse( chol_x ) 529 | 530 | return scipy_x 531 | 532 | 533 | ## Usage examples 534 | 535 | if __name__ == '__main__': 536 | # Q, R, E, rank = qr( scipy.sparse.identity(10) ) 537 | 538 | print( "Testing qr()" ) 539 | M = scipy.sparse.rand( 10, 8, density = 0.1 ) 540 | Q, R, E, rank = qr( M, tolerance = 0 ) 541 | print( abs( Q*R - M*permutation_vector_to_matrix(E) ).sum() ) 542 | 543 | print( "Testing solve(), using dense RHS" ) 544 | b = numpy.random.random(10) # one RHS, but could also have many (in shape (10,k)) 545 | x = solve( M, b, tolerance = 0 ) 546 | print( x ) 547 | ## This won't be true in general because M is rank deficient. 548 | # print( abs( M*x - b ).sum() ) 549 | print( b.shape ) 550 | print( x.shape ) 551 | B = numpy.random.random((10,5)) # many RHS 552 | x = solve( M, B, tolerance = 0 ) 553 | print( x ) 554 | ## This won't be true in general because M is rank deficient. 555 | # print( abs( M*x - B ).sum() ) 556 | print( B.shape ) 557 | print( x.shape ) 558 | 559 | print( "Testing solve(), using sparse RHS" ) 560 | B = scipy.sparse.rand( 10, 5, density = 0.1 ) # many RHS, sparse 561 | x = solve( M, B, tolerance = 0 ) 562 | print( x ) 563 | ## This won't be true in general because M is rank deficient. 564 | # print( abs( M*x - B ).sum() ) 565 | print( B.shape ) 566 | print( x.shape ) 567 | -------------------------------------------------------------------------------- /sparseqr/sparseqr_gen.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: Yotam Gingold 3 | License: Public Domain [CC0](http://creativecommons.org/publicdomain/zero/1.0/) 4 | Description: Wrapper for SuiteSparse qr() and solve() functions. Matlab and Julia have it, Python should have it, too. 5 | ''' 6 | 7 | import os 8 | import platform 9 | 10 | from cffi import FFI 11 | 12 | include_dirs = [] 13 | library_dirs = [] 14 | libraries = ['spqr'] 15 | 16 | ## If we're using conda, use the conda paths 17 | if 'CONDA_PREFIX' in os.environ: 18 | include_dirs.append( os.path.join(os.environ['CONDA_PREFIX'], 'include', 'suitesparse') ) 19 | library_dirs.append( os.path.join(os.environ['CONDA_PREFIX'], 'lib') ) 20 | 21 | ## Otherwise, add common system-wide directories 22 | else: 23 | if platform.system() == 'Windows': 24 | include_dirs.append( os.path.join('C:', 'Program Files', 'Python', 'suitesparse') ) 25 | else: 26 | include_dirs.append( '/usr/include/suitesparse' ) 27 | ## Homebrew on macOS arm64 puts headers and libraries 28 | ## in `/opt/homebrew`. That's not on the default path, so add them: 29 | include_dirs.append( '/opt/homebrew/include/suitesparse' ) 30 | library_dirs.append( '/opt/homebrew/lib' ) 31 | 32 | if platform.system() == 'Windows': 33 | # 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'] ) 36 | 37 | ffibuilder = FFI() 38 | 39 | ## Uncomment this and install with `pip install -v` to see the arguments to `set_source`. 40 | # print( "cffi include_dirs:", include_dirs ) 41 | # print( "cffi library_dirs:", library_dirs ) 42 | # print( "cffi libraries:", libraries ) 43 | 44 | ffibuilder.set_source( "sparseqr._sparseqr", 45 | """#include 46 | """, 47 | ## You may need to modify the following line, 48 | ## which is needed on Ubuntu and harmless on Mac OS. 49 | include_dirs = include_dirs, 50 | library_dirs = library_dirs, 51 | libraries = libraries ) 52 | 53 | ffibuilder.cdef(""" 54 | // The int... is a magic thing which tells the compiler to figure out what the right 55 | // integer type is. 56 | typedef int... SuiteSparse_long; 57 | 58 | /// Many of these are copied from "cholmod_core.h" 59 | 60 | // The cholmod_common struct can't be completely opaque, 61 | // since we need to allocate space for one. 62 | typedef struct cholmod_common { ...; } cholmod_common ; 63 | 64 | // We can keep the cholmod_sparse struct opaque since we will only ever 65 | // interact with it by converting to and from a triplet struct. 66 | typedef ... cholmod_sparse ; 67 | typedef struct cholmod_triplet_struct 68 | { 69 | size_t nrow ; /* the matrix is nrow-by-ncol */ 70 | size_t ncol ; 71 | size_t nzmax ; /* maximum number of entries in the matrix */ 72 | size_t nnz ; /* number of nonzeros in the matrix */ 73 | 74 | void *i ; /* i [0..nzmax-1], the row indices */ 75 | void *j ; /* j [0..nzmax-1], the column indices */ 76 | void *x ; /* size nzmax or 2*nzmax, if present */ 77 | void *z ; /* size nzmax, if present */ 78 | 79 | int stype ; /* Describes what parts of the matrix are considered: 80 | * 81 | * 0: matrix is "unsymmetric": use both upper and lower triangular parts 82 | * (the matrix may actually be symmetric in pattern and value, but 83 | * both parts are explicitly stored and used). May be square or 84 | * rectangular. 85 | * >0: matrix is square and symmetric. Entries in the lower triangular 86 | * part are transposed and added to the upper triangular part when 87 | * the matrix is converted to cholmod_sparse form. 88 | * <0: matrix is square and symmetric. Entries in the upper triangular 89 | * part are transposed and added to the lower triangular part when 90 | * the matrix is converted to cholmod_sparse form. 91 | * 92 | * Note that stype>0 and stype<0 are different for cholmod_sparse and 93 | * cholmod_triplet. The reason is simple. You can permute a symmetric 94 | * triplet matrix by simply replacing a row and column index with their 95 | * new row and column indices, via an inverse permutation. Suppose 96 | * P = L->Perm is your permutation, and Pinv is an array of size n. 97 | * Suppose a symmetric matrix A is represent by a triplet matrix T, with 98 | * entries only in the upper triangular part. Then the following code: 99 | * 100 | * Ti = T->i ; 101 | * Tj = T->j ; 102 | * for (k = 0 ; k < n ; k++) Pinv [P [k]] = k ; 103 | * for (k = 0 ; k < nz ; k++) Ti [k] = Pinv [Ti [k]] ; 104 | * for (k = 0 ; k < nz ; k++) Tj [k] = Pinv [Tj [k]] ; 105 | * 106 | * creates the triplet form of C=P*A*P'. However, if T initially 107 | * contains just the upper triangular entries (T->stype = 1), after 108 | * permutation it has entries in both the upper and lower triangular 109 | * parts. These entries should be transposed when constructing the 110 | * cholmod_sparse form of A, which is what cholmod_triplet_to_sparse 111 | * does. Thus: 112 | * 113 | * C = cholmod_triplet_to_sparse (T, 0, &Common) ; 114 | * 115 | * will return the matrix C = P*A*P'. 116 | * 117 | * Since the triplet matrix T is so simple to generate, it's quite easy 118 | * to remove entries that you do not want, prior to converting T to the 119 | * cholmod_sparse form. So if you include these entries in T, CHOLMOD 120 | * assumes that there must be a reason (such as the one above). Thus, 121 | * no entry in a triplet matrix is ever ignored. 122 | */ 123 | 124 | int itype ; /* CHOLMOD_LONG: i and j are SuiteSparse_long. Otherwise int */ 125 | int xtype ; /* pattern, real, complex, or zomplex */ 126 | int dtype ; /* x and z are double or float */ 127 | 128 | } cholmod_triplet ; 129 | 130 | 131 | /* -------------------------------------------------------------------------- */ 132 | /* cholmod_start: first call to CHOLMOD */ 133 | /* -------------------------------------------------------------------------- */ 134 | int cholmod_l_start (cholmod_common *) ; 135 | /* -------------------------------------------------------------------------- */ 136 | /* cholmod_finish: last call to CHOLMOD */ 137 | /* -------------------------------------------------------------------------- */ 138 | int cholmod_l_finish (cholmod_common *) ; 139 | 140 | /* -------------------------------------------------------------------------- */ 141 | /* cholmod_free_sparse: free a sparse matrix */ 142 | /* -------------------------------------------------------------------------- */ 143 | int cholmod_l_free_sparse 144 | ( 145 | /* ---- in/out --- */ 146 | cholmod_sparse **A, /* matrix to deallocate, NULL on output */ 147 | /* --------------- */ 148 | cholmod_common *Common 149 | ) ; 150 | 151 | /* ========================================================================== */ 152 | /* === Core/cholmod_dense =================================================== */ 153 | /* ========================================================================== */ 154 | 155 | /* A dense matrix in column-oriented form. It has no itype since it contains 156 | * no integers. Entry in row i and column j is located in x [i+j*d]. 157 | */ 158 | 159 | typedef struct cholmod_dense_struct 160 | { 161 | size_t nrow ; /* the matrix is nrow-by-ncol */ 162 | size_t ncol ; 163 | size_t nzmax ; /* maximum number of entries in the matrix */ 164 | size_t d ; /* leading dimension (d >= nrow must hold) */ 165 | void *x ; /* size nzmax or 2*nzmax, if present */ 166 | void *z ; /* size nzmax, if present */ 167 | int xtype ; /* pattern, real, complex, or zomplex */ 168 | int dtype ; /* x and z double or float */ 169 | 170 | } cholmod_dense ; 171 | 172 | /* -------------------------------------------------------------------------- */ 173 | /* cholmod_allocate_dense: allocate a dense matrix (contents uninitialized) */ 174 | /* -------------------------------------------------------------------------- */ 175 | 176 | cholmod_dense *cholmod_l_allocate_dense 177 | ( 178 | /* ---- input ---- */ 179 | size_t nrow, /* # of rows of matrix */ 180 | size_t ncol, /* # of columns of matrix */ 181 | size_t d, /* leading dimension */ 182 | int xtype, /* CHOLMOD_REAL, _COMPLEX, or _ZOMPLEX */ 183 | /* --------------- */ 184 | cholmod_common *Common 185 | ) ; 186 | 187 | /* -------------------------------------------------------------------------- */ 188 | /* cholmod_free_dense: free a dense matrix */ 189 | /* -------------------------------------------------------------------------- */ 190 | 191 | int cholmod_l_free_dense 192 | ( 193 | /* ---- in/out --- */ 194 | cholmod_dense **X, /* dense matrix to deallocate, NULL on output */ 195 | /* --------------- */ 196 | cholmod_common *Common 197 | ) ; 198 | 199 | /* 200 | * ============================================================================ 201 | * === cholmod_triplet ======================================================== 202 | * ============================================================================ 203 | * 204 | * A sparse matrix held in triplet form is the simplest one for a user to 205 | * create. It consists of a list of nz entries in arbitrary order, held in 206 | * three arrays: i, j, and x, each of length nk. The kth entry is in row i[k], 207 | * column j[k], with value x[k]. There may be duplicate values; if A(i,j) 208 | * appears more than once, its value is the sum of the entries with those row 209 | * and column indices. 210 | */ 211 | /* -------------------------------------------------------------------------- */ 212 | /* cholmod_allocate_triplet: allocate a triplet matrix */ 213 | /* -------------------------------------------------------------------------- */ 214 | cholmod_triplet *cholmod_l_allocate_triplet 215 | ( 216 | /* ---- input ---- */ 217 | size_t nrow, /* # of rows of T */ 218 | size_t ncol, /* # of columns of T */ 219 | size_t nzmax, /* max # of nonzeros of T */ 220 | int stype, /* stype of T */ 221 | int xtype, /* CHOLMOD_PATTERN, _REAL, _COMPLEX, or _ZOMPLEX */ 222 | /* --------------- */ 223 | cholmod_common *Common 224 | ) ; 225 | #define CHOLMOD_PATTERN ... /* pattern only, no numerical values */ 226 | #define CHOLMOD_REAL ... /* a real matrix */ 227 | #define CHOLMOD_COMPLEX ... /* a complex matrix (ANSI C99 compatible) */ 228 | #define CHOLMOD_ZOMPLEX ... /* a complex matrix (MATLAB compatible) */ 229 | 230 | /* -------------------------------------------------------------------------- */ 231 | /* cholmod_free_triplet: free a triplet matrix */ 232 | /* -------------------------------------------------------------------------- */ 233 | int cholmod_l_free_triplet 234 | ( 235 | /* ---- in/out --- */ 236 | cholmod_triplet **T, /* triplet matrix to deallocate, NULL on output */ 237 | /* --------------- */ 238 | cholmod_common *Common 239 | ) ; 240 | 241 | /* -------------------------------------------------------------------------- */ 242 | /* cholmod_check_triplet: check a sparse matrix in triplet form */ 243 | /* -------------------------------------------------------------------------- */ 244 | // This one is from "cholmod_check.h". 245 | // Returns TRUE (1) if successful, or FALSE (0) otherwise. 246 | int cholmod_l_check_triplet 247 | ( 248 | /* ---- input ---- */ 249 | cholmod_triplet *T, /* triplet matrix to check */ 250 | /* --------------- */ 251 | cholmod_common *Common 252 | ) ; 253 | 254 | /* -------------------------------------------------------------------------- */ 255 | /* cholmod_print_triplet: print a triplet matrix */ 256 | /* -------------------------------------------------------------------------- */ 257 | int cholmod_l_print_triplet 258 | ( 259 | /* ---- input ---- */ 260 | cholmod_triplet *T, /* triplet matrix to print */ 261 | const char *name, /* printed name of triplet matrix */ 262 | /* --------------- */ 263 | cholmod_common *Common 264 | ) ; 265 | 266 | /* -------------------------------------------------------------------------- */ 267 | /* cholmod_sparse_to_triplet: create a triplet matrix copy of a sparse matrix*/ 268 | /* -------------------------------------------------------------------------- */ 269 | cholmod_triplet *cholmod_l_sparse_to_triplet 270 | ( 271 | /* ---- input ---- */ 272 | cholmod_sparse *A, /* matrix to copy */ 273 | /* --------------- */ 274 | cholmod_common *Common 275 | ) ; 276 | 277 | /* -------------------------------------------------------------------------- */ 278 | /* cholmod_triplet_to_sparse: create a sparse matrix copy of a triplet matrix*/ 279 | /* -------------------------------------------------------------------------- */ 280 | cholmod_sparse *cholmod_l_triplet_to_sparse 281 | ( 282 | /* ---- input ---- */ 283 | cholmod_triplet *T, /* matrix to copy */ 284 | size_t nzmax, /* allocate at least this much space in output matrix */ 285 | /* --------------- */ 286 | cholmod_common *Common 287 | ) ; 288 | 289 | // We need to call: cholmod_l_free( A.ncols, sizeof (SuiteSparse_long), E, cc ) ; 290 | // UPDATE: Apparently we don't. 291 | void *cholmod_l_free /* always returns NULL */ 292 | ( 293 | /* ---- input ---- */ 294 | size_t n, /* number of items */ 295 | size_t size, /* size of each item */ 296 | /* ---- in/out --- */ 297 | void *p, /* block of memory to free */ 298 | /* --------------- */ 299 | cholmod_common *Common 300 | ) ; 301 | 302 | /* cs_spsolve 303 | int cs_spsolve 304 | { 305 | */ 306 | 307 | 308 | 309 | /* [Z,R,E] = rz(A), returning Z, R, and E */ 310 | SuiteSparse_long SuiteSparseQR_C /* returns ???, (-1) if failure */ 311 | ( 312 | /* inputs: */ 313 | int ordering, /* all, except 3:given treated as 0:fixed */ 314 | double tol, /* columns with 2-norm <= tol treated as 0 */ 315 | SuiteSparse_long econ, /* e = max(min(m,econ),rank(A)) */ 316 | int getCTX, /* 0:Z=C, 1:Z=c', 2: z=X */ 317 | cholmod_sparse *A, /* m-by-n sparse matrix to factorize */ 318 | cholmod_sparse *Bsparse, 319 | cholmod_dense *Bdense, 320 | /* outputs: */ 321 | cholmod_sparse **Zsparse, /* m-by-e sparse matrix */ 322 | cholmod_dense **Zdense, 323 | cholmod_sparse **R, /* e-by-n sparse matrix */ 324 | SuiteSparse_long **E, /* size n column perm, NULL if identity */ 325 | cholmod_sparse **H, /* m-by-nh Householder vectors */ 326 | SuiteSparse_long **HPinv, /* size m row permutation */ 327 | cholmod_dense **HTau, /* 1-by-nh Householder coefficients */ 328 | cholmod_common *cc /* workspace and parameters */ 329 | ) ; 330 | 331 | 332 | /* [Q,R,E] = qr(A), returning Q as a sparse matrix */ 333 | SuiteSparse_long SuiteSparseQR_C_QR /* returns rank(A) est., (-1) if failure */ 334 | ( 335 | /* inputs: */ 336 | int ordering, /* all, except 3:given treated as 0:fixed */ 337 | double tol, /* columns with 2-norm <= tol treated as 0 */ 338 | SuiteSparse_long econ, /* e = max(min(m,econ),rank(A)) */ 339 | cholmod_sparse *A, /* m-by-n sparse matrix to factorize */ 340 | /* outputs: */ 341 | cholmod_sparse **Q, /* m-by-e sparse matrix */ 342 | cholmod_sparse **R, /* e-by-n sparse matrix */ 343 | SuiteSparse_long **E, /* size n column perm, NULL if identity */ 344 | cholmod_common *cc /* workspace and parameters */ 345 | ) ; 346 | 347 | 348 | /* X = A\B where B is dense */ 349 | cholmod_dense *SuiteSparseQR_C_backslash /* returns X, NULL if failure */ 350 | ( 351 | int ordering, /* all, except 3:given treated as 0:fixed */ 352 | double tol, /* columns with 2-norm <= tol treated as 0 */ 353 | cholmod_sparse *A, /* m-by-n sparse matrix */ 354 | cholmod_dense *B, /* m-by-k */ 355 | cholmod_common *cc /* workspace and parameters */ 356 | ) ; 357 | 358 | /* X = A\B where B is sparse */ 359 | cholmod_sparse *SuiteSparseQR_C_backslash_sparse /* returns X, or NULL */ 360 | ( 361 | /* inputs: */ 362 | int ordering, /* all, except 3:given treated as 0:fixed */ 363 | double tol, /* columns with 2-norm <= tol treated as 0 */ 364 | cholmod_sparse *A, /* m-by-n sparse matrix */ 365 | cholmod_sparse *B, /* m-by-k */ 366 | cholmod_common *cc /* workspace and parameters */ 367 | ) ; 368 | 369 | /* ========================================================================== */ 370 | /* === SuiteSparseQR_C_factorization ======================================== */ 371 | /* ========================================================================== */ 372 | 373 | /* A real or complex QR factorization, computed by SuiteSparseQR_C_factorize */ 374 | typedef struct SuiteSparseQR_C_factorization_struct 375 | { 376 | int xtype ; /* CHOLMOD_REAL or CHOLMOD_COMPLEX */ 377 | void *factors ; /* from SuiteSparseQR_factorize or 378 | SuiteSparseQR_factorize */ 379 | 380 | } SuiteSparseQR_C_factorization ; 381 | 382 | /* ========================================================================== */ 383 | /* === SuiteSparseQR_C_factorize ============================================ */ 384 | /* ========================================================================== */ 385 | 386 | SuiteSparseQR_C_factorization *SuiteSparseQR_C_factorize 387 | ( 388 | /* inputs: */ 389 | int ordering, /* all, except 3:given treated as 0:fixed */ 390 | double tol, /* columns with 2-norm <= tol treated as 0 */ 391 | cholmod_sparse *A, /* m-by-n sparse matrix */ 392 | cholmod_common *cc /* workspace and parameters */ 393 | ) ; 394 | 395 | /* ========================================================================== */ 396 | /* === SuiteSparseQR_C_symbolic ============================================= */ 397 | /* ========================================================================== */ 398 | 399 | SuiteSparseQR_C_factorization *SuiteSparseQR_C_symbolic 400 | ( 401 | /* inputs: */ 402 | int ordering, /* all, except 3:given treated as 0:fixed */ 403 | int allow_tol, /* if TRUE allow tol for rank detection */ 404 | cholmod_sparse *A, /* m-by-n sparse matrix, A->x ignored */ 405 | cholmod_common *cc /* workspace and parameters */ 406 | ) ; 407 | 408 | /* ========================================================================== */ 409 | /* === SuiteSparseQR_C_numeric ============================================== */ 410 | /* ========================================================================== */ 411 | 412 | int SuiteSparseQR_C_numeric 413 | ( 414 | /* inputs: */ 415 | double tol, /* treat columns with 2-norm <= tol as zero */ 416 | cholmod_sparse *A, /* sparse matrix to factorize */ 417 | /* input/output: */ 418 | SuiteSparseQR_C_factorization *QR, 419 | cholmod_common *cc /* workspace and parameters */ 420 | ) ; 421 | 422 | /* ========================================================================== */ 423 | /* === SuiteSparseQR_C_free ================================================= */ 424 | /* ========================================================================== */ 425 | 426 | /* Free the QR factors computed by SuiteSparseQR_C_factorize */ 427 | int SuiteSparseQR_C_free /* returns TRUE (1) if OK, FALSE (0) otherwise*/ 428 | ( 429 | SuiteSparseQR_C_factorization **QR, 430 | cholmod_common *cc /* workspace and parameters */ 431 | ) ; 432 | 433 | /* ========================================================================== */ 434 | /* === SuiteSparseQR_C_solve ================================================ */ 435 | /* ========================================================================== */ 436 | 437 | cholmod_dense* SuiteSparseQR_C_solve /* returnx X, or NULL if failure */ 438 | ( 439 | int system, /* which system to solve */ 440 | SuiteSparseQR_C_factorization *QR, /* of an m-by-n sparse matrix A */ 441 | cholmod_dense *B, /* right-hand-side, m-by-k or n-by-k */ 442 | cholmod_common *cc /* workspace and parameters */ 443 | ) ; 444 | 445 | /* ========================================================================== */ 446 | /* === SuiteSparseQR_C_qmult ================================================ */ 447 | /* ========================================================================== */ 448 | 449 | /* 450 | Applies Q in Householder form (as stored in the QR factorization object 451 | returned by SuiteSparseQR_C_factorize) to a dense matrix X. 452 | 453 | method SPQR_QTX (0): Y = Q'*X 454 | method SPQR_QX (1): Y = Q*X 455 | method SPQR_XQT (2): Y = X*Q' 456 | method SPQR_XQ (3): Y = X*Q 457 | */ 458 | 459 | cholmod_dense *SuiteSparseQR_C_qmult /* returns Y, or NULL on failure */ 460 | ( 461 | /* inputs: */ 462 | int method, /* 0,1,2,3 */ 463 | SuiteSparseQR_C_factorization *QR, /* of an m-by-n sparse matrix A */ 464 | cholmod_dense *X, /* size m-by-n with leading dimension ldx */ 465 | cholmod_common *cc /* workspace and parameters */ 466 | ) ; 467 | 468 | #define SPQR_ORDERING_FIXED ... 469 | #define SPQR_ORDERING_NATURAL ... 470 | #define SPQR_ORDERING_COLAMD ... 471 | #define SPQR_ORDERING_GIVEN ... /* only used for C/C++ interface */ 472 | #define SPQR_ORDERING_CHOLMOD ... /* CHOLMOD best-effort (COLAMD, METIS,...)*/ 473 | #define SPQR_ORDERING_AMD ... /* AMD(A'*A) */ 474 | #define SPQR_ORDERING_METIS ... /* metis(A'*A) */ 475 | #define SPQR_ORDERING_DEFAULT ... /* SuiteSparseQR default ordering */ 476 | #define SPQR_ORDERING_BEST ... /* try COLAMD, AMD, and METIS; pick best */ 477 | #define SPQR_ORDERING_BESTAMD ... /* try COLAMD and AMD; pick best */ 478 | 479 | #define SPQR_DEFAULT_TOL ... /* if tol <= -2, the default tol is used */ 480 | #define SPQR_NO_TOL ... /* if -2 < tol < 0, then no tol is used */ 481 | """) 482 | 483 | def main(): 484 | ## Two dirnames because ffibuilder.set_source() 485 | ## passes the module name: "sparseqr._sparseqr" 486 | ffibuilder.compile( verbose = True, tmpdir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ) 487 | 488 | if __name__ == "__main__": 489 | main() 490 | -------------------------------------------------------------------------------- /test/test.py: -------------------------------------------------------------------------------- 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) --------------------------------------------------------------------------------