asd
This commit is contained in:
323
venv/lib/python3.12/site-packages/scipy/sparse/__init__.py
Normal file
323
venv/lib/python3.12/site-packages/scipy/sparse/__init__.py
Normal file
@ -0,0 +1,323 @@
|
||||
"""
|
||||
=====================================
|
||||
Sparse matrices (:mod:`scipy.sparse`)
|
||||
=====================================
|
||||
|
||||
.. currentmodule:: scipy.sparse
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
|
||||
sparse.csgraph
|
||||
sparse.linalg
|
||||
|
||||
SciPy 2-D sparse array package for numeric data.
|
||||
|
||||
.. note::
|
||||
|
||||
This package is switching to an array interface, compatible with
|
||||
NumPy arrays, from the older matrix interface. We recommend that
|
||||
you use the array objects (`bsr_array`, `coo_array`, etc.) for
|
||||
all new work.
|
||||
|
||||
When using the array interface, please note that:
|
||||
|
||||
- ``x * y`` no longer performs matrix multiplication, but
|
||||
element-wise multiplication (just like with NumPy arrays). To
|
||||
make code work with both arrays and matrices, use ``x @ y`` for
|
||||
matrix multiplication.
|
||||
- Operations such as `sum`, that used to produce dense matrices, now
|
||||
produce arrays, whose multiplication behavior differs similarly.
|
||||
- Sparse arrays currently must be two-dimensional. This also means
|
||||
that all *slicing* operations on these objects must produce
|
||||
two-dimensional results, or they will result in an error. This
|
||||
will be addressed in a future version.
|
||||
|
||||
The construction utilities (`eye`, `kron`, `random`, `diags`, etc.)
|
||||
have not yet been ported, but their results can be wrapped into arrays::
|
||||
|
||||
A = csr_array(eye(3))
|
||||
|
||||
Contents
|
||||
========
|
||||
|
||||
Sparse array classes
|
||||
--------------------
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
bsr_array - Block Sparse Row array
|
||||
coo_array - A sparse array in COOrdinate format
|
||||
csc_array - Compressed Sparse Column array
|
||||
csr_array - Compressed Sparse Row array
|
||||
dia_array - Sparse array with DIAgonal storage
|
||||
dok_array - Dictionary Of Keys based sparse array
|
||||
lil_array - Row-based list of lists sparse array
|
||||
sparray - Sparse array base class
|
||||
|
||||
Sparse matrix classes
|
||||
---------------------
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
bsr_matrix - Block Sparse Row matrix
|
||||
coo_matrix - A sparse matrix in COOrdinate format
|
||||
csc_matrix - Compressed Sparse Column matrix
|
||||
csr_matrix - Compressed Sparse Row matrix
|
||||
dia_matrix - Sparse matrix with DIAgonal storage
|
||||
dok_matrix - Dictionary Of Keys based sparse matrix
|
||||
lil_matrix - Row-based list of lists sparse matrix
|
||||
spmatrix - Sparse matrix base class
|
||||
|
||||
Functions
|
||||
---------
|
||||
|
||||
Building sparse arrays:
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
diags_array - Return a sparse array from diagonals
|
||||
eye_array - Sparse MxN array whose k-th diagonal is all ones
|
||||
random_array - Random values in a given shape array
|
||||
block_array - Build a sparse array from sub-blocks
|
||||
|
||||
Building sparse matrices:
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
eye - Sparse MxN matrix whose k-th diagonal is all ones
|
||||
identity - Identity matrix in sparse matrix format
|
||||
diags - Return a sparse matrix from diagonals
|
||||
spdiags - Return a sparse matrix from diagonals
|
||||
bmat - Build a sparse matrix from sparse sub-blocks
|
||||
random - Random values in a given shape matrix
|
||||
rand - Random values in a given shape matrix (old interface)
|
||||
|
||||
Building larger structures from smaller (array or matrix)
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
kron - kronecker product of two sparse matrices
|
||||
kronsum - kronecker sum of sparse matrices
|
||||
block_diag - Build a block diagonal sparse matrix
|
||||
tril - Lower triangular portion of a matrix in sparse format
|
||||
triu - Upper triangular portion of a matrix in sparse format
|
||||
hstack - Stack sparse matrices horizontally (column wise)
|
||||
vstack - Stack sparse matrices vertically (row wise)
|
||||
|
||||
Save and load sparse matrices:
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
save_npz - Save a sparse matrix/array to a file using ``.npz`` format.
|
||||
load_npz - Load a sparse matrix/array from a file using ``.npz`` format.
|
||||
|
||||
Sparse tools:
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
find
|
||||
|
||||
Identifying sparse arrays:
|
||||
|
||||
- use `isinstance(A, sp.sparse.sparray)` to check whether an array or matrix.
|
||||
- use `A.format == 'csr'` to check the sparse format
|
||||
|
||||
Identifying sparse matrices:
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
issparse
|
||||
isspmatrix
|
||||
isspmatrix_csc
|
||||
isspmatrix_csr
|
||||
isspmatrix_bsr
|
||||
isspmatrix_lil
|
||||
isspmatrix_dok
|
||||
isspmatrix_coo
|
||||
isspmatrix_dia
|
||||
|
||||
Submodules
|
||||
----------
|
||||
|
||||
.. autosummary::
|
||||
|
||||
csgraph - Compressed sparse graph routines
|
||||
linalg - sparse linear algebra routines
|
||||
|
||||
Exceptions
|
||||
----------
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
SparseEfficiencyWarning
|
||||
SparseWarning
|
||||
|
||||
|
||||
Usage information
|
||||
=================
|
||||
|
||||
There are seven available sparse array types:
|
||||
|
||||
1. `csc_array`: Compressed Sparse Column format
|
||||
2. `csr_array`: Compressed Sparse Row format
|
||||
3. `bsr_array`: Block Sparse Row format
|
||||
4. `lil_array`: List of Lists format
|
||||
5. `dok_array`: Dictionary of Keys format
|
||||
6. `coo_array`: COOrdinate format (aka IJV, triplet format)
|
||||
7. `dia_array`: DIAgonal format
|
||||
|
||||
To construct an array efficiently, use either `dok_array` or `lil_array`.
|
||||
The `lil_array` class supports basic slicing and fancy indexing with a
|
||||
similar syntax to NumPy arrays. As illustrated below, the COO format
|
||||
may also be used to efficiently construct arrays. Despite their
|
||||
similarity to NumPy arrays, it is **strongly discouraged** to use NumPy
|
||||
functions directly on these arrays because NumPy may not properly convert
|
||||
them for computations, leading to unexpected (and incorrect) results. If you
|
||||
do want to apply a NumPy function to these arrays, first check if SciPy has
|
||||
its own implementation for the given sparse array class, or **convert the
|
||||
sparse array to a NumPy array** (e.g., using the ``toarray`` method of the
|
||||
class) first before applying the method.
|
||||
|
||||
To perform manipulations such as multiplication or inversion, first
|
||||
convert the array to either CSC or CSR format. The `lil_array` format is
|
||||
row-based, so conversion to CSR is efficient, whereas conversion to CSC
|
||||
is less so.
|
||||
|
||||
All conversions among the CSR, CSC, and COO formats are efficient,
|
||||
linear-time operations.
|
||||
|
||||
Matrix vector product
|
||||
---------------------
|
||||
To do a vector product between a sparse array and a vector simply use
|
||||
the array ``dot`` method, as described in its docstring:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csr_array
|
||||
>>> A = csr_array([[1, 2, 0], [0, 0, 3], [4, 0, 5]])
|
||||
>>> v = np.array([1, 0, -1])
|
||||
>>> A.dot(v)
|
||||
array([ 1, -3, -1], dtype=int64)
|
||||
|
||||
.. warning:: As of NumPy 1.7, ``np.dot`` is not aware of sparse arrays,
|
||||
therefore using it will result on unexpected results or errors.
|
||||
The corresponding dense array should be obtained first instead:
|
||||
|
||||
>>> np.dot(A.toarray(), v)
|
||||
array([ 1, -3, -1], dtype=int64)
|
||||
|
||||
but then all the performance advantages would be lost.
|
||||
|
||||
The CSR format is especially suitable for fast matrix vector products.
|
||||
|
||||
Example 1
|
||||
---------
|
||||
Construct a 1000x1000 `lil_array` and add some values to it:
|
||||
|
||||
>>> from scipy.sparse import lil_array
|
||||
>>> from scipy.sparse.linalg import spsolve
|
||||
>>> from numpy.linalg import solve, norm
|
||||
>>> from numpy.random import rand
|
||||
|
||||
>>> A = lil_array((1000, 1000))
|
||||
>>> A[0, :100] = rand(100)
|
||||
>>> A.setdiag(rand(1000))
|
||||
|
||||
Now convert it to CSR format and solve A x = b for x:
|
||||
|
||||
>>> A = A.tocsr()
|
||||
>>> b = rand(1000)
|
||||
>>> x = spsolve(A, b)
|
||||
|
||||
Convert it to a dense array and solve, and check that the result
|
||||
is the same:
|
||||
|
||||
>>> x_ = solve(A.toarray(), b)
|
||||
|
||||
Now we can compute norm of the error with:
|
||||
|
||||
>>> err = norm(x-x_)
|
||||
>>> err < 1e-10
|
||||
True
|
||||
|
||||
It should be small :)
|
||||
|
||||
|
||||
Example 2
|
||||
---------
|
||||
|
||||
Construct an array in COO format:
|
||||
|
||||
>>> from scipy import sparse
|
||||
>>> from numpy import array
|
||||
>>> I = array([0,3,1,0])
|
||||
>>> J = array([0,3,1,2])
|
||||
>>> V = array([4,5,7,9])
|
||||
>>> A = sparse.coo_array((V,(I,J)),shape=(4,4))
|
||||
|
||||
Notice that the indices do not need to be sorted.
|
||||
|
||||
Duplicate (i,j) entries are summed when converting to CSR or CSC.
|
||||
|
||||
>>> I = array([0,0,1,3,1,0,0])
|
||||
>>> J = array([0,2,1,3,1,0,0])
|
||||
>>> V = array([1,1,1,1,1,1,1])
|
||||
>>> B = sparse.coo_array((V,(I,J)),shape=(4,4)).tocsr()
|
||||
|
||||
This is useful for constructing finite-element stiffness and mass matrices.
|
||||
|
||||
Further details
|
||||
---------------
|
||||
|
||||
CSR column indices are not necessarily sorted. Likewise for CSC row
|
||||
indices. Use the ``.sorted_indices()`` and ``.sort_indices()`` methods when
|
||||
sorted indices are required (e.g., when passing data to other libraries).
|
||||
|
||||
"""
|
||||
|
||||
# Original code by Travis Oliphant.
|
||||
# Modified and extended by Ed Schofield, Robert Cimrman,
|
||||
# Nathan Bell, and Jake Vanderplas.
|
||||
|
||||
import warnings as _warnings
|
||||
|
||||
from ._base import *
|
||||
from ._csr import *
|
||||
from ._csc import *
|
||||
from ._lil import *
|
||||
from ._dok import *
|
||||
from ._coo import *
|
||||
from ._dia import *
|
||||
from ._bsr import *
|
||||
from ._construct import *
|
||||
from ._extract import *
|
||||
from ._matrix import spmatrix
|
||||
from ._matrix_io import *
|
||||
|
||||
# For backward compatibility with v0.19.
|
||||
from . import csgraph
|
||||
|
||||
# Deprecated namespaces, to be removed in v2.0.0
|
||||
from . import (
|
||||
base, bsr, compressed, construct, coo, csc, csr, data, dia, dok, extract,
|
||||
lil, sparsetools, sputils
|
||||
)
|
||||
|
||||
__all__ = [s for s in dir() if not s.startswith('_')]
|
||||
|
||||
# Filter PendingDeprecationWarning for np.matrix introduced with numpy 1.15
|
||||
msg = 'the matrix subclass is not the recommended way'
|
||||
_warnings.filterwarnings('ignore', message=msg)
|
||||
|
||||
from scipy._lib._testutils import PytestTester
|
||||
test = PytestTester(__name__)
|
||||
del PytestTester
|
||||
1390
venv/lib/python3.12/site-packages/scipy/sparse/_base.py
Normal file
1390
venv/lib/python3.12/site-packages/scipy/sparse/_base.py
Normal file
File diff suppressed because it is too large
Load Diff
856
venv/lib/python3.12/site-packages/scipy/sparse/_bsr.py
Normal file
856
venv/lib/python3.12/site-packages/scipy/sparse/_bsr.py
Normal file
@ -0,0 +1,856 @@
|
||||
"""Compressed Block Sparse Row format"""
|
||||
|
||||
__docformat__ = "restructuredtext en"
|
||||
|
||||
__all__ = ['bsr_array', 'bsr_matrix', 'isspmatrix_bsr']
|
||||
|
||||
from warnings import warn
|
||||
|
||||
import numpy as np
|
||||
|
||||
from scipy._lib._util import copy_if_needed
|
||||
from ._matrix import spmatrix
|
||||
from ._data import _data_matrix, _minmax_mixin
|
||||
from ._compressed import _cs_matrix
|
||||
from ._base import issparse, _formats, _spbase, sparray
|
||||
from ._sputils import (isshape, getdtype, getdata, to_native, upcast,
|
||||
check_shape)
|
||||
from . import _sparsetools
|
||||
from ._sparsetools import (bsr_matvec, bsr_matvecs, csr_matmat_maxnnz,
|
||||
bsr_matmat, bsr_transpose, bsr_sort_indices,
|
||||
bsr_tocsr)
|
||||
|
||||
|
||||
class _bsr_base(_cs_matrix, _minmax_mixin):
|
||||
_format = 'bsr'
|
||||
|
||||
def __init__(self, arg1, shape=None, dtype=None, copy=False, blocksize=None):
|
||||
_data_matrix.__init__(self, arg1)
|
||||
|
||||
if issparse(arg1):
|
||||
if arg1.format == self.format and copy:
|
||||
arg1 = arg1.copy()
|
||||
else:
|
||||
arg1 = arg1.tobsr(blocksize=blocksize)
|
||||
self.indptr, self.indices, self.data, self._shape = (
|
||||
arg1.indptr, arg1.indices, arg1.data, arg1._shape
|
||||
)
|
||||
|
||||
elif isinstance(arg1,tuple):
|
||||
if isshape(arg1):
|
||||
# it's a tuple of matrix dimensions (M,N)
|
||||
self._shape = check_shape(arg1)
|
||||
M,N = self.shape
|
||||
# process blocksize
|
||||
if blocksize is None:
|
||||
blocksize = (1,1)
|
||||
else:
|
||||
if not isshape(blocksize):
|
||||
raise ValueError('invalid blocksize=%s' % blocksize)
|
||||
blocksize = tuple(blocksize)
|
||||
self.data = np.zeros((0,) + blocksize, getdtype(dtype, default=float))
|
||||
|
||||
R,C = blocksize
|
||||
if (M % R) != 0 or (N % C) != 0:
|
||||
raise ValueError('shape must be multiple of blocksize')
|
||||
|
||||
# Select index dtype large enough to pass array and
|
||||
# scalar parameters to sparsetools
|
||||
idx_dtype = self._get_index_dtype(maxval=max(M//R, N//C, R, C))
|
||||
self.indices = np.zeros(0, dtype=idx_dtype)
|
||||
self.indptr = np.zeros(M//R + 1, dtype=idx_dtype)
|
||||
|
||||
elif len(arg1) == 2:
|
||||
# (data,(row,col)) format
|
||||
coo = self._coo_container(arg1, dtype=dtype, shape=shape)
|
||||
bsr = coo.tobsr(blocksize=blocksize)
|
||||
self.indptr, self.indices, self.data, self._shape = (
|
||||
bsr.indptr, bsr.indices, bsr.data, bsr._shape
|
||||
)
|
||||
|
||||
elif len(arg1) == 3:
|
||||
# (data,indices,indptr) format
|
||||
(data, indices, indptr) = arg1
|
||||
|
||||
# Select index dtype large enough to pass array and
|
||||
# scalar parameters to sparsetools
|
||||
maxval = 1
|
||||
if shape is not None:
|
||||
maxval = max(shape)
|
||||
if blocksize is not None:
|
||||
maxval = max(maxval, max(blocksize))
|
||||
idx_dtype = self._get_index_dtype((indices, indptr), maxval=maxval,
|
||||
check_contents=True)
|
||||
if not copy:
|
||||
copy = copy_if_needed
|
||||
self.indices = np.array(indices, copy=copy, dtype=idx_dtype)
|
||||
self.indptr = np.array(indptr, copy=copy, dtype=idx_dtype)
|
||||
self.data = getdata(data, copy=copy, dtype=dtype)
|
||||
if self.data.ndim != 3:
|
||||
raise ValueError(
|
||||
f'BSR data must be 3-dimensional, got shape={self.data.shape}'
|
||||
)
|
||||
if blocksize is not None:
|
||||
if not isshape(blocksize):
|
||||
raise ValueError(f'invalid blocksize={blocksize}')
|
||||
if tuple(blocksize) != self.data.shape[1:]:
|
||||
raise ValueError(
|
||||
f'mismatching blocksize={blocksize}'
|
||||
f' vs {self.data.shape[1:]}'
|
||||
)
|
||||
else:
|
||||
raise ValueError('unrecognized bsr_array constructor usage')
|
||||
else:
|
||||
# must be dense
|
||||
try:
|
||||
arg1 = np.asarray(arg1)
|
||||
except Exception as e:
|
||||
raise ValueError("unrecognized form for"
|
||||
" %s_matrix constructor" % self.format) from e
|
||||
if isinstance(self, sparray) and arg1.ndim != 2:
|
||||
raise ValueError(f"BSR arrays don't support {arg1.ndim}D input. Use 2D")
|
||||
arg1 = self._coo_container(arg1, dtype=dtype).tobsr(blocksize=blocksize)
|
||||
self.indptr, self.indices, self.data, self._shape = (
|
||||
arg1.indptr, arg1.indices, arg1.data, arg1._shape
|
||||
)
|
||||
|
||||
if shape is not None:
|
||||
self._shape = check_shape(shape)
|
||||
else:
|
||||
if self.shape is None:
|
||||
# shape not already set, try to infer dimensions
|
||||
try:
|
||||
M = len(self.indptr) - 1
|
||||
N = self.indices.max() + 1
|
||||
except Exception as e:
|
||||
raise ValueError('unable to infer matrix dimensions') from e
|
||||
else:
|
||||
R,C = self.blocksize
|
||||
self._shape = check_shape((M*R,N*C))
|
||||
|
||||
if self.shape is None:
|
||||
if shape is None:
|
||||
# TODO infer shape here
|
||||
raise ValueError('need to infer shape')
|
||||
else:
|
||||
self._shape = check_shape(shape)
|
||||
|
||||
if dtype is not None:
|
||||
self.data = self.data.astype(dtype, copy=False)
|
||||
|
||||
self.check_format(full_check=False)
|
||||
|
||||
def check_format(self, full_check=True):
|
||||
"""Check whether the array/matrix respects the BSR format.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
full_check : bool, optional
|
||||
If `True`, run rigorous check, scanning arrays for valid values.
|
||||
Note that activating those check might copy arrays for casting,
|
||||
modifying indices and index pointers' inplace.
|
||||
If `False`, run basic checks on attributes. O(1) operations.
|
||||
Default is `True`.
|
||||
"""
|
||||
M,N = self.shape
|
||||
R,C = self.blocksize
|
||||
|
||||
# index arrays should have integer data types
|
||||
if self.indptr.dtype.kind != 'i':
|
||||
warn(f"indptr array has non-integer dtype ({self.indptr.dtype.name})",
|
||||
stacklevel=2)
|
||||
if self.indices.dtype.kind != 'i':
|
||||
warn(f"indices array has non-integer dtype ({self.indices.dtype.name})",
|
||||
stacklevel=2)
|
||||
|
||||
# check array shapes
|
||||
if self.indices.ndim != 1 or self.indptr.ndim != 1:
|
||||
raise ValueError("indices, and indptr should be 1-D")
|
||||
if self.data.ndim != 3:
|
||||
raise ValueError("data should be 3-D")
|
||||
|
||||
# check index pointer
|
||||
if (len(self.indptr) != M//R + 1):
|
||||
raise ValueError("index pointer size (%d) should be (%d)" %
|
||||
(len(self.indptr), M//R + 1))
|
||||
if (self.indptr[0] != 0):
|
||||
raise ValueError("index pointer should start with 0")
|
||||
|
||||
# check index and data arrays
|
||||
if (len(self.indices) != len(self.data)):
|
||||
raise ValueError("indices and data should have the same size")
|
||||
if (self.indptr[-1] > len(self.indices)):
|
||||
raise ValueError("Last value of index pointer should be less than "
|
||||
"the size of index and data arrays")
|
||||
|
||||
self.prune()
|
||||
|
||||
if full_check:
|
||||
# check format validity (more expensive)
|
||||
if self.nnz > 0:
|
||||
if self.indices.max() >= N//C:
|
||||
raise ValueError("column index values must be < %d (now max %d)"
|
||||
% (N//C, self.indices.max()))
|
||||
if self.indices.min() < 0:
|
||||
raise ValueError("column index values must be >= 0")
|
||||
if np.diff(self.indptr).min() < 0:
|
||||
raise ValueError("index pointer values must form a "
|
||||
"non-decreasing sequence")
|
||||
|
||||
idx_dtype = self._get_index_dtype((self.indices, self.indptr))
|
||||
self.indptr = np.asarray(self.indptr, dtype=idx_dtype)
|
||||
self.indices = np.asarray(self.indices, dtype=idx_dtype)
|
||||
self.data = to_native(self.data)
|
||||
# if not self.has_sorted_indices():
|
||||
# warn('Indices were not in sorted order. Sorting indices.')
|
||||
# self.sort_indices(check_first=False)
|
||||
|
||||
@property
|
||||
def blocksize(self) -> tuple:
|
||||
"""Block size of the matrix."""
|
||||
return self.data.shape[1:]
|
||||
|
||||
def _getnnz(self, axis=None):
|
||||
if axis is not None:
|
||||
raise NotImplementedError("_getnnz over an axis is not implemented "
|
||||
"for BSR format")
|
||||
R,C = self.blocksize
|
||||
return int(self.indptr[-1] * R * C)
|
||||
|
||||
_getnnz.__doc__ = _spbase._getnnz.__doc__
|
||||
|
||||
def __repr__(self):
|
||||
_, fmt = _formats[self.format]
|
||||
sparse_cls = 'array' if isinstance(self, sparray) else 'matrix'
|
||||
b = 'x'.join(str(x) for x in self.blocksize)
|
||||
return (
|
||||
f"<{fmt} sparse {sparse_cls} of dtype '{self.dtype}'\n"
|
||||
f"\twith {self.nnz} stored elements (blocksize={b}) and shape {self.shape}>"
|
||||
)
|
||||
|
||||
def diagonal(self, k=0):
|
||||
rows, cols = self.shape
|
||||
if k <= -rows or k >= cols:
|
||||
return np.empty(0, dtype=self.data.dtype)
|
||||
R, C = self.blocksize
|
||||
y = np.zeros(min(rows + min(k, 0), cols - max(k, 0)),
|
||||
dtype=upcast(self.dtype))
|
||||
_sparsetools.bsr_diagonal(k, rows // R, cols // C, R, C,
|
||||
self.indptr, self.indices,
|
||||
np.ravel(self.data), y)
|
||||
return y
|
||||
|
||||
diagonal.__doc__ = _spbase.diagonal.__doc__
|
||||
|
||||
##########################
|
||||
# NotImplemented methods #
|
||||
##########################
|
||||
|
||||
def __getitem__(self,key):
|
||||
raise NotImplementedError
|
||||
|
||||
def __setitem__(self,key,val):
|
||||
raise NotImplementedError
|
||||
|
||||
######################
|
||||
# Arithmetic methods #
|
||||
######################
|
||||
|
||||
def _add_dense(self, other):
|
||||
return self.tocoo(copy=False)._add_dense(other)
|
||||
|
||||
def _matmul_vector(self, other):
|
||||
M,N = self.shape
|
||||
R,C = self.blocksize
|
||||
|
||||
result = np.zeros(self.shape[0], dtype=upcast(self.dtype, other.dtype))
|
||||
|
||||
bsr_matvec(M//R, N//C, R, C,
|
||||
self.indptr, self.indices, self.data.ravel(),
|
||||
other, result)
|
||||
|
||||
return result
|
||||
|
||||
def _matmul_multivector(self,other):
|
||||
R,C = self.blocksize
|
||||
M,N = self.shape
|
||||
n_vecs = other.shape[1] # number of column vectors
|
||||
|
||||
result = np.zeros((M,n_vecs), dtype=upcast(self.dtype,other.dtype))
|
||||
|
||||
bsr_matvecs(M//R, N//C, n_vecs, R, C,
|
||||
self.indptr, self.indices, self.data.ravel(),
|
||||
other.ravel(), result.ravel())
|
||||
|
||||
return result
|
||||
|
||||
def _matmul_sparse(self, other):
|
||||
M, K1 = self.shape
|
||||
K2, N = other.shape
|
||||
|
||||
R,n = self.blocksize
|
||||
|
||||
# convert to this format
|
||||
if other.format == "bsr":
|
||||
C = other.blocksize[1]
|
||||
else:
|
||||
C = 1
|
||||
|
||||
if other.format == "csr" and n == 1:
|
||||
other = other.tobsr(blocksize=(n,C), copy=False) # lightweight conversion
|
||||
else:
|
||||
other = other.tobsr(blocksize=(n,C))
|
||||
|
||||
idx_dtype = self._get_index_dtype((self.indptr, self.indices,
|
||||
other.indptr, other.indices))
|
||||
|
||||
bnnz = csr_matmat_maxnnz(M//R, N//C,
|
||||
self.indptr.astype(idx_dtype),
|
||||
self.indices.astype(idx_dtype),
|
||||
other.indptr.astype(idx_dtype),
|
||||
other.indices.astype(idx_dtype))
|
||||
|
||||
idx_dtype = self._get_index_dtype((self.indptr, self.indices,
|
||||
other.indptr, other.indices),
|
||||
maxval=bnnz)
|
||||
indptr = np.empty(self.indptr.shape, dtype=idx_dtype)
|
||||
indices = np.empty(bnnz, dtype=idx_dtype)
|
||||
data = np.empty(R*C*bnnz, dtype=upcast(self.dtype,other.dtype))
|
||||
|
||||
bsr_matmat(bnnz, M//R, N//C, R, C, n,
|
||||
self.indptr.astype(idx_dtype),
|
||||
self.indices.astype(idx_dtype),
|
||||
np.ravel(self.data),
|
||||
other.indptr.astype(idx_dtype),
|
||||
other.indices.astype(idx_dtype),
|
||||
np.ravel(other.data),
|
||||
indptr,
|
||||
indices,
|
||||
data)
|
||||
|
||||
data = data.reshape(-1,R,C)
|
||||
|
||||
# TODO eliminate zeros
|
||||
|
||||
return self._bsr_container(
|
||||
(data, indices, indptr), shape=(M, N), blocksize=(R, C)
|
||||
)
|
||||
|
||||
######################
|
||||
# Conversion methods #
|
||||
######################
|
||||
|
||||
def tobsr(self, blocksize=None, copy=False):
|
||||
"""Convert this array/matrix into Block Sparse Row Format.
|
||||
|
||||
With copy=False, the data/indices may be shared between this
|
||||
array/matrix and the resultant bsr_array/bsr_matrix.
|
||||
|
||||
If blocksize=(R, C) is provided, it will be used for determining
|
||||
block size of the bsr_array/bsr_matrix.
|
||||
"""
|
||||
if blocksize not in [None, self.blocksize]:
|
||||
return self.tocsr().tobsr(blocksize=blocksize)
|
||||
if copy:
|
||||
return self.copy()
|
||||
else:
|
||||
return self
|
||||
|
||||
def tocsr(self, copy=False):
|
||||
M, N = self.shape
|
||||
R, C = self.blocksize
|
||||
nnz = self.nnz
|
||||
idx_dtype = self._get_index_dtype((self.indptr, self.indices),
|
||||
maxval=max(nnz, N))
|
||||
indptr = np.empty(M + 1, dtype=idx_dtype)
|
||||
indices = np.empty(nnz, dtype=idx_dtype)
|
||||
data = np.empty(nnz, dtype=upcast(self.dtype))
|
||||
|
||||
bsr_tocsr(M // R, # n_brow
|
||||
N // C, # n_bcol
|
||||
R, C,
|
||||
self.indptr.astype(idx_dtype, copy=False),
|
||||
self.indices.astype(idx_dtype, copy=False),
|
||||
self.data,
|
||||
indptr,
|
||||
indices,
|
||||
data)
|
||||
return self._csr_container((data, indices, indptr), shape=self.shape)
|
||||
|
||||
tocsr.__doc__ = _spbase.tocsr.__doc__
|
||||
|
||||
def tocsc(self, copy=False):
|
||||
return self.tocsr(copy=False).tocsc(copy=copy)
|
||||
|
||||
tocsc.__doc__ = _spbase.tocsc.__doc__
|
||||
|
||||
def tocoo(self, copy=True):
|
||||
"""Convert this array/matrix to COOrdinate format.
|
||||
|
||||
When copy=False the data array will be shared between
|
||||
this array/matrix and the resultant coo_array/coo_matrix.
|
||||
"""
|
||||
|
||||
M,N = self.shape
|
||||
R,C = self.blocksize
|
||||
|
||||
indptr_diff = np.diff(self.indptr)
|
||||
if indptr_diff.dtype.itemsize > np.dtype(np.intp).itemsize:
|
||||
# Check for potential overflow
|
||||
indptr_diff_limited = indptr_diff.astype(np.intp)
|
||||
if np.any(indptr_diff_limited != indptr_diff):
|
||||
raise ValueError("Matrix too big to convert")
|
||||
indptr_diff = indptr_diff_limited
|
||||
|
||||
idx_dtype = self._get_index_dtype(maxval=max(M, N))
|
||||
row = (R * np.arange(M//R, dtype=idx_dtype)).repeat(indptr_diff)
|
||||
row = row.repeat(R*C).reshape(-1,R,C)
|
||||
row += np.tile(np.arange(R, dtype=idx_dtype).reshape(-1,1), (1,C))
|
||||
row = row.reshape(-1)
|
||||
|
||||
col = ((C * self.indices).astype(idx_dtype, copy=False)
|
||||
.repeat(R*C).reshape(-1,R,C))
|
||||
col += np.tile(np.arange(C, dtype=idx_dtype), (R,1))
|
||||
col = col.reshape(-1)
|
||||
|
||||
data = self.data.reshape(-1)
|
||||
|
||||
if copy:
|
||||
data = data.copy()
|
||||
|
||||
return self._coo_container(
|
||||
(data, (row, col)), shape=self.shape
|
||||
)
|
||||
|
||||
def toarray(self, order=None, out=None):
|
||||
return self.tocoo(copy=False).toarray(order=order, out=out)
|
||||
|
||||
toarray.__doc__ = _spbase.toarray.__doc__
|
||||
|
||||
def transpose(self, axes=None, copy=False):
|
||||
if axes is not None and axes != (1, 0):
|
||||
raise ValueError("Sparse matrices do not support "
|
||||
"an 'axes' parameter because swapping "
|
||||
"dimensions is the only logical permutation.")
|
||||
|
||||
R, C = self.blocksize
|
||||
M, N = self.shape
|
||||
NBLK = self.nnz//(R*C)
|
||||
|
||||
if self.nnz == 0:
|
||||
return self._bsr_container((N, M), blocksize=(C, R),
|
||||
dtype=self.dtype, copy=copy)
|
||||
|
||||
indptr = np.empty(N//C + 1, dtype=self.indptr.dtype)
|
||||
indices = np.empty(NBLK, dtype=self.indices.dtype)
|
||||
data = np.empty((NBLK, C, R), dtype=self.data.dtype)
|
||||
|
||||
bsr_transpose(M//R, N//C, R, C,
|
||||
self.indptr, self.indices, self.data.ravel(),
|
||||
indptr, indices, data.ravel())
|
||||
|
||||
return self._bsr_container((data, indices, indptr),
|
||||
shape=(N, M), copy=copy)
|
||||
|
||||
transpose.__doc__ = _spbase.transpose.__doc__
|
||||
|
||||
##############################################################
|
||||
# methods that examine or modify the internal data structure #
|
||||
##############################################################
|
||||
|
||||
def eliminate_zeros(self):
|
||||
"""Remove zero elements in-place."""
|
||||
|
||||
if not self.nnz:
|
||||
return # nothing to do
|
||||
|
||||
R,C = self.blocksize
|
||||
M,N = self.shape
|
||||
|
||||
mask = (self.data != 0).reshape(-1,R*C).sum(axis=1) # nonzero blocks
|
||||
|
||||
nonzero_blocks = mask.nonzero()[0]
|
||||
|
||||
self.data[:len(nonzero_blocks)] = self.data[nonzero_blocks]
|
||||
|
||||
# modifies self.indptr and self.indices *in place*
|
||||
_sparsetools.csr_eliminate_zeros(M//R, N//C, self.indptr,
|
||||
self.indices, mask)
|
||||
self.prune()
|
||||
|
||||
def sum_duplicates(self):
|
||||
"""Eliminate duplicate array/matrix entries by adding them together
|
||||
|
||||
The is an *in place* operation
|
||||
"""
|
||||
if self.has_canonical_format:
|
||||
return
|
||||
self.sort_indices()
|
||||
R, C = self.blocksize
|
||||
M, N = self.shape
|
||||
|
||||
# port of _sparsetools.csr_sum_duplicates
|
||||
n_row = M // R
|
||||
nnz = 0
|
||||
row_end = 0
|
||||
for i in range(n_row):
|
||||
jj = row_end
|
||||
row_end = self.indptr[i+1]
|
||||
while jj < row_end:
|
||||
j = self.indices[jj]
|
||||
x = self.data[jj]
|
||||
jj += 1
|
||||
while jj < row_end and self.indices[jj] == j:
|
||||
x += self.data[jj]
|
||||
jj += 1
|
||||
self.indices[nnz] = j
|
||||
self.data[nnz] = x
|
||||
nnz += 1
|
||||
self.indptr[i+1] = nnz
|
||||
|
||||
self.prune() # nnz may have changed
|
||||
self.has_canonical_format = True
|
||||
|
||||
def sort_indices(self):
|
||||
"""Sort the indices of this array/matrix *in place*
|
||||
"""
|
||||
if self.has_sorted_indices:
|
||||
return
|
||||
|
||||
R,C = self.blocksize
|
||||
M,N = self.shape
|
||||
|
||||
bsr_sort_indices(M//R, N//C, R, C, self.indptr, self.indices, self.data.ravel())
|
||||
|
||||
self.has_sorted_indices = True
|
||||
|
||||
def prune(self):
|
||||
"""Remove empty space after all non-zero elements.
|
||||
"""
|
||||
|
||||
R,C = self.blocksize
|
||||
M,N = self.shape
|
||||
|
||||
if len(self.indptr) != M//R + 1:
|
||||
raise ValueError("index pointer has invalid length")
|
||||
|
||||
bnnz = self.indptr[-1]
|
||||
|
||||
if len(self.indices) < bnnz:
|
||||
raise ValueError("indices array has too few elements")
|
||||
if len(self.data) < bnnz:
|
||||
raise ValueError("data array has too few elements")
|
||||
|
||||
self.data = self.data[:bnnz]
|
||||
self.indices = self.indices[:bnnz]
|
||||
|
||||
# utility functions
|
||||
def _binopt(self, other, op, in_shape=None, out_shape=None):
|
||||
"""Apply the binary operation fn to two sparse matrices."""
|
||||
|
||||
# Ideally we'd take the GCDs of the blocksize dimensions
|
||||
# and explode self and other to match.
|
||||
other = self.__class__(other, blocksize=self.blocksize)
|
||||
|
||||
# e.g. bsr_plus_bsr, etc.
|
||||
fn = getattr(_sparsetools, self.format + op + self.format)
|
||||
|
||||
R,C = self.blocksize
|
||||
|
||||
max_bnnz = len(self.data) + len(other.data)
|
||||
idx_dtype = self._get_index_dtype((self.indptr, self.indices,
|
||||
other.indptr, other.indices),
|
||||
maxval=max_bnnz)
|
||||
indptr = np.empty(self.indptr.shape, dtype=idx_dtype)
|
||||
indices = np.empty(max_bnnz, dtype=idx_dtype)
|
||||
|
||||
bool_ops = ['_ne_', '_lt_', '_gt_', '_le_', '_ge_']
|
||||
if op in bool_ops:
|
||||
data = np.empty(R*C*max_bnnz, dtype=np.bool_)
|
||||
else:
|
||||
data = np.empty(R*C*max_bnnz, dtype=upcast(self.dtype,other.dtype))
|
||||
|
||||
fn(self.shape[0]//R, self.shape[1]//C, R, C,
|
||||
self.indptr.astype(idx_dtype),
|
||||
self.indices.astype(idx_dtype),
|
||||
self.data,
|
||||
other.indptr.astype(idx_dtype),
|
||||
other.indices.astype(idx_dtype),
|
||||
np.ravel(other.data),
|
||||
indptr,
|
||||
indices,
|
||||
data)
|
||||
|
||||
actual_bnnz = indptr[-1]
|
||||
indices = indices[:actual_bnnz]
|
||||
data = data[:R*C*actual_bnnz]
|
||||
|
||||
if actual_bnnz < max_bnnz/2:
|
||||
indices = indices.copy()
|
||||
data = data.copy()
|
||||
|
||||
data = data.reshape(-1,R,C)
|
||||
|
||||
return self.__class__((data, indices, indptr), shape=self.shape)
|
||||
|
||||
# needed by _data_matrix
|
||||
def _with_data(self,data,copy=True):
|
||||
"""Returns a matrix with the same sparsity structure as self,
|
||||
but with different data. By default the structure arrays
|
||||
(i.e. .indptr and .indices) are copied.
|
||||
"""
|
||||
if copy:
|
||||
return self.__class__((data,self.indices.copy(),self.indptr.copy()),
|
||||
shape=self.shape,dtype=data.dtype)
|
||||
else:
|
||||
return self.__class__((data,self.indices,self.indptr),
|
||||
shape=self.shape,dtype=data.dtype)
|
||||
|
||||
# # these functions are used by the parent class
|
||||
# # to remove redundancy between bsc_matrix and bsr_matrix
|
||||
# def _swap(self,x):
|
||||
# """swap the members of x if this is a column-oriented matrix
|
||||
# """
|
||||
# return (x[0],x[1])
|
||||
|
||||
|
||||
def isspmatrix_bsr(x):
|
||||
"""Is `x` of a bsr_matrix type?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x
|
||||
object to check for being a bsr matrix
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if `x` is a bsr matrix, False otherwise
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy.sparse import bsr_array, bsr_matrix, csr_matrix, isspmatrix_bsr
|
||||
>>> isspmatrix_bsr(bsr_matrix([[5]]))
|
||||
True
|
||||
>>> isspmatrix_bsr(bsr_array([[5]]))
|
||||
False
|
||||
>>> isspmatrix_bsr(csr_matrix([[5]]))
|
||||
False
|
||||
"""
|
||||
return isinstance(x, bsr_matrix)
|
||||
|
||||
|
||||
# This namespace class separates array from matrix with isinstance
|
||||
class bsr_array(_bsr_base, sparray):
|
||||
"""
|
||||
Block Sparse Row format sparse array.
|
||||
|
||||
This can be instantiated in several ways:
|
||||
bsr_array(D, [blocksize=(R,C)])
|
||||
where D is a 2-D ndarray.
|
||||
|
||||
bsr_array(S, [blocksize=(R,C)])
|
||||
with another sparse array or matrix S (equivalent to S.tobsr())
|
||||
|
||||
bsr_array((M, N), [blocksize=(R,C), dtype])
|
||||
to construct an empty sparse array with shape (M, N)
|
||||
dtype is optional, defaulting to dtype='d'.
|
||||
|
||||
bsr_array((data, ij), [blocksize=(R,C), shape=(M, N)])
|
||||
where ``data`` and ``ij`` satisfy ``a[ij[0, k], ij[1, k]] = data[k]``
|
||||
|
||||
bsr_array((data, indices, indptr), [shape=(M, N)])
|
||||
is the standard BSR representation where the block column
|
||||
indices for row i are stored in ``indices[indptr[i]:indptr[i+1]]``
|
||||
and their corresponding block values are stored in
|
||||
``data[ indptr[i]: indptr[i+1] ]``. If the shape parameter is not
|
||||
supplied, the array dimensions are inferred from the index arrays.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dtype : dtype
|
||||
Data type of the array
|
||||
shape : 2-tuple
|
||||
Shape of the array
|
||||
ndim : int
|
||||
Number of dimensions (this is always 2)
|
||||
nnz
|
||||
size
|
||||
data
|
||||
BSR format data array of the array
|
||||
indices
|
||||
BSR format index array of the array
|
||||
indptr
|
||||
BSR format index pointer array of the array
|
||||
blocksize
|
||||
Block size
|
||||
has_sorted_indices : bool
|
||||
Whether indices are sorted
|
||||
has_canonical_format : bool
|
||||
T
|
||||
|
||||
Notes
|
||||
-----
|
||||
Sparse arrays can be used in arithmetic operations: they support
|
||||
addition, subtraction, multiplication, division, and matrix power.
|
||||
|
||||
**Summary of BSR format**
|
||||
|
||||
The Block Sparse Row (BSR) format is very similar to the Compressed
|
||||
Sparse Row (CSR) format. BSR is appropriate for sparse matrices with dense
|
||||
sub matrices like the last example below. Such sparse block matrices often
|
||||
arise in vector-valued finite element discretizations. In such cases, BSR is
|
||||
considerably more efficient than CSR and CSC for many sparse arithmetic
|
||||
operations.
|
||||
|
||||
**Blocksize**
|
||||
|
||||
The blocksize (R,C) must evenly divide the shape of the sparse array (M,N).
|
||||
That is, R and C must satisfy the relationship ``M % R = 0`` and
|
||||
``N % C = 0``.
|
||||
|
||||
If no blocksize is specified, a simple heuristic is applied to determine
|
||||
an appropriate blocksize.
|
||||
|
||||
**Canonical Format**
|
||||
|
||||
In canonical format, there are no duplicate blocks and indices are sorted
|
||||
per row.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import bsr_array
|
||||
>>> bsr_array((3, 4), dtype=np.int8).toarray()
|
||||
array([[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0]], dtype=int8)
|
||||
|
||||
>>> row = np.array([0, 0, 1, 2, 2, 2])
|
||||
>>> col = np.array([0, 2, 2, 0, 1, 2])
|
||||
>>> data = np.array([1, 2, 3 ,4, 5, 6])
|
||||
>>> bsr_array((data, (row, col)), shape=(3, 3)).toarray()
|
||||
array([[1, 0, 2],
|
||||
[0, 0, 3],
|
||||
[4, 5, 6]])
|
||||
|
||||
>>> indptr = np.array([0, 2, 3, 6])
|
||||
>>> indices = np.array([0, 2, 2, 0, 1, 2])
|
||||
>>> data = np.array([1, 2, 3, 4, 5, 6]).repeat(4).reshape(6, 2, 2)
|
||||
>>> bsr_array((data,indices,indptr), shape=(6, 6)).toarray()
|
||||
array([[1, 1, 0, 0, 2, 2],
|
||||
[1, 1, 0, 0, 2, 2],
|
||||
[0, 0, 0, 0, 3, 3],
|
||||
[0, 0, 0, 0, 3, 3],
|
||||
[4, 4, 5, 5, 6, 6],
|
||||
[4, 4, 5, 5, 6, 6]])
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class bsr_matrix(spmatrix, _bsr_base):
|
||||
"""
|
||||
Block Sparse Row format sparse matrix.
|
||||
|
||||
This can be instantiated in several ways:
|
||||
bsr_matrix(D, [blocksize=(R,C)])
|
||||
where D is a 2-D ndarray.
|
||||
|
||||
bsr_matrix(S, [blocksize=(R,C)])
|
||||
with another sparse array or matrix S (equivalent to S.tobsr())
|
||||
|
||||
bsr_matrix((M, N), [blocksize=(R,C), dtype])
|
||||
to construct an empty sparse matrix with shape (M, N)
|
||||
dtype is optional, defaulting to dtype='d'.
|
||||
|
||||
bsr_matrix((data, ij), [blocksize=(R,C), shape=(M, N)])
|
||||
where ``data`` and ``ij`` satisfy ``a[ij[0, k], ij[1, k]] = data[k]``
|
||||
|
||||
bsr_matrix((data, indices, indptr), [shape=(M, N)])
|
||||
is the standard BSR representation where the block column
|
||||
indices for row i are stored in ``indices[indptr[i]:indptr[i+1]]``
|
||||
and their corresponding block values are stored in
|
||||
``data[ indptr[i]: indptr[i+1] ]``. If the shape parameter is not
|
||||
supplied, the matrix dimensions are inferred from the index arrays.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dtype : dtype
|
||||
Data type of the matrix
|
||||
shape : 2-tuple
|
||||
Shape of the matrix
|
||||
ndim : int
|
||||
Number of dimensions (this is always 2)
|
||||
nnz
|
||||
size
|
||||
data
|
||||
BSR format data array of the matrix
|
||||
indices
|
||||
BSR format index array of the matrix
|
||||
indptr
|
||||
BSR format index pointer array of the matrix
|
||||
blocksize
|
||||
Block size
|
||||
has_sorted_indices : bool
|
||||
Whether indices are sorted
|
||||
has_canonical_format : bool
|
||||
T
|
||||
|
||||
Notes
|
||||
-----
|
||||
Sparse matrices can be used in arithmetic operations: they support
|
||||
addition, subtraction, multiplication, division, and matrix power.
|
||||
|
||||
**Summary of BSR format**
|
||||
|
||||
The Block Sparse Row (BSR) format is very similar to the Compressed
|
||||
Sparse Row (CSR) format. BSR is appropriate for sparse matrices with dense
|
||||
sub matrices like the last example below. Such sparse block matrices often
|
||||
arise in vector-valued finite element discretizations. In such cases, BSR is
|
||||
considerably more efficient than CSR and CSC for many sparse arithmetic
|
||||
operations.
|
||||
|
||||
**Blocksize**
|
||||
|
||||
The blocksize (R,C) must evenly divide the shape of the sparse matrix (M,N).
|
||||
That is, R and C must satisfy the relationship ``M % R = 0`` and
|
||||
``N % C = 0``.
|
||||
|
||||
If no blocksize is specified, a simple heuristic is applied to determine
|
||||
an appropriate blocksize.
|
||||
|
||||
**Canonical Format**
|
||||
|
||||
In canonical format, there are no duplicate blocks and indices are sorted
|
||||
per row.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import bsr_matrix
|
||||
>>> bsr_matrix((3, 4), dtype=np.int8).toarray()
|
||||
array([[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0]], dtype=int8)
|
||||
|
||||
>>> row = np.array([0, 0, 1, 2, 2, 2])
|
||||
>>> col = np.array([0, 2, 2, 0, 1, 2])
|
||||
>>> data = np.array([1, 2, 3 ,4, 5, 6])
|
||||
>>> bsr_matrix((data, (row, col)), shape=(3, 3)).toarray()
|
||||
array([[1, 0, 2],
|
||||
[0, 0, 3],
|
||||
[4, 5, 6]])
|
||||
|
||||
>>> indptr = np.array([0, 2, 3, 6])
|
||||
>>> indices = np.array([0, 2, 2, 0, 1, 2])
|
||||
>>> data = np.array([1, 2, 3, 4, 5, 6]).repeat(4).reshape(6, 2, 2)
|
||||
>>> bsr_matrix((data,indices,indptr), shape=(6, 6)).toarray()
|
||||
array([[1, 1, 0, 0, 2, 2],
|
||||
[1, 1, 0, 0, 2, 2],
|
||||
[0, 0, 0, 0, 3, 3],
|
||||
[0, 0, 0, 0, 3, 3],
|
||||
[4, 4, 5, 5, 6, 6],
|
||||
[4, 4, 5, 5, 6, 6]])
|
||||
|
||||
"""
|
||||
|
||||
1463
venv/lib/python3.12/site-packages/scipy/sparse/_compressed.py
Normal file
1463
venv/lib/python3.12/site-packages/scipy/sparse/_compressed.py
Normal file
File diff suppressed because it is too large
Load Diff
1410
venv/lib/python3.12/site-packages/scipy/sparse/_construct.py
Normal file
1410
venv/lib/python3.12/site-packages/scipy/sparse/_construct.py
Normal file
File diff suppressed because it is too large
Load Diff
866
venv/lib/python3.12/site-packages/scipy/sparse/_coo.py
Normal file
866
venv/lib/python3.12/site-packages/scipy/sparse/_coo.py
Normal file
@ -0,0 +1,866 @@
|
||||
""" A sparse matrix in COOrdinate or 'triplet' format"""
|
||||
|
||||
__docformat__ = "restructuredtext en"
|
||||
|
||||
__all__ = ['coo_array', 'coo_matrix', 'isspmatrix_coo']
|
||||
|
||||
import math
|
||||
from warnings import warn
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .._lib._util import copy_if_needed
|
||||
from ._matrix import spmatrix
|
||||
from ._sparsetools import coo_tocsr, coo_todense, coo_matvec
|
||||
from ._base import issparse, SparseEfficiencyWarning, _spbase, sparray
|
||||
from ._data import _data_matrix, _minmax_mixin
|
||||
from ._sputils import (upcast_char, to_native, isshape, getdtype,
|
||||
getdata, downcast_intp_index, get_index_dtype,
|
||||
check_shape, check_reshape_kwargs)
|
||||
|
||||
import operator
|
||||
|
||||
|
||||
class _coo_base(_data_matrix, _minmax_mixin):
|
||||
_format = 'coo'
|
||||
|
||||
def __init__(self, arg1, shape=None, dtype=None, copy=False):
|
||||
_data_matrix.__init__(self, arg1)
|
||||
is_array = isinstance(self, sparray)
|
||||
if not copy:
|
||||
copy = copy_if_needed
|
||||
|
||||
if isinstance(arg1, tuple):
|
||||
if isshape(arg1, allow_1d=is_array):
|
||||
self._shape = check_shape(arg1, allow_1d=is_array)
|
||||
idx_dtype = self._get_index_dtype(maxval=max(self._shape))
|
||||
data_dtype = getdtype(dtype, default=float)
|
||||
self.coords = tuple(np.array([], dtype=idx_dtype)
|
||||
for _ in range(len(self._shape)))
|
||||
self.data = np.array([], dtype=data_dtype)
|
||||
self.has_canonical_format = True
|
||||
else:
|
||||
try:
|
||||
obj, coords = arg1
|
||||
except (TypeError, ValueError) as e:
|
||||
raise TypeError('invalid input format') from e
|
||||
|
||||
if shape is None:
|
||||
if any(len(idx) == 0 for idx in coords):
|
||||
raise ValueError('cannot infer dimensions from zero '
|
||||
'sized index arrays')
|
||||
shape = tuple(operator.index(np.max(idx)) + 1
|
||||
for idx in coords)
|
||||
self._shape = check_shape(shape, allow_1d=is_array)
|
||||
|
||||
idx_dtype = self._get_index_dtype(coords,
|
||||
maxval=max(self.shape),
|
||||
check_contents=True)
|
||||
self.coords = tuple(np.array(idx, copy=copy, dtype=idx_dtype)
|
||||
for idx in coords)
|
||||
self.data = getdata(obj, copy=copy, dtype=dtype)
|
||||
self.has_canonical_format = False
|
||||
else:
|
||||
if issparse(arg1):
|
||||
if arg1.format == self.format and copy:
|
||||
self.coords = tuple(idx.copy() for idx in arg1.coords)
|
||||
self.data = arg1.data.copy()
|
||||
self._shape = check_shape(arg1.shape, allow_1d=is_array)
|
||||
self.has_canonical_format = arg1.has_canonical_format
|
||||
else:
|
||||
coo = arg1.tocoo()
|
||||
self.coords = tuple(coo.coords)
|
||||
self.data = coo.data
|
||||
self._shape = check_shape(coo.shape, allow_1d=is_array)
|
||||
self.has_canonical_format = False
|
||||
else:
|
||||
# dense argument
|
||||
M = np.asarray(arg1)
|
||||
if not is_array:
|
||||
M = np.atleast_2d(M)
|
||||
if M.ndim != 2:
|
||||
raise TypeError(f'expected 2D array or matrix, not {M.ndim}D')
|
||||
|
||||
self._shape = check_shape(M.shape, allow_1d=is_array)
|
||||
if shape is not None:
|
||||
if check_shape(shape, allow_1d=is_array) != self._shape:
|
||||
message = f'inconsistent shapes: {shape} != {self._shape}'
|
||||
raise ValueError(message)
|
||||
index_dtype = self._get_index_dtype(maxval=max(self._shape))
|
||||
coords = M.nonzero()
|
||||
self.coords = tuple(idx.astype(index_dtype, copy=False)
|
||||
for idx in coords)
|
||||
self.data = M[coords]
|
||||
self.has_canonical_format = True
|
||||
|
||||
if dtype is not None:
|
||||
self.data = self.data.astype(dtype, copy=False)
|
||||
|
||||
self._check()
|
||||
|
||||
@property
|
||||
def row(self):
|
||||
if self.ndim > 1:
|
||||
return self.coords[-2]
|
||||
result = np.zeros_like(self.col)
|
||||
result.setflags(write=False)
|
||||
return result
|
||||
|
||||
|
||||
@row.setter
|
||||
def row(self, new_row):
|
||||
if self.ndim < 2:
|
||||
raise ValueError('cannot set row attribute of a 1-dimensional sparse array')
|
||||
new_row = np.asarray(new_row, dtype=self.coords[-2].dtype)
|
||||
self.coords = self.coords[:-2] + (new_row,) + self.coords[-1:]
|
||||
|
||||
@property
|
||||
def col(self):
|
||||
return self.coords[-1]
|
||||
|
||||
@col.setter
|
||||
def col(self, new_col):
|
||||
new_col = np.asarray(new_col, dtype=self.coords[-1].dtype)
|
||||
self.coords = self.coords[:-1] + (new_col,)
|
||||
|
||||
def reshape(self, *args, **kwargs):
|
||||
is_array = isinstance(self, sparray)
|
||||
shape = check_shape(args, self.shape, allow_1d=is_array)
|
||||
order, copy = check_reshape_kwargs(kwargs)
|
||||
|
||||
# Return early if reshape is not required
|
||||
if shape == self.shape:
|
||||
if copy:
|
||||
return self.copy()
|
||||
else:
|
||||
return self
|
||||
|
||||
# When reducing the number of dimensions, we need to be careful about
|
||||
# index overflow. This is why we can't simply call
|
||||
# `np.ravel_multi_index()` followed by `np.unravel_index()` here.
|
||||
flat_coords = _ravel_coords(self.coords, self.shape, order=order)
|
||||
if len(shape) == 2:
|
||||
if order == 'C':
|
||||
new_coords = divmod(flat_coords, shape[1])
|
||||
else:
|
||||
new_coords = divmod(flat_coords, shape[0])[::-1]
|
||||
else:
|
||||
new_coords = np.unravel_index(flat_coords, shape, order=order)
|
||||
|
||||
# Handle copy here rather than passing on to the constructor so that no
|
||||
# copy will be made of `new_coords` regardless.
|
||||
if copy:
|
||||
new_data = self.data.copy()
|
||||
else:
|
||||
new_data = self.data
|
||||
|
||||
return self.__class__((new_data, new_coords), shape=shape, copy=False)
|
||||
|
||||
reshape.__doc__ = _spbase.reshape.__doc__
|
||||
|
||||
def _getnnz(self, axis=None):
|
||||
if axis is None or (axis == 0 and self.ndim == 1):
|
||||
nnz = len(self.data)
|
||||
if any(len(idx) != nnz for idx in self.coords):
|
||||
raise ValueError('all index and data arrays must have the '
|
||||
'same length')
|
||||
|
||||
if self.data.ndim != 1 or any(idx.ndim != 1 for idx in self.coords):
|
||||
raise ValueError('row, column, and data arrays must be 1-D')
|
||||
|
||||
return int(nnz)
|
||||
|
||||
if axis < 0:
|
||||
axis += self.ndim
|
||||
if axis >= self.ndim:
|
||||
raise ValueError('axis out of bounds')
|
||||
if self.ndim > 2:
|
||||
raise NotImplementedError('per-axis nnz for COO arrays with >2 '
|
||||
'dimensions is not supported')
|
||||
return np.bincount(downcast_intp_index(self.coords[1 - axis]),
|
||||
minlength=self.shape[1 - axis])
|
||||
|
||||
_getnnz.__doc__ = _spbase._getnnz.__doc__
|
||||
|
||||
def _check(self):
|
||||
""" Checks data structure for consistency """
|
||||
if self.ndim != len(self.coords):
|
||||
raise ValueError('mismatching number of index arrays for shape; '
|
||||
f'got {len(self.coords)}, expected {self.ndim}')
|
||||
|
||||
# index arrays should have integer data types
|
||||
for i, idx in enumerate(self.coords):
|
||||
if idx.dtype.kind != 'i':
|
||||
warn(f'index array {i} has non-integer dtype ({idx.dtype.name})',
|
||||
stacklevel=3)
|
||||
|
||||
idx_dtype = self._get_index_dtype(self.coords, maxval=max(self.shape))
|
||||
self.coords = tuple(np.asarray(idx, dtype=idx_dtype)
|
||||
for idx in self.coords)
|
||||
self.data = to_native(self.data)
|
||||
|
||||
if self.nnz > 0:
|
||||
for i, idx in enumerate(self.coords):
|
||||
if idx.max() >= self.shape[i]:
|
||||
raise ValueError(f'axis {i} index {idx.max()} exceeds '
|
||||
f'matrix dimension {self.shape[i]}')
|
||||
if idx.min() < 0:
|
||||
raise ValueError(f'negative axis {i} index: {idx.min()}')
|
||||
|
||||
def transpose(self, axes=None, copy=False):
|
||||
if axes is None:
|
||||
axes = range(self.ndim)[::-1]
|
||||
elif isinstance(self, sparray):
|
||||
if len(axes) != self.ndim:
|
||||
raise ValueError("axes don't match matrix dimensions")
|
||||
if len(set(axes)) != self.ndim:
|
||||
raise ValueError("repeated axis in transpose")
|
||||
elif axes != (1, 0):
|
||||
raise ValueError("Sparse matrices do not support an 'axes' "
|
||||
"parameter because swapping dimensions is the "
|
||||
"only logical permutation.")
|
||||
|
||||
permuted_shape = tuple(self._shape[i] for i in axes)
|
||||
permuted_coords = tuple(self.coords[i] for i in axes)
|
||||
return self.__class__((self.data, permuted_coords),
|
||||
shape=permuted_shape, copy=copy)
|
||||
|
||||
transpose.__doc__ = _spbase.transpose.__doc__
|
||||
|
||||
def resize(self, *shape) -> None:
|
||||
is_array = isinstance(self, sparray)
|
||||
shape = check_shape(shape, allow_1d=is_array)
|
||||
|
||||
# Check for added dimensions.
|
||||
if len(shape) > self.ndim:
|
||||
flat_coords = _ravel_coords(self.coords, self.shape)
|
||||
max_size = math.prod(shape)
|
||||
self.coords = np.unravel_index(flat_coords[:max_size], shape)
|
||||
self.data = self.data[:max_size]
|
||||
self._shape = shape
|
||||
return
|
||||
|
||||
# Check for removed dimensions.
|
||||
if len(shape) < self.ndim:
|
||||
tmp_shape = (
|
||||
self._shape[:len(shape) - 1] # Original shape without last axis
|
||||
+ (-1,) # Last axis is used to flatten the array
|
||||
+ (1,) * (self.ndim - len(shape)) # Pad with ones
|
||||
)
|
||||
tmp = self.reshape(tmp_shape)
|
||||
self.coords = tmp.coords[:len(shape)]
|
||||
self._shape = tmp.shape[:len(shape)]
|
||||
|
||||
# Handle truncation of existing dimensions.
|
||||
is_truncating = any(old > new for old, new in zip(self.shape, shape))
|
||||
if is_truncating:
|
||||
mask = np.logical_and.reduce([
|
||||
idx < size for idx, size in zip(self.coords, shape)
|
||||
])
|
||||
if not mask.all():
|
||||
self.coords = tuple(idx[mask] for idx in self.coords)
|
||||
self.data = self.data[mask]
|
||||
|
||||
self._shape = shape
|
||||
|
||||
resize.__doc__ = _spbase.resize.__doc__
|
||||
|
||||
def toarray(self, order=None, out=None):
|
||||
B = self._process_toarray_args(order, out)
|
||||
fortran = int(B.flags.f_contiguous)
|
||||
if not fortran and not B.flags.c_contiguous:
|
||||
raise ValueError("Output array must be C or F contiguous")
|
||||
if self.ndim > 2:
|
||||
raise ValueError("Cannot densify higher-rank sparse array")
|
||||
# This handles both 0D and 1D cases correctly regardless of the
|
||||
# original shape.
|
||||
M, N = self._shape_as_2d
|
||||
coo_todense(M, N, self.nnz, self.row, self.col, self.data,
|
||||
B.ravel('A'), fortran)
|
||||
# Note: reshape() doesn't copy here, but does return a new array (view).
|
||||
return B.reshape(self.shape)
|
||||
|
||||
toarray.__doc__ = _spbase.toarray.__doc__
|
||||
|
||||
def tocsc(self, copy=False):
|
||||
"""Convert this array/matrix to Compressed Sparse Column format
|
||||
|
||||
Duplicate entries will be summed together.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from numpy import array
|
||||
>>> from scipy.sparse import coo_array
|
||||
>>> row = array([0, 0, 1, 3, 1, 0, 0])
|
||||
>>> col = array([0, 2, 1, 3, 1, 0, 0])
|
||||
>>> data = array([1, 1, 1, 1, 1, 1, 1])
|
||||
>>> A = coo_array((data, (row, col)), shape=(4, 4)).tocsc()
|
||||
>>> A.toarray()
|
||||
array([[3, 0, 1, 0],
|
||||
[0, 2, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 1]])
|
||||
|
||||
"""
|
||||
if self.ndim != 2:
|
||||
raise ValueError("Cannot convert a 1d sparse array to csc format")
|
||||
if self.nnz == 0:
|
||||
return self._csc_container(self.shape, dtype=self.dtype)
|
||||
else:
|
||||
from ._csc import csc_array
|
||||
indptr, indices, data, shape = self._coo_to_compressed(csc_array._swap)
|
||||
|
||||
x = self._csc_container((data, indices, indptr), shape=shape)
|
||||
if not self.has_canonical_format:
|
||||
x.sum_duplicates()
|
||||
return x
|
||||
|
||||
def tocsr(self, copy=False):
|
||||
"""Convert this array/matrix to Compressed Sparse Row format
|
||||
|
||||
Duplicate entries will be summed together.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from numpy import array
|
||||
>>> from scipy.sparse import coo_array
|
||||
>>> row = array([0, 0, 1, 3, 1, 0, 0])
|
||||
>>> col = array([0, 2, 1, 3, 1, 0, 0])
|
||||
>>> data = array([1, 1, 1, 1, 1, 1, 1])
|
||||
>>> A = coo_array((data, (row, col)), shape=(4, 4)).tocsr()
|
||||
>>> A.toarray()
|
||||
array([[3, 0, 1, 0],
|
||||
[0, 2, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 1]])
|
||||
|
||||
"""
|
||||
if self.nnz == 0:
|
||||
return self._csr_container(self.shape, dtype=self.dtype)
|
||||
else:
|
||||
from ._csr import csr_array
|
||||
arrays = self._coo_to_compressed(csr_array._swap, copy=copy)
|
||||
indptr, indices, data, shape = arrays
|
||||
|
||||
x = self._csr_container((data, indices, indptr), shape=self.shape)
|
||||
if not self.has_canonical_format:
|
||||
x.sum_duplicates()
|
||||
return x
|
||||
|
||||
def _coo_to_compressed(self, swap, copy=False):
|
||||
"""convert (shape, coords, data) to (indptr, indices, data, shape)"""
|
||||
M, N = swap(self._shape_as_2d)
|
||||
# convert idx_dtype intc to int32 for pythran.
|
||||
# tested in scipy/optimize/tests/test__numdiff.py::test_group_columns
|
||||
idx_dtype = self._get_index_dtype(self.coords, maxval=max(self.nnz, N))
|
||||
|
||||
if self.ndim == 1:
|
||||
indices = self.coords[0].copy() if copy else self.coords[0]
|
||||
nnz = len(indices)
|
||||
indptr = np.array([0, nnz], dtype=idx_dtype)
|
||||
data = self.data.copy() if copy else self.data
|
||||
return indptr, indices, data, self.shape
|
||||
|
||||
# ndim == 2
|
||||
major, minor = swap(self.coords)
|
||||
nnz = len(major)
|
||||
major = major.astype(idx_dtype, copy=False)
|
||||
minor = minor.astype(idx_dtype, copy=False)
|
||||
|
||||
indptr = np.empty(M + 1, dtype=idx_dtype)
|
||||
indices = np.empty_like(minor, dtype=idx_dtype)
|
||||
data = np.empty_like(self.data, dtype=self.dtype)
|
||||
|
||||
coo_tocsr(M, N, nnz, major, minor, self.data, indptr, indices, data)
|
||||
return indptr, indices, data, self.shape
|
||||
|
||||
def tocoo(self, copy=False):
|
||||
if copy:
|
||||
return self.copy()
|
||||
else:
|
||||
return self
|
||||
|
||||
tocoo.__doc__ = _spbase.tocoo.__doc__
|
||||
|
||||
def todia(self, copy=False):
|
||||
if self.ndim != 2:
|
||||
raise ValueError("Cannot convert a 1d sparse array to dia format")
|
||||
self.sum_duplicates()
|
||||
ks = self.col - self.row # the diagonal for each nonzero
|
||||
diags, diag_idx = np.unique(ks, return_inverse=True)
|
||||
|
||||
if len(diags) > 100:
|
||||
# probably undesired, should todia() have a maxdiags parameter?
|
||||
warn("Constructing a DIA matrix with %d diagonals "
|
||||
"is inefficient" % len(diags),
|
||||
SparseEfficiencyWarning, stacklevel=2)
|
||||
|
||||
#initialize and fill in data array
|
||||
if self.data.size == 0:
|
||||
data = np.zeros((0, 0), dtype=self.dtype)
|
||||
else:
|
||||
data = np.zeros((len(diags), self.col.max()+1), dtype=self.dtype)
|
||||
data[diag_idx, self.col] = self.data
|
||||
|
||||
return self._dia_container((data, diags), shape=self.shape)
|
||||
|
||||
todia.__doc__ = _spbase.todia.__doc__
|
||||
|
||||
def todok(self, copy=False):
|
||||
self.sum_duplicates()
|
||||
dok = self._dok_container(self.shape, dtype=self.dtype)
|
||||
# ensure that 1d coordinates are not tuples
|
||||
if self.ndim == 1:
|
||||
coords = self.coords[0]
|
||||
else:
|
||||
coords = zip(*self.coords)
|
||||
|
||||
dok._dict = dict(zip(coords, self.data))
|
||||
return dok
|
||||
|
||||
todok.__doc__ = _spbase.todok.__doc__
|
||||
|
||||
def diagonal(self, k=0):
|
||||
if self.ndim != 2:
|
||||
raise ValueError("diagonal requires two dimensions")
|
||||
rows, cols = self.shape
|
||||
if k <= -rows or k >= cols:
|
||||
return np.empty(0, dtype=self.data.dtype)
|
||||
diag = np.zeros(min(rows + min(k, 0), cols - max(k, 0)),
|
||||
dtype=self.dtype)
|
||||
diag_mask = (self.row + k) == self.col
|
||||
|
||||
if self.has_canonical_format:
|
||||
row = self.row[diag_mask]
|
||||
data = self.data[diag_mask]
|
||||
else:
|
||||
inds = tuple(idx[diag_mask] for idx in self.coords)
|
||||
(row, _), data = self._sum_duplicates(inds, self.data[diag_mask])
|
||||
diag[row + min(k, 0)] = data
|
||||
|
||||
return diag
|
||||
|
||||
diagonal.__doc__ = _data_matrix.diagonal.__doc__
|
||||
|
||||
def _setdiag(self, values, k):
|
||||
if self.ndim != 2:
|
||||
raise ValueError("setting a diagonal requires two dimensions")
|
||||
M, N = self.shape
|
||||
if values.ndim and not len(values):
|
||||
return
|
||||
idx_dtype = self.row.dtype
|
||||
|
||||
# Determine which triples to keep and where to put the new ones.
|
||||
full_keep = self.col - self.row != k
|
||||
if k < 0:
|
||||
max_index = min(M+k, N)
|
||||
if values.ndim:
|
||||
max_index = min(max_index, len(values))
|
||||
keep = np.logical_or(full_keep, self.col >= max_index)
|
||||
new_row = np.arange(-k, -k + max_index, dtype=idx_dtype)
|
||||
new_col = np.arange(max_index, dtype=idx_dtype)
|
||||
else:
|
||||
max_index = min(M, N-k)
|
||||
if values.ndim:
|
||||
max_index = min(max_index, len(values))
|
||||
keep = np.logical_or(full_keep, self.row >= max_index)
|
||||
new_row = np.arange(max_index, dtype=idx_dtype)
|
||||
new_col = np.arange(k, k + max_index, dtype=idx_dtype)
|
||||
|
||||
# Define the array of data consisting of the entries to be added.
|
||||
if values.ndim:
|
||||
new_data = values[:max_index]
|
||||
else:
|
||||
new_data = np.empty(max_index, dtype=self.dtype)
|
||||
new_data[:] = values
|
||||
|
||||
# Update the internal structure.
|
||||
self.coords = (np.concatenate((self.row[keep], new_row)),
|
||||
np.concatenate((self.col[keep], new_col)))
|
||||
self.data = np.concatenate((self.data[keep], new_data))
|
||||
self.has_canonical_format = False
|
||||
|
||||
# needed by _data_matrix
|
||||
def _with_data(self, data, copy=True):
|
||||
"""Returns a matrix with the same sparsity structure as self,
|
||||
but with different data. By default the index arrays are copied.
|
||||
"""
|
||||
if copy:
|
||||
coords = tuple(idx.copy() for idx in self.coords)
|
||||
else:
|
||||
coords = self.coords
|
||||
return self.__class__((data, coords), shape=self.shape, dtype=data.dtype)
|
||||
|
||||
def sum_duplicates(self) -> None:
|
||||
"""Eliminate duplicate entries by adding them together
|
||||
|
||||
This is an *in place* operation
|
||||
"""
|
||||
if self.has_canonical_format:
|
||||
return
|
||||
summed = self._sum_duplicates(self.coords, self.data)
|
||||
self.coords, self.data = summed
|
||||
self.has_canonical_format = True
|
||||
|
||||
def _sum_duplicates(self, coords, data):
|
||||
# Assumes coords not in canonical format.
|
||||
if len(data) == 0:
|
||||
return coords, data
|
||||
# Sort coords w.r.t. rows, then cols. This corresponds to C-order,
|
||||
# which we rely on for argmin/argmax to return the first index in the
|
||||
# same way that numpy does (in the case of ties).
|
||||
order = np.lexsort(coords[::-1])
|
||||
coords = tuple(idx[order] for idx in coords)
|
||||
data = data[order]
|
||||
unique_mask = np.logical_or.reduce([
|
||||
idx[1:] != idx[:-1] for idx in coords
|
||||
])
|
||||
unique_mask = np.append(True, unique_mask)
|
||||
coords = tuple(idx[unique_mask] for idx in coords)
|
||||
unique_inds, = np.nonzero(unique_mask)
|
||||
data = np.add.reduceat(data, unique_inds, dtype=self.dtype)
|
||||
return coords, data
|
||||
|
||||
def eliminate_zeros(self):
|
||||
"""Remove zero entries from the array/matrix
|
||||
|
||||
This is an *in place* operation
|
||||
"""
|
||||
mask = self.data != 0
|
||||
self.data = self.data[mask]
|
||||
self.coords = tuple(idx[mask] for idx in self.coords)
|
||||
|
||||
#######################
|
||||
# Arithmetic handlers #
|
||||
#######################
|
||||
|
||||
def _add_dense(self, other):
|
||||
if other.shape != self.shape:
|
||||
raise ValueError(f'Incompatible shapes ({self.shape} and {other.shape})')
|
||||
dtype = upcast_char(self.dtype.char, other.dtype.char)
|
||||
result = np.array(other, dtype=dtype, copy=True)
|
||||
fortran = int(result.flags.f_contiguous)
|
||||
M, N = self._shape_as_2d
|
||||
coo_todense(M, N, self.nnz, self.row, self.col, self.data,
|
||||
result.ravel('A'), fortran)
|
||||
return self._container(result, copy=False)
|
||||
|
||||
def _matmul_vector(self, other):
|
||||
result_shape = self.shape[0] if self.ndim > 1 else 1
|
||||
result = np.zeros(result_shape,
|
||||
dtype=upcast_char(self.dtype.char, other.dtype.char))
|
||||
|
||||
if self.ndim == 2:
|
||||
col = self.col
|
||||
row = self.row
|
||||
elif self.ndim == 1:
|
||||
col = self.coords[0]
|
||||
row = np.zeros_like(col)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"coo_matvec not implemented for ndim={self.ndim}")
|
||||
|
||||
coo_matvec(self.nnz, row, col, self.data, other, result)
|
||||
# Array semantics return a scalar here, not a single-element array.
|
||||
if isinstance(self, sparray) and result_shape == 1:
|
||||
return result[0]
|
||||
return result
|
||||
|
||||
def _matmul_multivector(self, other):
|
||||
result_dtype = upcast_char(self.dtype.char, other.dtype.char)
|
||||
if self.ndim == 2:
|
||||
result_shape = (other.shape[1], self.shape[0])
|
||||
col = self.col
|
||||
row = self.row
|
||||
elif self.ndim == 1:
|
||||
result_shape = (other.shape[1],)
|
||||
col = self.coords[0]
|
||||
row = np.zeros_like(col)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"coo_matvec not implemented for ndim={self.ndim}")
|
||||
|
||||
result = np.zeros(result_shape, dtype=result_dtype)
|
||||
for i, other_col in enumerate(other.T):
|
||||
coo_matvec(self.nnz, row, col, self.data, other_col, result[i:i + 1])
|
||||
return result.T.view(type=type(other))
|
||||
|
||||
|
||||
def _ravel_coords(coords, shape, order='C'):
|
||||
"""Like np.ravel_multi_index, but avoids some overflow issues."""
|
||||
if len(coords) == 1:
|
||||
return coords[0]
|
||||
# Handle overflow as in https://github.com/scipy/scipy/pull/9132
|
||||
if len(coords) == 2:
|
||||
nrows, ncols = shape
|
||||
row, col = coords
|
||||
if order == 'C':
|
||||
maxval = (ncols * max(0, nrows - 1) + max(0, ncols - 1))
|
||||
idx_dtype = get_index_dtype(maxval=maxval)
|
||||
return np.multiply(ncols, row, dtype=idx_dtype) + col
|
||||
elif order == 'F':
|
||||
maxval = (nrows * max(0, ncols - 1) + max(0, nrows - 1))
|
||||
idx_dtype = get_index_dtype(maxval=maxval)
|
||||
return np.multiply(nrows, col, dtype=idx_dtype) + row
|
||||
else:
|
||||
raise ValueError("'order' must be 'C' or 'F'")
|
||||
return np.ravel_multi_index(coords, shape, order=order)
|
||||
|
||||
|
||||
def isspmatrix_coo(x):
|
||||
"""Is `x` of coo_matrix type?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x
|
||||
object to check for being a coo matrix
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if `x` is a coo matrix, False otherwise
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy.sparse import coo_array, coo_matrix, csr_matrix, isspmatrix_coo
|
||||
>>> isspmatrix_coo(coo_matrix([[5]]))
|
||||
True
|
||||
>>> isspmatrix_coo(coo_array([[5]]))
|
||||
False
|
||||
>>> isspmatrix_coo(csr_matrix([[5]]))
|
||||
False
|
||||
"""
|
||||
return isinstance(x, coo_matrix)
|
||||
|
||||
|
||||
# This namespace class separates array from matrix with isinstance
|
||||
class coo_array(_coo_base, sparray):
|
||||
"""
|
||||
A sparse array in COOrdinate format.
|
||||
|
||||
Also known as the 'ijv' or 'triplet' format.
|
||||
|
||||
This can be instantiated in several ways:
|
||||
coo_array(D)
|
||||
where D is an ndarray
|
||||
|
||||
coo_array(S)
|
||||
with another sparse array or matrix S (equivalent to S.tocoo())
|
||||
|
||||
coo_array(shape, [dtype])
|
||||
to construct an empty sparse array with shape `shape`
|
||||
dtype is optional, defaulting to dtype='d'.
|
||||
|
||||
coo_array((data, coords), [shape])
|
||||
to construct from existing data and index arrays:
|
||||
1. data[:] the entries of the sparse array, in any order
|
||||
2. coords[i][:] the axis-i coordinates of the data entries
|
||||
|
||||
Where ``A[coords] = data``, and coords is a tuple of index arrays.
|
||||
When shape is not specified, it is inferred from the index arrays.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dtype : dtype
|
||||
Data type of the sparse array
|
||||
shape : tuple of integers
|
||||
Shape of the sparse array
|
||||
ndim : int
|
||||
Number of dimensions of the sparse array
|
||||
nnz
|
||||
size
|
||||
data
|
||||
COO format data array of the sparse array
|
||||
coords
|
||||
COO format tuple of index arrays
|
||||
has_canonical_format : bool
|
||||
Whether the matrix has sorted coordinates and no duplicates
|
||||
format
|
||||
T
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
Sparse arrays can be used in arithmetic operations: they support
|
||||
addition, subtraction, multiplication, division, and matrix power.
|
||||
|
||||
Advantages of the COO format
|
||||
- facilitates fast conversion among sparse formats
|
||||
- permits duplicate entries (see example)
|
||||
- very fast conversion to and from CSR/CSC formats
|
||||
|
||||
Disadvantages of the COO format
|
||||
- does not directly support:
|
||||
+ arithmetic operations
|
||||
+ slicing
|
||||
|
||||
Intended Usage
|
||||
- COO is a fast format for constructing sparse arrays
|
||||
- Once a COO array has been constructed, convert to CSR or
|
||||
CSC format for fast arithmetic and matrix vector operations
|
||||
- By default when converting to CSR or CSC format, duplicate (i,j)
|
||||
entries will be summed together. This facilitates efficient
|
||||
construction of finite element matrices and the like. (see example)
|
||||
|
||||
Canonical format
|
||||
- Entries and coordinates sorted by row, then column.
|
||||
- There are no duplicate entries (i.e. duplicate (i,j) locations)
|
||||
- Data arrays MAY have explicit zeros.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> # Constructing an empty sparse array
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import coo_array
|
||||
>>> coo_array((3, 4), dtype=np.int8).toarray()
|
||||
array([[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0]], dtype=int8)
|
||||
|
||||
>>> # Constructing a sparse array using ijv format
|
||||
>>> row = np.array([0, 3, 1, 0])
|
||||
>>> col = np.array([0, 3, 1, 2])
|
||||
>>> data = np.array([4, 5, 7, 9])
|
||||
>>> coo_array((data, (row, col)), shape=(4, 4)).toarray()
|
||||
array([[4, 0, 9, 0],
|
||||
[0, 7, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 5]])
|
||||
|
||||
>>> # Constructing a sparse array with duplicate coordinates
|
||||
>>> row = np.array([0, 0, 1, 3, 1, 0, 0])
|
||||
>>> col = np.array([0, 2, 1, 3, 1, 0, 0])
|
||||
>>> data = np.array([1, 1, 1, 1, 1, 1, 1])
|
||||
>>> coo = coo_array((data, (row, col)), shape=(4, 4))
|
||||
>>> # Duplicate coordinates are maintained until implicitly or explicitly summed
|
||||
>>> np.max(coo.data)
|
||||
1
|
||||
>>> coo.toarray()
|
||||
array([[3, 0, 1, 0],
|
||||
[0, 2, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 1]])
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class coo_matrix(spmatrix, _coo_base):
|
||||
"""
|
||||
A sparse matrix in COOrdinate format.
|
||||
|
||||
Also known as the 'ijv' or 'triplet' format.
|
||||
|
||||
This can be instantiated in several ways:
|
||||
coo_matrix(D)
|
||||
where D is a 2-D ndarray
|
||||
|
||||
coo_matrix(S)
|
||||
with another sparse array or matrix S (equivalent to S.tocoo())
|
||||
|
||||
coo_matrix((M, N), [dtype])
|
||||
to construct an empty matrix with shape (M, N)
|
||||
dtype is optional, defaulting to dtype='d'.
|
||||
|
||||
coo_matrix((data, (i, j)), [shape=(M, N)])
|
||||
to construct from three arrays:
|
||||
1. data[:] the entries of the matrix, in any order
|
||||
2. i[:] the row indices of the matrix entries
|
||||
3. j[:] the column indices of the matrix entries
|
||||
|
||||
Where ``A[i[k], j[k]] = data[k]``. When shape is not
|
||||
specified, it is inferred from the index arrays
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dtype : dtype
|
||||
Data type of the matrix
|
||||
shape : 2-tuple
|
||||
Shape of the matrix
|
||||
ndim : int
|
||||
Number of dimensions (this is always 2)
|
||||
nnz
|
||||
size
|
||||
data
|
||||
COO format data array of the matrix
|
||||
row
|
||||
COO format row index array of the matrix
|
||||
col
|
||||
COO format column index array of the matrix
|
||||
has_canonical_format : bool
|
||||
Whether the matrix has sorted indices and no duplicates
|
||||
format
|
||||
T
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
Sparse matrices can be used in arithmetic operations: they support
|
||||
addition, subtraction, multiplication, division, and matrix power.
|
||||
|
||||
Advantages of the COO format
|
||||
- facilitates fast conversion among sparse formats
|
||||
- permits duplicate entries (see example)
|
||||
- very fast conversion to and from CSR/CSC formats
|
||||
|
||||
Disadvantages of the COO format
|
||||
- does not directly support:
|
||||
+ arithmetic operations
|
||||
+ slicing
|
||||
|
||||
Intended Usage
|
||||
- COO is a fast format for constructing sparse matrices
|
||||
- Once a COO matrix has been constructed, convert to CSR or
|
||||
CSC format for fast arithmetic and matrix vector operations
|
||||
- By default when converting to CSR or CSC format, duplicate (i,j)
|
||||
entries will be summed together. This facilitates efficient
|
||||
construction of finite element matrices and the like. (see example)
|
||||
|
||||
Canonical format
|
||||
- Entries and coordinates sorted by row, then column.
|
||||
- There are no duplicate entries (i.e. duplicate (i,j) locations)
|
||||
- Data arrays MAY have explicit zeros.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> # Constructing an empty matrix
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import coo_matrix
|
||||
>>> coo_matrix((3, 4), dtype=np.int8).toarray()
|
||||
array([[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0]], dtype=int8)
|
||||
|
||||
>>> # Constructing a matrix using ijv format
|
||||
>>> row = np.array([0, 3, 1, 0])
|
||||
>>> col = np.array([0, 3, 1, 2])
|
||||
>>> data = np.array([4, 5, 7, 9])
|
||||
>>> coo_matrix((data, (row, col)), shape=(4, 4)).toarray()
|
||||
array([[4, 0, 9, 0],
|
||||
[0, 7, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 5]])
|
||||
|
||||
>>> # Constructing a matrix with duplicate coordinates
|
||||
>>> row = np.array([0, 0, 1, 3, 1, 0, 0])
|
||||
>>> col = np.array([0, 2, 1, 3, 1, 0, 0])
|
||||
>>> data = np.array([1, 1, 1, 1, 1, 1, 1])
|
||||
>>> coo = coo_matrix((data, (row, col)), shape=(4, 4))
|
||||
>>> # Duplicate coordinates are maintained until implicitly or explicitly summed
|
||||
>>> np.max(coo.data)
|
||||
1
|
||||
>>> coo.toarray()
|
||||
array([[3, 0, 1, 0],
|
||||
[0, 2, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 1]])
|
||||
|
||||
"""
|
||||
|
||||
def __setstate__(self, state):
|
||||
if 'coords' not in state:
|
||||
# For retro-compatibility with the previous attributes
|
||||
# storing nnz coordinates for 2D COO matrix.
|
||||
state['coords'] = (state.pop('row'), state.pop('col'))
|
||||
self.__dict__.update(state)
|
||||
364
venv/lib/python3.12/site-packages/scipy/sparse/_csc.py
Normal file
364
venv/lib/python3.12/site-packages/scipy/sparse/_csc.py
Normal file
@ -0,0 +1,364 @@
|
||||
"""Compressed Sparse Column matrix format"""
|
||||
__docformat__ = "restructuredtext en"
|
||||
|
||||
__all__ = ['csc_array', 'csc_matrix', 'isspmatrix_csc']
|
||||
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ._matrix import spmatrix
|
||||
from ._base import _spbase, sparray
|
||||
from ._sparsetools import csc_tocsr, expandptr
|
||||
from ._sputils import upcast
|
||||
|
||||
from ._compressed import _cs_matrix
|
||||
|
||||
|
||||
class _csc_base(_cs_matrix):
|
||||
_format = 'csc'
|
||||
|
||||
def transpose(self, axes=None, copy=False):
|
||||
if axes is not None and axes != (1, 0):
|
||||
raise ValueError("Sparse arrays/matrices do not support "
|
||||
"an 'axes' parameter because swapping "
|
||||
"dimensions is the only logical permutation.")
|
||||
|
||||
M, N = self.shape
|
||||
|
||||
return self._csr_container((self.data, self.indices,
|
||||
self.indptr), (N, M), copy=copy)
|
||||
|
||||
transpose.__doc__ = _spbase.transpose.__doc__
|
||||
|
||||
def __iter__(self):
|
||||
yield from self.tocsr()
|
||||
|
||||
def tocsc(self, copy=False):
|
||||
if copy:
|
||||
return self.copy()
|
||||
else:
|
||||
return self
|
||||
|
||||
tocsc.__doc__ = _spbase.tocsc.__doc__
|
||||
|
||||
def tocsr(self, copy=False):
|
||||
M,N = self.shape
|
||||
idx_dtype = self._get_index_dtype((self.indptr, self.indices),
|
||||
maxval=max(self.nnz, N))
|
||||
indptr = np.empty(M + 1, dtype=idx_dtype)
|
||||
indices = np.empty(self.nnz, dtype=idx_dtype)
|
||||
data = np.empty(self.nnz, dtype=upcast(self.dtype))
|
||||
|
||||
csc_tocsr(M, N,
|
||||
self.indptr.astype(idx_dtype),
|
||||
self.indices.astype(idx_dtype),
|
||||
self.data,
|
||||
indptr,
|
||||
indices,
|
||||
data)
|
||||
|
||||
A = self._csr_container(
|
||||
(data, indices, indptr),
|
||||
shape=self.shape, copy=False
|
||||
)
|
||||
A.has_sorted_indices = True
|
||||
return A
|
||||
|
||||
tocsr.__doc__ = _spbase.tocsr.__doc__
|
||||
|
||||
def nonzero(self):
|
||||
# CSC can't use _cs_matrix's .nonzero method because it
|
||||
# returns the indices sorted for self transposed.
|
||||
|
||||
# Get row and col indices, from _cs_matrix.tocoo
|
||||
major_dim, minor_dim = self._swap(self.shape)
|
||||
minor_indices = self.indices
|
||||
major_indices = np.empty(len(minor_indices), dtype=self.indices.dtype)
|
||||
expandptr(major_dim, self.indptr, major_indices)
|
||||
row, col = self._swap((major_indices, minor_indices))
|
||||
|
||||
# Remove explicit zeros
|
||||
nz_mask = self.data != 0
|
||||
row = row[nz_mask]
|
||||
col = col[nz_mask]
|
||||
|
||||
# Sort them to be in C-style order
|
||||
ind = np.argsort(row, kind='mergesort')
|
||||
row = row[ind]
|
||||
col = col[ind]
|
||||
|
||||
return row, col
|
||||
|
||||
nonzero.__doc__ = _cs_matrix.nonzero.__doc__
|
||||
|
||||
def _getrow(self, i):
|
||||
"""Returns a copy of row i of the matrix, as a (1 x n)
|
||||
CSR matrix (row vector).
|
||||
"""
|
||||
M, N = self.shape
|
||||
i = int(i)
|
||||
if i < 0:
|
||||
i += M
|
||||
if i < 0 or i >= M:
|
||||
raise IndexError('index (%d) out of range' % i)
|
||||
return self._get_submatrix(minor=i).tocsr()
|
||||
|
||||
def _getcol(self, i):
|
||||
"""Returns a copy of column i of the matrix, as a (m x 1)
|
||||
CSC matrix (column vector).
|
||||
"""
|
||||
M, N = self.shape
|
||||
i = int(i)
|
||||
if i < 0:
|
||||
i += N
|
||||
if i < 0 or i >= N:
|
||||
raise IndexError('index (%d) out of range' % i)
|
||||
return self._get_submatrix(major=i, copy=True)
|
||||
|
||||
def _get_intXarray(self, row, col):
|
||||
return self._major_index_fancy(col)._get_submatrix(minor=row)
|
||||
|
||||
def _get_intXslice(self, row, col):
|
||||
if col.step in (1, None):
|
||||
return self._get_submatrix(major=col, minor=row, copy=True)
|
||||
return self._major_slice(col)._get_submatrix(minor=row)
|
||||
|
||||
def _get_sliceXint(self, row, col):
|
||||
if row.step in (1, None):
|
||||
return self._get_submatrix(major=col, minor=row, copy=True)
|
||||
return self._get_submatrix(major=col)._minor_slice(row)
|
||||
|
||||
def _get_sliceXarray(self, row, col):
|
||||
return self._major_index_fancy(col)._minor_slice(row)
|
||||
|
||||
def _get_arrayXint(self, row, col):
|
||||
return self._get_submatrix(major=col)._minor_index_fancy(row)
|
||||
|
||||
def _get_arrayXslice(self, row, col):
|
||||
return self._major_slice(col)._minor_index_fancy(row)
|
||||
|
||||
# these functions are used by the parent class (_cs_matrix)
|
||||
# to remove redundancy between csc_array and csr_matrix
|
||||
@staticmethod
|
||||
def _swap(x):
|
||||
"""swap the members of x if this is a column-oriented matrix
|
||||
"""
|
||||
return x[1], x[0]
|
||||
|
||||
|
||||
def isspmatrix_csc(x):
|
||||
"""Is `x` of csc_matrix type?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x
|
||||
object to check for being a csc matrix
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if `x` is a csc matrix, False otherwise
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy.sparse import csc_array, csc_matrix, coo_matrix, isspmatrix_csc
|
||||
>>> isspmatrix_csc(csc_matrix([[5]]))
|
||||
True
|
||||
>>> isspmatrix_csc(csc_array([[5]]))
|
||||
False
|
||||
>>> isspmatrix_csc(coo_matrix([[5]]))
|
||||
False
|
||||
"""
|
||||
return isinstance(x, csc_matrix)
|
||||
|
||||
|
||||
# This namespace class separates array from matrix with isinstance
|
||||
class csc_array(_csc_base, sparray):
|
||||
"""
|
||||
Compressed Sparse Column array.
|
||||
|
||||
This can be instantiated in several ways:
|
||||
csc_array(D)
|
||||
where D is a 2-D ndarray
|
||||
|
||||
csc_array(S)
|
||||
with another sparse array or matrix S (equivalent to S.tocsc())
|
||||
|
||||
csc_array((M, N), [dtype])
|
||||
to construct an empty array with shape (M, N)
|
||||
dtype is optional, defaulting to dtype='d'.
|
||||
|
||||
csc_array((data, (row_ind, col_ind)), [shape=(M, N)])
|
||||
where ``data``, ``row_ind`` and ``col_ind`` satisfy the
|
||||
relationship ``a[row_ind[k], col_ind[k]] = data[k]``.
|
||||
|
||||
csc_array((data, indices, indptr), [shape=(M, N)])
|
||||
is the standard CSC representation where the row indices for
|
||||
column i are stored in ``indices[indptr[i]:indptr[i+1]]``
|
||||
and their corresponding values are stored in
|
||||
``data[indptr[i]:indptr[i+1]]``. If the shape parameter is
|
||||
not supplied, the array dimensions are inferred from
|
||||
the index arrays.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dtype : dtype
|
||||
Data type of the array
|
||||
shape : 2-tuple
|
||||
Shape of the array
|
||||
ndim : int
|
||||
Number of dimensions (this is always 2)
|
||||
nnz
|
||||
size
|
||||
data
|
||||
CSC format data array of the array
|
||||
indices
|
||||
CSC format index array of the array
|
||||
indptr
|
||||
CSC format index pointer array of the array
|
||||
has_sorted_indices
|
||||
has_canonical_format
|
||||
T
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
Sparse arrays can be used in arithmetic operations: they support
|
||||
addition, subtraction, multiplication, division, and matrix power.
|
||||
|
||||
Advantages of the CSC format
|
||||
- efficient arithmetic operations CSC + CSC, CSC * CSC, etc.
|
||||
- efficient column slicing
|
||||
- fast matrix vector products (CSR, BSR may be faster)
|
||||
|
||||
Disadvantages of the CSC format
|
||||
- slow row slicing operations (consider CSR)
|
||||
- changes to the sparsity structure are expensive (consider LIL or DOK)
|
||||
|
||||
Canonical format
|
||||
- Within each column, indices are sorted by row.
|
||||
- There are no duplicate entries.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csc_array
|
||||
>>> csc_array((3, 4), dtype=np.int8).toarray()
|
||||
array([[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0]], dtype=int8)
|
||||
|
||||
>>> row = np.array([0, 2, 2, 0, 1, 2])
|
||||
>>> col = np.array([0, 0, 1, 2, 2, 2])
|
||||
>>> data = np.array([1, 2, 3, 4, 5, 6])
|
||||
>>> csc_array((data, (row, col)), shape=(3, 3)).toarray()
|
||||
array([[1, 0, 4],
|
||||
[0, 0, 5],
|
||||
[2, 3, 6]])
|
||||
|
||||
>>> indptr = np.array([0, 2, 3, 6])
|
||||
>>> indices = np.array([0, 2, 2, 0, 1, 2])
|
||||
>>> data = np.array([1, 2, 3, 4, 5, 6])
|
||||
>>> csc_array((data, indices, indptr), shape=(3, 3)).toarray()
|
||||
array([[1, 0, 4],
|
||||
[0, 0, 5],
|
||||
[2, 3, 6]])
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class csc_matrix(spmatrix, _csc_base):
|
||||
"""
|
||||
Compressed Sparse Column matrix.
|
||||
|
||||
This can be instantiated in several ways:
|
||||
csc_matrix(D)
|
||||
where D is a 2-D ndarray
|
||||
|
||||
csc_matrix(S)
|
||||
with another sparse array or matrix S (equivalent to S.tocsc())
|
||||
|
||||
csc_matrix((M, N), [dtype])
|
||||
to construct an empty matrix with shape (M, N)
|
||||
dtype is optional, defaulting to dtype='d'.
|
||||
|
||||
csc_matrix((data, (row_ind, col_ind)), [shape=(M, N)])
|
||||
where ``data``, ``row_ind`` and ``col_ind`` satisfy the
|
||||
relationship ``a[row_ind[k], col_ind[k]] = data[k]``.
|
||||
|
||||
csc_matrix((data, indices, indptr), [shape=(M, N)])
|
||||
is the standard CSC representation where the row indices for
|
||||
column i are stored in ``indices[indptr[i]:indptr[i+1]]``
|
||||
and their corresponding values are stored in
|
||||
``data[indptr[i]:indptr[i+1]]``. If the shape parameter is
|
||||
not supplied, the matrix dimensions are inferred from
|
||||
the index arrays.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dtype : dtype
|
||||
Data type of the matrix
|
||||
shape : 2-tuple
|
||||
Shape of the matrix
|
||||
ndim : int
|
||||
Number of dimensions (this is always 2)
|
||||
nnz
|
||||
size
|
||||
data
|
||||
CSC format data array of the matrix
|
||||
indices
|
||||
CSC format index array of the matrix
|
||||
indptr
|
||||
CSC format index pointer array of the matrix
|
||||
has_sorted_indices
|
||||
has_canonical_format
|
||||
T
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
Sparse matrices can be used in arithmetic operations: they support
|
||||
addition, subtraction, multiplication, division, and matrix power.
|
||||
|
||||
Advantages of the CSC format
|
||||
- efficient arithmetic operations CSC + CSC, CSC * CSC, etc.
|
||||
- efficient column slicing
|
||||
- fast matrix vector products (CSR, BSR may be faster)
|
||||
|
||||
Disadvantages of the CSC format
|
||||
- slow row slicing operations (consider CSR)
|
||||
- changes to the sparsity structure are expensive (consider LIL or DOK)
|
||||
|
||||
Canonical format
|
||||
- Within each column, indices are sorted by row.
|
||||
- There are no duplicate entries.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> csc_matrix((3, 4), dtype=np.int8).toarray()
|
||||
array([[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0]], dtype=int8)
|
||||
|
||||
>>> row = np.array([0, 2, 2, 0, 1, 2])
|
||||
>>> col = np.array([0, 0, 1, 2, 2, 2])
|
||||
>>> data = np.array([1, 2, 3, 4, 5, 6])
|
||||
>>> csc_matrix((data, (row, col)), shape=(3, 3)).toarray()
|
||||
array([[1, 0, 4],
|
||||
[0, 0, 5],
|
||||
[2, 3, 6]])
|
||||
|
||||
>>> indptr = np.array([0, 2, 3, 6])
|
||||
>>> indices = np.array([0, 2, 2, 0, 1, 2])
|
||||
>>> data = np.array([1, 2, 3, 4, 5, 6])
|
||||
>>> csc_matrix((data, indices, indptr), shape=(3, 3)).toarray()
|
||||
array([[1, 0, 4],
|
||||
[0, 0, 5],
|
||||
[2, 3, 6]])
|
||||
|
||||
"""
|
||||
|
||||
Binary file not shown.
551
venv/lib/python3.12/site-packages/scipy/sparse/_csr.py
Normal file
551
venv/lib/python3.12/site-packages/scipy/sparse/_csr.py
Normal file
@ -0,0 +1,551 @@
|
||||
"""Compressed Sparse Row matrix format"""
|
||||
|
||||
__docformat__ = "restructuredtext en"
|
||||
|
||||
__all__ = ['csr_array', 'csr_matrix', 'isspmatrix_csr']
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ._matrix import spmatrix
|
||||
from ._base import _spbase, sparray
|
||||
from ._sparsetools import (csr_tocsc, csr_tobsr, csr_count_blocks,
|
||||
get_csr_submatrix)
|
||||
from ._sputils import upcast
|
||||
|
||||
from ._compressed import _cs_matrix
|
||||
|
||||
|
||||
class _csr_base(_cs_matrix):
|
||||
_format = 'csr'
|
||||
|
||||
# override IndexMixin.__getitem__ for 1d case until fully implemented
|
||||
def __getitem__(self, key):
|
||||
if self.ndim == 2:
|
||||
return super().__getitem__(key)
|
||||
|
||||
if isinstance(key, tuple) and len(key) == 1:
|
||||
key = key[0]
|
||||
INT_TYPES = (int, np.integer)
|
||||
if isinstance(key, INT_TYPES):
|
||||
if key < 0:
|
||||
key += self.shape[-1]
|
||||
if key < 0 or key >= self.shape[-1]:
|
||||
raise IndexError('index value out of bounds')
|
||||
return self._get_int(key)
|
||||
else:
|
||||
raise IndexError('array/slice index for 1d csr_array not yet supported')
|
||||
|
||||
# override IndexMixin.__setitem__ for 1d case until fully implemented
|
||||
def __setitem__(self, key, value):
|
||||
if self.ndim == 2:
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
if isinstance(key, tuple) and len(key) == 1:
|
||||
key = key[0]
|
||||
INT_TYPES = (int, np.integer)
|
||||
if isinstance(key, INT_TYPES):
|
||||
if key < 0:
|
||||
key += self.shape[-1]
|
||||
if key < 0 or key >= self.shape[-1]:
|
||||
raise IndexError('index value out of bounds')
|
||||
return self._set_int(key, value)
|
||||
else:
|
||||
raise IndexError('array index for 1d csr_array not yet provided')
|
||||
|
||||
def transpose(self, axes=None, copy=False):
|
||||
if axes is not None and axes != (1, 0):
|
||||
raise ValueError("Sparse arrays/matrices do not support "
|
||||
"an 'axes' parameter because swapping "
|
||||
"dimensions is the only logical permutation.")
|
||||
|
||||
if self.ndim == 1:
|
||||
return self.copy() if copy else self
|
||||
M, N = self.shape
|
||||
return self._csc_container((self.data, self.indices,
|
||||
self.indptr), shape=(N, M), copy=copy)
|
||||
|
||||
transpose.__doc__ = _spbase.transpose.__doc__
|
||||
|
||||
def tolil(self, copy=False):
|
||||
if self.ndim != 2:
|
||||
raise ValueError("Cannot convert a 1d sparse array to lil format")
|
||||
lil = self._lil_container(self.shape, dtype=self.dtype)
|
||||
|
||||
self.sum_duplicates()
|
||||
ptr,ind,dat = self.indptr,self.indices,self.data
|
||||
rows, data = lil.rows, lil.data
|
||||
|
||||
for n in range(self.shape[0]):
|
||||
start = ptr[n]
|
||||
end = ptr[n+1]
|
||||
rows[n] = ind[start:end].tolist()
|
||||
data[n] = dat[start:end].tolist()
|
||||
|
||||
return lil
|
||||
|
||||
tolil.__doc__ = _spbase.tolil.__doc__
|
||||
|
||||
def tocsr(self, copy=False):
|
||||
if copy:
|
||||
return self.copy()
|
||||
else:
|
||||
return self
|
||||
|
||||
tocsr.__doc__ = _spbase.tocsr.__doc__
|
||||
|
||||
def tocsc(self, copy=False):
|
||||
if self.ndim != 2:
|
||||
raise ValueError("Cannot convert a 1d sparse array to csc format")
|
||||
M, N = self.shape
|
||||
idx_dtype = self._get_index_dtype((self.indptr, self.indices),
|
||||
maxval=max(self.nnz, M))
|
||||
indptr = np.empty(N + 1, dtype=idx_dtype)
|
||||
indices = np.empty(self.nnz, dtype=idx_dtype)
|
||||
data = np.empty(self.nnz, dtype=upcast(self.dtype))
|
||||
|
||||
csr_tocsc(M, N,
|
||||
self.indptr.astype(idx_dtype),
|
||||
self.indices.astype(idx_dtype),
|
||||
self.data,
|
||||
indptr,
|
||||
indices,
|
||||
data)
|
||||
|
||||
A = self._csc_container((data, indices, indptr), shape=self.shape)
|
||||
A.has_sorted_indices = True
|
||||
return A
|
||||
|
||||
tocsc.__doc__ = _spbase.tocsc.__doc__
|
||||
|
||||
def tobsr(self, blocksize=None, copy=True):
|
||||
if self.ndim != 2:
|
||||
raise ValueError("Cannot convert a 1d sparse array to bsr format")
|
||||
if blocksize is None:
|
||||
from ._spfuncs import estimate_blocksize
|
||||
return self.tobsr(blocksize=estimate_blocksize(self))
|
||||
|
||||
elif blocksize == (1,1):
|
||||
arg1 = (self.data.reshape(-1,1,1),self.indices,self.indptr)
|
||||
return self._bsr_container(arg1, shape=self.shape, copy=copy)
|
||||
|
||||
else:
|
||||
R,C = blocksize
|
||||
M,N = self.shape
|
||||
|
||||
if R < 1 or C < 1 or M % R != 0 or N % C != 0:
|
||||
raise ValueError('invalid blocksize %s' % blocksize)
|
||||
|
||||
blks = csr_count_blocks(M,N,R,C,self.indptr,self.indices)
|
||||
|
||||
idx_dtype = self._get_index_dtype((self.indptr, self.indices),
|
||||
maxval=max(N//C, blks))
|
||||
indptr = np.empty(M//R+1, dtype=idx_dtype)
|
||||
indices = np.empty(blks, dtype=idx_dtype)
|
||||
data = np.zeros((blks,R,C), dtype=self.dtype)
|
||||
|
||||
csr_tobsr(M, N, R, C,
|
||||
self.indptr.astype(idx_dtype),
|
||||
self.indices.astype(idx_dtype),
|
||||
self.data,
|
||||
indptr, indices, data.ravel())
|
||||
|
||||
return self._bsr_container(
|
||||
(data, indices, indptr), shape=self.shape
|
||||
)
|
||||
|
||||
tobsr.__doc__ = _spbase.tobsr.__doc__
|
||||
|
||||
# these functions are used by the parent class (_cs_matrix)
|
||||
# to remove redundancy between csc_matrix and csr_array
|
||||
@staticmethod
|
||||
def _swap(x):
|
||||
"""swap the members of x if this is a column-oriented matrix
|
||||
"""
|
||||
return x
|
||||
|
||||
def __iter__(self):
|
||||
if self.ndim == 1:
|
||||
zero = self.dtype.type(0)
|
||||
u = 0
|
||||
for v, d in zip(self.indices, self.data):
|
||||
for _ in range(v - u):
|
||||
yield zero
|
||||
yield d
|
||||
u = v + 1
|
||||
for _ in range(self.shape[0] - u):
|
||||
yield zero
|
||||
return
|
||||
|
||||
indptr = np.zeros(2, dtype=self.indptr.dtype)
|
||||
# return 1d (sparray) or 2drow (spmatrix)
|
||||
shape = self.shape[1:] if isinstance(self, sparray) else (1, self.shape[1])
|
||||
i0 = 0
|
||||
for i1 in self.indptr[1:]:
|
||||
indptr[1] = i1 - i0
|
||||
indices = self.indices[i0:i1]
|
||||
data = self.data[i0:i1]
|
||||
yield self.__class__((data, indices, indptr), shape=shape, copy=True)
|
||||
i0 = i1
|
||||
|
||||
def _getrow(self, i):
|
||||
"""Returns a copy of row i of the matrix, as a (1 x n)
|
||||
CSR matrix (row vector).
|
||||
"""
|
||||
if self.ndim == 1:
|
||||
if i not in (0, -1):
|
||||
raise IndexError(f'index ({i}) out of range')
|
||||
return self.reshape((1, self.shape[0]), copy=True)
|
||||
|
||||
M, N = self.shape
|
||||
i = int(i)
|
||||
if i < 0:
|
||||
i += M
|
||||
if i < 0 or i >= M:
|
||||
raise IndexError('index (%d) out of range' % i)
|
||||
indptr, indices, data = get_csr_submatrix(
|
||||
M, N, self.indptr, self.indices, self.data, i, i + 1, 0, N)
|
||||
return self.__class__((data, indices, indptr), shape=(1, N),
|
||||
dtype=self.dtype, copy=False)
|
||||
|
||||
def _getcol(self, i):
|
||||
"""Returns a copy of column i. A (m x 1) sparse array (column vector).
|
||||
"""
|
||||
if self.ndim == 1:
|
||||
raise ValueError("getcol not provided for 1d arrays. Use indexing A[j]")
|
||||
M, N = self.shape
|
||||
i = int(i)
|
||||
if i < 0:
|
||||
i += N
|
||||
if i < 0 or i >= N:
|
||||
raise IndexError('index (%d) out of range' % i)
|
||||
indptr, indices, data = get_csr_submatrix(
|
||||
M, N, self.indptr, self.indices, self.data, 0, M, i, i + 1)
|
||||
return self.__class__((data, indices, indptr), shape=(M, 1),
|
||||
dtype=self.dtype, copy=False)
|
||||
|
||||
def _get_intXarray(self, row, col):
|
||||
return self._getrow(row)._minor_index_fancy(col)
|
||||
|
||||
def _get_intXslice(self, row, col):
|
||||
if col.step in (1, None):
|
||||
return self._get_submatrix(row, col, copy=True)
|
||||
# TODO: uncomment this once it's faster:
|
||||
# return self._getrow(row)._minor_slice(col)
|
||||
|
||||
M, N = self.shape
|
||||
start, stop, stride = col.indices(N)
|
||||
|
||||
ii, jj = self.indptr[row:row+2]
|
||||
row_indices = self.indices[ii:jj]
|
||||
row_data = self.data[ii:jj]
|
||||
|
||||
if stride > 0:
|
||||
ind = (row_indices >= start) & (row_indices < stop)
|
||||
else:
|
||||
ind = (row_indices <= start) & (row_indices > stop)
|
||||
|
||||
if abs(stride) > 1:
|
||||
ind &= (row_indices - start) % stride == 0
|
||||
|
||||
row_indices = (row_indices[ind] - start) // stride
|
||||
row_data = row_data[ind]
|
||||
row_indptr = np.array([0, len(row_indices)])
|
||||
|
||||
if stride < 0:
|
||||
row_data = row_data[::-1]
|
||||
row_indices = abs(row_indices[::-1])
|
||||
|
||||
shape = (1, max(0, int(np.ceil(float(stop - start) / stride))))
|
||||
return self.__class__((row_data, row_indices, row_indptr), shape=shape,
|
||||
dtype=self.dtype, copy=False)
|
||||
|
||||
def _get_sliceXint(self, row, col):
|
||||
if row.step in (1, None):
|
||||
return self._get_submatrix(row, col, copy=True)
|
||||
return self._major_slice(row)._get_submatrix(minor=col)
|
||||
|
||||
def _get_sliceXarray(self, row, col):
|
||||
return self._major_slice(row)._minor_index_fancy(col)
|
||||
|
||||
def _get_arrayXint(self, row, col):
|
||||
return self._major_index_fancy(row)._get_submatrix(minor=col)
|
||||
|
||||
def _get_arrayXslice(self, row, col):
|
||||
if col.step not in (1, None):
|
||||
col = np.arange(*col.indices(self.shape[1]))
|
||||
return self._get_arrayXarray(row, col)
|
||||
return self._major_index_fancy(row)._get_submatrix(minor=col)
|
||||
|
||||
|
||||
def isspmatrix_csr(x):
|
||||
"""Is `x` of csr_matrix type?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x
|
||||
object to check for being a csr matrix
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if `x` is a csr matrix, False otherwise
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy.sparse import csr_array, csr_matrix, coo_matrix, isspmatrix_csr
|
||||
>>> isspmatrix_csr(csr_matrix([[5]]))
|
||||
True
|
||||
>>> isspmatrix_csr(csr_array([[5]]))
|
||||
False
|
||||
>>> isspmatrix_csr(coo_matrix([[5]]))
|
||||
False
|
||||
"""
|
||||
return isinstance(x, csr_matrix)
|
||||
|
||||
|
||||
# This namespace class separates array from matrix with isinstance
|
||||
class csr_array(_csr_base, sparray):
|
||||
"""
|
||||
Compressed Sparse Row array.
|
||||
|
||||
This can be instantiated in several ways:
|
||||
csr_array(D)
|
||||
where D is a 2-D ndarray
|
||||
|
||||
csr_array(S)
|
||||
with another sparse array or matrix S (equivalent to S.tocsr())
|
||||
|
||||
csr_array((M, N), [dtype])
|
||||
to construct an empty array with shape (M, N)
|
||||
dtype is optional, defaulting to dtype='d'.
|
||||
|
||||
csr_array((data, (row_ind, col_ind)), [shape=(M, N)])
|
||||
where ``data``, ``row_ind`` and ``col_ind`` satisfy the
|
||||
relationship ``a[row_ind[k], col_ind[k]] = data[k]``.
|
||||
|
||||
csr_array((data, indices, indptr), [shape=(M, N)])
|
||||
is the standard CSR representation where the column indices for
|
||||
row i are stored in ``indices[indptr[i]:indptr[i+1]]`` and their
|
||||
corresponding values are stored in ``data[indptr[i]:indptr[i+1]]``.
|
||||
If the shape parameter is not supplied, the array dimensions
|
||||
are inferred from the index arrays.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dtype : dtype
|
||||
Data type of the array
|
||||
shape : 2-tuple
|
||||
Shape of the array
|
||||
ndim : int
|
||||
Number of dimensions (this is always 2)
|
||||
nnz
|
||||
size
|
||||
data
|
||||
CSR format data array of the array
|
||||
indices
|
||||
CSR format index array of the array
|
||||
indptr
|
||||
CSR format index pointer array of the array
|
||||
has_sorted_indices
|
||||
has_canonical_format
|
||||
T
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
Sparse arrays can be used in arithmetic operations: they support
|
||||
addition, subtraction, multiplication, division, and matrix power.
|
||||
|
||||
Advantages of the CSR format
|
||||
- efficient arithmetic operations CSR + CSR, CSR * CSR, etc.
|
||||
- efficient row slicing
|
||||
- fast matrix vector products
|
||||
|
||||
Disadvantages of the CSR format
|
||||
- slow column slicing operations (consider CSC)
|
||||
- changes to the sparsity structure are expensive (consider LIL or DOK)
|
||||
|
||||
Canonical Format
|
||||
- Within each row, indices are sorted by column.
|
||||
- There are no duplicate entries.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csr_array
|
||||
>>> csr_array((3, 4), dtype=np.int8).toarray()
|
||||
array([[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0]], dtype=int8)
|
||||
|
||||
>>> row = np.array([0, 0, 1, 2, 2, 2])
|
||||
>>> col = np.array([0, 2, 2, 0, 1, 2])
|
||||
>>> data = np.array([1, 2, 3, 4, 5, 6])
|
||||
>>> csr_array((data, (row, col)), shape=(3, 3)).toarray()
|
||||
array([[1, 0, 2],
|
||||
[0, 0, 3],
|
||||
[4, 5, 6]])
|
||||
|
||||
>>> indptr = np.array([0, 2, 3, 6])
|
||||
>>> indices = np.array([0, 2, 2, 0, 1, 2])
|
||||
>>> data = np.array([1, 2, 3, 4, 5, 6])
|
||||
>>> csr_array((data, indices, indptr), shape=(3, 3)).toarray()
|
||||
array([[1, 0, 2],
|
||||
[0, 0, 3],
|
||||
[4, 5, 6]])
|
||||
|
||||
Duplicate entries are summed together:
|
||||
|
||||
>>> row = np.array([0, 1, 2, 0])
|
||||
>>> col = np.array([0, 1, 1, 0])
|
||||
>>> data = np.array([1, 2, 4, 8])
|
||||
>>> csr_array((data, (row, col)), shape=(3, 3)).toarray()
|
||||
array([[9, 0, 0],
|
||||
[0, 2, 0],
|
||||
[0, 4, 0]])
|
||||
|
||||
As an example of how to construct a CSR array incrementally,
|
||||
the following snippet builds a term-document array from texts:
|
||||
|
||||
>>> docs = [["hello", "world", "hello"], ["goodbye", "cruel", "world"]]
|
||||
>>> indptr = [0]
|
||||
>>> indices = []
|
||||
>>> data = []
|
||||
>>> vocabulary = {}
|
||||
>>> for d in docs:
|
||||
... for term in d:
|
||||
... index = vocabulary.setdefault(term, len(vocabulary))
|
||||
... indices.append(index)
|
||||
... data.append(1)
|
||||
... indptr.append(len(indices))
|
||||
...
|
||||
>>> csr_array((data, indices, indptr), dtype=int).toarray()
|
||||
array([[2, 1, 0, 0],
|
||||
[0, 1, 1, 1]])
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class csr_matrix(spmatrix, _csr_base):
|
||||
"""
|
||||
Compressed Sparse Row matrix.
|
||||
|
||||
This can be instantiated in several ways:
|
||||
csr_matrix(D)
|
||||
where D is a 2-D ndarray
|
||||
|
||||
csr_matrix(S)
|
||||
with another sparse array or matrix S (equivalent to S.tocsr())
|
||||
|
||||
csr_matrix((M, N), [dtype])
|
||||
to construct an empty matrix with shape (M, N)
|
||||
dtype is optional, defaulting to dtype='d'.
|
||||
|
||||
csr_matrix((data, (row_ind, col_ind)), [shape=(M, N)])
|
||||
where ``data``, ``row_ind`` and ``col_ind`` satisfy the
|
||||
relationship ``a[row_ind[k], col_ind[k]] = data[k]``.
|
||||
|
||||
csr_matrix((data, indices, indptr), [shape=(M, N)])
|
||||
is the standard CSR representation where the column indices for
|
||||
row i are stored in ``indices[indptr[i]:indptr[i+1]]`` and their
|
||||
corresponding values are stored in ``data[indptr[i]:indptr[i+1]]``.
|
||||
If the shape parameter is not supplied, the matrix dimensions
|
||||
are inferred from the index arrays.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dtype : dtype
|
||||
Data type of the matrix
|
||||
shape : 2-tuple
|
||||
Shape of the matrix
|
||||
ndim : int
|
||||
Number of dimensions (this is always 2)
|
||||
nnz
|
||||
size
|
||||
data
|
||||
CSR format data array of the matrix
|
||||
indices
|
||||
CSR format index array of the matrix
|
||||
indptr
|
||||
CSR format index pointer array of the matrix
|
||||
has_sorted_indices
|
||||
has_canonical_format
|
||||
T
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
Sparse matrices can be used in arithmetic operations: they support
|
||||
addition, subtraction, multiplication, division, and matrix power.
|
||||
|
||||
Advantages of the CSR format
|
||||
- efficient arithmetic operations CSR + CSR, CSR * CSR, etc.
|
||||
- efficient row slicing
|
||||
- fast matrix vector products
|
||||
|
||||
Disadvantages of the CSR format
|
||||
- slow column slicing operations (consider CSC)
|
||||
- changes to the sparsity structure are expensive (consider LIL or DOK)
|
||||
|
||||
Canonical Format
|
||||
- Within each row, indices are sorted by column.
|
||||
- There are no duplicate entries.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csr_matrix
|
||||
>>> csr_matrix((3, 4), dtype=np.int8).toarray()
|
||||
array([[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0]], dtype=int8)
|
||||
|
||||
>>> row = np.array([0, 0, 1, 2, 2, 2])
|
||||
>>> col = np.array([0, 2, 2, 0, 1, 2])
|
||||
>>> data = np.array([1, 2, 3, 4, 5, 6])
|
||||
>>> csr_matrix((data, (row, col)), shape=(3, 3)).toarray()
|
||||
array([[1, 0, 2],
|
||||
[0, 0, 3],
|
||||
[4, 5, 6]])
|
||||
|
||||
>>> indptr = np.array([0, 2, 3, 6])
|
||||
>>> indices = np.array([0, 2, 2, 0, 1, 2])
|
||||
>>> data = np.array([1, 2, 3, 4, 5, 6])
|
||||
>>> csr_matrix((data, indices, indptr), shape=(3, 3)).toarray()
|
||||
array([[1, 0, 2],
|
||||
[0, 0, 3],
|
||||
[4, 5, 6]])
|
||||
|
||||
Duplicate entries are summed together:
|
||||
|
||||
>>> row = np.array([0, 1, 2, 0])
|
||||
>>> col = np.array([0, 1, 1, 0])
|
||||
>>> data = np.array([1, 2, 4, 8])
|
||||
>>> csr_matrix((data, (row, col)), shape=(3, 3)).toarray()
|
||||
array([[9, 0, 0],
|
||||
[0, 2, 0],
|
||||
[0, 4, 0]])
|
||||
|
||||
As an example of how to construct a CSR matrix incrementally,
|
||||
the following snippet builds a term-document matrix from texts:
|
||||
|
||||
>>> docs = [["hello", "world", "hello"], ["goodbye", "cruel", "world"]]
|
||||
>>> indptr = [0]
|
||||
>>> indices = []
|
||||
>>> data = []
|
||||
>>> vocabulary = {}
|
||||
>>> for d in docs:
|
||||
... for term in d:
|
||||
... index = vocabulary.setdefault(term, len(vocabulary))
|
||||
... indices.append(index)
|
||||
... data.append(1)
|
||||
... indptr.append(len(indices))
|
||||
...
|
||||
>>> csr_matrix((data, indices, indptr), dtype=int).toarray()
|
||||
array([[2, 1, 0, 0],
|
||||
[0, 1, 1, 1]])
|
||||
|
||||
"""
|
||||
|
||||
515
venv/lib/python3.12/site-packages/scipy/sparse/_data.py
Normal file
515
venv/lib/python3.12/site-packages/scipy/sparse/_data.py
Normal file
@ -0,0 +1,515 @@
|
||||
"""Base class for sparse matrice with a .data attribute
|
||||
|
||||
subclasses must provide a _with_data() method that
|
||||
creates a new matrix with the same sparsity pattern
|
||||
as self but with a different data array
|
||||
|
||||
"""
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
from ._base import _spbase, sparray, _ufuncs_with_fixed_point_at_zero
|
||||
from ._sputils import isscalarlike, validateaxis
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
# TODO implement all relevant operations
|
||||
# use .data.__methods__() instead of /=, *=, etc.
|
||||
class _data_matrix(_spbase):
|
||||
def __init__(self, arg1):
|
||||
_spbase.__init__(self, arg1)
|
||||
|
||||
@property
|
||||
def dtype(self):
|
||||
return self.data.dtype
|
||||
|
||||
@dtype.setter
|
||||
def dtype(self, newtype):
|
||||
self.data.dtype = newtype
|
||||
|
||||
def _deduped_data(self):
|
||||
if hasattr(self, 'sum_duplicates'):
|
||||
self.sum_duplicates()
|
||||
return self.data
|
||||
|
||||
def __abs__(self):
|
||||
return self._with_data(abs(self._deduped_data()))
|
||||
|
||||
def __round__(self, ndigits=0):
|
||||
return self._with_data(np.around(self._deduped_data(), decimals=ndigits))
|
||||
|
||||
def _real(self):
|
||||
return self._with_data(self.data.real)
|
||||
|
||||
def _imag(self):
|
||||
return self._with_data(self.data.imag)
|
||||
|
||||
def __neg__(self):
|
||||
if self.dtype.kind == 'b':
|
||||
raise NotImplementedError('negating a boolean sparse array is not '
|
||||
'supported')
|
||||
return self._with_data(-self.data)
|
||||
|
||||
def __imul__(self, other): # self *= other
|
||||
if isscalarlike(other):
|
||||
self.data *= other
|
||||
return self
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __itruediv__(self, other): # self /= other
|
||||
if isscalarlike(other):
|
||||
recip = 1.0 / other
|
||||
self.data *= recip
|
||||
return self
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def astype(self, dtype, casting='unsafe', copy=True):
|
||||
dtype = np.dtype(dtype)
|
||||
if self.dtype != dtype:
|
||||
matrix = self._with_data(
|
||||
self.data.astype(dtype, casting=casting, copy=True),
|
||||
copy=True
|
||||
)
|
||||
return matrix._with_data(matrix._deduped_data(), copy=False)
|
||||
elif copy:
|
||||
return self.copy()
|
||||
else:
|
||||
return self
|
||||
|
||||
astype.__doc__ = _spbase.astype.__doc__
|
||||
|
||||
def conjugate(self, copy=True):
|
||||
if np.issubdtype(self.dtype, np.complexfloating):
|
||||
return self._with_data(self.data.conjugate(), copy=copy)
|
||||
elif copy:
|
||||
return self.copy()
|
||||
else:
|
||||
return self
|
||||
|
||||
conjugate.__doc__ = _spbase.conjugate.__doc__
|
||||
|
||||
def copy(self):
|
||||
return self._with_data(self.data.copy(), copy=True)
|
||||
|
||||
copy.__doc__ = _spbase.copy.__doc__
|
||||
|
||||
def count_nonzero(self):
|
||||
return np.count_nonzero(self._deduped_data())
|
||||
|
||||
count_nonzero.__doc__ = _spbase.count_nonzero.__doc__
|
||||
|
||||
def power(self, n, dtype=None):
|
||||
"""
|
||||
This function performs element-wise power.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n : scalar
|
||||
n is a non-zero scalar (nonzero avoids dense ones creation)
|
||||
If zero power is desired, special case it to use `np.ones`
|
||||
|
||||
dtype : If dtype is not specified, the current dtype will be preserved.
|
||||
|
||||
Raises
|
||||
------
|
||||
NotImplementedError : if n is a zero scalar
|
||||
If zero power is desired, special case it to use
|
||||
`np.ones(A.shape, dtype=A.dtype)`
|
||||
"""
|
||||
if not isscalarlike(n):
|
||||
raise NotImplementedError("input is not scalar")
|
||||
if not n:
|
||||
raise NotImplementedError(
|
||||
"zero power is not supported as it would densify the matrix.\n"
|
||||
"Use `np.ones(A.shape, dtype=A.dtype)` for this case."
|
||||
)
|
||||
|
||||
data = self._deduped_data()
|
||||
if dtype is not None:
|
||||
data = data.astype(dtype)
|
||||
return self._with_data(data ** n)
|
||||
|
||||
###########################
|
||||
# Multiplication handlers #
|
||||
###########################
|
||||
|
||||
def _mul_scalar(self, other):
|
||||
return self._with_data(self.data * other)
|
||||
|
||||
|
||||
# Add the numpy unary ufuncs for which func(0) = 0 to _data_matrix.
|
||||
for npfunc in _ufuncs_with_fixed_point_at_zero:
|
||||
name = npfunc.__name__
|
||||
|
||||
def _create_method(op):
|
||||
def method(self):
|
||||
result = op(self._deduped_data())
|
||||
return self._with_data(result, copy=True)
|
||||
|
||||
method.__doc__ = (f"Element-wise {name}.\n\n"
|
||||
f"See `numpy.{name}` for more information.")
|
||||
method.__name__ = name
|
||||
|
||||
return method
|
||||
|
||||
setattr(_data_matrix, name, _create_method(npfunc))
|
||||
|
||||
|
||||
def _find_missing_index(ind, n):
|
||||
for k, a in enumerate(ind):
|
||||
if k != a:
|
||||
return k
|
||||
|
||||
k += 1
|
||||
if k < n:
|
||||
return k
|
||||
else:
|
||||
return -1
|
||||
|
||||
|
||||
class _minmax_mixin:
|
||||
"""Mixin for min and max methods.
|
||||
|
||||
These are not implemented for dia_matrix, hence the separate class.
|
||||
"""
|
||||
|
||||
def _min_or_max_axis(self, axis, min_or_max):
|
||||
N = self.shape[axis]
|
||||
if N == 0:
|
||||
raise ValueError("zero-size array to reduction operation")
|
||||
M = self.shape[1 - axis]
|
||||
idx_dtype = self._get_index_dtype(maxval=M)
|
||||
|
||||
mat = self.tocsc() if axis == 0 else self.tocsr()
|
||||
mat.sum_duplicates()
|
||||
|
||||
major_index, value = mat._minor_reduce(min_or_max)
|
||||
not_full = np.diff(mat.indptr)[major_index] < N
|
||||
value[not_full] = min_or_max(value[not_full], 0)
|
||||
|
||||
mask = value != 0
|
||||
major_index = np.compress(mask, major_index)
|
||||
value = np.compress(mask, value)
|
||||
|
||||
if isinstance(self, sparray):
|
||||
coords = (major_index,)
|
||||
shape = (M,)
|
||||
return self._coo_container((value, coords), shape=shape, dtype=self.dtype)
|
||||
|
||||
if axis == 0:
|
||||
return self._coo_container(
|
||||
(value, (np.zeros(len(value), dtype=idx_dtype), major_index)),
|
||||
dtype=self.dtype, shape=(1, M)
|
||||
)
|
||||
else:
|
||||
return self._coo_container(
|
||||
(value, (major_index, np.zeros(len(value), dtype=idx_dtype))),
|
||||
dtype=self.dtype, shape=(M, 1)
|
||||
)
|
||||
|
||||
def _min_or_max(self, axis, out, min_or_max):
|
||||
if out is not None:
|
||||
raise ValueError("Sparse arrays do not support an 'out' parameter.")
|
||||
|
||||
validateaxis(axis)
|
||||
if self.ndim == 1:
|
||||
if axis not in (None, 0, -1):
|
||||
raise ValueError("axis out of range")
|
||||
axis = None # avoid calling special axis case. no impact on 1d
|
||||
|
||||
if axis is None:
|
||||
if 0 in self.shape:
|
||||
raise ValueError("zero-size array to reduction operation")
|
||||
|
||||
zero = self.dtype.type(0)
|
||||
if self.nnz == 0:
|
||||
return zero
|
||||
m = min_or_max.reduce(self._deduped_data().ravel())
|
||||
if self.nnz != math.prod(self.shape):
|
||||
m = min_or_max(zero, m)
|
||||
return m
|
||||
|
||||
if axis < 0:
|
||||
axis += 2
|
||||
|
||||
if (axis == 0) or (axis == 1):
|
||||
return self._min_or_max_axis(axis, min_or_max)
|
||||
else:
|
||||
raise ValueError("axis out of range")
|
||||
|
||||
def _arg_min_or_max_axis(self, axis, argmin_or_argmax, compare):
|
||||
if self.shape[axis] == 0:
|
||||
raise ValueError("Cannot apply the operation along a zero-sized dimension.")
|
||||
|
||||
if axis < 0:
|
||||
axis += 2
|
||||
|
||||
zero = self.dtype.type(0)
|
||||
|
||||
mat = self.tocsc() if axis == 0 else self.tocsr()
|
||||
mat.sum_duplicates()
|
||||
|
||||
ret_size, line_size = mat._swap(mat.shape)
|
||||
ret = np.zeros(ret_size, dtype=int)
|
||||
|
||||
nz_lines, = np.nonzero(np.diff(mat.indptr))
|
||||
for i in nz_lines:
|
||||
p, q = mat.indptr[i:i + 2]
|
||||
data = mat.data[p:q]
|
||||
indices = mat.indices[p:q]
|
||||
extreme_index = argmin_or_argmax(data)
|
||||
extreme_value = data[extreme_index]
|
||||
if compare(extreme_value, zero) or q - p == line_size:
|
||||
ret[i] = indices[extreme_index]
|
||||
else:
|
||||
zero_ind = _find_missing_index(indices, line_size)
|
||||
if extreme_value == zero:
|
||||
ret[i] = min(extreme_index, zero_ind)
|
||||
else:
|
||||
ret[i] = zero_ind
|
||||
|
||||
if isinstance(self, sparray):
|
||||
return ret
|
||||
|
||||
if axis == 1:
|
||||
ret = ret.reshape(-1, 1)
|
||||
|
||||
return self._ascontainer(ret)
|
||||
|
||||
def _arg_min_or_max(self, axis, out, argmin_or_argmax, compare):
|
||||
if out is not None:
|
||||
raise ValueError("Sparse types do not support an 'out' parameter.")
|
||||
|
||||
validateaxis(axis)
|
||||
|
||||
if self.ndim == 1:
|
||||
if axis not in (None, 0, -1):
|
||||
raise ValueError("axis out of range")
|
||||
axis = None # avoid calling special axis case. no impact on 1d
|
||||
|
||||
if axis is not None:
|
||||
return self._arg_min_or_max_axis(axis, argmin_or_argmax, compare)
|
||||
|
||||
if 0 in self.shape:
|
||||
raise ValueError("Cannot apply the operation to an empty matrix.")
|
||||
|
||||
if self.nnz == 0:
|
||||
return 0
|
||||
|
||||
zero = self.dtype.type(0)
|
||||
mat = self.tocoo()
|
||||
# Convert to canonical form: no duplicates, sorted indices.
|
||||
mat.sum_duplicates()
|
||||
extreme_index = argmin_or_argmax(mat.data)
|
||||
extreme_value = mat.data[extreme_index]
|
||||
num_col = mat.shape[-1]
|
||||
|
||||
# If the min value is less than zero, or max is greater than zero,
|
||||
# then we do not need to worry about implicit zeros.
|
||||
if compare(extreme_value, zero):
|
||||
# cast to Python int to avoid overflow and RuntimeError
|
||||
return int(mat.row[extreme_index]) * num_col + int(mat.col[extreme_index])
|
||||
|
||||
# Cheap test for the rare case where we have no implicit zeros.
|
||||
size = math.prod(self.shape)
|
||||
if size == mat.nnz:
|
||||
return int(mat.row[extreme_index]) * num_col + int(mat.col[extreme_index])
|
||||
|
||||
# At this stage, any implicit zero could be the min or max value.
|
||||
# After sum_duplicates(), the `row` and `col` arrays are guaranteed to
|
||||
# be sorted in C-order, which means the linearized indices are sorted.
|
||||
linear_indices = mat.row * num_col + mat.col
|
||||
first_implicit_zero_index = _find_missing_index(linear_indices, size)
|
||||
if extreme_value == zero:
|
||||
return min(first_implicit_zero_index, extreme_index)
|
||||
return first_implicit_zero_index
|
||||
|
||||
def max(self, axis=None, out=None):
|
||||
"""
|
||||
Return the maximum of the array/matrix or maximum along an axis.
|
||||
This takes all elements into account, not just the non-zero ones.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
axis : {-2, -1, 0, 1, None} optional
|
||||
Axis along which the sum is computed. The default is to
|
||||
compute the maximum over all elements, returning
|
||||
a scalar (i.e., `axis` = `None`).
|
||||
|
||||
out : None, optional
|
||||
This argument is in the signature *solely* for NumPy
|
||||
compatibility reasons. Do not pass in anything except
|
||||
for the default value, as this argument is not used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
amax : coo_matrix or scalar
|
||||
Maximum of `a`. If `axis` is None, the result is a scalar value.
|
||||
If `axis` is given, the result is a sparse.coo_matrix of dimension
|
||||
``a.ndim - 1``.
|
||||
|
||||
See Also
|
||||
--------
|
||||
min : The minimum value of a sparse array/matrix along a given axis.
|
||||
numpy.matrix.max : NumPy's implementation of 'max' for matrices
|
||||
|
||||
"""
|
||||
return self._min_or_max(axis, out, np.maximum)
|
||||
|
||||
def min(self, axis=None, out=None):
|
||||
"""
|
||||
Return the minimum of the array/matrix or maximum along an axis.
|
||||
This takes all elements into account, not just the non-zero ones.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
axis : {-2, -1, 0, 1, None} optional
|
||||
Axis along which the sum is computed. The default is to
|
||||
compute the minimum over all elements, returning
|
||||
a scalar (i.e., `axis` = `None`).
|
||||
|
||||
out : None, optional
|
||||
This argument is in the signature *solely* for NumPy
|
||||
compatibility reasons. Do not pass in anything except for
|
||||
the default value, as this argument is not used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
amin : coo_matrix or scalar
|
||||
Minimum of `a`. If `axis` is None, the result is a scalar value.
|
||||
If `axis` is given, the result is a sparse.coo_matrix of dimension
|
||||
``a.ndim - 1``.
|
||||
|
||||
See Also
|
||||
--------
|
||||
max : The maximum value of a sparse array/matrix along a given axis.
|
||||
numpy.matrix.min : NumPy's implementation of 'min' for matrices
|
||||
|
||||
"""
|
||||
return self._min_or_max(axis, out, np.minimum)
|
||||
|
||||
def nanmax(self, axis=None, out=None):
|
||||
"""
|
||||
Return the maximum of the array/matrix or maximum along an axis, ignoring any
|
||||
NaNs. This takes all elements into account, not just the non-zero
|
||||
ones.
|
||||
|
||||
.. versionadded:: 1.11.0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
axis : {-2, -1, 0, 1, None} optional
|
||||
Axis along which the maximum is computed. The default is to
|
||||
compute the maximum over all elements, returning
|
||||
a scalar (i.e., `axis` = `None`).
|
||||
|
||||
out : None, optional
|
||||
This argument is in the signature *solely* for NumPy
|
||||
compatibility reasons. Do not pass in anything except
|
||||
for the default value, as this argument is not used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
amax : coo_matrix or scalar
|
||||
Maximum of `a`. If `axis` is None, the result is a scalar value.
|
||||
If `axis` is given, the result is a sparse.coo_matrix of dimension
|
||||
``a.ndim - 1``.
|
||||
|
||||
See Also
|
||||
--------
|
||||
nanmin : The minimum value of a sparse array/matrix along a given axis,
|
||||
ignoring NaNs.
|
||||
max : The maximum value of a sparse array/matrix along a given axis,
|
||||
propagating NaNs.
|
||||
numpy.nanmax : NumPy's implementation of 'nanmax'.
|
||||
|
||||
"""
|
||||
return self._min_or_max(axis, out, np.fmax)
|
||||
|
||||
def nanmin(self, axis=None, out=None):
|
||||
"""
|
||||
Return the minimum of the array/matrix or minimum along an axis, ignoring any
|
||||
NaNs. This takes all elements into account, not just the non-zero
|
||||
ones.
|
||||
|
||||
.. versionadded:: 1.11.0
|
||||
|
||||
Parameters
|
||||
----------
|
||||
axis : {-2, -1, 0, 1, None} optional
|
||||
Axis along which the minimum is computed. The default is to
|
||||
compute the minimum over all elements, returning
|
||||
a scalar (i.e., `axis` = `None`).
|
||||
|
||||
out : None, optional
|
||||
This argument is in the signature *solely* for NumPy
|
||||
compatibility reasons. Do not pass in anything except for
|
||||
the default value, as this argument is not used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
amin : coo_matrix or scalar
|
||||
Minimum of `a`. If `axis` is None, the result is a scalar value.
|
||||
If `axis` is given, the result is a sparse.coo_matrix of dimension
|
||||
``a.ndim - 1``.
|
||||
|
||||
See Also
|
||||
--------
|
||||
nanmax : The maximum value of a sparse array/matrix along a given axis,
|
||||
ignoring NaNs.
|
||||
min : The minimum value of a sparse array/matrix along a given axis,
|
||||
propagating NaNs.
|
||||
numpy.nanmin : NumPy's implementation of 'nanmin'.
|
||||
|
||||
"""
|
||||
return self._min_or_max(axis, out, np.fmin)
|
||||
|
||||
def argmax(self, axis=None, out=None):
|
||||
"""Return indices of maximum elements along an axis.
|
||||
|
||||
Implicit zero elements are also taken into account. If there are
|
||||
several maximum values, the index of the first occurrence is returned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
axis : {-2, -1, 0, 1, None}, optional
|
||||
Axis along which the argmax is computed. If None (default), index
|
||||
of the maximum element in the flatten data is returned.
|
||||
out : None, optional
|
||||
This argument is in the signature *solely* for NumPy
|
||||
compatibility reasons. Do not pass in anything except for
|
||||
the default value, as this argument is not used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ind : numpy.matrix or int
|
||||
Indices of maximum elements. If matrix, its size along `axis` is 1.
|
||||
"""
|
||||
return self._arg_min_or_max(axis, out, np.argmax, np.greater)
|
||||
|
||||
def argmin(self, axis=None, out=None):
|
||||
"""Return indices of minimum elements along an axis.
|
||||
|
||||
Implicit zero elements are also taken into account. If there are
|
||||
several minimum values, the index of the first occurrence is returned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
axis : {-2, -1, 0, 1, None}, optional
|
||||
Axis along which the argmin is computed. If None (default), index
|
||||
of the minimum element in the flatten data is returned.
|
||||
out : None, optional
|
||||
This argument is in the signature *solely* for NumPy
|
||||
compatibility reasons. Do not pass in anything except for
|
||||
the default value, as this argument is not used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ind : numpy.matrix or int
|
||||
Indices of minimum elements. If matrix, its size along `axis` is 1.
|
||||
"""
|
||||
return self._arg_min_or_max(axis, out, np.argmin, np.less)
|
||||
585
venv/lib/python3.12/site-packages/scipy/sparse/_dia.py
Normal file
585
venv/lib/python3.12/site-packages/scipy/sparse/_dia.py
Normal file
@ -0,0 +1,585 @@
|
||||
"""Sparse DIAgonal format"""
|
||||
|
||||
__docformat__ = "restructuredtext en"
|
||||
|
||||
__all__ = ['dia_array', 'dia_matrix', 'isspmatrix_dia']
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .._lib._util import copy_if_needed
|
||||
from ._matrix import spmatrix
|
||||
from ._base import issparse, _formats, _spbase, sparray
|
||||
from ._data import _data_matrix
|
||||
from ._sputils import (
|
||||
isshape, upcast_char, getdtype, get_sum_dtype, validateaxis, check_shape
|
||||
)
|
||||
from ._sparsetools import dia_matvec
|
||||
|
||||
|
||||
class _dia_base(_data_matrix):
|
||||
_format = 'dia'
|
||||
|
||||
def __init__(self, arg1, shape=None, dtype=None, copy=False):
|
||||
_data_matrix.__init__(self, arg1)
|
||||
|
||||
if issparse(arg1):
|
||||
if arg1.format == "dia":
|
||||
if copy:
|
||||
arg1 = arg1.copy()
|
||||
self.data = arg1.data
|
||||
self.offsets = arg1.offsets
|
||||
self._shape = check_shape(arg1.shape)
|
||||
else:
|
||||
if arg1.format == self.format and copy:
|
||||
A = arg1.copy()
|
||||
else:
|
||||
A = arg1.todia()
|
||||
self.data = A.data
|
||||
self.offsets = A.offsets
|
||||
self._shape = check_shape(A.shape)
|
||||
elif isinstance(arg1, tuple):
|
||||
if isshape(arg1):
|
||||
# It's a tuple of matrix dimensions (M, N)
|
||||
# create empty matrix
|
||||
self._shape = check_shape(arg1)
|
||||
self.data = np.zeros((0,0), getdtype(dtype, default=float))
|
||||
idx_dtype = self._get_index_dtype(maxval=max(self.shape))
|
||||
self.offsets = np.zeros((0), dtype=idx_dtype)
|
||||
else:
|
||||
try:
|
||||
# Try interpreting it as (data, offsets)
|
||||
data, offsets = arg1
|
||||
except Exception as e:
|
||||
message = 'unrecognized form for dia_array constructor'
|
||||
raise ValueError(message) from e
|
||||
else:
|
||||
if shape is None:
|
||||
raise ValueError('expected a shape argument')
|
||||
if not copy:
|
||||
copy = copy_if_needed
|
||||
self.data = np.atleast_2d(np.array(arg1[0], dtype=dtype, copy=copy))
|
||||
offsets = np.array(arg1[1],
|
||||
dtype=self._get_index_dtype(maxval=max(shape)),
|
||||
copy=copy)
|
||||
self.offsets = np.atleast_1d(offsets)
|
||||
self._shape = check_shape(shape)
|
||||
else:
|
||||
# must be dense, convert to COO first, then to DIA
|
||||
try:
|
||||
arg1 = np.asarray(arg1)
|
||||
except Exception as e:
|
||||
raise ValueError("unrecognized form for"
|
||||
" %s_matrix constructor" % self.format) from e
|
||||
if isinstance(self, sparray) and arg1.ndim != 2:
|
||||
raise ValueError(f"DIA arrays don't support {arg1.ndim}D input. Use 2D")
|
||||
A = self._coo_container(arg1, dtype=dtype, shape=shape).todia()
|
||||
self.data = A.data
|
||||
self.offsets = A.offsets
|
||||
self._shape = check_shape(A.shape)
|
||||
|
||||
if dtype is not None:
|
||||
self.data = self.data.astype(dtype)
|
||||
|
||||
# check format
|
||||
if self.offsets.ndim != 1:
|
||||
raise ValueError('offsets array must have rank 1')
|
||||
|
||||
if self.data.ndim != 2:
|
||||
raise ValueError('data array must have rank 2')
|
||||
|
||||
if self.data.shape[0] != len(self.offsets):
|
||||
raise ValueError('number of diagonals (%d) '
|
||||
'does not match the number of offsets (%d)'
|
||||
% (self.data.shape[0], len(self.offsets)))
|
||||
|
||||
if len(np.unique(self.offsets)) != len(self.offsets):
|
||||
raise ValueError('offset array contains duplicate values')
|
||||
|
||||
def __repr__(self):
|
||||
_, fmt = _formats[self.format]
|
||||
sparse_cls = 'array' if isinstance(self, sparray) else 'matrix'
|
||||
d = self.data.shape[0]
|
||||
return (
|
||||
f"<{fmt} sparse {sparse_cls} of dtype '{self.dtype}'\n"
|
||||
f"\twith {self.nnz} stored elements ({d} diagonals) and shape {self.shape}>"
|
||||
)
|
||||
|
||||
def _data_mask(self):
|
||||
"""Returns a mask of the same shape as self.data, where
|
||||
mask[i,j] is True when data[i,j] corresponds to a stored element."""
|
||||
num_rows, num_cols = self.shape
|
||||
offset_inds = np.arange(self.data.shape[1])
|
||||
row = offset_inds - self.offsets[:,None]
|
||||
mask = (row >= 0)
|
||||
mask &= (row < num_rows)
|
||||
mask &= (offset_inds < num_cols)
|
||||
return mask
|
||||
|
||||
def count_nonzero(self):
|
||||
mask = self._data_mask()
|
||||
return np.count_nonzero(self.data[mask])
|
||||
|
||||
def _getnnz(self, axis=None):
|
||||
if axis is not None:
|
||||
raise NotImplementedError("_getnnz over an axis is not implemented "
|
||||
"for DIA format")
|
||||
M,N = self.shape
|
||||
nnz = 0
|
||||
for k in self.offsets:
|
||||
if k > 0:
|
||||
nnz += min(M,N-k)
|
||||
else:
|
||||
nnz += min(M+k,N)
|
||||
return int(nnz)
|
||||
|
||||
_getnnz.__doc__ = _spbase._getnnz.__doc__
|
||||
count_nonzero.__doc__ = _spbase.count_nonzero.__doc__
|
||||
|
||||
def sum(self, axis=None, dtype=None, out=None):
|
||||
validateaxis(axis)
|
||||
|
||||
if axis is not None and axis < 0:
|
||||
axis += 2
|
||||
|
||||
res_dtype = get_sum_dtype(self.dtype)
|
||||
num_rows, num_cols = self.shape
|
||||
ret = None
|
||||
|
||||
if axis == 0:
|
||||
mask = self._data_mask()
|
||||
x = (self.data * mask).sum(axis=0)
|
||||
if x.shape[0] == num_cols:
|
||||
res = x
|
||||
else:
|
||||
res = np.zeros(num_cols, dtype=x.dtype)
|
||||
res[:x.shape[0]] = x
|
||||
ret = self._ascontainer(res, dtype=res_dtype)
|
||||
|
||||
else:
|
||||
row_sums = np.zeros((num_rows, 1), dtype=res_dtype)
|
||||
one = np.ones(num_cols, dtype=res_dtype)
|
||||
dia_matvec(num_rows, num_cols, len(self.offsets),
|
||||
self.data.shape[1], self.offsets, self.data, one, row_sums)
|
||||
|
||||
row_sums = self._ascontainer(row_sums)
|
||||
|
||||
if axis is None:
|
||||
return row_sums.sum(dtype=dtype, out=out)
|
||||
|
||||
ret = self._ascontainer(row_sums.sum(axis=axis))
|
||||
|
||||
if out is not None and out.shape != ret.shape:
|
||||
raise ValueError("dimensions do not match")
|
||||
|
||||
return ret.sum(axis=(), dtype=dtype, out=out)
|
||||
|
||||
sum.__doc__ = _spbase.sum.__doc__
|
||||
|
||||
def _add_sparse(self, other):
|
||||
# If other is not DIA format, let them handle us instead.
|
||||
if not isinstance(other, _dia_base):
|
||||
return other._add_sparse(self)
|
||||
|
||||
# Fast path for exact equality of the sparsity structure.
|
||||
if np.array_equal(self.offsets, other.offsets):
|
||||
return self._with_data(self.data + other.data)
|
||||
|
||||
# Find the union of the offsets (which will be sorted and unique).
|
||||
new_offsets = np.union1d(self.offsets, other.offsets)
|
||||
self_idx = np.searchsorted(new_offsets, self.offsets)
|
||||
other_idx = np.searchsorted(new_offsets, other.offsets)
|
||||
|
||||
self_d = self.data.shape[1]
|
||||
other_d = other.data.shape[1]
|
||||
# Fast path for a sparsity structure where the final offsets are a
|
||||
# permutation of the existing offsets and the diagonal lengths match.
|
||||
if self_d == other_d and len(new_offsets) == len(self.offsets):
|
||||
new_data = self.data[_invert_index(self_idx)]
|
||||
new_data[other_idx, :] += other.data
|
||||
elif self_d == other_d and len(new_offsets) == len(other.offsets):
|
||||
new_data = other.data[_invert_index(other_idx)]
|
||||
new_data[self_idx, :] += self.data
|
||||
else:
|
||||
# Maximum diagonal length of the result.
|
||||
d = min(self.shape[0] + new_offsets[-1], self.shape[1])
|
||||
|
||||
# Add all diagonals to a freshly-allocated data array.
|
||||
new_data = np.zeros(
|
||||
(len(new_offsets), d),
|
||||
dtype=np.result_type(self.data, other.data),
|
||||
)
|
||||
new_data[self_idx, :self_d] += self.data[:, :d]
|
||||
new_data[other_idx, :other_d] += other.data[:, :d]
|
||||
return self._dia_container((new_data, new_offsets), shape=self.shape)
|
||||
|
||||
def _mul_scalar(self, other):
|
||||
return self._with_data(self.data * other)
|
||||
|
||||
def _matmul_vector(self, other):
|
||||
x = other
|
||||
|
||||
y = np.zeros(self.shape[0], dtype=upcast_char(self.dtype.char,
|
||||
x.dtype.char))
|
||||
|
||||
L = self.data.shape[1]
|
||||
|
||||
M,N = self.shape
|
||||
|
||||
dia_matvec(M,N, len(self.offsets), L, self.offsets, self.data,
|
||||
x.ravel(), y.ravel())
|
||||
|
||||
return y
|
||||
|
||||
def _setdiag(self, values, k=0):
|
||||
M, N = self.shape
|
||||
|
||||
if values.ndim == 0:
|
||||
# broadcast
|
||||
values_n = np.inf
|
||||
else:
|
||||
values_n = len(values)
|
||||
|
||||
if k < 0:
|
||||
n = min(M + k, N, values_n)
|
||||
min_index = 0
|
||||
max_index = n
|
||||
else:
|
||||
n = min(M, N - k, values_n)
|
||||
min_index = k
|
||||
max_index = k + n
|
||||
|
||||
if values.ndim != 0:
|
||||
# allow also longer sequences
|
||||
values = values[:n]
|
||||
|
||||
data_rows, data_cols = self.data.shape
|
||||
if k in self.offsets:
|
||||
if max_index > data_cols:
|
||||
data = np.zeros((data_rows, max_index), dtype=self.data.dtype)
|
||||
data[:, :data_cols] = self.data
|
||||
self.data = data
|
||||
self.data[self.offsets == k, min_index:max_index] = values
|
||||
else:
|
||||
self.offsets = np.append(self.offsets, self.offsets.dtype.type(k))
|
||||
m = max(max_index, data_cols)
|
||||
data = np.zeros((data_rows + 1, m), dtype=self.data.dtype)
|
||||
data[:-1, :data_cols] = self.data
|
||||
data[-1, min_index:max_index] = values
|
||||
self.data = data
|
||||
|
||||
def todia(self, copy=False):
|
||||
if copy:
|
||||
return self.copy()
|
||||
else:
|
||||
return self
|
||||
|
||||
todia.__doc__ = _spbase.todia.__doc__
|
||||
|
||||
def transpose(self, axes=None, copy=False):
|
||||
if axes is not None and axes != (1, 0):
|
||||
raise ValueError("Sparse arrays/matrices do not support "
|
||||
"an 'axes' parameter because swapping "
|
||||
"dimensions is the only logical permutation.")
|
||||
|
||||
num_rows, num_cols = self.shape
|
||||
max_dim = max(self.shape)
|
||||
|
||||
# flip diagonal offsets
|
||||
offsets = -self.offsets
|
||||
|
||||
# re-align the data matrix
|
||||
r = np.arange(len(offsets), dtype=np.intc)[:, None]
|
||||
c = np.arange(num_rows, dtype=np.intc) - (offsets % max_dim)[:, None]
|
||||
pad_amount = max(0, max_dim-self.data.shape[1])
|
||||
data = np.hstack((self.data, np.zeros((self.data.shape[0], pad_amount),
|
||||
dtype=self.data.dtype)))
|
||||
data = data[r, c]
|
||||
return self._dia_container((data, offsets), shape=(
|
||||
num_cols, num_rows), copy=copy)
|
||||
|
||||
transpose.__doc__ = _spbase.transpose.__doc__
|
||||
|
||||
def diagonal(self, k=0):
|
||||
rows, cols = self.shape
|
||||
if k <= -rows or k >= cols:
|
||||
return np.empty(0, dtype=self.data.dtype)
|
||||
idx, = np.nonzero(self.offsets == k)
|
||||
first_col = max(0, k)
|
||||
last_col = min(rows + k, cols)
|
||||
result_size = last_col - first_col
|
||||
if idx.size == 0:
|
||||
return np.zeros(result_size, dtype=self.data.dtype)
|
||||
result = self.data[idx[0], first_col:last_col]
|
||||
padding = result_size - len(result)
|
||||
if padding > 0:
|
||||
result = np.pad(result, (0, padding), mode='constant')
|
||||
return result
|
||||
|
||||
diagonal.__doc__ = _spbase.diagonal.__doc__
|
||||
|
||||
def tocsc(self, copy=False):
|
||||
if self.nnz == 0:
|
||||
return self._csc_container(self.shape, dtype=self.dtype)
|
||||
|
||||
num_rows, num_cols = self.shape
|
||||
num_offsets, offset_len = self.data.shape
|
||||
offset_inds = np.arange(offset_len)
|
||||
|
||||
row = offset_inds - self.offsets[:,None]
|
||||
mask = (row >= 0)
|
||||
mask &= (row < num_rows)
|
||||
mask &= (offset_inds < num_cols)
|
||||
mask &= (self.data != 0)
|
||||
|
||||
idx_dtype = self._get_index_dtype(maxval=max(self.shape))
|
||||
indptr = np.zeros(num_cols + 1, dtype=idx_dtype)
|
||||
indptr[1:offset_len+1] = np.cumsum(mask.sum(axis=0)[:num_cols])
|
||||
if offset_len < num_cols:
|
||||
indptr[offset_len+1:] = indptr[offset_len]
|
||||
indices = row.T[mask.T].astype(idx_dtype, copy=False)
|
||||
data = self.data.T[mask.T]
|
||||
return self._csc_container((data, indices, indptr), shape=self.shape,
|
||||
dtype=self.dtype)
|
||||
|
||||
tocsc.__doc__ = _spbase.tocsc.__doc__
|
||||
|
||||
def tocoo(self, copy=False):
|
||||
num_rows, num_cols = self.shape
|
||||
num_offsets, offset_len = self.data.shape
|
||||
offset_inds = np.arange(offset_len)
|
||||
|
||||
row = offset_inds - self.offsets[:,None]
|
||||
mask = (row >= 0)
|
||||
mask &= (row < num_rows)
|
||||
mask &= (offset_inds < num_cols)
|
||||
mask &= (self.data != 0)
|
||||
row = row[mask]
|
||||
col = np.tile(offset_inds, num_offsets)[mask.ravel()]
|
||||
idx_dtype = self._get_index_dtype(
|
||||
arrays=(self.offsets,), maxval=max(self.shape)
|
||||
)
|
||||
row = row.astype(idx_dtype, copy=False)
|
||||
col = col.astype(idx_dtype, copy=False)
|
||||
data = self.data[mask]
|
||||
# Note: this cannot set has_canonical_format=True, because despite the
|
||||
# lack of duplicates, we do not generate sorted indices.
|
||||
return self._coo_container(
|
||||
(data, (row, col)), shape=self.shape, dtype=self.dtype, copy=False
|
||||
)
|
||||
|
||||
tocoo.__doc__ = _spbase.tocoo.__doc__
|
||||
|
||||
# needed by _data_matrix
|
||||
def _with_data(self, data, copy=True):
|
||||
"""Returns a matrix with the same sparsity structure as self,
|
||||
but with different data. By default the structure arrays are copied.
|
||||
"""
|
||||
if copy:
|
||||
return self._dia_container(
|
||||
(data, self.offsets.copy()), shape=self.shape
|
||||
)
|
||||
else:
|
||||
return self._dia_container(
|
||||
(data, self.offsets), shape=self.shape
|
||||
)
|
||||
|
||||
def resize(self, *shape):
|
||||
shape = check_shape(shape)
|
||||
M, N = shape
|
||||
# we do not need to handle the case of expanding N
|
||||
self.data = self.data[:, :N]
|
||||
|
||||
if (M > self.shape[0] and
|
||||
np.any(self.offsets + self.shape[0] < self.data.shape[1])):
|
||||
# explicitly clear values that were previously hidden
|
||||
mask = (self.offsets[:, None] + self.shape[0] <=
|
||||
np.arange(self.data.shape[1]))
|
||||
self.data[mask] = 0
|
||||
|
||||
self._shape = shape
|
||||
|
||||
resize.__doc__ = _spbase.resize.__doc__
|
||||
|
||||
|
||||
def _invert_index(idx):
|
||||
"""Helper function to invert an index array."""
|
||||
inv = np.zeros_like(idx)
|
||||
inv[idx] = np.arange(len(idx))
|
||||
return inv
|
||||
|
||||
|
||||
def isspmatrix_dia(x):
|
||||
"""Is `x` of dia_matrix type?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x
|
||||
object to check for being a dia matrix
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if `x` is a dia matrix, False otherwise
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy.sparse import dia_array, dia_matrix, coo_matrix, isspmatrix_dia
|
||||
>>> isspmatrix_dia(dia_matrix([[5]]))
|
||||
True
|
||||
>>> isspmatrix_dia(dia_array([[5]]))
|
||||
False
|
||||
>>> isspmatrix_dia(coo_matrix([[5]]))
|
||||
False
|
||||
"""
|
||||
return isinstance(x, dia_matrix)
|
||||
|
||||
|
||||
# This namespace class separates array from matrix with isinstance
|
||||
class dia_array(_dia_base, sparray):
|
||||
"""
|
||||
Sparse array with DIAgonal storage.
|
||||
|
||||
This can be instantiated in several ways:
|
||||
dia_array(D)
|
||||
where D is a 2-D ndarray
|
||||
|
||||
dia_array(S)
|
||||
with another sparse array or matrix S (equivalent to S.todia())
|
||||
|
||||
dia_array((M, N), [dtype])
|
||||
to construct an empty array with shape (M, N),
|
||||
dtype is optional, defaulting to dtype='d'.
|
||||
|
||||
dia_array((data, offsets), shape=(M, N))
|
||||
where the ``data[k,:]`` stores the diagonal entries for
|
||||
diagonal ``offsets[k]`` (See example below)
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dtype : dtype
|
||||
Data type of the array
|
||||
shape : 2-tuple
|
||||
Shape of the array
|
||||
ndim : int
|
||||
Number of dimensions (this is always 2)
|
||||
nnz
|
||||
size
|
||||
data
|
||||
DIA format data array of the array
|
||||
offsets
|
||||
DIA format offset array of the array
|
||||
T
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
Sparse arrays can be used in arithmetic operations: they support
|
||||
addition, subtraction, multiplication, division, and matrix power.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import dia_array
|
||||
>>> dia_array((3, 4), dtype=np.int8).toarray()
|
||||
array([[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0]], dtype=int8)
|
||||
|
||||
>>> data = np.array([[1, 2, 3, 4]]).repeat(3, axis=0)
|
||||
>>> offsets = np.array([0, -1, 2])
|
||||
>>> dia_array((data, offsets), shape=(4, 4)).toarray()
|
||||
array([[1, 0, 3, 0],
|
||||
[1, 2, 0, 4],
|
||||
[0, 2, 3, 0],
|
||||
[0, 0, 3, 4]])
|
||||
|
||||
>>> from scipy.sparse import dia_array
|
||||
>>> n = 10
|
||||
>>> ex = np.ones(n)
|
||||
>>> data = np.array([ex, 2 * ex, ex])
|
||||
>>> offsets = np.array([-1, 0, 1])
|
||||
>>> dia_array((data, offsets), shape=(n, n)).toarray()
|
||||
array([[2., 1., 0., ..., 0., 0., 0.],
|
||||
[1., 2., 1., ..., 0., 0., 0.],
|
||||
[0., 1., 2., ..., 0., 0., 0.],
|
||||
...,
|
||||
[0., 0., 0., ..., 2., 1., 0.],
|
||||
[0., 0., 0., ..., 1., 2., 1.],
|
||||
[0., 0., 0., ..., 0., 1., 2.]])
|
||||
"""
|
||||
|
||||
|
||||
class dia_matrix(spmatrix, _dia_base):
|
||||
"""
|
||||
Sparse matrix with DIAgonal storage.
|
||||
|
||||
This can be instantiated in several ways:
|
||||
dia_matrix(D)
|
||||
where D is a 2-D ndarray
|
||||
|
||||
dia_matrix(S)
|
||||
with another sparse array or matrix S (equivalent to S.todia())
|
||||
|
||||
dia_matrix((M, N), [dtype])
|
||||
to construct an empty matrix with shape (M, N),
|
||||
dtype is optional, defaulting to dtype='d'.
|
||||
|
||||
dia_matrix((data, offsets), shape=(M, N))
|
||||
where the ``data[k,:]`` stores the diagonal entries for
|
||||
diagonal ``offsets[k]`` (See example below)
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dtype : dtype
|
||||
Data type of the matrix
|
||||
shape : 2-tuple
|
||||
Shape of the matrix
|
||||
ndim : int
|
||||
Number of dimensions (this is always 2)
|
||||
nnz
|
||||
size
|
||||
data
|
||||
DIA format data array of the matrix
|
||||
offsets
|
||||
DIA format offset array of the matrix
|
||||
T
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
Sparse matrices can be used in arithmetic operations: they support
|
||||
addition, subtraction, multiplication, division, and matrix power.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import dia_matrix
|
||||
>>> dia_matrix((3, 4), dtype=np.int8).toarray()
|
||||
array([[0, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 0, 0]], dtype=int8)
|
||||
|
||||
>>> data = np.array([[1, 2, 3, 4]]).repeat(3, axis=0)
|
||||
>>> offsets = np.array([0, -1, 2])
|
||||
>>> dia_matrix((data, offsets), shape=(4, 4)).toarray()
|
||||
array([[1, 0, 3, 0],
|
||||
[1, 2, 0, 4],
|
||||
[0, 2, 3, 0],
|
||||
[0, 0, 3, 4]])
|
||||
|
||||
>>> from scipy.sparse import dia_matrix
|
||||
>>> n = 10
|
||||
>>> ex = np.ones(n)
|
||||
>>> data = np.array([ex, 2 * ex, ex])
|
||||
>>> offsets = np.array([-1, 0, 1])
|
||||
>>> dia_matrix((data, offsets), shape=(n, n)).toarray()
|
||||
array([[2., 1., 0., ..., 0., 0., 0.],
|
||||
[1., 2., 1., ..., 0., 0., 0.],
|
||||
[0., 1., 2., ..., 0., 0., 0.],
|
||||
...,
|
||||
[0., 0., 0., ..., 2., 1., 0.],
|
||||
[0., 0., 0., ..., 1., 2., 1.],
|
||||
[0., 0., 0., ..., 0., 1., 2.]])
|
||||
"""
|
||||
684
venv/lib/python3.12/site-packages/scipy/sparse/_dok.py
Normal file
684
venv/lib/python3.12/site-packages/scipy/sparse/_dok.py
Normal file
@ -0,0 +1,684 @@
|
||||
"""Dictionary Of Keys based matrix"""
|
||||
|
||||
__docformat__ = "restructuredtext en"
|
||||
|
||||
__all__ = ['dok_array', 'dok_matrix', 'isspmatrix_dok']
|
||||
|
||||
import itertools
|
||||
from warnings import warn
|
||||
import numpy as np
|
||||
|
||||
from ._matrix import spmatrix
|
||||
from ._base import _spbase, sparray, issparse
|
||||
from ._index import IndexMixin
|
||||
from ._sputils import (isdense, getdtype, isshape, isintlike, isscalarlike,
|
||||
upcast, upcast_scalar, check_shape)
|
||||
|
||||
|
||||
class _dok_base(_spbase, IndexMixin, dict):
|
||||
_format = 'dok'
|
||||
|
||||
def __init__(self, arg1, shape=None, dtype=None, copy=False):
|
||||
_spbase.__init__(self, arg1)
|
||||
|
||||
is_array = isinstance(self, sparray)
|
||||
if isinstance(arg1, tuple) and isshape(arg1, allow_1d=is_array):
|
||||
self._shape = check_shape(arg1, allow_1d=is_array)
|
||||
self._dict = {}
|
||||
self.dtype = getdtype(dtype, default=float)
|
||||
elif issparse(arg1): # Sparse ctor
|
||||
if arg1.format == self.format:
|
||||
arg1 = arg1.copy() if copy else arg1
|
||||
else:
|
||||
arg1 = arg1.todok()
|
||||
|
||||
if dtype is not None:
|
||||
arg1 = arg1.astype(dtype, copy=False)
|
||||
|
||||
self._dict = arg1._dict
|
||||
self._shape = check_shape(arg1.shape, allow_1d=is_array)
|
||||
self.dtype = arg1.dtype
|
||||
else: # Dense ctor
|
||||
try:
|
||||
arg1 = np.asarray(arg1)
|
||||
except Exception as e:
|
||||
raise TypeError('Invalid input format.') from e
|
||||
|
||||
if arg1.ndim > 2:
|
||||
raise TypeError('Expected rank <=2 dense array or matrix.')
|
||||
|
||||
if arg1.ndim == 1:
|
||||
if dtype is not None:
|
||||
arg1 = arg1.astype(dtype)
|
||||
self._dict = {i: v for i, v in enumerate(arg1) if v != 0}
|
||||
self.dtype = arg1.dtype
|
||||
else:
|
||||
d = self._coo_container(arg1, dtype=dtype).todok()
|
||||
self._dict = d._dict
|
||||
self.dtype = d.dtype
|
||||
self._shape = check_shape(arg1.shape, allow_1d=is_array)
|
||||
|
||||
def update(self, val):
|
||||
# Prevent direct usage of update
|
||||
raise NotImplementedError("Direct update to DOK sparse format is not allowed.")
|
||||
|
||||
def _getnnz(self, axis=None):
|
||||
if axis is not None:
|
||||
raise NotImplementedError(
|
||||
"_getnnz over an axis is not implemented for DOK format."
|
||||
)
|
||||
return len(self._dict)
|
||||
|
||||
def count_nonzero(self):
|
||||
return sum(x != 0 for x in self.values())
|
||||
|
||||
_getnnz.__doc__ = _spbase._getnnz.__doc__
|
||||
count_nonzero.__doc__ = _spbase.count_nonzero.__doc__
|
||||
|
||||
def __len__(self):
|
||||
return len(self._dict)
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._dict
|
||||
|
||||
def setdefault(self, key, default=None, /):
|
||||
return self._dict.setdefault(key, default)
|
||||
|
||||
def __delitem__(self, key, /):
|
||||
del self._dict[key]
|
||||
|
||||
def clear(self):
|
||||
return self._dict.clear()
|
||||
|
||||
def pop(self, /, *args):
|
||||
return self._dict.pop(*args)
|
||||
|
||||
def __reversed__(self):
|
||||
raise TypeError("reversed is not defined for dok_array type")
|
||||
|
||||
def __or__(self, other):
|
||||
type_names = f"{type(self).__name__} and {type(other).__name__}"
|
||||
raise TypeError(f"unsupported operand type for |: {type_names}")
|
||||
|
||||
def __ror__(self, other):
|
||||
type_names = f"{type(self).__name__} and {type(other).__name__}"
|
||||
raise TypeError(f"unsupported operand type for |: {type_names}")
|
||||
|
||||
def __ior__(self, other):
|
||||
type_names = f"{type(self).__name__} and {type(other).__name__}"
|
||||
raise TypeError(f"unsupported operand type for |: {type_names}")
|
||||
|
||||
def popitem(self):
|
||||
return self._dict.popitem()
|
||||
|
||||
def items(self):
|
||||
return self._dict.items()
|
||||
|
||||
def keys(self):
|
||||
return self._dict.keys()
|
||||
|
||||
def values(self):
|
||||
return self._dict.values()
|
||||
|
||||
def get(self, key, default=0.0):
|
||||
"""This provides dict.get method functionality with type checking"""
|
||||
if key in self._dict:
|
||||
return self._dict[key]
|
||||
if isintlike(key) and self.ndim == 1:
|
||||
key = (key,)
|
||||
if self.ndim != len(key):
|
||||
raise IndexError(f'Index {key} length needs to match self.shape')
|
||||
try:
|
||||
for i in key:
|
||||
assert isintlike(i)
|
||||
except (AssertionError, TypeError, ValueError) as e:
|
||||
raise IndexError('Index must be or consist of integers.') from e
|
||||
key = tuple(i + M if i < 0 else i for i, M in zip(key, self.shape))
|
||||
if any(i < 0 or i >= M for i, M in zip(key, self.shape)):
|
||||
raise IndexError('Index out of bounds.')
|
||||
if self.ndim == 1:
|
||||
key = key[0]
|
||||
return self._dict.get(key, default)
|
||||
|
||||
# override IndexMixin.__getitem__ for 1d case until fully implemented
|
||||
def __getitem__(self, key):
|
||||
if self.ndim == 2:
|
||||
return super().__getitem__(key)
|
||||
|
||||
if isinstance(key, tuple) and len(key) == 1:
|
||||
key = key[0]
|
||||
INT_TYPES = (int, np.integer)
|
||||
if isinstance(key, INT_TYPES):
|
||||
if key < 0:
|
||||
key += self.shape[-1]
|
||||
if key < 0 or key >= self.shape[-1]:
|
||||
raise IndexError('index value out of bounds')
|
||||
return self._get_int(key)
|
||||
else:
|
||||
raise IndexError('array/slice index for 1d dok_array not yet supported')
|
||||
|
||||
# 1D get methods
|
||||
def _get_int(self, idx):
|
||||
return self._dict.get(idx, self.dtype.type(0))
|
||||
|
||||
# 2D get methods
|
||||
def _get_intXint(self, row, col):
|
||||
return self._dict.get((row, col), self.dtype.type(0))
|
||||
|
||||
def _get_intXslice(self, row, col):
|
||||
return self._get_sliceXslice(slice(row, row + 1), col)
|
||||
|
||||
def _get_sliceXint(self, row, col):
|
||||
return self._get_sliceXslice(row, slice(col, col + 1))
|
||||
|
||||
def _get_sliceXslice(self, row, col):
|
||||
row_start, row_stop, row_step = row.indices(self.shape[0])
|
||||
col_start, col_stop, col_step = col.indices(self.shape[1])
|
||||
row_range = range(row_start, row_stop, row_step)
|
||||
col_range = range(col_start, col_stop, col_step)
|
||||
shape = (len(row_range), len(col_range))
|
||||
# Switch paths only when advantageous
|
||||
# (count the iterations in the loops, adjust for complexity)
|
||||
if len(self) >= 2 * shape[0] * shape[1]:
|
||||
# O(nr*nc) path: loop over <row x col>
|
||||
return self._get_columnXarray(row_range, col_range)
|
||||
# O(nnz) path: loop over entries of self
|
||||
newdok = self._dok_container(shape, dtype=self.dtype)
|
||||
for key in self.keys():
|
||||
i, ri = divmod(int(key[0]) - row_start, row_step)
|
||||
if ri != 0 or i < 0 or i >= shape[0]:
|
||||
continue
|
||||
j, rj = divmod(int(key[1]) - col_start, col_step)
|
||||
if rj != 0 or j < 0 or j >= shape[1]:
|
||||
continue
|
||||
newdok._dict[i, j] = self._dict[key]
|
||||
return newdok
|
||||
|
||||
def _get_intXarray(self, row, col):
|
||||
col = col.squeeze()
|
||||
return self._get_columnXarray([row], col)
|
||||
|
||||
def _get_arrayXint(self, row, col):
|
||||
row = row.squeeze()
|
||||
return self._get_columnXarray(row, [col])
|
||||
|
||||
def _get_sliceXarray(self, row, col):
|
||||
row = list(range(*row.indices(self.shape[0])))
|
||||
return self._get_columnXarray(row, col)
|
||||
|
||||
def _get_arrayXslice(self, row, col):
|
||||
col = list(range(*col.indices(self.shape[1])))
|
||||
return self._get_columnXarray(row, col)
|
||||
|
||||
def _get_columnXarray(self, row, col):
|
||||
# outer indexing
|
||||
newdok = self._dok_container((len(row), len(col)), dtype=self.dtype)
|
||||
|
||||
for i, r in enumerate(row):
|
||||
for j, c in enumerate(col):
|
||||
v = self._dict.get((r, c), 0)
|
||||
if v:
|
||||
newdok._dict[i, j] = v
|
||||
return newdok
|
||||
|
||||
def _get_arrayXarray(self, row, col):
|
||||
# inner indexing
|
||||
i, j = map(np.atleast_2d, np.broadcast_arrays(row, col))
|
||||
newdok = self._dok_container(i.shape, dtype=self.dtype)
|
||||
|
||||
for key in itertools.product(range(i.shape[0]), range(i.shape[1])):
|
||||
v = self._dict.get((i[key], j[key]), 0)
|
||||
if v:
|
||||
newdok._dict[key] = v
|
||||
return newdok
|
||||
|
||||
# override IndexMixin.__setitem__ for 1d case until fully implemented
|
||||
def __setitem__(self, key, value):
|
||||
if self.ndim == 2:
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
if isinstance(key, tuple) and len(key) == 1:
|
||||
key = key[0]
|
||||
INT_TYPES = (int, np.integer)
|
||||
if isinstance(key, INT_TYPES):
|
||||
if key < 0:
|
||||
key += self.shape[-1]
|
||||
if key < 0 or key >= self.shape[-1]:
|
||||
raise IndexError('index value out of bounds')
|
||||
return self._set_int(key, value)
|
||||
else:
|
||||
raise IndexError('array index for 1d dok_array not yet provided')
|
||||
|
||||
# 1D set methods
|
||||
def _set_int(self, idx, x):
|
||||
if x:
|
||||
self._dict[idx] = x
|
||||
elif idx in self._dict:
|
||||
del self._dict[idx]
|
||||
|
||||
# 2D set methods
|
||||
def _set_intXint(self, row, col, x):
|
||||
key = (row, col)
|
||||
if x:
|
||||
self._dict[key] = x
|
||||
elif key in self._dict:
|
||||
del self._dict[key]
|
||||
|
||||
def _set_arrayXarray(self, row, col, x):
|
||||
row = list(map(int, row.ravel()))
|
||||
col = list(map(int, col.ravel()))
|
||||
x = x.ravel()
|
||||
self._dict.update(zip(zip(row, col), x))
|
||||
|
||||
for i in np.nonzero(x == 0)[0]:
|
||||
key = (row[i], col[i])
|
||||
if self._dict[key] == 0:
|
||||
# may have been superseded by later update
|
||||
del self._dict[key]
|
||||
|
||||
def __add__(self, other):
|
||||
if isscalarlike(other):
|
||||
res_dtype = upcast_scalar(self.dtype, other)
|
||||
new = self._dok_container(self.shape, dtype=res_dtype)
|
||||
# Add this scalar to each element.
|
||||
for key in itertools.product(*[range(d) for d in self.shape]):
|
||||
aij = self._dict.get(key, 0) + other
|
||||
if aij:
|
||||
new[key] = aij
|
||||
elif issparse(other):
|
||||
if other.shape != self.shape:
|
||||
raise ValueError("Matrix dimensions are not equal.")
|
||||
res_dtype = upcast(self.dtype, other.dtype)
|
||||
new = self._dok_container(self.shape, dtype=res_dtype)
|
||||
new._dict = self._dict.copy()
|
||||
if other.format == "dok":
|
||||
o_items = other.items()
|
||||
else:
|
||||
other = other.tocoo()
|
||||
if self.ndim == 1:
|
||||
o_items = zip(other.coords[0], other.data)
|
||||
else:
|
||||
o_items = zip(zip(*other.coords), other.data)
|
||||
with np.errstate(over='ignore'):
|
||||
new._dict.update((k, new[k] + v) for k, v in o_items)
|
||||
elif isdense(other):
|
||||
new = self.todense() + other
|
||||
else:
|
||||
return NotImplemented
|
||||
return new
|
||||
|
||||
def __radd__(self, other):
|
||||
return self + other # addition is comutative
|
||||
|
||||
def __neg__(self):
|
||||
if self.dtype.kind == 'b':
|
||||
raise NotImplementedError(
|
||||
'Negating a sparse boolean matrix is not supported.'
|
||||
)
|
||||
new = self._dok_container(self.shape, dtype=self.dtype)
|
||||
new._dict.update((k, -v) for k, v in self.items())
|
||||
return new
|
||||
|
||||
def _mul_scalar(self, other):
|
||||
res_dtype = upcast_scalar(self.dtype, other)
|
||||
# Multiply this scalar by every element.
|
||||
new = self._dok_container(self.shape, dtype=res_dtype)
|
||||
new._dict.update(((k, v * other) for k, v in self.items()))
|
||||
return new
|
||||
|
||||
def _matmul_vector(self, other):
|
||||
res_dtype = upcast(self.dtype, other.dtype)
|
||||
|
||||
# vector @ vector
|
||||
if self.ndim == 1:
|
||||
if issparse(other):
|
||||
if other.format == "dok":
|
||||
keys = self.keys() & other.keys()
|
||||
else:
|
||||
keys = self.keys() & other.tocoo().coords[0]
|
||||
return res_dtype(sum(self._dict[k] * other._dict[k] for k in keys))
|
||||
elif isdense(other):
|
||||
return res_dtype(sum(other[k] * v for k, v in self.items()))
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
# matrix @ vector
|
||||
result = np.zeros(self.shape[0], dtype=res_dtype)
|
||||
for (i, j), v in self.items():
|
||||
result[i] += v * other[j]
|
||||
return result
|
||||
|
||||
def _matmul_multivector(self, other):
|
||||
result_dtype = upcast(self.dtype, other.dtype)
|
||||
# vector @ multivector
|
||||
if self.ndim == 1:
|
||||
# works for other 1d or 2d
|
||||
return sum(v * other[j] for j, v in self._dict.items())
|
||||
|
||||
# matrix @ multivector
|
||||
M = self.shape[0]
|
||||
new_shape = (M,) if other.ndim == 1 else (M, other.shape[1])
|
||||
result = np.zeros(new_shape, dtype=result_dtype)
|
||||
for (i, j), v in self.items():
|
||||
result[i] += v * other[j]
|
||||
return result
|
||||
|
||||
def __imul__(self, other):
|
||||
if isscalarlike(other):
|
||||
self._dict.update((k, v * other) for k, v in self.items())
|
||||
return self
|
||||
return NotImplemented
|
||||
|
||||
def __truediv__(self, other):
|
||||
if isscalarlike(other):
|
||||
res_dtype = upcast_scalar(self.dtype, other)
|
||||
new = self._dok_container(self.shape, dtype=res_dtype)
|
||||
new._dict.update(((k, v / other) for k, v in self.items()))
|
||||
return new
|
||||
return self.tocsr() / other
|
||||
|
||||
def __itruediv__(self, other):
|
||||
if isscalarlike(other):
|
||||
self._dict.update((k, v / other) for k, v in self.items())
|
||||
return self
|
||||
return NotImplemented
|
||||
|
||||
def __reduce__(self):
|
||||
# this approach is necessary because __setstate__ is called after
|
||||
# __setitem__ upon unpickling and since __init__ is not called there
|
||||
# is no shape attribute hence it is not possible to unpickle it.
|
||||
return dict.__reduce__(self)
|
||||
|
||||
def diagonal(self, k=0):
|
||||
if self.ndim == 2:
|
||||
return super().diagonal(k)
|
||||
raise ValueError("diagonal requires two dimensions")
|
||||
|
||||
def transpose(self, axes=None, copy=False):
|
||||
if self.ndim == 1:
|
||||
return self.copy()
|
||||
|
||||
if axes is not None and axes != (1, 0):
|
||||
raise ValueError(
|
||||
"Sparse arrays/matrices do not support "
|
||||
"an 'axes' parameter because swapping "
|
||||
"dimensions is the only logical permutation."
|
||||
)
|
||||
|
||||
M, N = self.shape
|
||||
new = self._dok_container((N, M), dtype=self.dtype, copy=copy)
|
||||
new._dict.update((((right, left), val) for (left, right), val in self.items()))
|
||||
return new
|
||||
|
||||
transpose.__doc__ = _spbase.transpose.__doc__
|
||||
|
||||
def conjtransp(self):
|
||||
"""DEPRECATED: Return the conjugate transpose.
|
||||
|
||||
.. deprecated:: 1.14.0
|
||||
|
||||
`conjtransp` is deprecated and will be removed in v1.16.0.
|
||||
Use `.T.conj()` instead.
|
||||
"""
|
||||
msg = ("`conjtransp` is deprecated and will be removed in v1.16.0. "
|
||||
"Use `.T.conj()` instead.")
|
||||
warn(msg, DeprecationWarning, stacklevel=2)
|
||||
|
||||
if self.ndim == 1:
|
||||
new = self.tocoo()
|
||||
new.data = new.data.conjugate()
|
||||
return new
|
||||
|
||||
M, N = self.shape
|
||||
new = self._dok_container((N, M), dtype=self.dtype)
|
||||
new._dict = {(right, left): np.conj(val) for (left, right), val in self.items()}
|
||||
return new
|
||||
|
||||
def copy(self):
|
||||
new = self._dok_container(self.shape, dtype=self.dtype)
|
||||
new._dict.update(self._dict)
|
||||
return new
|
||||
|
||||
copy.__doc__ = _spbase.copy.__doc__
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, value=1, /):
|
||||
tmp = dict.fromkeys(iterable, value)
|
||||
if isinstance(next(iter(tmp)), tuple):
|
||||
shape = tuple(max(idx) + 1 for idx in zip(*tmp))
|
||||
else:
|
||||
shape = (max(tmp) + 1,)
|
||||
result = cls(shape, dtype=type(value))
|
||||
result._dict = tmp
|
||||
return result
|
||||
|
||||
def tocoo(self, copy=False):
|
||||
nnz = self.nnz
|
||||
if nnz == 0:
|
||||
return self._coo_container(self.shape, dtype=self.dtype)
|
||||
|
||||
idx_dtype = self._get_index_dtype(maxval=max(self.shape))
|
||||
data = np.fromiter(self.values(), dtype=self.dtype, count=nnz)
|
||||
# handle 1d keys specially b/c not a tuple
|
||||
inds = zip(*self.keys()) if self.ndim > 1 else (self.keys(),)
|
||||
coords = tuple(np.fromiter(ix, dtype=idx_dtype, count=nnz) for ix in inds)
|
||||
A = self._coo_container((data, coords), shape=self.shape, dtype=self.dtype)
|
||||
A.has_canonical_format = True
|
||||
return A
|
||||
|
||||
tocoo.__doc__ = _spbase.tocoo.__doc__
|
||||
|
||||
def todok(self, copy=False):
|
||||
if copy:
|
||||
return self.copy()
|
||||
return self
|
||||
|
||||
todok.__doc__ = _spbase.todok.__doc__
|
||||
|
||||
def tocsc(self, copy=False):
|
||||
if self.ndim == 1:
|
||||
raise NotImplementedError("tocsr() not valid for 1d sparse array")
|
||||
return self.tocoo(copy=False).tocsc(copy=copy)
|
||||
|
||||
tocsc.__doc__ = _spbase.tocsc.__doc__
|
||||
|
||||
def resize(self, *shape):
|
||||
is_array = isinstance(self, sparray)
|
||||
shape = check_shape(shape, allow_1d=is_array)
|
||||
if len(shape) != len(self.shape):
|
||||
# TODO implement resize across dimensions
|
||||
raise NotImplementedError
|
||||
|
||||
if self.ndim == 1:
|
||||
newN = shape[-1]
|
||||
for i in list(self._dict):
|
||||
if i >= newN:
|
||||
del self._dict[i]
|
||||
self._shape = shape
|
||||
return
|
||||
|
||||
newM, newN = shape
|
||||
M, N = self.shape
|
||||
if newM < M or newN < N:
|
||||
# Remove all elements outside new dimensions
|
||||
for i, j in list(self.keys()):
|
||||
if i >= newM or j >= newN:
|
||||
del self._dict[i, j]
|
||||
self._shape = shape
|
||||
|
||||
resize.__doc__ = _spbase.resize.__doc__
|
||||
|
||||
# Added for 1d to avoid `tocsr` from _base.py
|
||||
def astype(self, dtype, casting='unsafe', copy=True):
|
||||
dtype = np.dtype(dtype)
|
||||
if self.dtype != dtype:
|
||||
result = self._dok_container(self.shape, dtype=dtype)
|
||||
data = np.array(list(self._dict.values()), dtype=dtype)
|
||||
result._dict = dict(zip(self._dict, data))
|
||||
return result
|
||||
elif copy:
|
||||
return self.copy()
|
||||
return self
|
||||
|
||||
|
||||
def isspmatrix_dok(x):
|
||||
"""Is `x` of dok_array type?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x
|
||||
object to check for being a dok matrix
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if `x` is a dok matrix, False otherwise
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy.sparse import dok_array, dok_matrix, coo_matrix, isspmatrix_dok
|
||||
>>> isspmatrix_dok(dok_matrix([[5]]))
|
||||
True
|
||||
>>> isspmatrix_dok(dok_array([[5]]))
|
||||
False
|
||||
>>> isspmatrix_dok(coo_matrix([[5]]))
|
||||
False
|
||||
"""
|
||||
return isinstance(x, dok_matrix)
|
||||
|
||||
|
||||
# This namespace class separates array from matrix with isinstance
|
||||
class dok_array(_dok_base, sparray):
|
||||
"""
|
||||
Dictionary Of Keys based sparse array.
|
||||
|
||||
This is an efficient structure for constructing sparse
|
||||
arrays incrementally.
|
||||
|
||||
This can be instantiated in several ways:
|
||||
dok_array(D)
|
||||
where D is a 2-D ndarray
|
||||
|
||||
dok_array(S)
|
||||
with another sparse array or matrix S (equivalent to S.todok())
|
||||
|
||||
dok_array((M,N), [dtype])
|
||||
create the array with initial shape (M,N)
|
||||
dtype is optional, defaulting to dtype='d'
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dtype : dtype
|
||||
Data type of the array
|
||||
shape : 2-tuple
|
||||
Shape of the array
|
||||
ndim : int
|
||||
Number of dimensions (this is always 2)
|
||||
nnz
|
||||
Number of nonzero elements
|
||||
size
|
||||
T
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
Sparse arrays can be used in arithmetic operations: they support
|
||||
addition, subtraction, multiplication, division, and matrix power.
|
||||
|
||||
- Allows for efficient O(1) access of individual elements.
|
||||
- Duplicates are not allowed.
|
||||
- Can be efficiently converted to a coo_array once constructed.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import dok_array
|
||||
>>> S = dok_array((5, 5), dtype=np.float32)
|
||||
>>> for i in range(5):
|
||||
... for j in range(5):
|
||||
... S[i, j] = i + j # Update element
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class dok_matrix(spmatrix, _dok_base):
|
||||
"""
|
||||
Dictionary Of Keys based sparse matrix.
|
||||
|
||||
This is an efficient structure for constructing sparse
|
||||
matrices incrementally.
|
||||
|
||||
This can be instantiated in several ways:
|
||||
dok_matrix(D)
|
||||
where D is a 2-D ndarray
|
||||
|
||||
dok_matrix(S)
|
||||
with another sparse array or matrix S (equivalent to S.todok())
|
||||
|
||||
dok_matrix((M,N), [dtype])
|
||||
create the matrix with initial shape (M,N)
|
||||
dtype is optional, defaulting to dtype='d'
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dtype : dtype
|
||||
Data type of the matrix
|
||||
shape : 2-tuple
|
||||
Shape of the matrix
|
||||
ndim : int
|
||||
Number of dimensions (this is always 2)
|
||||
nnz
|
||||
Number of nonzero elements
|
||||
size
|
||||
T
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
Sparse matrices can be used in arithmetic operations: they support
|
||||
addition, subtraction, multiplication, division, and matrix power.
|
||||
|
||||
- Allows for efficient O(1) access of individual elements.
|
||||
- Duplicates are not allowed.
|
||||
- Can be efficiently converted to a coo_matrix once constructed.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import dok_matrix
|
||||
>>> S = dok_matrix((5, 5), dtype=np.float32)
|
||||
>>> for i in range(5):
|
||||
... for j in range(5):
|
||||
... S[i, j] = i + j # Update element
|
||||
|
||||
"""
|
||||
|
||||
def set_shape(self, shape):
|
||||
new_matrix = self.reshape(shape, copy=False).asformat(self.format)
|
||||
self.__dict__ = new_matrix.__dict__
|
||||
|
||||
def get_shape(self):
|
||||
"""Get shape of a sparse matrix."""
|
||||
return self._shape
|
||||
|
||||
shape = property(fget=get_shape, fset=set_shape)
|
||||
|
||||
def __reversed__(self):
|
||||
return self._dict.__reversed__()
|
||||
|
||||
def __or__(self, other):
|
||||
if isinstance(other, _dok_base):
|
||||
return self._dict | other._dict
|
||||
return self._dict | other
|
||||
|
||||
def __ror__(self, other):
|
||||
if isinstance(other, _dok_base):
|
||||
return self._dict | other._dict
|
||||
return self._dict | other
|
||||
|
||||
def __ior__(self, other):
|
||||
if isinstance(other, _dok_base):
|
||||
self._dict |= other._dict
|
||||
else:
|
||||
self._dict |= other
|
||||
return self
|
||||
178
venv/lib/python3.12/site-packages/scipy/sparse/_extract.py
Normal file
178
venv/lib/python3.12/site-packages/scipy/sparse/_extract.py
Normal file
@ -0,0 +1,178 @@
|
||||
"""Functions to extract parts of sparse matrices
|
||||
"""
|
||||
|
||||
__docformat__ = "restructuredtext en"
|
||||
|
||||
__all__ = ['find', 'tril', 'triu']
|
||||
|
||||
|
||||
from ._coo import coo_matrix, coo_array
|
||||
from ._base import sparray
|
||||
|
||||
|
||||
def find(A):
|
||||
"""Return the indices and values of the nonzero elements of a matrix
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : dense or sparse array or matrix
|
||||
Matrix whose nonzero elements are desired.
|
||||
|
||||
Returns
|
||||
-------
|
||||
(I,J,V) : tuple of arrays
|
||||
I,J, and V contain the row indices, column indices, and values
|
||||
of the nonzero entries.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy.sparse import csr_array, find
|
||||
>>> A = csr_array([[7.0, 8.0, 0],[0, 0, 9.0]])
|
||||
>>> find(A)
|
||||
(array([0, 0, 1], dtype=int32),
|
||||
array([0, 1, 2], dtype=int32),
|
||||
array([ 7., 8., 9.]))
|
||||
|
||||
"""
|
||||
|
||||
A = coo_array(A, copy=True)
|
||||
A.sum_duplicates()
|
||||
# remove explicit zeros
|
||||
nz_mask = A.data != 0
|
||||
return A.row[nz_mask], A.col[nz_mask], A.data[nz_mask]
|
||||
|
||||
|
||||
def tril(A, k=0, format=None):
|
||||
"""Return the lower triangular portion of a sparse array or matrix
|
||||
|
||||
Returns the elements on or below the k-th diagonal of A.
|
||||
- k = 0 corresponds to the main diagonal
|
||||
- k > 0 is above the main diagonal
|
||||
- k < 0 is below the main diagonal
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : dense or sparse array or matrix
|
||||
Matrix whose lower trianglar portion is desired.
|
||||
k : integer : optional
|
||||
The top-most diagonal of the lower triangle.
|
||||
format : string
|
||||
Sparse format of the result, e.g. format="csr", etc.
|
||||
|
||||
Returns
|
||||
-------
|
||||
L : sparse matrix
|
||||
Lower triangular portion of A in sparse format.
|
||||
|
||||
See Also
|
||||
--------
|
||||
triu : upper triangle in sparse format
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy.sparse import csr_array, tril
|
||||
>>> A = csr_array([[1, 2, 0, 0, 3], [4, 5, 0, 6, 7], [0, 0, 8, 9, 0]],
|
||||
... dtype='int32')
|
||||
>>> A.toarray()
|
||||
array([[1, 2, 0, 0, 3],
|
||||
[4, 5, 0, 6, 7],
|
||||
[0, 0, 8, 9, 0]])
|
||||
>>> tril(A).toarray()
|
||||
array([[1, 0, 0, 0, 0],
|
||||
[4, 5, 0, 0, 0],
|
||||
[0, 0, 8, 0, 0]])
|
||||
>>> tril(A).nnz
|
||||
4
|
||||
>>> tril(A, k=1).toarray()
|
||||
array([[1, 2, 0, 0, 0],
|
||||
[4, 5, 0, 0, 0],
|
||||
[0, 0, 8, 9, 0]])
|
||||
>>> tril(A, k=-1).toarray()
|
||||
array([[0, 0, 0, 0, 0],
|
||||
[4, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0]])
|
||||
>>> tril(A, format='csc')
|
||||
<Compressed Sparse Column sparse array of dtype 'int32'
|
||||
with 4 stored elements and shape (3, 5)>
|
||||
|
||||
"""
|
||||
coo_sparse = coo_array if isinstance(A, sparray) else coo_matrix
|
||||
|
||||
# convert to COOrdinate format where things are easy
|
||||
A = coo_sparse(A, copy=False)
|
||||
mask = A.row + k >= A.col
|
||||
|
||||
row = A.row[mask]
|
||||
col = A.col[mask]
|
||||
data = A.data[mask]
|
||||
new_coo = coo_sparse((data, (row, col)), shape=A.shape, dtype=A.dtype)
|
||||
return new_coo.asformat(format)
|
||||
|
||||
|
||||
def triu(A, k=0, format=None):
|
||||
"""Return the upper triangular portion of a sparse array or matrix
|
||||
|
||||
Returns the elements on or above the k-th diagonal of A.
|
||||
- k = 0 corresponds to the main diagonal
|
||||
- k > 0 is above the main diagonal
|
||||
- k < 0 is below the main diagonal
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : dense or sparse array or matrix
|
||||
Matrix whose upper trianglar portion is desired.
|
||||
k : integer : optional
|
||||
The bottom-most diagonal of the upper triangle.
|
||||
format : string
|
||||
Sparse format of the result, e.g. format="csr", etc.
|
||||
|
||||
Returns
|
||||
-------
|
||||
L : sparse array or matrix
|
||||
Upper triangular portion of A in sparse format.
|
||||
Sparse array if A is a sparse array, otherwise matrix.
|
||||
|
||||
See Also
|
||||
--------
|
||||
tril : lower triangle in sparse format
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy.sparse import csr_array, triu
|
||||
>>> A = csr_array([[1, 2, 0, 0, 3], [4, 5, 0, 6, 7], [0, 0, 8, 9, 0]],
|
||||
... dtype='int32')
|
||||
>>> A.toarray()
|
||||
array([[1, 2, 0, 0, 3],
|
||||
[4, 5, 0, 6, 7],
|
||||
[0, 0, 8, 9, 0]])
|
||||
>>> triu(A).toarray()
|
||||
array([[1, 2, 0, 0, 3],
|
||||
[0, 5, 0, 6, 7],
|
||||
[0, 0, 8, 9, 0]])
|
||||
>>> triu(A).nnz
|
||||
8
|
||||
>>> triu(A, k=1).toarray()
|
||||
array([[0, 2, 0, 0, 3],
|
||||
[0, 0, 0, 6, 7],
|
||||
[0, 0, 0, 9, 0]])
|
||||
>>> triu(A, k=-1).toarray()
|
||||
array([[1, 2, 0, 0, 3],
|
||||
[4, 5, 0, 6, 7],
|
||||
[0, 0, 8, 9, 0]])
|
||||
>>> triu(A, format='csc')
|
||||
<Compressed Sparse Column sparse array of dtype 'int32'
|
||||
with 8 stored elements and shape (3, 5)>
|
||||
|
||||
"""
|
||||
coo_sparse = coo_array if isinstance(A, sparray) else coo_matrix
|
||||
|
||||
# convert to COOrdinate format where things are easy
|
||||
A = coo_sparse(A, copy=False)
|
||||
mask = A.row + k <= A.col
|
||||
|
||||
row = A.row[mask]
|
||||
col = A.col[mask]
|
||||
data = A.data[mask]
|
||||
new_coo = coo_sparse((data, (row, col)), shape=A.shape, dtype=A.dtype)
|
||||
return new_coo.asformat(format)
|
||||
392
venv/lib/python3.12/site-packages/scipy/sparse/_index.py
Normal file
392
venv/lib/python3.12/site-packages/scipy/sparse/_index.py
Normal file
@ -0,0 +1,392 @@
|
||||
"""Indexing mixin for sparse array/matrix classes.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
from ._sputils import isintlike
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy.typing as npt
|
||||
|
||||
INT_TYPES = (int, np.integer)
|
||||
|
||||
|
||||
def _broadcast_arrays(a, b):
|
||||
"""
|
||||
Same as np.broadcast_arrays(a, b) but old writeability rules.
|
||||
|
||||
NumPy >= 1.17.0 transitions broadcast_arrays to return
|
||||
read-only arrays. Set writeability explicitly to avoid warnings.
|
||||
Retain the old writeability rules, as our Cython code assumes
|
||||
the old behavior.
|
||||
"""
|
||||
x, y = np.broadcast_arrays(a, b)
|
||||
x.flags.writeable = a.flags.writeable
|
||||
y.flags.writeable = b.flags.writeable
|
||||
return x, y
|
||||
|
||||
|
||||
class IndexMixin:
|
||||
"""
|
||||
This class provides common dispatching and validation logic for indexing.
|
||||
"""
|
||||
def _raise_on_1d_array_slice(self):
|
||||
"""We do not currently support 1D sparse arrays.
|
||||
|
||||
This function is called each time that a 1D array would
|
||||
result, raising an error instead.
|
||||
|
||||
Once 1D sparse arrays are implemented, it should be removed.
|
||||
"""
|
||||
from scipy.sparse import sparray
|
||||
|
||||
if isinstance(self, sparray):
|
||||
raise NotImplementedError(
|
||||
'We have not yet implemented 1D sparse slices; '
|
||||
'please index using explicit indices, e.g. `x[:, [0]]`'
|
||||
)
|
||||
|
||||
def __getitem__(self, key):
|
||||
row, col = self._validate_indices(key)
|
||||
|
||||
# Dispatch to specialized methods.
|
||||
if isinstance(row, INT_TYPES):
|
||||
if isinstance(col, INT_TYPES):
|
||||
return self._get_intXint(row, col)
|
||||
elif isinstance(col, slice):
|
||||
self._raise_on_1d_array_slice()
|
||||
return self._get_intXslice(row, col)
|
||||
elif col.ndim == 1:
|
||||
self._raise_on_1d_array_slice()
|
||||
return self._get_intXarray(row, col)
|
||||
elif col.ndim == 2:
|
||||
return self._get_intXarray(row, col)
|
||||
raise IndexError('index results in >2 dimensions')
|
||||
elif isinstance(row, slice):
|
||||
if isinstance(col, INT_TYPES):
|
||||
self._raise_on_1d_array_slice()
|
||||
return self._get_sliceXint(row, col)
|
||||
elif isinstance(col, slice):
|
||||
if row == slice(None) and row == col:
|
||||
return self.copy()
|
||||
return self._get_sliceXslice(row, col)
|
||||
elif col.ndim == 1:
|
||||
return self._get_sliceXarray(row, col)
|
||||
raise IndexError('index results in >2 dimensions')
|
||||
elif row.ndim == 1:
|
||||
if isinstance(col, INT_TYPES):
|
||||
self._raise_on_1d_array_slice()
|
||||
return self._get_arrayXint(row, col)
|
||||
elif isinstance(col, slice):
|
||||
return self._get_arrayXslice(row, col)
|
||||
else: # row.ndim == 2
|
||||
if isinstance(col, INT_TYPES):
|
||||
return self._get_arrayXint(row, col)
|
||||
elif isinstance(col, slice):
|
||||
raise IndexError('index results in >2 dimensions')
|
||||
elif row.shape[1] == 1 and (col.ndim == 1 or col.shape[0] == 1):
|
||||
# special case for outer indexing
|
||||
return self._get_columnXarray(row[:,0], col.ravel())
|
||||
|
||||
# The only remaining case is inner (fancy) indexing
|
||||
row, col = _broadcast_arrays(row, col)
|
||||
if row.shape != col.shape:
|
||||
raise IndexError('number of row and column indices differ')
|
||||
if row.size == 0:
|
||||
return self.__class__(np.atleast_2d(row).shape, dtype=self.dtype)
|
||||
return self._get_arrayXarray(row, col)
|
||||
|
||||
def __setitem__(self, key, x):
|
||||
row, col = self._validate_indices(key)
|
||||
|
||||
if isinstance(row, INT_TYPES) and isinstance(col, INT_TYPES):
|
||||
x = np.asarray(x, dtype=self.dtype)
|
||||
if x.size != 1:
|
||||
raise ValueError('Trying to assign a sequence to an item')
|
||||
self._set_intXint(row, col, x.flat[0])
|
||||
return
|
||||
|
||||
if isinstance(row, slice):
|
||||
row = np.arange(*row.indices(self.shape[0]))[:, None]
|
||||
else:
|
||||
row = np.atleast_1d(row)
|
||||
|
||||
if isinstance(col, slice):
|
||||
col = np.arange(*col.indices(self.shape[1]))[None, :]
|
||||
if row.ndim == 1:
|
||||
row = row[:, None]
|
||||
else:
|
||||
col = np.atleast_1d(col)
|
||||
|
||||
i, j = _broadcast_arrays(row, col)
|
||||
if i.shape != j.shape:
|
||||
raise IndexError('number of row and column indices differ')
|
||||
|
||||
from ._base import issparse
|
||||
if issparse(x):
|
||||
if i.ndim == 1:
|
||||
# Inner indexing, so treat them like row vectors.
|
||||
i = i[None]
|
||||
j = j[None]
|
||||
broadcast_row = x.shape[0] == 1 and i.shape[0] != 1
|
||||
broadcast_col = x.shape[1] == 1 and i.shape[1] != 1
|
||||
if not ((broadcast_row or x.shape[0] == i.shape[0]) and
|
||||
(broadcast_col or x.shape[1] == i.shape[1])):
|
||||
raise ValueError('shape mismatch in assignment')
|
||||
if x.shape[0] == 0 or x.shape[1] == 0:
|
||||
return
|
||||
x = x.tocoo(copy=True)
|
||||
x.sum_duplicates()
|
||||
self._set_arrayXarray_sparse(i, j, x)
|
||||
else:
|
||||
# Make x and i into the same shape
|
||||
x = np.asarray(x, dtype=self.dtype)
|
||||
if x.squeeze().shape != i.squeeze().shape:
|
||||
x = np.broadcast_to(x, i.shape)
|
||||
if x.size == 0:
|
||||
return
|
||||
x = x.reshape(i.shape)
|
||||
self._set_arrayXarray(i, j, x)
|
||||
|
||||
def _validate_indices(self, key):
|
||||
# First, check if indexing with single boolean matrix.
|
||||
from ._base import _spbase
|
||||
if (isinstance(key, (_spbase, np.ndarray)) and
|
||||
key.ndim == 2 and key.dtype.kind == 'b'):
|
||||
if key.shape != self.shape:
|
||||
raise IndexError('boolean index shape does not match array shape')
|
||||
row, col = key.nonzero()
|
||||
else:
|
||||
row, col = _unpack_index(key)
|
||||
M, N = self.shape
|
||||
|
||||
def _validate_bool_idx(
|
||||
idx: npt.NDArray[np.bool_],
|
||||
axis_size: int,
|
||||
axis_name: str
|
||||
) -> npt.NDArray[np.int_]:
|
||||
if len(idx) != axis_size:
|
||||
raise IndexError(
|
||||
f"boolean {axis_name} index has incorrect length: {len(idx)} "
|
||||
f"instead of {axis_size}"
|
||||
)
|
||||
return _boolean_index_to_array(idx)
|
||||
|
||||
if isintlike(row):
|
||||
row = int(row)
|
||||
if row < -M or row >= M:
|
||||
raise IndexError('row index (%d) out of range' % row)
|
||||
if row < 0:
|
||||
row += M
|
||||
elif (bool_row := _compatible_boolean_index(row)) is not None:
|
||||
row = _validate_bool_idx(bool_row, M, "row")
|
||||
elif not isinstance(row, slice):
|
||||
row = self._asindices(row, M)
|
||||
|
||||
if isintlike(col):
|
||||
col = int(col)
|
||||
if col < -N or col >= N:
|
||||
raise IndexError('column index (%d) out of range' % col)
|
||||
if col < 0:
|
||||
col += N
|
||||
elif (bool_col := _compatible_boolean_index(col)) is not None:
|
||||
col = _validate_bool_idx(bool_col, N, "column")
|
||||
elif not isinstance(col, slice):
|
||||
col = self._asindices(col, N)
|
||||
|
||||
return row, col
|
||||
|
||||
def _asindices(self, idx, length):
|
||||
"""Convert `idx` to a valid index for an axis with a given length.
|
||||
|
||||
Subclasses that need special validation can override this method.
|
||||
"""
|
||||
try:
|
||||
x = np.asarray(idx)
|
||||
except (ValueError, TypeError, MemoryError) as e:
|
||||
raise IndexError('invalid index') from e
|
||||
|
||||
if x.ndim not in (1, 2):
|
||||
raise IndexError('Index dimension must be 1 or 2')
|
||||
|
||||
if x.size == 0:
|
||||
return x
|
||||
|
||||
# Check bounds
|
||||
max_indx = x.max()
|
||||
if max_indx >= length:
|
||||
raise IndexError('index (%d) out of range' % max_indx)
|
||||
|
||||
min_indx = x.min()
|
||||
if min_indx < 0:
|
||||
if min_indx < -length:
|
||||
raise IndexError('index (%d) out of range' % min_indx)
|
||||
if x is idx or not x.flags.owndata:
|
||||
x = x.copy()
|
||||
x[x < 0] += length
|
||||
return x
|
||||
|
||||
def _getrow(self, i):
|
||||
"""Return a copy of row i of the matrix, as a (1 x n) row vector.
|
||||
"""
|
||||
M, N = self.shape
|
||||
i = int(i)
|
||||
if i < -M or i >= M:
|
||||
raise IndexError('index (%d) out of range' % i)
|
||||
if i < 0:
|
||||
i += M
|
||||
return self._get_intXslice(i, slice(None))
|
||||
|
||||
def _getcol(self, i):
|
||||
"""Return a copy of column i of the matrix, as a (m x 1) column vector.
|
||||
"""
|
||||
M, N = self.shape
|
||||
i = int(i)
|
||||
if i < -N or i >= N:
|
||||
raise IndexError('index (%d) out of range' % i)
|
||||
if i < 0:
|
||||
i += N
|
||||
return self._get_sliceXint(slice(None), i)
|
||||
|
||||
def _get_intXint(self, row, col):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_intXarray(self, row, col):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_intXslice(self, row, col):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_sliceXint(self, row, col):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_sliceXslice(self, row, col):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_sliceXarray(self, row, col):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_arrayXint(self, row, col):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_arrayXslice(self, row, col):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_columnXarray(self, row, col):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_arrayXarray(self, row, col):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _set_intXint(self, row, col, x):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _set_arrayXarray(self, row, col, x):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _set_arrayXarray_sparse(self, row, col, x):
|
||||
# Fall back to densifying x
|
||||
x = np.asarray(x.toarray(), dtype=self.dtype)
|
||||
x, _ = _broadcast_arrays(x, row)
|
||||
self._set_arrayXarray(row, col, x)
|
||||
|
||||
|
||||
def _unpack_index(index) -> tuple[
|
||||
int | slice | npt.NDArray[np.bool_ | np.int_],
|
||||
int | slice | npt.NDArray[np.bool_ | np.int_]
|
||||
]:
|
||||
""" Parse index. Always return a tuple of the form (row, col).
|
||||
Valid type for row/col is integer, slice, array of bool, or array of integers.
|
||||
"""
|
||||
# Parse any ellipses.
|
||||
index = _check_ellipsis(index)
|
||||
|
||||
# Next, parse the tuple or object
|
||||
if isinstance(index, tuple):
|
||||
if len(index) == 2:
|
||||
row, col = index
|
||||
elif len(index) == 1:
|
||||
row, col = index[0], slice(None)
|
||||
else:
|
||||
raise IndexError('invalid number of indices')
|
||||
else:
|
||||
idx = _compatible_boolean_index(index)
|
||||
if idx is None:
|
||||
row, col = index, slice(None)
|
||||
elif idx.ndim < 2:
|
||||
return idx, slice(None)
|
||||
elif idx.ndim == 2:
|
||||
return idx.nonzero()
|
||||
# Next, check for validity and transform the index as needed.
|
||||
from ._base import issparse
|
||||
if issparse(row) or issparse(col):
|
||||
# Supporting sparse boolean indexing with both row and col does
|
||||
# not work because spmatrix.ndim is always 2.
|
||||
raise IndexError(
|
||||
'Indexing with sparse matrices is not supported '
|
||||
'except boolean indexing where matrix and index '
|
||||
'are equal shapes.')
|
||||
return row, col
|
||||
|
||||
|
||||
def _check_ellipsis(index):
|
||||
"""Process indices with Ellipsis. Returns modified index."""
|
||||
if index is Ellipsis:
|
||||
return (slice(None), slice(None))
|
||||
|
||||
if not isinstance(index, tuple):
|
||||
return index
|
||||
|
||||
# Find any Ellipsis objects.
|
||||
ellipsis_indices = [i for i, v in enumerate(index) if v is Ellipsis]
|
||||
if not ellipsis_indices:
|
||||
return index
|
||||
if len(ellipsis_indices) > 1:
|
||||
raise IndexError("an index can only have a single ellipsis ('...')")
|
||||
|
||||
# Replace the Ellipsis object with 0, 1, or 2 null-slices as needed.
|
||||
i, = ellipsis_indices
|
||||
num_slices = max(0, 3 - len(index))
|
||||
return index[:i] + (slice(None),) * num_slices + index[i + 1:]
|
||||
|
||||
|
||||
def _maybe_bool_ndarray(idx):
|
||||
"""Returns a compatible array if elements are boolean.
|
||||
"""
|
||||
idx = np.asanyarray(idx)
|
||||
if idx.dtype.kind == 'b':
|
||||
return idx
|
||||
return None
|
||||
|
||||
|
||||
def _first_element_bool(idx, max_dim=2):
|
||||
"""Returns True if first element of the incompatible
|
||||
array type is boolean.
|
||||
"""
|
||||
if max_dim < 1:
|
||||
return None
|
||||
try:
|
||||
first = next(iter(idx), None)
|
||||
except TypeError:
|
||||
return None
|
||||
if isinstance(first, bool):
|
||||
return True
|
||||
return _first_element_bool(first, max_dim-1)
|
||||
|
||||
|
||||
def _compatible_boolean_index(idx):
|
||||
"""Returns a boolean index array that can be converted to
|
||||
integer array. Returns None if no such array exists.
|
||||
"""
|
||||
# Presence of attribute `ndim` indicates a compatible array type.
|
||||
if hasattr(idx, 'ndim') or _first_element_bool(idx):
|
||||
return _maybe_bool_ndarray(idx)
|
||||
return None
|
||||
|
||||
|
||||
def _boolean_index_to_array(idx):
|
||||
if idx.ndim > 1:
|
||||
raise IndexError('invalid index shape')
|
||||
return np.where(idx)[0]
|
||||
612
venv/lib/python3.12/site-packages/scipy/sparse/_lil.py
Normal file
612
venv/lib/python3.12/site-packages/scipy/sparse/_lil.py
Normal file
@ -0,0 +1,612 @@
|
||||
"""List of Lists sparse matrix class
|
||||
"""
|
||||
|
||||
__docformat__ = "restructuredtext en"
|
||||
|
||||
__all__ = ['lil_array', 'lil_matrix', 'isspmatrix_lil']
|
||||
|
||||
from bisect import bisect_left
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ._matrix import spmatrix
|
||||
from ._base import _spbase, sparray, issparse
|
||||
from ._index import IndexMixin, INT_TYPES, _broadcast_arrays
|
||||
from ._sputils import (getdtype, isshape, isscalarlike, upcast_scalar,
|
||||
check_shape, check_reshape_kwargs)
|
||||
from . import _csparsetools
|
||||
|
||||
|
||||
class _lil_base(_spbase, IndexMixin):
|
||||
_format = 'lil'
|
||||
|
||||
def __init__(self, arg1, shape=None, dtype=None, copy=False):
|
||||
_spbase.__init__(self, arg1)
|
||||
self.dtype = getdtype(dtype, arg1, default=float)
|
||||
|
||||
# First get the shape
|
||||
if issparse(arg1):
|
||||
if arg1.format == "lil" and copy:
|
||||
A = arg1.copy()
|
||||
else:
|
||||
A = arg1.tolil()
|
||||
|
||||
if dtype is not None:
|
||||
A = A.astype(dtype, copy=False)
|
||||
|
||||
self._shape = check_shape(A.shape)
|
||||
self.dtype = A.dtype
|
||||
self.rows = A.rows
|
||||
self.data = A.data
|
||||
elif isinstance(arg1,tuple):
|
||||
if isshape(arg1):
|
||||
if shape is not None:
|
||||
raise ValueError('invalid use of shape parameter')
|
||||
M, N = arg1
|
||||
self._shape = check_shape((M, N))
|
||||
self.rows = np.empty((M,), dtype=object)
|
||||
self.data = np.empty((M,), dtype=object)
|
||||
for i in range(M):
|
||||
self.rows[i] = []
|
||||
self.data[i] = []
|
||||
else:
|
||||
raise TypeError('unrecognized lil_array constructor usage')
|
||||
else:
|
||||
# assume A is dense
|
||||
try:
|
||||
A = self._ascontainer(arg1)
|
||||
except TypeError as e:
|
||||
raise TypeError('unsupported matrix type') from e
|
||||
if isinstance(self, sparray) and A.ndim != 2:
|
||||
raise ValueError(f"LIL arrays don't support {A.ndim}D input. Use 2D")
|
||||
A = self._csr_container(A, dtype=dtype).tolil()
|
||||
|
||||
self._shape = check_shape(A.shape)
|
||||
self.dtype = A.dtype
|
||||
self.rows = A.rows
|
||||
self.data = A.data
|
||||
|
||||
def __iadd__(self,other):
|
||||
self[:,:] = self + other
|
||||
return self
|
||||
|
||||
def __isub__(self,other):
|
||||
self[:,:] = self - other
|
||||
return self
|
||||
|
||||
def __imul__(self,other):
|
||||
if isscalarlike(other):
|
||||
self[:,:] = self * other
|
||||
return self
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __itruediv__(self,other):
|
||||
if isscalarlike(other):
|
||||
self[:,:] = self / other
|
||||
return self
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
# Whenever the dimensions change, empty lists should be created for each
|
||||
# row
|
||||
|
||||
def _getnnz(self, axis=None):
|
||||
if axis is None:
|
||||
return sum([len(rowvals) for rowvals in self.data])
|
||||
if axis < 0:
|
||||
axis += 2
|
||||
if axis == 0:
|
||||
out = np.zeros(self.shape[1], dtype=np.intp)
|
||||
for row in self.rows:
|
||||
out[row] += 1
|
||||
return out
|
||||
elif axis == 1:
|
||||
return np.array([len(rowvals) for rowvals in self.data], dtype=np.intp)
|
||||
else:
|
||||
raise ValueError('axis out of bounds')
|
||||
|
||||
def count_nonzero(self):
|
||||
return sum(np.count_nonzero(rowvals) for rowvals in self.data)
|
||||
|
||||
_getnnz.__doc__ = _spbase._getnnz.__doc__
|
||||
count_nonzero.__doc__ = _spbase.count_nonzero.__doc__
|
||||
|
||||
def getrowview(self, i):
|
||||
"""Returns a view of the 'i'th row (without copying).
|
||||
"""
|
||||
new = self._lil_container((1, self.shape[1]), dtype=self.dtype)
|
||||
new.rows[0] = self.rows[i]
|
||||
new.data[0] = self.data[i]
|
||||
return new
|
||||
|
||||
def getrow(self, i):
|
||||
"""Returns a copy of the 'i'th row.
|
||||
"""
|
||||
M, N = self.shape
|
||||
if i < 0:
|
||||
i += M
|
||||
if i < 0 or i >= M:
|
||||
raise IndexError('row index out of bounds')
|
||||
new = self._lil_container((1, N), dtype=self.dtype)
|
||||
new.rows[0] = self.rows[i][:]
|
||||
new.data[0] = self.data[i][:]
|
||||
return new
|
||||
|
||||
def __getitem__(self, key):
|
||||
# Fast path for simple (int, int) indexing.
|
||||
if (isinstance(key, tuple) and len(key) == 2 and
|
||||
isinstance(key[0], INT_TYPES) and
|
||||
isinstance(key[1], INT_TYPES)):
|
||||
# lil_get1 handles validation for us.
|
||||
return self._get_intXint(*key)
|
||||
# Everything else takes the normal path.
|
||||
return IndexMixin.__getitem__(self, key)
|
||||
|
||||
def _asindices(self, idx, N):
|
||||
# LIL routines handle bounds-checking for us, so don't do it here.
|
||||
try:
|
||||
x = np.asarray(idx)
|
||||
except (ValueError, TypeError, MemoryError) as e:
|
||||
raise IndexError('invalid index') from e
|
||||
if x.ndim not in (1, 2):
|
||||
raise IndexError('Index dimension must be <= 2')
|
||||
return x
|
||||
|
||||
def _get_intXint(self, row, col):
|
||||
v = _csparsetools.lil_get1(self.shape[0], self.shape[1], self.rows,
|
||||
self.data, row, col)
|
||||
return self.dtype.type(v)
|
||||
|
||||
def _get_sliceXint(self, row, col):
|
||||
row = range(*row.indices(self.shape[0]))
|
||||
return self._get_row_ranges(row, slice(col, col+1))
|
||||
|
||||
def _get_arrayXint(self, row, col):
|
||||
row = row.squeeze()
|
||||
return self._get_row_ranges(row, slice(col, col+1))
|
||||
|
||||
def _get_intXslice(self, row, col):
|
||||
return self._get_row_ranges((row,), col)
|
||||
|
||||
def _get_sliceXslice(self, row, col):
|
||||
row = range(*row.indices(self.shape[0]))
|
||||
return self._get_row_ranges(row, col)
|
||||
|
||||
def _get_arrayXslice(self, row, col):
|
||||
return self._get_row_ranges(row, col)
|
||||
|
||||
def _get_intXarray(self, row, col):
|
||||
row = np.array(row, dtype=col.dtype, ndmin=1)
|
||||
return self._get_columnXarray(row, col)
|
||||
|
||||
def _get_sliceXarray(self, row, col):
|
||||
row = np.arange(*row.indices(self.shape[0]))
|
||||
return self._get_columnXarray(row, col)
|
||||
|
||||
def _get_columnXarray(self, row, col):
|
||||
# outer indexing
|
||||
row, col = _broadcast_arrays(row[:,None], col)
|
||||
return self._get_arrayXarray(row, col)
|
||||
|
||||
def _get_arrayXarray(self, row, col):
|
||||
# inner indexing
|
||||
i, j = map(np.atleast_2d, _prepare_index_for_memoryview(row, col))
|
||||
new = self._lil_container(i.shape, dtype=self.dtype)
|
||||
_csparsetools.lil_fancy_get(self.shape[0], self.shape[1],
|
||||
self.rows, self.data,
|
||||
new.rows, new.data,
|
||||
i, j)
|
||||
return new
|
||||
|
||||
def _get_row_ranges(self, rows, col_slice):
|
||||
"""
|
||||
Fast path for indexing in the case where column index is slice.
|
||||
|
||||
This gains performance improvement over brute force by more
|
||||
efficient skipping of zeros, by accessing the elements
|
||||
column-wise in order.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
rows : sequence or range
|
||||
Rows indexed. If range, must be within valid bounds.
|
||||
col_slice : slice
|
||||
Columns indexed
|
||||
|
||||
"""
|
||||
j_start, j_stop, j_stride = col_slice.indices(self.shape[1])
|
||||
col_range = range(j_start, j_stop, j_stride)
|
||||
nj = len(col_range)
|
||||
new = self._lil_container((len(rows), nj), dtype=self.dtype)
|
||||
|
||||
_csparsetools.lil_get_row_ranges(self.shape[0], self.shape[1],
|
||||
self.rows, self.data,
|
||||
new.rows, new.data,
|
||||
rows,
|
||||
j_start, j_stop, j_stride, nj)
|
||||
|
||||
return new
|
||||
|
||||
def _set_intXint(self, row, col, x):
|
||||
_csparsetools.lil_insert(self.shape[0], self.shape[1], self.rows,
|
||||
self.data, row, col, x)
|
||||
|
||||
def _set_arrayXarray(self, row, col, x):
|
||||
i, j, x = map(np.atleast_2d, _prepare_index_for_memoryview(row, col, x))
|
||||
_csparsetools.lil_fancy_set(self.shape[0], self.shape[1],
|
||||
self.rows, self.data,
|
||||
i, j, x)
|
||||
|
||||
def _set_arrayXarray_sparse(self, row, col, x):
|
||||
# Fall back to densifying x
|
||||
x = np.asarray(x.toarray(), dtype=self.dtype)
|
||||
x, _ = _broadcast_arrays(x, row)
|
||||
self._set_arrayXarray(row, col, x)
|
||||
|
||||
def __setitem__(self, key, x):
|
||||
if isinstance(key, tuple) and len(key) == 2:
|
||||
row, col = key
|
||||
# Fast path for simple (int, int) indexing.
|
||||
if isinstance(row, INT_TYPES) and isinstance(col, INT_TYPES):
|
||||
x = self.dtype.type(x)
|
||||
if x.size > 1:
|
||||
raise ValueError("Trying to assign a sequence to an item")
|
||||
return self._set_intXint(row, col, x)
|
||||
# Fast path for full-matrix sparse assignment.
|
||||
if (isinstance(row, slice) and isinstance(col, slice) and
|
||||
row == slice(None) and col == slice(None) and
|
||||
issparse(x) and x.shape == self.shape):
|
||||
x = self._lil_container(x, dtype=self.dtype)
|
||||
self.rows = x.rows
|
||||
self.data = x.data
|
||||
return
|
||||
# Everything else takes the normal path.
|
||||
IndexMixin.__setitem__(self, key, x)
|
||||
|
||||
def _mul_scalar(self, other):
|
||||
if other == 0:
|
||||
# Multiply by zero: return the zero matrix
|
||||
new = self._lil_container(self.shape, dtype=self.dtype)
|
||||
else:
|
||||
res_dtype = upcast_scalar(self.dtype, other)
|
||||
|
||||
new = self.copy()
|
||||
new = new.astype(res_dtype)
|
||||
# Multiply this scalar by every element.
|
||||
for j, rowvals in enumerate(new.data):
|
||||
new.data[j] = [val*other for val in rowvals]
|
||||
return new
|
||||
|
||||
def __truediv__(self, other): # self / other
|
||||
if isscalarlike(other):
|
||||
new = self.copy()
|
||||
new.dtype = np.result_type(self, other)
|
||||
# Divide every element by this scalar
|
||||
for j, rowvals in enumerate(new.data):
|
||||
new.data[j] = [val/other for val in rowvals]
|
||||
return new
|
||||
else:
|
||||
return self.tocsr() / other
|
||||
|
||||
def copy(self):
|
||||
M, N = self.shape
|
||||
new = self._lil_container(self.shape, dtype=self.dtype)
|
||||
# This is ~14x faster than calling deepcopy() on rows and data.
|
||||
_csparsetools.lil_get_row_ranges(M, N, self.rows, self.data,
|
||||
new.rows, new.data, range(M),
|
||||
0, N, 1, N)
|
||||
return new
|
||||
|
||||
copy.__doc__ = _spbase.copy.__doc__
|
||||
|
||||
def reshape(self, *args, **kwargs):
|
||||
shape = check_shape(args, self.shape)
|
||||
order, copy = check_reshape_kwargs(kwargs)
|
||||
|
||||
# Return early if reshape is not required
|
||||
if shape == self.shape:
|
||||
if copy:
|
||||
return self.copy()
|
||||
else:
|
||||
return self
|
||||
|
||||
new = self._lil_container(shape, dtype=self.dtype)
|
||||
|
||||
if order == 'C':
|
||||
ncols = self.shape[1]
|
||||
for i, row in enumerate(self.rows):
|
||||
for col, j in enumerate(row):
|
||||
new_r, new_c = np.unravel_index(i * ncols + j, shape)
|
||||
new[new_r, new_c] = self[i, j]
|
||||
elif order == 'F':
|
||||
nrows = self.shape[0]
|
||||
for i, row in enumerate(self.rows):
|
||||
for col, j in enumerate(row):
|
||||
new_r, new_c = np.unravel_index(i + j * nrows, shape, order)
|
||||
new[new_r, new_c] = self[i, j]
|
||||
else:
|
||||
raise ValueError("'order' must be 'C' or 'F'")
|
||||
|
||||
return new
|
||||
|
||||
reshape.__doc__ = _spbase.reshape.__doc__
|
||||
|
||||
def resize(self, *shape):
|
||||
shape = check_shape(shape)
|
||||
new_M, new_N = shape
|
||||
M, N = self.shape
|
||||
|
||||
if new_M < M:
|
||||
self.rows = self.rows[:new_M]
|
||||
self.data = self.data[:new_M]
|
||||
elif new_M > M:
|
||||
self.rows = np.resize(self.rows, new_M)
|
||||
self.data = np.resize(self.data, new_M)
|
||||
for i in range(M, new_M):
|
||||
self.rows[i] = []
|
||||
self.data[i] = []
|
||||
|
||||
if new_N < N:
|
||||
for row, data in zip(self.rows, self.data):
|
||||
trunc = bisect_left(row, new_N)
|
||||
del row[trunc:]
|
||||
del data[trunc:]
|
||||
|
||||
self._shape = shape
|
||||
|
||||
resize.__doc__ = _spbase.resize.__doc__
|
||||
|
||||
def toarray(self, order=None, out=None):
|
||||
d = self._process_toarray_args(order, out)
|
||||
for i, row in enumerate(self.rows):
|
||||
for pos, j in enumerate(row):
|
||||
d[i, j] = self.data[i][pos]
|
||||
return d
|
||||
|
||||
toarray.__doc__ = _spbase.toarray.__doc__
|
||||
|
||||
def transpose(self, axes=None, copy=False):
|
||||
return self.tocsr(copy=copy).transpose(axes=axes, copy=False).tolil(copy=False)
|
||||
|
||||
transpose.__doc__ = _spbase.transpose.__doc__
|
||||
|
||||
def tolil(self, copy=False):
|
||||
if copy:
|
||||
return self.copy()
|
||||
else:
|
||||
return self
|
||||
|
||||
tolil.__doc__ = _spbase.tolil.__doc__
|
||||
|
||||
def tocsr(self, copy=False):
|
||||
M, N = self.shape
|
||||
if M == 0 or N == 0:
|
||||
return self._csr_container((M, N), dtype=self.dtype)
|
||||
|
||||
# construct indptr array
|
||||
if M*N <= np.iinfo(np.int32).max:
|
||||
# fast path: it is known that 64-bit indexing will not be needed.
|
||||
idx_dtype = np.int32
|
||||
indptr = np.empty(M + 1, dtype=idx_dtype)
|
||||
indptr[0] = 0
|
||||
_csparsetools.lil_get_lengths(self.rows, indptr[1:])
|
||||
np.cumsum(indptr, out=indptr)
|
||||
nnz = indptr[-1]
|
||||
else:
|
||||
idx_dtype = self._get_index_dtype(maxval=N)
|
||||
lengths = np.empty(M, dtype=idx_dtype)
|
||||
_csparsetools.lil_get_lengths(self.rows, lengths)
|
||||
nnz = lengths.sum(dtype=np.int64)
|
||||
idx_dtype = self._get_index_dtype(maxval=max(N, nnz))
|
||||
indptr = np.empty(M + 1, dtype=idx_dtype)
|
||||
indptr[0] = 0
|
||||
np.cumsum(lengths, dtype=idx_dtype, out=indptr[1:])
|
||||
|
||||
indices = np.empty(nnz, dtype=idx_dtype)
|
||||
data = np.empty(nnz, dtype=self.dtype)
|
||||
_csparsetools.lil_flatten_to_array(self.rows, indices)
|
||||
_csparsetools.lil_flatten_to_array(self.data, data)
|
||||
|
||||
# init csr matrix
|
||||
return self._csr_container((data, indices, indptr), shape=self.shape)
|
||||
|
||||
tocsr.__doc__ = _spbase.tocsr.__doc__
|
||||
|
||||
|
||||
def _prepare_index_for_memoryview(i, j, x=None):
|
||||
"""
|
||||
Convert index and data arrays to form suitable for passing to the
|
||||
Cython fancy getset routines.
|
||||
|
||||
The conversions are necessary since to (i) ensure the integer
|
||||
index arrays are in one of the accepted types, and (ii) to ensure
|
||||
the arrays are writable so that Cython memoryview support doesn't
|
||||
choke on them.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
i, j
|
||||
Index arrays
|
||||
x : optional
|
||||
Data arrays
|
||||
|
||||
Returns
|
||||
-------
|
||||
i, j, x
|
||||
Re-formatted arrays (x is omitted, if input was None)
|
||||
|
||||
"""
|
||||
if i.dtype > j.dtype:
|
||||
j = j.astype(i.dtype)
|
||||
elif i.dtype < j.dtype:
|
||||
i = i.astype(j.dtype)
|
||||
|
||||
if not i.flags.writeable or i.dtype not in (np.int32, np.int64):
|
||||
i = i.astype(np.intp)
|
||||
if not j.flags.writeable or j.dtype not in (np.int32, np.int64):
|
||||
j = j.astype(np.intp)
|
||||
|
||||
if x is not None:
|
||||
if not x.flags.writeable:
|
||||
x = x.copy()
|
||||
return i, j, x
|
||||
else:
|
||||
return i, j
|
||||
|
||||
|
||||
def isspmatrix_lil(x):
|
||||
"""Is `x` of lil_matrix type?
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x
|
||||
object to check for being a lil matrix
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if `x` is a lil matrix, False otherwise
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy.sparse import lil_array, lil_matrix, coo_matrix, isspmatrix_lil
|
||||
>>> isspmatrix_lil(lil_matrix([[5]]))
|
||||
True
|
||||
>>> isspmatrix_lil(lil_array([[5]]))
|
||||
False
|
||||
>>> isspmatrix_lil(coo_matrix([[5]]))
|
||||
False
|
||||
"""
|
||||
return isinstance(x, lil_matrix)
|
||||
|
||||
|
||||
# This namespace class separates array from matrix with isinstance
|
||||
class lil_array(_lil_base, sparray):
|
||||
"""
|
||||
Row-based LIst of Lists sparse array.
|
||||
|
||||
This is a structure for constructing sparse arrays incrementally.
|
||||
Note that inserting a single item can take linear time in the worst case;
|
||||
to construct the array efficiently, make sure the items are pre-sorted by
|
||||
index, per row.
|
||||
|
||||
This can be instantiated in several ways:
|
||||
lil_array(D)
|
||||
where D is a 2-D ndarray
|
||||
|
||||
lil_array(S)
|
||||
with another sparse array or matrix S (equivalent to S.tolil())
|
||||
|
||||
lil_array((M, N), [dtype])
|
||||
to construct an empty array with shape (M, N)
|
||||
dtype is optional, defaulting to dtype='d'.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dtype : dtype
|
||||
Data type of the array
|
||||
shape : 2-tuple
|
||||
Shape of the array
|
||||
ndim : int
|
||||
Number of dimensions (this is always 2)
|
||||
nnz
|
||||
size
|
||||
data
|
||||
LIL format data array of the array
|
||||
rows
|
||||
LIL format row index array of the array
|
||||
T
|
||||
|
||||
Notes
|
||||
-----
|
||||
Sparse arrays can be used in arithmetic operations: they support
|
||||
addition, subtraction, multiplication, division, and matrix power.
|
||||
|
||||
Advantages of the LIL format
|
||||
- supports flexible slicing
|
||||
- changes to the array sparsity structure are efficient
|
||||
|
||||
Disadvantages of the LIL format
|
||||
- arithmetic operations LIL + LIL are slow (consider CSR or CSC)
|
||||
- slow column slicing (consider CSC)
|
||||
- slow matrix vector products (consider CSR or CSC)
|
||||
|
||||
Intended Usage
|
||||
- LIL is a convenient format for constructing sparse arrays
|
||||
- once an array has been constructed, convert to CSR or
|
||||
CSC format for fast arithmetic and matrix vector operations
|
||||
- consider using the COO format when constructing large arrays
|
||||
|
||||
Data Structure
|
||||
- An array (``self.rows``) of rows, each of which is a sorted
|
||||
list of column indices of non-zero elements.
|
||||
- The corresponding nonzero values are stored in similar
|
||||
fashion in ``self.data``.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class lil_matrix(spmatrix, _lil_base):
|
||||
"""
|
||||
Row-based LIst of Lists sparse matrix.
|
||||
|
||||
This is a structure for constructing sparse matrices incrementally.
|
||||
Note that inserting a single item can take linear time in the worst case;
|
||||
to construct the matrix efficiently, make sure the items are pre-sorted by
|
||||
index, per row.
|
||||
|
||||
This can be instantiated in several ways:
|
||||
lil_matrix(D)
|
||||
where D is a 2-D ndarray
|
||||
|
||||
lil_matrix(S)
|
||||
with another sparse array or matrix S (equivalent to S.tolil())
|
||||
|
||||
lil_matrix((M, N), [dtype])
|
||||
to construct an empty matrix with shape (M, N)
|
||||
dtype is optional, defaulting to dtype='d'.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dtype : dtype
|
||||
Data type of the matrix
|
||||
shape : 2-tuple
|
||||
Shape of the matrix
|
||||
ndim : int
|
||||
Number of dimensions (this is always 2)
|
||||
nnz
|
||||
size
|
||||
data
|
||||
LIL format data array of the matrix
|
||||
rows
|
||||
LIL format row index array of the matrix
|
||||
T
|
||||
|
||||
Notes
|
||||
-----
|
||||
Sparse matrices can be used in arithmetic operations: they support
|
||||
addition, subtraction, multiplication, division, and matrix power.
|
||||
|
||||
Advantages of the LIL format
|
||||
- supports flexible slicing
|
||||
- changes to the matrix sparsity structure are efficient
|
||||
|
||||
Disadvantages of the LIL format
|
||||
- arithmetic operations LIL + LIL are slow (consider CSR or CSC)
|
||||
- slow column slicing (consider CSC)
|
||||
- slow matrix vector products (consider CSR or CSC)
|
||||
|
||||
Intended Usage
|
||||
- LIL is a convenient format for constructing sparse matrices
|
||||
- once a matrix has been constructed, convert to CSR or
|
||||
CSC format for fast arithmetic and matrix vector operations
|
||||
- consider using the COO format when constructing large matrices
|
||||
|
||||
Data Structure
|
||||
- An array (``self.rows``) of rows, each of which is a sorted
|
||||
list of column indices of non-zero elements.
|
||||
- The corresponding nonzero values are stored in similar
|
||||
fashion in ``self.data``.
|
||||
|
||||
"""
|
||||
113
venv/lib/python3.12/site-packages/scipy/sparse/_matrix.py
Normal file
113
venv/lib/python3.12/site-packages/scipy/sparse/_matrix.py
Normal file
@ -0,0 +1,113 @@
|
||||
class spmatrix:
|
||||
"""This class provides a base class for all sparse matrix classes.
|
||||
|
||||
It cannot be instantiated. Most of the work is provided by subclasses.
|
||||
"""
|
||||
|
||||
@property
|
||||
def _bsr_container(self):
|
||||
from ._bsr import bsr_matrix
|
||||
return bsr_matrix
|
||||
|
||||
@property
|
||||
def _coo_container(self):
|
||||
from ._coo import coo_matrix
|
||||
return coo_matrix
|
||||
|
||||
@property
|
||||
def _csc_container(self):
|
||||
from ._csc import csc_matrix
|
||||
return csc_matrix
|
||||
|
||||
@property
|
||||
def _csr_container(self):
|
||||
from ._csr import csr_matrix
|
||||
return csr_matrix
|
||||
|
||||
@property
|
||||
def _dia_container(self):
|
||||
from ._dia import dia_matrix
|
||||
return dia_matrix
|
||||
|
||||
@property
|
||||
def _dok_container(self):
|
||||
from ._dok import dok_matrix
|
||||
return dok_matrix
|
||||
|
||||
@property
|
||||
def _lil_container(self):
|
||||
from ._lil import lil_matrix
|
||||
return lil_matrix
|
||||
|
||||
# Restore matrix multiplication
|
||||
def __mul__(self, other):
|
||||
return self._matmul_dispatch(other)
|
||||
|
||||
def __rmul__(self, other):
|
||||
return self._rmatmul_dispatch(other)
|
||||
|
||||
# Restore matrix power
|
||||
def __pow__(self, power):
|
||||
from .linalg import matrix_power
|
||||
|
||||
return matrix_power(self, power)
|
||||
|
||||
## Backward compatibility
|
||||
|
||||
def set_shape(self, shape):
|
||||
"""Set the shape of the matrix in-place"""
|
||||
# Make sure copy is False since this is in place
|
||||
# Make sure format is unchanged because we are doing a __dict__ swap
|
||||
new_self = self.reshape(shape, copy=False).asformat(self.format)
|
||||
self.__dict__ = new_self.__dict__
|
||||
|
||||
def get_shape(self):
|
||||
"""Get the shape of the matrix"""
|
||||
return self._shape
|
||||
|
||||
shape = property(fget=get_shape, fset=set_shape,
|
||||
doc="Shape of the matrix")
|
||||
|
||||
def asfptype(self):
|
||||
"""Upcast matrix to a floating point format (if necessary)"""
|
||||
return self._asfptype()
|
||||
|
||||
def getmaxprint(self):
|
||||
"""Maximum number of elements to display when printed."""
|
||||
return self._getmaxprint()
|
||||
|
||||
def getformat(self):
|
||||
"""Matrix storage format"""
|
||||
return self.format
|
||||
|
||||
def getnnz(self, axis=None):
|
||||
"""Number of stored values, including explicit zeros.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
axis : None, 0, or 1
|
||||
Select between the number of values across the whole array, in
|
||||
each column, or in each row.
|
||||
"""
|
||||
return self._getnnz(axis=axis)
|
||||
|
||||
def getH(self):
|
||||
"""Return the Hermitian transpose of this matrix.
|
||||
|
||||
See Also
|
||||
--------
|
||||
numpy.matrix.getH : NumPy's implementation of `getH` for matrices
|
||||
"""
|
||||
return self.conjugate().transpose()
|
||||
|
||||
def getcol(self, j):
|
||||
"""Returns a copy of column j of the matrix, as an (m x 1) sparse
|
||||
matrix (column vector).
|
||||
"""
|
||||
return self._getcol(j)
|
||||
|
||||
def getrow(self, i):
|
||||
"""Returns a copy of row i of the matrix, as a (1 x n) sparse
|
||||
matrix (row vector).
|
||||
"""
|
||||
return self._getrow(i)
|
||||
167
venv/lib/python3.12/site-packages/scipy/sparse/_matrix_io.py
Normal file
167
venv/lib/python3.12/site-packages/scipy/sparse/_matrix_io.py
Normal file
@ -0,0 +1,167 @@
|
||||
import numpy as np
|
||||
import scipy as sp
|
||||
|
||||
__all__ = ['save_npz', 'load_npz']
|
||||
|
||||
|
||||
# Make loading safe vs. malicious input
|
||||
PICKLE_KWARGS = dict(allow_pickle=False)
|
||||
|
||||
|
||||
def save_npz(file, matrix, compressed=True):
|
||||
""" Save a sparse matrix or array to a file using ``.npz`` format.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
file : str or file-like object
|
||||
Either the file name (string) or an open file (file-like object)
|
||||
where the data will be saved. If file is a string, the ``.npz``
|
||||
extension will be appended to the file name if it is not already
|
||||
there.
|
||||
matrix: spmatrix or sparray
|
||||
The sparse matrix or array to save.
|
||||
Supported formats: ``csc``, ``csr``, ``bsr``, ``dia`` or ``coo``.
|
||||
compressed : bool, optional
|
||||
Allow compressing the file. Default: True
|
||||
|
||||
See Also
|
||||
--------
|
||||
scipy.sparse.load_npz: Load a sparse matrix from a file using ``.npz`` format.
|
||||
numpy.savez: Save several arrays into a ``.npz`` archive.
|
||||
numpy.savez_compressed : Save several arrays into a compressed ``.npz`` archive.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Store sparse matrix to disk, and load it again:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> import scipy as sp
|
||||
>>> sparse_matrix = sp.sparse.csc_matrix([[0, 0, 3], [4, 0, 0]])
|
||||
>>> sparse_matrix
|
||||
<Compressed Sparse Column sparse matrix of dtype 'int64'
|
||||
with 2 stored elements and shape (2, 3)>
|
||||
>>> sparse_matrix.toarray()
|
||||
array([[0, 0, 3],
|
||||
[4, 0, 0]], dtype=int64)
|
||||
|
||||
>>> sp.sparse.save_npz('/tmp/sparse_matrix.npz', sparse_matrix)
|
||||
>>> sparse_matrix = sp.sparse.load_npz('/tmp/sparse_matrix.npz')
|
||||
|
||||
>>> sparse_matrix
|
||||
<Compressed Sparse Column sparse matrix of dtype 'int64'
|
||||
with 2 stored elements and shape (2, 3)>
|
||||
>>> sparse_matrix.toarray()
|
||||
array([[0, 0, 3],
|
||||
[4, 0, 0]], dtype=int64)
|
||||
"""
|
||||
arrays_dict = {}
|
||||
if matrix.format in ('csc', 'csr', 'bsr'):
|
||||
arrays_dict.update(indices=matrix.indices, indptr=matrix.indptr)
|
||||
elif matrix.format == 'dia':
|
||||
arrays_dict.update(offsets=matrix.offsets)
|
||||
elif matrix.format == 'coo':
|
||||
arrays_dict.update(row=matrix.row, col=matrix.col)
|
||||
else:
|
||||
msg = f'Save is not implemented for sparse matrix of format {matrix.format}.'
|
||||
raise NotImplementedError(msg)
|
||||
arrays_dict.update(
|
||||
format=matrix.format.encode('ascii'),
|
||||
shape=matrix.shape,
|
||||
data=matrix.data
|
||||
)
|
||||
if isinstance(matrix, sp.sparse.sparray):
|
||||
arrays_dict.update(_is_array=True)
|
||||
if compressed:
|
||||
np.savez_compressed(file, **arrays_dict)
|
||||
else:
|
||||
np.savez(file, **arrays_dict)
|
||||
|
||||
|
||||
def load_npz(file):
|
||||
""" Load a sparse array/matrix from a file using ``.npz`` format.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
file : str or file-like object
|
||||
Either the file name (string) or an open file (file-like object)
|
||||
where the data will be loaded.
|
||||
|
||||
Returns
|
||||
-------
|
||||
result : csc_array, csr_array, bsr_array, dia_array or coo_array
|
||||
A sparse array/matrix containing the loaded data.
|
||||
|
||||
Raises
|
||||
------
|
||||
OSError
|
||||
If the input file does not exist or cannot be read.
|
||||
|
||||
See Also
|
||||
--------
|
||||
scipy.sparse.save_npz: Save a sparse array/matrix to a file using ``.npz`` format.
|
||||
numpy.load: Load several arrays from a ``.npz`` archive.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Store sparse array/matrix to disk, and load it again:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> import scipy as sp
|
||||
>>> sparse_array = sp.sparse.csc_array([[0, 0, 3], [4, 0, 0]])
|
||||
>>> sparse_array
|
||||
<Compressed Sparse Column sparse array of dtype 'int64'
|
||||
with 2 stored elements and shape (2, 3)>
|
||||
>>> sparse_array.toarray()
|
||||
array([[0, 0, 3],
|
||||
[4, 0, 0]], dtype=int64)
|
||||
|
||||
>>> sp.sparse.save_npz('/tmp/sparse_array.npz', sparse_array)
|
||||
>>> sparse_array = sp.sparse.load_npz('/tmp/sparse_array.npz')
|
||||
|
||||
>>> sparse_array
|
||||
<Compressed Sparse Column sparse array of dtype 'int64'
|
||||
with 2 stored elements and shape (2, 3)>
|
||||
>>> sparse_array.toarray()
|
||||
array([[0, 0, 3],
|
||||
[4, 0, 0]], dtype=int64)
|
||||
|
||||
In this example we force the result to be csr_array from csr_matrix
|
||||
>>> sparse_matrix = sp.sparse.csc_matrix([[0, 0, 3], [4, 0, 0]])
|
||||
>>> sp.sparse.save_npz('/tmp/sparse_matrix.npz', sparse_matrix)
|
||||
>>> tmp = sp.sparse.load_npz('/tmp/sparse_matrix.npz')
|
||||
>>> sparse_array = sp.sparse.csr_array(tmp)
|
||||
"""
|
||||
with np.load(file, **PICKLE_KWARGS) as loaded:
|
||||
sparse_format = loaded.get('format')
|
||||
if sparse_format is None:
|
||||
raise ValueError(f'The file {file} does not contain '
|
||||
f'a sparse array or matrix.')
|
||||
sparse_format = sparse_format.item()
|
||||
|
||||
if not isinstance(sparse_format, str):
|
||||
# Play safe with Python 2 vs 3 backward compatibility;
|
||||
# files saved with SciPy < 1.0.0 may contain unicode or bytes.
|
||||
sparse_format = sparse_format.decode('ascii')
|
||||
|
||||
if loaded.get('_is_array'):
|
||||
sparse_type = sparse_format + '_array'
|
||||
else:
|
||||
sparse_type = sparse_format + '_matrix'
|
||||
|
||||
try:
|
||||
cls = getattr(sp.sparse, f'{sparse_type}')
|
||||
except AttributeError as e:
|
||||
raise ValueError(f'Unknown format "{sparse_type}"') from e
|
||||
|
||||
if sparse_format in ('csc', 'csr', 'bsr'):
|
||||
return cls((loaded['data'], loaded['indices'], loaded['indptr']),
|
||||
shape=loaded['shape'])
|
||||
elif sparse_format == 'dia':
|
||||
return cls((loaded['data'], loaded['offsets']),
|
||||
shape=loaded['shape'])
|
||||
elif sparse_format == 'coo':
|
||||
return cls((loaded['data'], (loaded['row'], loaded['col'])),
|
||||
shape=loaded['shape'])
|
||||
else:
|
||||
raise NotImplementedError(f'Load is not implemented for '
|
||||
f'sparse matrix of format {sparse_format}.')
|
||||
Binary file not shown.
76
venv/lib/python3.12/site-packages/scipy/sparse/_spfuncs.py
Normal file
76
venv/lib/python3.12/site-packages/scipy/sparse/_spfuncs.py
Normal file
@ -0,0 +1,76 @@
|
||||
""" Functions that operate on sparse matrices
|
||||
"""
|
||||
|
||||
__all__ = ['count_blocks','estimate_blocksize']
|
||||
|
||||
from ._base import issparse
|
||||
from ._csr import csr_array
|
||||
from ._sparsetools import csr_count_blocks
|
||||
|
||||
|
||||
def estimate_blocksize(A,efficiency=0.7):
|
||||
"""Attempt to determine the blocksize of a sparse matrix
|
||||
|
||||
Returns a blocksize=(r,c) such that
|
||||
- A.nnz / A.tobsr( (r,c) ).nnz > efficiency
|
||||
"""
|
||||
if not (issparse(A) and A.format in ("csc", "csr")):
|
||||
A = csr_array(A)
|
||||
|
||||
if A.nnz == 0:
|
||||
return (1,1)
|
||||
|
||||
if not 0 < efficiency < 1.0:
|
||||
raise ValueError('efficiency must satisfy 0.0 < efficiency < 1.0')
|
||||
|
||||
high_efficiency = (1.0 + efficiency) / 2.0
|
||||
nnz = float(A.nnz)
|
||||
M,N = A.shape
|
||||
|
||||
if M % 2 == 0 and N % 2 == 0:
|
||||
e22 = nnz / (4 * count_blocks(A,(2,2)))
|
||||
else:
|
||||
e22 = 0.0
|
||||
|
||||
if M % 3 == 0 and N % 3 == 0:
|
||||
e33 = nnz / (9 * count_blocks(A,(3,3)))
|
||||
else:
|
||||
e33 = 0.0
|
||||
|
||||
if e22 > high_efficiency and e33 > high_efficiency:
|
||||
e66 = nnz / (36 * count_blocks(A,(6,6)))
|
||||
if e66 > efficiency:
|
||||
return (6,6)
|
||||
else:
|
||||
return (3,3)
|
||||
else:
|
||||
if M % 4 == 0 and N % 4 == 0:
|
||||
e44 = nnz / (16 * count_blocks(A,(4,4)))
|
||||
else:
|
||||
e44 = 0.0
|
||||
|
||||
if e44 > efficiency:
|
||||
return (4,4)
|
||||
elif e33 > efficiency:
|
||||
return (3,3)
|
||||
elif e22 > efficiency:
|
||||
return (2,2)
|
||||
else:
|
||||
return (1,1)
|
||||
|
||||
|
||||
def count_blocks(A,blocksize):
|
||||
"""For a given blocksize=(r,c) count the number of occupied
|
||||
blocks in a sparse matrix A
|
||||
"""
|
||||
r,c = blocksize
|
||||
if r < 1 or c < 1:
|
||||
raise ValueError('r and c must be positive')
|
||||
|
||||
if issparse(A):
|
||||
if A.format == "csr":
|
||||
M,N = A.shape
|
||||
return csr_count_blocks(M,N,r,c,A.indptr,A.indices)
|
||||
elif A.format == "csc":
|
||||
return count_blocks(A.T,(c,r))
|
||||
return count_blocks(csr_array(A),blocksize)
|
||||
451
venv/lib/python3.12/site-packages/scipy/sparse/_sputils.py
Normal file
451
venv/lib/python3.12/site-packages/scipy/sparse/_sputils.py
Normal file
@ -0,0 +1,451 @@
|
||||
""" Utility functions for sparse matrix module
|
||||
"""
|
||||
|
||||
import sys
|
||||
from typing import Any, Literal, Optional, Union
|
||||
import operator
|
||||
import numpy as np
|
||||
from math import prod
|
||||
import scipy.sparse as sp
|
||||
from scipy._lib._util import np_long, np_ulong
|
||||
|
||||
|
||||
__all__ = ['upcast', 'getdtype', 'getdata', 'isscalarlike', 'isintlike',
|
||||
'isshape', 'issequence', 'isdense', 'ismatrix', 'get_sum_dtype']
|
||||
|
||||
supported_dtypes = [np.bool_, np.byte, np.ubyte, np.short, np.ushort, np.intc,
|
||||
np.uintc, np_long, np_ulong, np.longlong, np.ulonglong,
|
||||
np.float32, np.float64, np.longdouble,
|
||||
np.complex64, np.complex128, np.clongdouble]
|
||||
|
||||
_upcast_memo = {}
|
||||
|
||||
|
||||
def upcast(*args):
|
||||
"""Returns the nearest supported sparse dtype for the
|
||||
combination of one or more types.
|
||||
|
||||
upcast(t0, t1, ..., tn) -> T where T is a supported dtype
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy.sparse._sputils import upcast
|
||||
>>> upcast('int32')
|
||||
<type 'numpy.int32'>
|
||||
>>> upcast('bool')
|
||||
<type 'numpy.bool_'>
|
||||
>>> upcast('int32','float32')
|
||||
<type 'numpy.float64'>
|
||||
>>> upcast('bool',complex,float)
|
||||
<type 'numpy.complex128'>
|
||||
|
||||
"""
|
||||
|
||||
t = _upcast_memo.get(hash(args))
|
||||
if t is not None:
|
||||
return t
|
||||
|
||||
upcast = np.result_type(*args)
|
||||
|
||||
for t in supported_dtypes:
|
||||
if np.can_cast(upcast, t):
|
||||
_upcast_memo[hash(args)] = t
|
||||
return t
|
||||
|
||||
raise TypeError(f'no supported conversion for types: {args!r}')
|
||||
|
||||
|
||||
def upcast_char(*args):
|
||||
"""Same as `upcast` but taking dtype.char as input (faster)."""
|
||||
t = _upcast_memo.get(args)
|
||||
if t is not None:
|
||||
return t
|
||||
t = upcast(*map(np.dtype, args))
|
||||
_upcast_memo[args] = t
|
||||
return t
|
||||
|
||||
|
||||
def upcast_scalar(dtype, scalar):
|
||||
"""Determine data type for binary operation between an array of
|
||||
type `dtype` and a scalar.
|
||||
"""
|
||||
return (np.array([0], dtype=dtype) * scalar).dtype
|
||||
|
||||
|
||||
def downcast_intp_index(arr):
|
||||
"""
|
||||
Down-cast index array to np.intp dtype if it is of a larger dtype.
|
||||
|
||||
Raise an error if the array contains a value that is too large for
|
||||
intp.
|
||||
"""
|
||||
if arr.dtype.itemsize > np.dtype(np.intp).itemsize:
|
||||
if arr.size == 0:
|
||||
return arr.astype(np.intp)
|
||||
maxval = arr.max()
|
||||
minval = arr.min()
|
||||
if maxval > np.iinfo(np.intp).max or minval < np.iinfo(np.intp).min:
|
||||
raise ValueError("Cannot deal with arrays with indices larger "
|
||||
"than the machine maximum address size "
|
||||
"(e.g. 64-bit indices on 32-bit machine).")
|
||||
return arr.astype(np.intp)
|
||||
return arr
|
||||
|
||||
|
||||
def to_native(A):
|
||||
"""
|
||||
Ensure that the data type of the NumPy array `A` has native byte order.
|
||||
|
||||
`A` must be a NumPy array. If the data type of `A` does not have native
|
||||
byte order, a copy of `A` with a native byte order is returned. Otherwise
|
||||
`A` is returned.
|
||||
"""
|
||||
dt = A.dtype
|
||||
if dt.isnative:
|
||||
# Don't call `asarray()` if A is already native, to avoid unnecessarily
|
||||
# creating a view of the input array.
|
||||
return A
|
||||
return np.asarray(A, dtype=dt.newbyteorder('native'))
|
||||
|
||||
|
||||
def getdtype(dtype, a=None, default=None):
|
||||
"""Function used to simplify argument processing. If 'dtype' is not
|
||||
specified (is None), returns a.dtype; otherwise returns a np.dtype
|
||||
object created from the specified dtype argument. If 'dtype' and 'a'
|
||||
are both None, construct a data type out of the 'default' parameter.
|
||||
Furthermore, 'dtype' must be in 'allowed' set.
|
||||
"""
|
||||
# TODO is this really what we want?
|
||||
if dtype is None:
|
||||
try:
|
||||
newdtype = a.dtype
|
||||
except AttributeError as e:
|
||||
if default is not None:
|
||||
newdtype = np.dtype(default)
|
||||
else:
|
||||
raise TypeError("could not interpret data type") from e
|
||||
else:
|
||||
newdtype = np.dtype(dtype)
|
||||
if newdtype == np.object_:
|
||||
raise ValueError(
|
||||
"object dtype is not supported by sparse matrices"
|
||||
)
|
||||
|
||||
return newdtype
|
||||
|
||||
|
||||
def getdata(obj, dtype=None, copy=False) -> np.ndarray:
|
||||
"""
|
||||
This is a wrapper of `np.array(obj, dtype=dtype, copy=copy)`
|
||||
that will generate a warning if the result is an object array.
|
||||
"""
|
||||
data = np.array(obj, dtype=dtype, copy=copy)
|
||||
# Defer to getdtype for checking that the dtype is OK.
|
||||
# This is called for the validation only; we don't need the return value.
|
||||
getdtype(data.dtype)
|
||||
return data
|
||||
|
||||
|
||||
def get_index_dtype(arrays=(), maxval=None, check_contents=False):
|
||||
"""
|
||||
Based on input (integer) arrays `a`, determine a suitable index data
|
||||
type that can hold the data in the arrays.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arrays : tuple of array_like
|
||||
Input arrays whose types/contents to check
|
||||
maxval : float, optional
|
||||
Maximum value needed
|
||||
check_contents : bool, optional
|
||||
Whether to check the values in the arrays and not just their types.
|
||||
Default: False (check only the types)
|
||||
|
||||
Returns
|
||||
-------
|
||||
dtype : dtype
|
||||
Suitable index data type (int32 or int64)
|
||||
|
||||
"""
|
||||
|
||||
int32min = np.int32(np.iinfo(np.int32).min)
|
||||
int32max = np.int32(np.iinfo(np.int32).max)
|
||||
|
||||
# not using intc directly due to misinteractions with pythran
|
||||
dtype = np.int32 if np.intc().itemsize == 4 else np.int64
|
||||
if maxval is not None:
|
||||
maxval = np.int64(maxval)
|
||||
if maxval > int32max:
|
||||
dtype = np.int64
|
||||
|
||||
if isinstance(arrays, np.ndarray):
|
||||
arrays = (arrays,)
|
||||
|
||||
for arr in arrays:
|
||||
arr = np.asarray(arr)
|
||||
if not np.can_cast(arr.dtype, np.int32):
|
||||
if check_contents:
|
||||
if arr.size == 0:
|
||||
# a bigger type not needed
|
||||
continue
|
||||
elif np.issubdtype(arr.dtype, np.integer):
|
||||
maxval = arr.max()
|
||||
minval = arr.min()
|
||||
if minval >= int32min and maxval <= int32max:
|
||||
# a bigger type not needed
|
||||
continue
|
||||
|
||||
dtype = np.int64
|
||||
break
|
||||
|
||||
return dtype
|
||||
|
||||
|
||||
def get_sum_dtype(dtype: np.dtype) -> np.dtype:
|
||||
"""Mimic numpy's casting for np.sum"""
|
||||
if dtype.kind == 'u' and np.can_cast(dtype, np.uint):
|
||||
return np.uint
|
||||
if np.can_cast(dtype, np.int_):
|
||||
return np.int_
|
||||
return dtype
|
||||
|
||||
|
||||
def isscalarlike(x) -> bool:
|
||||
"""Is x either a scalar, an array scalar, or a 0-dim array?"""
|
||||
return np.isscalar(x) or (isdense(x) and x.ndim == 0)
|
||||
|
||||
|
||||
def isintlike(x) -> bool:
|
||||
"""Is x appropriate as an index into a sparse matrix? Returns True
|
||||
if it can be cast safely to a machine int.
|
||||
"""
|
||||
# Fast-path check to eliminate non-scalar values. operator.index would
|
||||
# catch this case too, but the exception catching is slow.
|
||||
if np.ndim(x) != 0:
|
||||
return False
|
||||
try:
|
||||
operator.index(x)
|
||||
except (TypeError, ValueError):
|
||||
try:
|
||||
loose_int = bool(int(x) == x)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
if loose_int:
|
||||
msg = "Inexact indices into sparse matrices are not allowed"
|
||||
raise ValueError(msg)
|
||||
return loose_int
|
||||
return True
|
||||
|
||||
|
||||
def isshape(x, nonneg=False, *, allow_1d=False) -> bool:
|
||||
"""Is x a valid tuple of dimensions?
|
||||
|
||||
If nonneg, also checks that the dimensions are non-negative.
|
||||
If allow_1d, shapes of length 1 or 2 are allowed.
|
||||
"""
|
||||
ndim = len(x)
|
||||
if ndim != 2 and not (allow_1d and ndim == 1):
|
||||
return False
|
||||
for d in x:
|
||||
if not isintlike(d):
|
||||
return False
|
||||
if nonneg and d < 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def issequence(t) -> bool:
|
||||
return ((isinstance(t, (list, tuple)) and
|
||||
(len(t) == 0 or np.isscalar(t[0]))) or
|
||||
(isinstance(t, np.ndarray) and (t.ndim == 1)))
|
||||
|
||||
|
||||
def ismatrix(t) -> bool:
|
||||
return ((isinstance(t, (list, tuple)) and
|
||||
len(t) > 0 and issequence(t[0])) or
|
||||
(isinstance(t, np.ndarray) and t.ndim == 2))
|
||||
|
||||
|
||||
def isdense(x) -> bool:
|
||||
return isinstance(x, np.ndarray)
|
||||
|
||||
|
||||
def validateaxis(axis) -> None:
|
||||
if axis is None:
|
||||
return
|
||||
axis_type = type(axis)
|
||||
|
||||
# In NumPy, you can pass in tuples for 'axis', but they are
|
||||
# not very useful for sparse matrices given their limited
|
||||
# dimensions, so let's make it explicit that they are not
|
||||
# allowed to be passed in
|
||||
if axis_type == tuple:
|
||||
raise TypeError("Tuples are not accepted for the 'axis' parameter. "
|
||||
"Please pass in one of the following: "
|
||||
"{-2, -1, 0, 1, None}.")
|
||||
|
||||
# If not a tuple, check that the provided axis is actually
|
||||
# an integer and raise a TypeError similar to NumPy's
|
||||
if not np.issubdtype(np.dtype(axis_type), np.integer):
|
||||
raise TypeError(f"axis must be an integer, not {axis_type.__name__}")
|
||||
|
||||
if not (-2 <= axis <= 1):
|
||||
raise ValueError("axis out of range")
|
||||
|
||||
|
||||
def check_shape(args, current_shape=None, *, allow_1d=False) -> tuple[int, ...]:
|
||||
"""Imitate numpy.matrix handling of shape arguments
|
||||
|
||||
Parameters
|
||||
----------
|
||||
args : array_like
|
||||
Data structures providing information about the shape of the sparse array.
|
||||
current_shape : tuple, optional
|
||||
The current shape of the sparse array or matrix.
|
||||
If None (default), the current shape will be inferred from args.
|
||||
allow_1d : bool, optional
|
||||
If True, then 1-D or 2-D arrays are accepted.
|
||||
If False (default), then only 2-D arrays are accepted and an error is
|
||||
raised otherwise.
|
||||
|
||||
Returns
|
||||
-------
|
||||
new_shape: tuple
|
||||
The new shape after validation.
|
||||
"""
|
||||
if len(args) == 0:
|
||||
raise TypeError("function missing 1 required positional argument: "
|
||||
"'shape'")
|
||||
if len(args) == 1:
|
||||
try:
|
||||
shape_iter = iter(args[0])
|
||||
except TypeError:
|
||||
new_shape = (operator.index(args[0]), )
|
||||
else:
|
||||
new_shape = tuple(operator.index(arg) for arg in shape_iter)
|
||||
else:
|
||||
new_shape = tuple(operator.index(arg) for arg in args)
|
||||
|
||||
if current_shape is None:
|
||||
if allow_1d:
|
||||
if len(new_shape) not in (1, 2):
|
||||
raise ValueError('shape must be a 1- or 2-tuple of positive '
|
||||
'integers')
|
||||
elif len(new_shape) != 2:
|
||||
raise ValueError('shape must be a 2-tuple of positive integers')
|
||||
if any(d < 0 for d in new_shape):
|
||||
raise ValueError("'shape' elements cannot be negative")
|
||||
else:
|
||||
# Check the current size only if needed
|
||||
current_size = prod(current_shape)
|
||||
|
||||
# Check for negatives
|
||||
negative_indexes = [i for i, x in enumerate(new_shape) if x < 0]
|
||||
if not negative_indexes:
|
||||
new_size = prod(new_shape)
|
||||
if new_size != current_size:
|
||||
raise ValueError('cannot reshape array of size {} into shape {}'
|
||||
.format(current_size, new_shape))
|
||||
elif len(negative_indexes) == 1:
|
||||
skip = negative_indexes[0]
|
||||
specified = prod(new_shape[:skip] + new_shape[skip+1:])
|
||||
unspecified, remainder = divmod(current_size, specified)
|
||||
if remainder != 0:
|
||||
err_shape = tuple('newshape' if x < 0 else x for x in new_shape)
|
||||
raise ValueError('cannot reshape array of size {} into shape {}'
|
||||
''.format(current_size, err_shape))
|
||||
new_shape = new_shape[:skip] + (unspecified,) + new_shape[skip+1:]
|
||||
else:
|
||||
raise ValueError('can only specify one unknown dimension')
|
||||
|
||||
if len(new_shape) != 2 and not (allow_1d and len(new_shape) == 1):
|
||||
raise ValueError('matrix shape must be two-dimensional')
|
||||
|
||||
return new_shape
|
||||
|
||||
|
||||
def check_reshape_kwargs(kwargs):
|
||||
"""Unpack keyword arguments for reshape function.
|
||||
|
||||
This is useful because keyword arguments after star arguments are not
|
||||
allowed in Python 2, but star keyword arguments are. This function unpacks
|
||||
'order' and 'copy' from the star keyword arguments (with defaults) and
|
||||
throws an error for any remaining.
|
||||
"""
|
||||
|
||||
order = kwargs.pop('order', 'C')
|
||||
copy = kwargs.pop('copy', False)
|
||||
if kwargs: # Some unused kwargs remain
|
||||
raise TypeError('reshape() got unexpected keywords arguments: {}'
|
||||
.format(', '.join(kwargs.keys())))
|
||||
return order, copy
|
||||
|
||||
|
||||
def is_pydata_spmatrix(m) -> bool:
|
||||
"""
|
||||
Check whether object is pydata/sparse matrix, avoiding importing the module.
|
||||
"""
|
||||
base_cls = getattr(sys.modules.get('sparse'), 'SparseArray', None)
|
||||
return base_cls is not None and isinstance(m, base_cls)
|
||||
|
||||
|
||||
def convert_pydata_sparse_to_scipy(
|
||||
arg: Any, target_format: Optional[Literal["csc", "csr"]] = None
|
||||
) -> Union[Any, "sp.spmatrix"]:
|
||||
"""
|
||||
Convert a pydata/sparse array to scipy sparse matrix,
|
||||
pass through anything else.
|
||||
"""
|
||||
if is_pydata_spmatrix(arg):
|
||||
arg = arg.to_scipy_sparse()
|
||||
if target_format is not None:
|
||||
arg = arg.asformat(target_format)
|
||||
elif arg.format not in ("csc", "csr"):
|
||||
arg = arg.tocsc()
|
||||
return arg
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Wrappers for NumPy types that are deprecated
|
||||
|
||||
# Numpy versions of these functions raise deprecation warnings, the
|
||||
# ones below do not.
|
||||
|
||||
def matrix(*args, **kwargs):
|
||||
return np.array(*args, **kwargs).view(np.matrix)
|
||||
|
||||
|
||||
def asmatrix(data, dtype=None):
|
||||
if isinstance(data, np.matrix) and (dtype is None or data.dtype == dtype):
|
||||
return data
|
||||
return np.asarray(data, dtype=dtype).view(np.matrix)
|
||||
|
||||
###############################################################################
|
||||
|
||||
|
||||
def _todata(s) -> np.ndarray:
|
||||
"""Access nonzero values, possibly after summing duplicates.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
s : sparse array
|
||||
Input sparse array.
|
||||
|
||||
Returns
|
||||
-------
|
||||
data: ndarray
|
||||
Nonzero values of the array, with shape (s.nnz,)
|
||||
|
||||
"""
|
||||
if isinstance(s, sp._data._data_matrix):
|
||||
return s._deduped_data()
|
||||
|
||||
if isinstance(s, sp.dok_array):
|
||||
return np.fromiter(s.values(), dtype=s.dtype, count=s.nnz)
|
||||
|
||||
if isinstance(s, sp.lil_array):
|
||||
data = np.empty(s.nnz, dtype=s.dtype)
|
||||
sp._csparsetools.lil_flatten_to_array(s.data, data)
|
||||
return data
|
||||
|
||||
return s.tocoo()._deduped_data()
|
||||
33
venv/lib/python3.12/site-packages/scipy/sparse/base.py
Normal file
33
venv/lib/python3.12/site-packages/scipy/sparse/base.py
Normal file
@ -0,0 +1,33 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.sparse` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'MAXPRINT',
|
||||
'SparseEfficiencyWarning',
|
||||
'SparseFormatWarning',
|
||||
'SparseWarning',
|
||||
'asmatrix',
|
||||
'check_reshape_kwargs',
|
||||
'check_shape',
|
||||
'get_sum_dtype',
|
||||
'isdense',
|
||||
'isscalarlike',
|
||||
'issparse',
|
||||
'isspmatrix',
|
||||
'spmatrix',
|
||||
'validateaxis',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="sparse", module="base",
|
||||
private_modules=["_base"], all=__all__,
|
||||
attribute=name)
|
||||
36
venv/lib/python3.12/site-packages/scipy/sparse/bsr.py
Normal file
36
venv/lib/python3.12/site-packages/scipy/sparse/bsr.py
Normal file
@ -0,0 +1,36 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.sparse` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'bsr_matmat',
|
||||
'bsr_matrix',
|
||||
'bsr_matvec',
|
||||
'bsr_matvecs',
|
||||
'bsr_sort_indices',
|
||||
'bsr_tocsr',
|
||||
'bsr_transpose',
|
||||
'check_shape',
|
||||
'csr_matmat_maxnnz',
|
||||
'getdata',
|
||||
'getdtype',
|
||||
'isshape',
|
||||
'isspmatrix_bsr',
|
||||
'spmatrix',
|
||||
'to_native',
|
||||
'upcast',
|
||||
'warn',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="sparse", module="bsr",
|
||||
private_modules=["_bsr"], all=__all__,
|
||||
attribute=name)
|
||||
43
venv/lib/python3.12/site-packages/scipy/sparse/compressed.py
Normal file
43
venv/lib/python3.12/site-packages/scipy/sparse/compressed.py
Normal file
@ -0,0 +1,43 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.sparse` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'IndexMixin',
|
||||
'SparseEfficiencyWarning',
|
||||
'check_shape',
|
||||
'csr_column_index1',
|
||||
'csr_column_index2',
|
||||
'csr_row_index',
|
||||
'csr_row_slice',
|
||||
'csr_sample_offsets',
|
||||
'csr_sample_values',
|
||||
'csr_todense',
|
||||
'downcast_intp_index',
|
||||
'get_csr_submatrix',
|
||||
'get_sum_dtype',
|
||||
'getdtype',
|
||||
'is_pydata_spmatrix',
|
||||
'isdense',
|
||||
'isintlike',
|
||||
'isscalarlike',
|
||||
'isshape',
|
||||
'operator',
|
||||
'to_native',
|
||||
'upcast',
|
||||
'upcast_char',
|
||||
'warn',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="sparse", module="compressed",
|
||||
private_modules=["_compressed"], all=__all__,
|
||||
attribute=name)
|
||||
44
venv/lib/python3.12/site-packages/scipy/sparse/construct.py
Normal file
44
venv/lib/python3.12/site-packages/scipy/sparse/construct.py
Normal file
@ -0,0 +1,44 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.sparse` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'block_diag',
|
||||
'bmat',
|
||||
'bsr_matrix',
|
||||
'check_random_state',
|
||||
'coo_matrix',
|
||||
'csc_matrix',
|
||||
'csr_hstack',
|
||||
'csr_matrix',
|
||||
'dia_matrix',
|
||||
'diags',
|
||||
'eye',
|
||||
'get_index_dtype',
|
||||
'hstack',
|
||||
'identity',
|
||||
'isscalarlike',
|
||||
'issparse',
|
||||
'kron',
|
||||
'kronsum',
|
||||
'numbers',
|
||||
'rand',
|
||||
'random',
|
||||
'rng_integers',
|
||||
'spdiags',
|
||||
'upcast',
|
||||
'vstack',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="sparse", module="construct",
|
||||
private_modules=["_construct"], all=__all__,
|
||||
attribute=name)
|
||||
37
venv/lib/python3.12/site-packages/scipy/sparse/coo.py
Normal file
37
venv/lib/python3.12/site-packages/scipy/sparse/coo.py
Normal file
@ -0,0 +1,37 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.sparse` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'SparseEfficiencyWarning',
|
||||
'check_reshape_kwargs',
|
||||
'check_shape',
|
||||
'coo_matrix',
|
||||
'coo_matvec',
|
||||
'coo_tocsr',
|
||||
'coo_todense',
|
||||
'downcast_intp_index',
|
||||
'getdata',
|
||||
'getdtype',
|
||||
'isshape',
|
||||
'isspmatrix_coo',
|
||||
'operator',
|
||||
'spmatrix',
|
||||
'to_native',
|
||||
'upcast',
|
||||
'upcast_char',
|
||||
'warn',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="sparse", module="coo",
|
||||
private_modules=["_coo"], all=__all__,
|
||||
attribute=name)
|
||||
25
venv/lib/python3.12/site-packages/scipy/sparse/csc.py
Normal file
25
venv/lib/python3.12/site-packages/scipy/sparse/csc.py
Normal file
@ -0,0 +1,25 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.sparse` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'csc_matrix',
|
||||
'csc_tocsr',
|
||||
'expandptr',
|
||||
'isspmatrix_csc',
|
||||
'spmatrix',
|
||||
'upcast',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="sparse", module="csc",
|
||||
private_modules=["_csc"], all=__all__,
|
||||
attribute=name)
|
||||
@ -0,0 +1,210 @@
|
||||
r"""
|
||||
Compressed sparse graph routines (:mod:`scipy.sparse.csgraph`)
|
||||
==============================================================
|
||||
|
||||
.. currentmodule:: scipy.sparse.csgraph
|
||||
|
||||
Fast graph algorithms based on sparse matrix representations.
|
||||
|
||||
Contents
|
||||
--------
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
connected_components -- determine connected components of a graph
|
||||
laplacian -- compute the laplacian of a graph
|
||||
shortest_path -- compute the shortest path between points on a positive graph
|
||||
dijkstra -- use Dijkstra's algorithm for shortest path
|
||||
floyd_warshall -- use the Floyd-Warshall algorithm for shortest path
|
||||
bellman_ford -- use the Bellman-Ford algorithm for shortest path
|
||||
johnson -- use Johnson's algorithm for shortest path
|
||||
yen -- use Yen's algorithm for K-shortest paths between to nodes.
|
||||
breadth_first_order -- compute a breadth-first order of nodes
|
||||
depth_first_order -- compute a depth-first order of nodes
|
||||
breadth_first_tree -- construct the breadth-first tree from a given node
|
||||
depth_first_tree -- construct a depth-first tree from a given node
|
||||
minimum_spanning_tree -- construct the minimum spanning tree of a graph
|
||||
reverse_cuthill_mckee -- compute permutation for reverse Cuthill-McKee ordering
|
||||
maximum_flow -- solve the maximum flow problem for a graph
|
||||
maximum_bipartite_matching -- compute a maximum matching of a bipartite graph
|
||||
min_weight_full_bipartite_matching - compute a minimum weight full matching of a bipartite graph
|
||||
structural_rank -- compute the structural rank of a graph
|
||||
NegativeCycleError
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
construct_dist_matrix
|
||||
csgraph_from_dense
|
||||
csgraph_from_masked
|
||||
csgraph_masked_from_dense
|
||||
csgraph_to_dense
|
||||
csgraph_to_masked
|
||||
reconstruct_path
|
||||
|
||||
Graph Representations
|
||||
---------------------
|
||||
This module uses graphs which are stored in a matrix format. A
|
||||
graph with N nodes can be represented by an (N x N) adjacency matrix G.
|
||||
If there is a connection from node i to node j, then G[i, j] = w, where
|
||||
w is the weight of the connection. For nodes i and j which are
|
||||
not connected, the value depends on the representation:
|
||||
|
||||
- for dense array representations, non-edges are represented by
|
||||
G[i, j] = 0, infinity, or NaN.
|
||||
|
||||
- for dense masked representations (of type np.ma.MaskedArray), non-edges
|
||||
are represented by masked values. This can be useful when graphs with
|
||||
zero-weight edges are desired.
|
||||
|
||||
- for sparse array representations, non-edges are represented by
|
||||
non-entries in the matrix. This sort of sparse representation also
|
||||
allows for edges with zero weights.
|
||||
|
||||
As a concrete example, imagine that you would like to represent the following
|
||||
undirected graph::
|
||||
|
||||
G
|
||||
|
||||
(0)
|
||||
/ \
|
||||
1 2
|
||||
/ \
|
||||
(2) (1)
|
||||
|
||||
This graph has three nodes, where node 0 and 1 are connected by an edge of
|
||||
weight 2, and nodes 0 and 2 are connected by an edge of weight 1.
|
||||
We can construct the dense, masked, and sparse representations as follows,
|
||||
keeping in mind that an undirected graph is represented by a symmetric matrix::
|
||||
|
||||
>>> import numpy as np
|
||||
>>> G_dense = np.array([[0, 2, 1],
|
||||
... [2, 0, 0],
|
||||
... [1, 0, 0]])
|
||||
>>> G_masked = np.ma.masked_values(G_dense, 0)
|
||||
>>> from scipy.sparse import csr_matrix
|
||||
>>> G_sparse = csr_matrix(G_dense)
|
||||
|
||||
This becomes more difficult when zero edges are significant. For example,
|
||||
consider the situation when we slightly modify the above graph::
|
||||
|
||||
G2
|
||||
|
||||
(0)
|
||||
/ \
|
||||
0 2
|
||||
/ \
|
||||
(2) (1)
|
||||
|
||||
This is identical to the previous graph, except nodes 0 and 2 are connected
|
||||
by an edge of zero weight. In this case, the dense representation above
|
||||
leads to ambiguities: how can non-edges be represented if zero is a meaningful
|
||||
value? In this case, either a masked or sparse representation must be used
|
||||
to eliminate the ambiguity::
|
||||
|
||||
>>> import numpy as np
|
||||
>>> G2_data = np.array([[np.inf, 2, 0 ],
|
||||
... [2, np.inf, np.inf],
|
||||
... [0, np.inf, np.inf]])
|
||||
>>> G2_masked = np.ma.masked_invalid(G2_data)
|
||||
>>> from scipy.sparse.csgraph import csgraph_from_dense
|
||||
>>> # G2_sparse = csr_matrix(G2_data) would give the wrong result
|
||||
>>> G2_sparse = csgraph_from_dense(G2_data, null_value=np.inf)
|
||||
>>> G2_sparse.data
|
||||
array([ 2., 0., 2., 0.])
|
||||
|
||||
Here we have used a utility routine from the csgraph submodule in order to
|
||||
convert the dense representation to a sparse representation which can be
|
||||
understood by the algorithms in submodule. By viewing the data array, we
|
||||
can see that the zero values are explicitly encoded in the graph.
|
||||
|
||||
Directed vs. undirected
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Matrices may represent either directed or undirected graphs. This is
|
||||
specified throughout the csgraph module by a boolean keyword. Graphs are
|
||||
assumed to be directed by default. In a directed graph, traversal from node
|
||||
i to node j can be accomplished over the edge G[i, j], but not the edge
|
||||
G[j, i]. Consider the following dense graph::
|
||||
|
||||
>>> import numpy as np
|
||||
>>> G_dense = np.array([[0, 1, 0],
|
||||
... [2, 0, 3],
|
||||
... [0, 4, 0]])
|
||||
|
||||
When ``directed=True`` we get the graph::
|
||||
|
||||
---1--> ---3-->
|
||||
(0) (1) (2)
|
||||
<--2--- <--4---
|
||||
|
||||
In a non-directed graph, traversal from node i to node j can be
|
||||
accomplished over either G[i, j] or G[j, i]. If both edges are not null,
|
||||
and the two have unequal weights, then the smaller of the two is used.
|
||||
|
||||
So for the same graph, when ``directed=False`` we get the graph::
|
||||
|
||||
(0)--1--(1)--3--(2)
|
||||
|
||||
Note that a symmetric matrix will represent an undirected graph, regardless
|
||||
of whether the 'directed' keyword is set to True or False. In this case,
|
||||
using ``directed=True`` generally leads to more efficient computation.
|
||||
|
||||
The routines in this module accept as input either scipy.sparse representations
|
||||
(csr, csc, or lil format), masked representations, or dense representations
|
||||
with non-edges indicated by zeros, infinities, and NaN entries.
|
||||
""" # noqa: E501
|
||||
|
||||
__docformat__ = "restructuredtext en"
|
||||
|
||||
__all__ = ['connected_components',
|
||||
'laplacian',
|
||||
'shortest_path',
|
||||
'floyd_warshall',
|
||||
'dijkstra',
|
||||
'bellman_ford',
|
||||
'johnson',
|
||||
'yen',
|
||||
'breadth_first_order',
|
||||
'depth_first_order',
|
||||
'breadth_first_tree',
|
||||
'depth_first_tree',
|
||||
'minimum_spanning_tree',
|
||||
'reverse_cuthill_mckee',
|
||||
'maximum_flow',
|
||||
'maximum_bipartite_matching',
|
||||
'min_weight_full_bipartite_matching',
|
||||
'structural_rank',
|
||||
'construct_dist_matrix',
|
||||
'reconstruct_path',
|
||||
'csgraph_masked_from_dense',
|
||||
'csgraph_from_dense',
|
||||
'csgraph_from_masked',
|
||||
'csgraph_to_dense',
|
||||
'csgraph_to_masked',
|
||||
'NegativeCycleError']
|
||||
|
||||
from ._laplacian import laplacian
|
||||
from ._shortest_path import (
|
||||
shortest_path, floyd_warshall, dijkstra, bellman_ford, johnson, yen,
|
||||
NegativeCycleError
|
||||
)
|
||||
from ._traversal import (
|
||||
breadth_first_order, depth_first_order, breadth_first_tree,
|
||||
depth_first_tree, connected_components
|
||||
)
|
||||
from ._min_spanning_tree import minimum_spanning_tree
|
||||
from ._flow import maximum_flow
|
||||
from ._matching import (
|
||||
maximum_bipartite_matching, min_weight_full_bipartite_matching
|
||||
)
|
||||
from ._reordering import reverse_cuthill_mckee, structural_rank
|
||||
from ._tools import (
|
||||
construct_dist_matrix, reconstruct_path, csgraph_from_dense,
|
||||
csgraph_to_dense, csgraph_masked_from_dense, csgraph_from_masked,
|
||||
csgraph_to_masked
|
||||
)
|
||||
|
||||
from scipy._lib._testutils import PytestTester
|
||||
test = PytestTester(__name__)
|
||||
del PytestTester
|
||||
Binary file not shown.
@ -0,0 +1,562 @@
|
||||
"""
|
||||
Laplacian of a compressed-sparse graph
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from scipy.sparse import issparse
|
||||
from scipy.sparse.linalg import LinearOperator
|
||||
from scipy.sparse._sputils import convert_pydata_sparse_to_scipy, is_pydata_spmatrix
|
||||
|
||||
|
||||
###############################################################################
|
||||
# Graph laplacian
|
||||
def laplacian(
|
||||
csgraph,
|
||||
normed=False,
|
||||
return_diag=False,
|
||||
use_out_degree=False,
|
||||
*,
|
||||
copy=True,
|
||||
form="array",
|
||||
dtype=None,
|
||||
symmetrized=False,
|
||||
):
|
||||
"""
|
||||
Return the Laplacian of a directed graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
csgraph : array_like or sparse matrix, 2 dimensions
|
||||
compressed-sparse graph, with shape (N, N).
|
||||
normed : bool, optional
|
||||
If True, then compute symmetrically normalized Laplacian.
|
||||
Default: False.
|
||||
return_diag : bool, optional
|
||||
If True, then also return an array related to vertex degrees.
|
||||
Default: False.
|
||||
use_out_degree : bool, optional
|
||||
If True, then use out-degree instead of in-degree.
|
||||
This distinction matters only if the graph is asymmetric.
|
||||
Default: False.
|
||||
copy: bool, optional
|
||||
If False, then change `csgraph` in place if possible,
|
||||
avoiding doubling the memory use.
|
||||
Default: True, for backward compatibility.
|
||||
form: 'array', or 'function', or 'lo'
|
||||
Determines the format of the output Laplacian:
|
||||
|
||||
* 'array' is a numpy array;
|
||||
* 'function' is a pointer to evaluating the Laplacian-vector
|
||||
or Laplacian-matrix product;
|
||||
* 'lo' results in the format of the `LinearOperator`.
|
||||
|
||||
Choosing 'function' or 'lo' always avoids doubling
|
||||
the memory use, ignoring `copy` value.
|
||||
Default: 'array', for backward compatibility.
|
||||
dtype: None or one of numeric numpy dtypes, optional
|
||||
The dtype of the output. If ``dtype=None``, the dtype of the
|
||||
output matches the dtype of the input csgraph, except for
|
||||
the case ``normed=True`` and integer-like csgraph, where
|
||||
the output dtype is 'float' allowing accurate normalization,
|
||||
but dramatically increasing the memory use.
|
||||
Default: None, for backward compatibility.
|
||||
symmetrized: bool, optional
|
||||
If True, then the output Laplacian is symmetric/Hermitian.
|
||||
The symmetrization is done by ``csgraph + csgraph.T.conj``
|
||||
without dividing by 2 to preserve integer dtypes if possible
|
||||
prior to the construction of the Laplacian.
|
||||
The symmetrization will increase the memory footprint of
|
||||
sparse matrices unless the sparsity pattern is symmetric or
|
||||
`form` is 'function' or 'lo'.
|
||||
Default: False, for backward compatibility.
|
||||
|
||||
Returns
|
||||
-------
|
||||
lap : ndarray, or sparse matrix, or `LinearOperator`
|
||||
The N x N Laplacian of csgraph. It will be a NumPy array (dense)
|
||||
if the input was dense, or a sparse matrix otherwise, or
|
||||
the format of a function or `LinearOperator` if
|
||||
`form` equals 'function' or 'lo', respectively.
|
||||
diag : ndarray, optional
|
||||
The length-N main diagonal of the Laplacian matrix.
|
||||
For the normalized Laplacian, this is the array of square roots
|
||||
of vertex degrees or 1 if the degree is zero.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The Laplacian matrix of a graph is sometimes referred to as the
|
||||
"Kirchhoff matrix" or just the "Laplacian", and is useful in many
|
||||
parts of spectral graph theory.
|
||||
In particular, the eigen-decomposition of the Laplacian can give
|
||||
insight into many properties of the graph, e.g.,
|
||||
is commonly used for spectral data embedding and clustering.
|
||||
|
||||
The constructed Laplacian doubles the memory use if ``copy=True`` and
|
||||
``form="array"`` which is the default.
|
||||
Choosing ``copy=False`` has no effect unless ``form="array"``
|
||||
or the matrix is sparse in the ``coo`` format, or dense array, except
|
||||
for the integer input with ``normed=True`` that forces the float output.
|
||||
|
||||
Sparse input is reformatted into ``coo`` if ``form="array"``,
|
||||
which is the default.
|
||||
|
||||
If the input adjacency matrix is not symmetric, the Laplacian is
|
||||
also non-symmetric unless ``symmetrized=True`` is used.
|
||||
|
||||
Diagonal entries of the input adjacency matrix are ignored and
|
||||
replaced with zeros for the purpose of normalization where ``normed=True``.
|
||||
The normalization uses the inverse square roots of row-sums of the input
|
||||
adjacency matrix, and thus may fail if the row-sums contain
|
||||
negative or complex with a non-zero imaginary part values.
|
||||
|
||||
The normalization is symmetric, making the normalized Laplacian also
|
||||
symmetric if the input csgraph was symmetric.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Laplacian matrix. https://en.wikipedia.org/wiki/Laplacian_matrix
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csgraph
|
||||
|
||||
Our first illustration is the symmetric graph
|
||||
|
||||
>>> G = np.arange(4) * np.arange(4)[:, np.newaxis]
|
||||
>>> G
|
||||
array([[0, 0, 0, 0],
|
||||
[0, 1, 2, 3],
|
||||
[0, 2, 4, 6],
|
||||
[0, 3, 6, 9]])
|
||||
|
||||
and its symmetric Laplacian matrix
|
||||
|
||||
>>> csgraph.laplacian(G)
|
||||
array([[ 0, 0, 0, 0],
|
||||
[ 0, 5, -2, -3],
|
||||
[ 0, -2, 8, -6],
|
||||
[ 0, -3, -6, 9]])
|
||||
|
||||
The non-symmetric graph
|
||||
|
||||
>>> G = np.arange(9).reshape(3, 3)
|
||||
>>> G
|
||||
array([[0, 1, 2],
|
||||
[3, 4, 5],
|
||||
[6, 7, 8]])
|
||||
|
||||
has different row- and column sums, resulting in two varieties
|
||||
of the Laplacian matrix, using an in-degree, which is the default
|
||||
|
||||
>>> L_in_degree = csgraph.laplacian(G)
|
||||
>>> L_in_degree
|
||||
array([[ 9, -1, -2],
|
||||
[-3, 8, -5],
|
||||
[-6, -7, 7]])
|
||||
|
||||
or alternatively an out-degree
|
||||
|
||||
>>> L_out_degree = csgraph.laplacian(G, use_out_degree=True)
|
||||
>>> L_out_degree
|
||||
array([[ 3, -1, -2],
|
||||
[-3, 8, -5],
|
||||
[-6, -7, 13]])
|
||||
|
||||
Constructing a symmetric Laplacian matrix, one can add the two as
|
||||
|
||||
>>> L_in_degree + L_out_degree.T
|
||||
array([[ 12, -4, -8],
|
||||
[ -4, 16, -12],
|
||||
[ -8, -12, 20]])
|
||||
|
||||
or use the ``symmetrized=True`` option
|
||||
|
||||
>>> csgraph.laplacian(G, symmetrized=True)
|
||||
array([[ 12, -4, -8],
|
||||
[ -4, 16, -12],
|
||||
[ -8, -12, 20]])
|
||||
|
||||
that is equivalent to symmetrizing the original graph
|
||||
|
||||
>>> csgraph.laplacian(G + G.T)
|
||||
array([[ 12, -4, -8],
|
||||
[ -4, 16, -12],
|
||||
[ -8, -12, 20]])
|
||||
|
||||
The goal of normalization is to make the non-zero diagonal entries
|
||||
of the Laplacian matrix to be all unit, also scaling off-diagonal
|
||||
entries correspondingly. The normalization can be done manually, e.g.,
|
||||
|
||||
>>> G = np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]])
|
||||
>>> L, d = csgraph.laplacian(G, return_diag=True)
|
||||
>>> L
|
||||
array([[ 2, -1, -1],
|
||||
[-1, 2, -1],
|
||||
[-1, -1, 2]])
|
||||
>>> d
|
||||
array([2, 2, 2])
|
||||
>>> scaling = np.sqrt(d)
|
||||
>>> scaling
|
||||
array([1.41421356, 1.41421356, 1.41421356])
|
||||
>>> (1/scaling)*L*(1/scaling)
|
||||
array([[ 1. , -0.5, -0.5],
|
||||
[-0.5, 1. , -0.5],
|
||||
[-0.5, -0.5, 1. ]])
|
||||
|
||||
Or using ``normed=True`` option
|
||||
|
||||
>>> L, d = csgraph.laplacian(G, return_diag=True, normed=True)
|
||||
>>> L
|
||||
array([[ 1. , -0.5, -0.5],
|
||||
[-0.5, 1. , -0.5],
|
||||
[-0.5, -0.5, 1. ]])
|
||||
|
||||
which now instead of the diagonal returns the scaling coefficients
|
||||
|
||||
>>> d
|
||||
array([1.41421356, 1.41421356, 1.41421356])
|
||||
|
||||
Zero scaling coefficients are substituted with 1s, where scaling
|
||||
has thus no effect, e.g.,
|
||||
|
||||
>>> G = np.array([[0, 0, 0], [0, 0, 1], [0, 1, 0]])
|
||||
>>> G
|
||||
array([[0, 0, 0],
|
||||
[0, 0, 1],
|
||||
[0, 1, 0]])
|
||||
>>> L, d = csgraph.laplacian(G, return_diag=True, normed=True)
|
||||
>>> L
|
||||
array([[ 0., -0., -0.],
|
||||
[-0., 1., -1.],
|
||||
[-0., -1., 1.]])
|
||||
>>> d
|
||||
array([1., 1., 1.])
|
||||
|
||||
Only the symmetric normalization is implemented, resulting
|
||||
in a symmetric Laplacian matrix if and only if its graph is symmetric
|
||||
and has all non-negative degrees, like in the examples above.
|
||||
|
||||
The output Laplacian matrix is by default a dense array or a sparse matrix
|
||||
inferring its shape, format, and dtype from the input graph matrix:
|
||||
|
||||
>>> G = np.array([[0, 1, 1], [1, 0, 1], [1, 1, 0]]).astype(np.float32)
|
||||
>>> G
|
||||
array([[0., 1., 1.],
|
||||
[1., 0., 1.],
|
||||
[1., 1., 0.]], dtype=float32)
|
||||
>>> csgraph.laplacian(G)
|
||||
array([[ 2., -1., -1.],
|
||||
[-1., 2., -1.],
|
||||
[-1., -1., 2.]], dtype=float32)
|
||||
|
||||
but can alternatively be generated matrix-free as a LinearOperator:
|
||||
|
||||
>>> L = csgraph.laplacian(G, form="lo")
|
||||
>>> L
|
||||
<3x3 _CustomLinearOperator with dtype=float32>
|
||||
>>> L(np.eye(3))
|
||||
array([[ 2., -1., -1.],
|
||||
[-1., 2., -1.],
|
||||
[-1., -1., 2.]])
|
||||
|
||||
or as a lambda-function:
|
||||
|
||||
>>> L = csgraph.laplacian(G, form="function")
|
||||
>>> L
|
||||
<function _laplace.<locals>.<lambda> at 0x0000012AE6F5A598>
|
||||
>>> L(np.eye(3))
|
||||
array([[ 2., -1., -1.],
|
||||
[-1., 2., -1.],
|
||||
[-1., -1., 2.]])
|
||||
|
||||
The Laplacian matrix is used for
|
||||
spectral data clustering and embedding
|
||||
as well as for spectral graph partitioning.
|
||||
Our final example illustrates the latter
|
||||
for a noisy directed linear graph.
|
||||
|
||||
>>> from scipy.sparse import diags, random
|
||||
>>> from scipy.sparse.linalg import lobpcg
|
||||
|
||||
Create a directed linear graph with ``N=35`` vertices
|
||||
using a sparse adjacency matrix ``G``:
|
||||
|
||||
>>> N = 35
|
||||
>>> G = diags(np.ones(N-1), 1, format="csr")
|
||||
|
||||
Fix a random seed ``rng`` and add a random sparse noise to the graph ``G``:
|
||||
|
||||
>>> rng = np.random.default_rng()
|
||||
>>> G += 1e-2 * random(N, N, density=0.1, random_state=rng)
|
||||
|
||||
Set initial approximations for eigenvectors:
|
||||
|
||||
>>> X = rng.random((N, 2))
|
||||
|
||||
The constant vector of ones is always a trivial eigenvector
|
||||
of the non-normalized Laplacian to be filtered out:
|
||||
|
||||
>>> Y = np.ones((N, 1))
|
||||
|
||||
Alternating (1) the sign of the graph weights allows determining
|
||||
labels for spectral max- and min- cuts in a single loop.
|
||||
Since the graph is undirected, the option ``symmetrized=True``
|
||||
must be used in the construction of the Laplacian.
|
||||
The option ``normed=True`` cannot be used in (2) for the negative weights
|
||||
here as the symmetric normalization evaluates square roots.
|
||||
The option ``form="lo"`` in (2) is matrix-free, i.e., guarantees
|
||||
a fixed memory footprint and read-only access to the graph.
|
||||
Calling the eigenvalue solver ``lobpcg`` (3) computes the Fiedler vector
|
||||
that determines the labels as the signs of its components in (5).
|
||||
Since the sign in an eigenvector is not deterministic and can flip,
|
||||
we fix the sign of the first component to be always +1 in (4).
|
||||
|
||||
>>> for cut in ["max", "min"]:
|
||||
... G = -G # 1.
|
||||
... L = csgraph.laplacian(G, symmetrized=True, form="lo") # 2.
|
||||
... _, eves = lobpcg(L, X, Y=Y, largest=False, tol=1e-2) # 3.
|
||||
... eves *= np.sign(eves[0, 0]) # 4.
|
||||
... print(cut + "-cut labels:\\n", 1 * (eves[:, 0]>0)) # 5.
|
||||
max-cut labels:
|
||||
[1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1]
|
||||
min-cut labels:
|
||||
[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
||||
|
||||
As anticipated for a (slightly noisy) linear graph,
|
||||
the max-cut strips all the edges of the graph coloring all
|
||||
odd vertices into one color and all even vertices into another one,
|
||||
while the balanced min-cut partitions the graph
|
||||
in the middle by deleting a single edge.
|
||||
Both determined partitions are optimal.
|
||||
"""
|
||||
is_pydata_sparse = is_pydata_spmatrix(csgraph)
|
||||
if is_pydata_sparse:
|
||||
pydata_sparse_cls = csgraph.__class__
|
||||
csgraph = convert_pydata_sparse_to_scipy(csgraph)
|
||||
if csgraph.ndim != 2 or csgraph.shape[0] != csgraph.shape[1]:
|
||||
raise ValueError('csgraph must be a square matrix or array')
|
||||
|
||||
if normed and (
|
||||
np.issubdtype(csgraph.dtype, np.signedinteger)
|
||||
or np.issubdtype(csgraph.dtype, np.uint)
|
||||
):
|
||||
csgraph = csgraph.astype(np.float64)
|
||||
|
||||
if form == "array":
|
||||
create_lap = (
|
||||
_laplacian_sparse if issparse(csgraph) else _laplacian_dense
|
||||
)
|
||||
else:
|
||||
create_lap = (
|
||||
_laplacian_sparse_flo
|
||||
if issparse(csgraph)
|
||||
else _laplacian_dense_flo
|
||||
)
|
||||
|
||||
degree_axis = 1 if use_out_degree else 0
|
||||
|
||||
lap, d = create_lap(
|
||||
csgraph,
|
||||
normed=normed,
|
||||
axis=degree_axis,
|
||||
copy=copy,
|
||||
form=form,
|
||||
dtype=dtype,
|
||||
symmetrized=symmetrized,
|
||||
)
|
||||
if is_pydata_sparse:
|
||||
lap = pydata_sparse_cls.from_scipy_sparse(lap)
|
||||
if return_diag:
|
||||
return lap, d
|
||||
return lap
|
||||
|
||||
|
||||
def _setdiag_dense(m, d):
|
||||
step = len(d) + 1
|
||||
m.flat[::step] = d
|
||||
|
||||
|
||||
def _laplace(m, d):
|
||||
return lambda v: v * d[:, np.newaxis] - m @ v
|
||||
|
||||
|
||||
def _laplace_normed(m, d, nd):
|
||||
laplace = _laplace(m, d)
|
||||
return lambda v: nd[:, np.newaxis] * laplace(v * nd[:, np.newaxis])
|
||||
|
||||
|
||||
def _laplace_sym(m, d):
|
||||
return (
|
||||
lambda v: v * d[:, np.newaxis]
|
||||
- m @ v
|
||||
- np.transpose(np.conjugate(np.transpose(np.conjugate(v)) @ m))
|
||||
)
|
||||
|
||||
|
||||
def _laplace_normed_sym(m, d, nd):
|
||||
laplace_sym = _laplace_sym(m, d)
|
||||
return lambda v: nd[:, np.newaxis] * laplace_sym(v * nd[:, np.newaxis])
|
||||
|
||||
|
||||
def _linearoperator(mv, shape, dtype):
|
||||
return LinearOperator(matvec=mv, matmat=mv, shape=shape, dtype=dtype)
|
||||
|
||||
|
||||
def _laplacian_sparse_flo(graph, normed, axis, copy, form, dtype, symmetrized):
|
||||
# The keyword argument `copy` is unused and has no effect here.
|
||||
del copy
|
||||
|
||||
if dtype is None:
|
||||
dtype = graph.dtype
|
||||
|
||||
graph_sum = np.asarray(graph.sum(axis=axis)).ravel()
|
||||
graph_diagonal = graph.diagonal()
|
||||
diag = graph_sum - graph_diagonal
|
||||
if symmetrized:
|
||||
graph_sum += np.asarray(graph.sum(axis=1 - axis)).ravel()
|
||||
diag = graph_sum - graph_diagonal - graph_diagonal
|
||||
|
||||
if normed:
|
||||
isolated_node_mask = diag == 0
|
||||
w = np.where(isolated_node_mask, 1, np.sqrt(diag))
|
||||
if symmetrized:
|
||||
md = _laplace_normed_sym(graph, graph_sum, 1.0 / w)
|
||||
else:
|
||||
md = _laplace_normed(graph, graph_sum, 1.0 / w)
|
||||
if form == "function":
|
||||
return md, w.astype(dtype, copy=False)
|
||||
elif form == "lo":
|
||||
m = _linearoperator(md, shape=graph.shape, dtype=dtype)
|
||||
return m, w.astype(dtype, copy=False)
|
||||
else:
|
||||
raise ValueError(f"Invalid form: {form!r}")
|
||||
else:
|
||||
if symmetrized:
|
||||
md = _laplace_sym(graph, graph_sum)
|
||||
else:
|
||||
md = _laplace(graph, graph_sum)
|
||||
if form == "function":
|
||||
return md, diag.astype(dtype, copy=False)
|
||||
elif form == "lo":
|
||||
m = _linearoperator(md, shape=graph.shape, dtype=dtype)
|
||||
return m, diag.astype(dtype, copy=False)
|
||||
else:
|
||||
raise ValueError(f"Invalid form: {form!r}")
|
||||
|
||||
|
||||
def _laplacian_sparse(graph, normed, axis, copy, form, dtype, symmetrized):
|
||||
# The keyword argument `form` is unused and has no effect here.
|
||||
del form
|
||||
|
||||
if dtype is None:
|
||||
dtype = graph.dtype
|
||||
|
||||
needs_copy = False
|
||||
if graph.format in ('lil', 'dok'):
|
||||
m = graph.tocoo()
|
||||
else:
|
||||
m = graph
|
||||
if copy:
|
||||
needs_copy = True
|
||||
|
||||
if symmetrized:
|
||||
m += m.T.conj()
|
||||
|
||||
w = np.asarray(m.sum(axis=axis)).ravel() - m.diagonal()
|
||||
if normed:
|
||||
m = m.tocoo(copy=needs_copy)
|
||||
isolated_node_mask = (w == 0)
|
||||
w = np.where(isolated_node_mask, 1, np.sqrt(w))
|
||||
m.data /= w[m.row]
|
||||
m.data /= w[m.col]
|
||||
m.data *= -1
|
||||
m.setdiag(1 - isolated_node_mask)
|
||||
else:
|
||||
if m.format == 'dia':
|
||||
m = m.copy()
|
||||
else:
|
||||
m = m.tocoo(copy=needs_copy)
|
||||
m.data *= -1
|
||||
m.setdiag(w)
|
||||
|
||||
return m.astype(dtype, copy=False), w.astype(dtype)
|
||||
|
||||
|
||||
def _laplacian_dense_flo(graph, normed, axis, copy, form, dtype, symmetrized):
|
||||
|
||||
if copy:
|
||||
m = np.array(graph)
|
||||
else:
|
||||
m = np.asarray(graph)
|
||||
|
||||
if dtype is None:
|
||||
dtype = m.dtype
|
||||
|
||||
graph_sum = m.sum(axis=axis)
|
||||
graph_diagonal = m.diagonal()
|
||||
diag = graph_sum - graph_diagonal
|
||||
if symmetrized:
|
||||
graph_sum += m.sum(axis=1 - axis)
|
||||
diag = graph_sum - graph_diagonal - graph_diagonal
|
||||
|
||||
if normed:
|
||||
isolated_node_mask = diag == 0
|
||||
w = np.where(isolated_node_mask, 1, np.sqrt(diag))
|
||||
if symmetrized:
|
||||
md = _laplace_normed_sym(m, graph_sum, 1.0 / w)
|
||||
else:
|
||||
md = _laplace_normed(m, graph_sum, 1.0 / w)
|
||||
if form == "function":
|
||||
return md, w.astype(dtype, copy=False)
|
||||
elif form == "lo":
|
||||
m = _linearoperator(md, shape=graph.shape, dtype=dtype)
|
||||
return m, w.astype(dtype, copy=False)
|
||||
else:
|
||||
raise ValueError(f"Invalid form: {form!r}")
|
||||
else:
|
||||
if symmetrized:
|
||||
md = _laplace_sym(m, graph_sum)
|
||||
else:
|
||||
md = _laplace(m, graph_sum)
|
||||
if form == "function":
|
||||
return md, diag.astype(dtype, copy=False)
|
||||
elif form == "lo":
|
||||
m = _linearoperator(md, shape=graph.shape, dtype=dtype)
|
||||
return m, diag.astype(dtype, copy=False)
|
||||
else:
|
||||
raise ValueError(f"Invalid form: {form!r}")
|
||||
|
||||
|
||||
def _laplacian_dense(graph, normed, axis, copy, form, dtype, symmetrized):
|
||||
|
||||
if form != "array":
|
||||
raise ValueError(f'{form!r} must be "array"')
|
||||
|
||||
if dtype is None:
|
||||
dtype = graph.dtype
|
||||
|
||||
if copy:
|
||||
m = np.array(graph)
|
||||
else:
|
||||
m = np.asarray(graph)
|
||||
|
||||
if dtype is None:
|
||||
dtype = m.dtype
|
||||
|
||||
if symmetrized:
|
||||
m += m.T.conj()
|
||||
np.fill_diagonal(m, 0)
|
||||
w = m.sum(axis=axis)
|
||||
if normed:
|
||||
isolated_node_mask = (w == 0)
|
||||
w = np.where(isolated_node_mask, 1, np.sqrt(w))
|
||||
m /= w
|
||||
m /= w[:, np.newaxis]
|
||||
m *= -1
|
||||
_setdiag_dense(m, 1 - isolated_node_mask)
|
||||
else:
|
||||
m *= -1
|
||||
_setdiag_dense(m, w)
|
||||
|
||||
return m.astype(dtype, copy=False), w.astype(dtype, copy=False)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,61 @@
|
||||
import numpy as np
|
||||
from scipy.sparse import csr_matrix, issparse
|
||||
from scipy.sparse._sputils import convert_pydata_sparse_to_scipy
|
||||
from scipy.sparse.csgraph._tools import (
|
||||
csgraph_to_dense, csgraph_from_dense,
|
||||
csgraph_masked_from_dense, csgraph_from_masked
|
||||
)
|
||||
|
||||
DTYPE = np.float64
|
||||
|
||||
|
||||
def validate_graph(csgraph, directed, dtype=DTYPE,
|
||||
csr_output=True, dense_output=True,
|
||||
copy_if_dense=False, copy_if_sparse=False,
|
||||
null_value_in=0, null_value_out=np.inf,
|
||||
infinity_null=True, nan_null=True):
|
||||
"""Routine for validation and conversion of csgraph inputs"""
|
||||
if not (csr_output or dense_output):
|
||||
raise ValueError("Internal: dense or csr output must be true")
|
||||
|
||||
csgraph = convert_pydata_sparse_to_scipy(csgraph)
|
||||
|
||||
# if undirected and csc storage, then transposing in-place
|
||||
# is quicker than later converting to csr.
|
||||
if (not directed) and issparse(csgraph) and csgraph.format == "csc":
|
||||
csgraph = csgraph.T
|
||||
|
||||
if issparse(csgraph):
|
||||
if csr_output:
|
||||
csgraph = csr_matrix(csgraph, dtype=DTYPE, copy=copy_if_sparse)
|
||||
else:
|
||||
csgraph = csgraph_to_dense(csgraph, null_value=null_value_out)
|
||||
elif np.ma.isMaskedArray(csgraph):
|
||||
if dense_output:
|
||||
mask = csgraph.mask
|
||||
csgraph = np.array(csgraph.data, dtype=DTYPE, copy=copy_if_dense)
|
||||
csgraph[mask] = null_value_out
|
||||
else:
|
||||
csgraph = csgraph_from_masked(csgraph)
|
||||
else:
|
||||
if dense_output:
|
||||
csgraph = csgraph_masked_from_dense(csgraph,
|
||||
copy=copy_if_dense,
|
||||
null_value=null_value_in,
|
||||
nan_null=nan_null,
|
||||
infinity_null=infinity_null)
|
||||
mask = csgraph.mask
|
||||
csgraph = np.asarray(csgraph.data, dtype=DTYPE)
|
||||
csgraph[mask] = null_value_out
|
||||
else:
|
||||
csgraph = csgraph_from_dense(csgraph, null_value=null_value_in,
|
||||
infinity_null=infinity_null,
|
||||
nan_null=nan_null)
|
||||
|
||||
if csgraph.ndim != 2:
|
||||
raise ValueError("compressed-sparse graph must be 2-D")
|
||||
|
||||
if csgraph.shape[0] != csgraph.shape[1]:
|
||||
raise ValueError("compressed-sparse graph must be shape (N, N)")
|
||||
|
||||
return csgraph
|
||||
@ -0,0 +1,119 @@
|
||||
import numpy as np
|
||||
from numpy.testing import assert_equal, assert_array_almost_equal
|
||||
from scipy.sparse import csgraph, csr_array
|
||||
|
||||
|
||||
def test_weak_connections():
|
||||
Xde = np.array([[0, 1, 0],
|
||||
[0, 0, 0],
|
||||
[0, 0, 0]])
|
||||
|
||||
Xsp = csgraph.csgraph_from_dense(Xde, null_value=0)
|
||||
|
||||
for X in Xsp, Xde:
|
||||
n_components, labels =\
|
||||
csgraph.connected_components(X, directed=True,
|
||||
connection='weak')
|
||||
|
||||
assert_equal(n_components, 2)
|
||||
assert_array_almost_equal(labels, [0, 0, 1])
|
||||
|
||||
|
||||
def test_strong_connections():
|
||||
X1de = np.array([[0, 1, 0],
|
||||
[0, 0, 0],
|
||||
[0, 0, 0]])
|
||||
X2de = X1de + X1de.T
|
||||
|
||||
X1sp = csgraph.csgraph_from_dense(X1de, null_value=0)
|
||||
X2sp = csgraph.csgraph_from_dense(X2de, null_value=0)
|
||||
|
||||
for X in X1sp, X1de:
|
||||
n_components, labels =\
|
||||
csgraph.connected_components(X, directed=True,
|
||||
connection='strong')
|
||||
|
||||
assert_equal(n_components, 3)
|
||||
labels.sort()
|
||||
assert_array_almost_equal(labels, [0, 1, 2])
|
||||
|
||||
for X in X2sp, X2de:
|
||||
n_components, labels =\
|
||||
csgraph.connected_components(X, directed=True,
|
||||
connection='strong')
|
||||
|
||||
assert_equal(n_components, 2)
|
||||
labels.sort()
|
||||
assert_array_almost_equal(labels, [0, 0, 1])
|
||||
|
||||
|
||||
def test_strong_connections2():
|
||||
X = np.array([[0, 0, 0, 0, 0, 0],
|
||||
[1, 0, 1, 0, 0, 0],
|
||||
[0, 0, 0, 1, 0, 0],
|
||||
[0, 0, 1, 0, 1, 0],
|
||||
[0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 1, 0]])
|
||||
n_components, labels =\
|
||||
csgraph.connected_components(X, directed=True,
|
||||
connection='strong')
|
||||
assert_equal(n_components, 5)
|
||||
labels.sort()
|
||||
assert_array_almost_equal(labels, [0, 1, 2, 2, 3, 4])
|
||||
|
||||
|
||||
def test_weak_connections2():
|
||||
X = np.array([[0, 0, 0, 0, 0, 0],
|
||||
[1, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 1, 0, 0],
|
||||
[0, 0, 1, 0, 1, 0],
|
||||
[0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 1, 0]])
|
||||
n_components, labels =\
|
||||
csgraph.connected_components(X, directed=True,
|
||||
connection='weak')
|
||||
assert_equal(n_components, 2)
|
||||
labels.sort()
|
||||
assert_array_almost_equal(labels, [0, 0, 1, 1, 1, 1])
|
||||
|
||||
|
||||
def test_ticket1876():
|
||||
# Regression test: this failed in the original implementation
|
||||
# There should be two strongly-connected components; previously gave one
|
||||
g = np.array([[0, 1, 1, 0],
|
||||
[1, 0, 0, 1],
|
||||
[0, 0, 0, 1],
|
||||
[0, 0, 1, 0]])
|
||||
n_components, labels = csgraph.connected_components(g, connection='strong')
|
||||
|
||||
assert_equal(n_components, 2)
|
||||
assert_equal(labels[0], labels[1])
|
||||
assert_equal(labels[2], labels[3])
|
||||
|
||||
|
||||
def test_fully_connected_graph():
|
||||
# Fully connected dense matrices raised an exception.
|
||||
# https://github.com/scipy/scipy/issues/3818
|
||||
g = np.ones((4, 4))
|
||||
n_components, labels = csgraph.connected_components(g)
|
||||
assert_equal(n_components, 1)
|
||||
|
||||
|
||||
def test_int64_indices_undirected():
|
||||
# See https://github.com/scipy/scipy/issues/18716
|
||||
g = csr_array(([1], np.array([[0], [1]], dtype=np.int64)), shape=(2, 2))
|
||||
assert g.indices.dtype == np.int64
|
||||
n, labels = csgraph.connected_components(g, directed=False)
|
||||
assert n == 1
|
||||
assert_array_almost_equal(labels, [0, 0])
|
||||
|
||||
|
||||
def test_int64_indices_directed():
|
||||
# See https://github.com/scipy/scipy/issues/18716
|
||||
g = csr_array(([1], np.array([[0], [1]], dtype=np.int64)), shape=(2, 2))
|
||||
assert g.indices.dtype == np.int64
|
||||
n, labels = csgraph.connected_components(g, directed=True,
|
||||
connection='strong')
|
||||
assert n == 2
|
||||
assert_array_almost_equal(labels, [1, 0])
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
import numpy as np
|
||||
from numpy.testing import assert_array_almost_equal
|
||||
from scipy.sparse import csr_matrix
|
||||
from scipy.sparse.csgraph import csgraph_from_dense, csgraph_to_dense
|
||||
|
||||
|
||||
def test_csgraph_from_dense():
|
||||
np.random.seed(1234)
|
||||
G = np.random.random((10, 10))
|
||||
some_nulls = (G < 0.4)
|
||||
all_nulls = (G < 0.8)
|
||||
|
||||
for null_value in [0, np.nan, np.inf]:
|
||||
G[all_nulls] = null_value
|
||||
with np.errstate(invalid="ignore"):
|
||||
G_csr = csgraph_from_dense(G, null_value=0)
|
||||
|
||||
G[all_nulls] = 0
|
||||
assert_array_almost_equal(G, G_csr.toarray())
|
||||
|
||||
for null_value in [np.nan, np.inf]:
|
||||
G[all_nulls] = 0
|
||||
G[some_nulls] = null_value
|
||||
with np.errstate(invalid="ignore"):
|
||||
G_csr = csgraph_from_dense(G, null_value=0)
|
||||
|
||||
G[all_nulls] = 0
|
||||
assert_array_almost_equal(G, G_csr.toarray())
|
||||
|
||||
|
||||
def test_csgraph_to_dense():
|
||||
np.random.seed(1234)
|
||||
G = np.random.random((10, 10))
|
||||
nulls = (G < 0.8)
|
||||
G[nulls] = np.inf
|
||||
|
||||
G_csr = csgraph_from_dense(G)
|
||||
|
||||
for null_value in [0, 10, -np.inf, np.inf]:
|
||||
G[nulls] = null_value
|
||||
assert_array_almost_equal(G, csgraph_to_dense(G_csr, null_value))
|
||||
|
||||
|
||||
def test_multiple_edges():
|
||||
# create a random square matrix with an even number of elements
|
||||
np.random.seed(1234)
|
||||
X = np.random.random((10, 10))
|
||||
Xcsr = csr_matrix(X)
|
||||
|
||||
# now double-up every other column
|
||||
Xcsr.indices[::2] = Xcsr.indices[1::2]
|
||||
|
||||
# normal sparse toarray() will sum the duplicated edges
|
||||
Xdense = Xcsr.toarray()
|
||||
assert_array_almost_equal(Xdense[:, 1::2],
|
||||
X[:, ::2] + X[:, 1::2])
|
||||
|
||||
# csgraph_to_dense chooses the minimum of each duplicated edge
|
||||
Xdense = csgraph_to_dense(Xcsr)
|
||||
assert_array_almost_equal(Xdense[:, 1::2],
|
||||
np.minimum(X[:, ::2], X[:, 1::2]))
|
||||
@ -0,0 +1,201 @@
|
||||
import numpy as np
|
||||
from numpy.testing import assert_array_equal
|
||||
import pytest
|
||||
|
||||
from scipy.sparse import csr_matrix, csc_matrix
|
||||
from scipy.sparse.csgraph import maximum_flow
|
||||
from scipy.sparse.csgraph._flow import (
|
||||
_add_reverse_edges, _make_edge_pointers, _make_tails
|
||||
)
|
||||
|
||||
methods = ['edmonds_karp', 'dinic']
|
||||
|
||||
def test_raises_on_dense_input():
|
||||
with pytest.raises(TypeError):
|
||||
graph = np.array([[0, 1], [0, 0]])
|
||||
maximum_flow(graph, 0, 1)
|
||||
maximum_flow(graph, 0, 1, method='edmonds_karp')
|
||||
|
||||
|
||||
def test_raises_on_csc_input():
|
||||
with pytest.raises(TypeError):
|
||||
graph = csc_matrix([[0, 1], [0, 0]])
|
||||
maximum_flow(graph, 0, 1)
|
||||
maximum_flow(graph, 0, 1, method='edmonds_karp')
|
||||
|
||||
|
||||
def test_raises_on_floating_point_input():
|
||||
with pytest.raises(ValueError):
|
||||
graph = csr_matrix([[0, 1.5], [0, 0]], dtype=np.float64)
|
||||
maximum_flow(graph, 0, 1)
|
||||
maximum_flow(graph, 0, 1, method='edmonds_karp')
|
||||
|
||||
|
||||
def test_raises_on_non_square_input():
|
||||
with pytest.raises(ValueError):
|
||||
graph = csr_matrix([[0, 1, 2], [2, 1, 0]])
|
||||
maximum_flow(graph, 0, 1)
|
||||
|
||||
|
||||
def test_raises_when_source_is_sink():
|
||||
with pytest.raises(ValueError):
|
||||
graph = csr_matrix([[0, 1], [0, 0]])
|
||||
maximum_flow(graph, 0, 0)
|
||||
maximum_flow(graph, 0, 0, method='edmonds_karp')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', methods)
|
||||
@pytest.mark.parametrize('source', [-1, 2, 3])
|
||||
def test_raises_when_source_is_out_of_bounds(source, method):
|
||||
with pytest.raises(ValueError):
|
||||
graph = csr_matrix([[0, 1], [0, 0]])
|
||||
maximum_flow(graph, source, 1, method=method)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', methods)
|
||||
@pytest.mark.parametrize('sink', [-1, 2, 3])
|
||||
def test_raises_when_sink_is_out_of_bounds(sink, method):
|
||||
with pytest.raises(ValueError):
|
||||
graph = csr_matrix([[0, 1], [0, 0]])
|
||||
maximum_flow(graph, 0, sink, method=method)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', methods)
|
||||
def test_simple_graph(method):
|
||||
# This graph looks as follows:
|
||||
# (0) --5--> (1)
|
||||
graph = csr_matrix([[0, 5], [0, 0]])
|
||||
res = maximum_flow(graph, 0, 1, method=method)
|
||||
assert res.flow_value == 5
|
||||
expected_flow = np.array([[0, 5], [-5, 0]])
|
||||
assert_array_equal(res.flow.toarray(), expected_flow)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', methods)
|
||||
def test_bottle_neck_graph(method):
|
||||
# This graph cannot use the full capacity between 0 and 1:
|
||||
# (0) --5--> (1) --3--> (2)
|
||||
graph = csr_matrix([[0, 5, 0], [0, 0, 3], [0, 0, 0]])
|
||||
res = maximum_flow(graph, 0, 2, method=method)
|
||||
assert res.flow_value == 3
|
||||
expected_flow = np.array([[0, 3, 0], [-3, 0, 3], [0, -3, 0]])
|
||||
assert_array_equal(res.flow.toarray(), expected_flow)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', methods)
|
||||
def test_backwards_flow(method):
|
||||
# This example causes backwards flow between vertices 3 and 4,
|
||||
# and so this test ensures that we handle that accordingly. See
|
||||
# https://stackoverflow.com/q/38843963/5085211
|
||||
# for more information.
|
||||
graph = csr_matrix([[0, 10, 0, 0, 10, 0, 0, 0],
|
||||
[0, 0, 10, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 10, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 10],
|
||||
[0, 0, 0, 10, 0, 10, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 10, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 10],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0]])
|
||||
res = maximum_flow(graph, 0, 7, method=method)
|
||||
assert res.flow_value == 20
|
||||
expected_flow = np.array([[0, 10, 0, 0, 10, 0, 0, 0],
|
||||
[-10, 0, 10, 0, 0, 0, 0, 0],
|
||||
[0, -10, 0, 10, 0, 0, 0, 0],
|
||||
[0, 0, -10, 0, 0, 0, 0, 10],
|
||||
[-10, 0, 0, 0, 0, 10, 0, 0],
|
||||
[0, 0, 0, 0, -10, 0, 10, 0],
|
||||
[0, 0, 0, 0, 0, -10, 0, 10],
|
||||
[0, 0, 0, -10, 0, 0, -10, 0]])
|
||||
assert_array_equal(res.flow.toarray(), expected_flow)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', methods)
|
||||
def test_example_from_clrs_chapter_26_1(method):
|
||||
# See page 659 in CLRS second edition, but note that the maximum flow
|
||||
# we find is slightly different than the one in CLRS; we push a flow of
|
||||
# 12 to v_1 instead of v_2.
|
||||
graph = csr_matrix([[0, 16, 13, 0, 0, 0],
|
||||
[0, 0, 10, 12, 0, 0],
|
||||
[0, 4, 0, 0, 14, 0],
|
||||
[0, 0, 9, 0, 0, 20],
|
||||
[0, 0, 0, 7, 0, 4],
|
||||
[0, 0, 0, 0, 0, 0]])
|
||||
res = maximum_flow(graph, 0, 5, method=method)
|
||||
assert res.flow_value == 23
|
||||
expected_flow = np.array([[0, 12, 11, 0, 0, 0],
|
||||
[-12, 0, 0, 12, 0, 0],
|
||||
[-11, 0, 0, 0, 11, 0],
|
||||
[0, -12, 0, 0, -7, 19],
|
||||
[0, 0, -11, 7, 0, 4],
|
||||
[0, 0, 0, -19, -4, 0]])
|
||||
assert_array_equal(res.flow.toarray(), expected_flow)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', methods)
|
||||
def test_disconnected_graph(method):
|
||||
# This tests the following disconnected graph:
|
||||
# (0) --5--> (1) (2) --3--> (3)
|
||||
graph = csr_matrix([[0, 5, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 9, 3],
|
||||
[0, 0, 0, 0]])
|
||||
res = maximum_flow(graph, 0, 3, method=method)
|
||||
assert res.flow_value == 0
|
||||
expected_flow = np.zeros((4, 4), dtype=np.int32)
|
||||
assert_array_equal(res.flow.toarray(), expected_flow)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', methods)
|
||||
def test_add_reverse_edges_large_graph(method):
|
||||
# Regression test for https://github.com/scipy/scipy/issues/14385
|
||||
n = 100_000
|
||||
indices = np.arange(1, n)
|
||||
indptr = np.array(list(range(n)) + [n - 1])
|
||||
data = np.ones(n - 1, dtype=np.int32)
|
||||
graph = csr_matrix((data, indices, indptr), shape=(n, n))
|
||||
res = maximum_flow(graph, 0, n - 1, method=method)
|
||||
assert res.flow_value == 1
|
||||
expected_flow = graph - graph.transpose()
|
||||
assert_array_equal(res.flow.data, expected_flow.data)
|
||||
assert_array_equal(res.flow.indices, expected_flow.indices)
|
||||
assert_array_equal(res.flow.indptr, expected_flow.indptr)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("a,b_data_expected", [
|
||||
([[]], []),
|
||||
([[0], [0]], []),
|
||||
([[1, 0, 2], [0, 0, 0], [0, 3, 0]], [1, 2, 0, 0, 3]),
|
||||
([[9, 8, 7], [4, 5, 6], [0, 0, 0]], [9, 8, 7, 4, 5, 6, 0, 0])])
|
||||
def test_add_reverse_edges(a, b_data_expected):
|
||||
"""Test that the reversal of the edges of the input graph works
|
||||
as expected.
|
||||
"""
|
||||
a = csr_matrix(a, dtype=np.int32, shape=(len(a), len(a)))
|
||||
b = _add_reverse_edges(a)
|
||||
assert_array_equal(b.data, b_data_expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("a,expected", [
|
||||
([[]], []),
|
||||
([[0]], []),
|
||||
([[1]], [0]),
|
||||
([[0, 1], [10, 0]], [1, 0]),
|
||||
([[1, 0, 2], [0, 0, 3], [4, 5, 0]], [0, 3, 4, 1, 2])
|
||||
])
|
||||
def test_make_edge_pointers(a, expected):
|
||||
a = csr_matrix(a, dtype=np.int32)
|
||||
rev_edge_ptr = _make_edge_pointers(a)
|
||||
assert_array_equal(rev_edge_ptr, expected)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("a,expected", [
|
||||
([[]], []),
|
||||
([[0]], []),
|
||||
([[1]], [0]),
|
||||
([[0, 1], [10, 0]], [0, 1]),
|
||||
([[1, 0, 2], [0, 0, 3], [4, 5, 0]], [0, 0, 1, 2, 2])
|
||||
])
|
||||
def test_make_tails(a, expected):
|
||||
a = csr_matrix(a, dtype=np.int32)
|
||||
tails = _make_tails(a)
|
||||
assert_array_equal(tails, expected)
|
||||
@ -0,0 +1,369 @@
|
||||
import pytest
|
||||
import numpy as np
|
||||
from numpy.testing import assert_allclose
|
||||
from pytest import raises as assert_raises
|
||||
from scipy import sparse
|
||||
|
||||
from scipy.sparse import csgraph
|
||||
from scipy._lib._util import np_long, np_ulong
|
||||
|
||||
|
||||
def check_int_type(mat):
|
||||
return np.issubdtype(mat.dtype, np.signedinteger) or np.issubdtype(
|
||||
mat.dtype, np_ulong
|
||||
)
|
||||
|
||||
|
||||
def test_laplacian_value_error():
|
||||
for t in int, float, complex:
|
||||
for m in ([1, 1],
|
||||
[[[1]]],
|
||||
[[1, 2, 3], [4, 5, 6]],
|
||||
[[1, 2], [3, 4], [5, 5]]):
|
||||
A = np.array(m, dtype=t)
|
||||
assert_raises(ValueError, csgraph.laplacian, A)
|
||||
|
||||
|
||||
def _explicit_laplacian(x, normed=False):
|
||||
if sparse.issparse(x):
|
||||
x = x.toarray()
|
||||
x = np.asarray(x)
|
||||
y = -1.0 * x
|
||||
for j in range(y.shape[0]):
|
||||
y[j,j] = x[j,j+1:].sum() + x[j,:j].sum()
|
||||
if normed:
|
||||
d = np.diag(y).copy()
|
||||
d[d == 0] = 1.0
|
||||
y /= d[:,None]**.5
|
||||
y /= d[None,:]**.5
|
||||
return y
|
||||
|
||||
|
||||
def _check_symmetric_graph_laplacian(mat, normed, copy=True):
|
||||
if not hasattr(mat, 'shape'):
|
||||
mat = eval(mat, dict(np=np, sparse=sparse))
|
||||
|
||||
if sparse.issparse(mat):
|
||||
sp_mat = mat
|
||||
mat = sp_mat.toarray()
|
||||
else:
|
||||
sp_mat = sparse.csr_matrix(mat)
|
||||
|
||||
mat_copy = np.copy(mat)
|
||||
sp_mat_copy = sparse.csr_matrix(sp_mat, copy=True)
|
||||
|
||||
n_nodes = mat.shape[0]
|
||||
explicit_laplacian = _explicit_laplacian(mat, normed=normed)
|
||||
laplacian = csgraph.laplacian(mat, normed=normed, copy=copy)
|
||||
sp_laplacian = csgraph.laplacian(sp_mat, normed=normed,
|
||||
copy=copy)
|
||||
|
||||
if copy:
|
||||
assert_allclose(mat, mat_copy)
|
||||
_assert_allclose_sparse(sp_mat, sp_mat_copy)
|
||||
else:
|
||||
if not (normed and check_int_type(mat)):
|
||||
assert_allclose(laplacian, mat)
|
||||
if sp_mat.format == 'coo':
|
||||
_assert_allclose_sparse(sp_laplacian, sp_mat)
|
||||
|
||||
assert_allclose(laplacian, sp_laplacian.toarray())
|
||||
|
||||
for tested in [laplacian, sp_laplacian.toarray()]:
|
||||
if not normed:
|
||||
assert_allclose(tested.sum(axis=0), np.zeros(n_nodes))
|
||||
assert_allclose(tested.T, tested)
|
||||
assert_allclose(tested, explicit_laplacian)
|
||||
|
||||
|
||||
def test_symmetric_graph_laplacian():
|
||||
symmetric_mats = (
|
||||
'np.arange(10) * np.arange(10)[:, np.newaxis]',
|
||||
'np.ones((7, 7))',
|
||||
'np.eye(19)',
|
||||
'sparse.diags([1, 1], [-1, 1], shape=(4, 4))',
|
||||
'sparse.diags([1, 1], [-1, 1], shape=(4, 4)).toarray()',
|
||||
'sparse.diags([1, 1], [-1, 1], shape=(4, 4)).todense()',
|
||||
'np.vander(np.arange(4)) + np.vander(np.arange(4)).T'
|
||||
)
|
||||
for mat in symmetric_mats:
|
||||
for normed in True, False:
|
||||
for copy in True, False:
|
||||
_check_symmetric_graph_laplacian(mat, normed, copy)
|
||||
|
||||
|
||||
def _assert_allclose_sparse(a, b, **kwargs):
|
||||
# helper function that can deal with sparse matrices
|
||||
if sparse.issparse(a):
|
||||
a = a.toarray()
|
||||
if sparse.issparse(b):
|
||||
b = b.toarray()
|
||||
assert_allclose(a, b, **kwargs)
|
||||
|
||||
|
||||
def _check_laplacian_dtype_none(
|
||||
A, desired_L, desired_d, normed, use_out_degree, copy, dtype, arr_type
|
||||
):
|
||||
mat = arr_type(A, dtype=dtype)
|
||||
L, d = csgraph.laplacian(
|
||||
mat,
|
||||
normed=normed,
|
||||
return_diag=True,
|
||||
use_out_degree=use_out_degree,
|
||||
copy=copy,
|
||||
dtype=None,
|
||||
)
|
||||
if normed and check_int_type(mat):
|
||||
assert L.dtype == np.float64
|
||||
assert d.dtype == np.float64
|
||||
_assert_allclose_sparse(L, desired_L, atol=1e-12)
|
||||
_assert_allclose_sparse(d, desired_d, atol=1e-12)
|
||||
else:
|
||||
assert L.dtype == dtype
|
||||
assert d.dtype == dtype
|
||||
desired_L = np.asarray(desired_L).astype(dtype)
|
||||
desired_d = np.asarray(desired_d).astype(dtype)
|
||||
_assert_allclose_sparse(L, desired_L, atol=1e-12)
|
||||
_assert_allclose_sparse(d, desired_d, atol=1e-12)
|
||||
|
||||
if not copy:
|
||||
if not (normed and check_int_type(mat)):
|
||||
if type(mat) is np.ndarray:
|
||||
assert_allclose(L, mat)
|
||||
elif mat.format == "coo":
|
||||
_assert_allclose_sparse(L, mat)
|
||||
|
||||
|
||||
def _check_laplacian_dtype(
|
||||
A, desired_L, desired_d, normed, use_out_degree, copy, dtype, arr_type
|
||||
):
|
||||
mat = arr_type(A, dtype=dtype)
|
||||
L, d = csgraph.laplacian(
|
||||
mat,
|
||||
normed=normed,
|
||||
return_diag=True,
|
||||
use_out_degree=use_out_degree,
|
||||
copy=copy,
|
||||
dtype=dtype,
|
||||
)
|
||||
assert L.dtype == dtype
|
||||
assert d.dtype == dtype
|
||||
desired_L = np.asarray(desired_L).astype(dtype)
|
||||
desired_d = np.asarray(desired_d).astype(dtype)
|
||||
_assert_allclose_sparse(L, desired_L, atol=1e-12)
|
||||
_assert_allclose_sparse(d, desired_d, atol=1e-12)
|
||||
|
||||
if not copy:
|
||||
if not (normed and check_int_type(mat)):
|
||||
if type(mat) is np.ndarray:
|
||||
assert_allclose(L, mat)
|
||||
elif mat.format == 'coo':
|
||||
_assert_allclose_sparse(L, mat)
|
||||
|
||||
|
||||
INT_DTYPES = {np.intc, np_long, np.longlong}
|
||||
REAL_DTYPES = {np.float32, np.float64, np.longdouble}
|
||||
COMPLEX_DTYPES = {np.complex64, np.complex128, np.clongdouble}
|
||||
# use sorted list to ensure fixed order of tests
|
||||
DTYPES = sorted(INT_DTYPES ^ REAL_DTYPES ^ COMPLEX_DTYPES, key=str)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("arr_type", [np.array,
|
||||
sparse.csr_matrix,
|
||||
sparse.coo_matrix,
|
||||
sparse.csr_array,
|
||||
sparse.coo_array])
|
||||
@pytest.mark.parametrize("copy", [True, False])
|
||||
@pytest.mark.parametrize("normed", [True, False])
|
||||
@pytest.mark.parametrize("use_out_degree", [True, False])
|
||||
def test_asymmetric_laplacian(use_out_degree, normed,
|
||||
copy, dtype, arr_type):
|
||||
# adjacency matrix
|
||||
A = [[0, 1, 0],
|
||||
[4, 2, 0],
|
||||
[0, 0, 0]]
|
||||
A = arr_type(np.array(A), dtype=dtype)
|
||||
A_copy = A.copy()
|
||||
|
||||
if not normed and use_out_degree:
|
||||
# Laplacian matrix using out-degree
|
||||
L = [[1, -1, 0],
|
||||
[-4, 4, 0],
|
||||
[0, 0, 0]]
|
||||
d = [1, 4, 0]
|
||||
|
||||
if normed and use_out_degree:
|
||||
# normalized Laplacian matrix using out-degree
|
||||
L = [[1, -0.5, 0],
|
||||
[-2, 1, 0],
|
||||
[0, 0, 0]]
|
||||
d = [1, 2, 1]
|
||||
|
||||
if not normed and not use_out_degree:
|
||||
# Laplacian matrix using in-degree
|
||||
L = [[4, -1, 0],
|
||||
[-4, 1, 0],
|
||||
[0, 0, 0]]
|
||||
d = [4, 1, 0]
|
||||
|
||||
if normed and not use_out_degree:
|
||||
# normalized Laplacian matrix using in-degree
|
||||
L = [[1, -0.5, 0],
|
||||
[-2, 1, 0],
|
||||
[0, 0, 0]]
|
||||
d = [2, 1, 1]
|
||||
|
||||
_check_laplacian_dtype_none(
|
||||
A,
|
||||
L,
|
||||
d,
|
||||
normed=normed,
|
||||
use_out_degree=use_out_degree,
|
||||
copy=copy,
|
||||
dtype=dtype,
|
||||
arr_type=arr_type,
|
||||
)
|
||||
|
||||
_check_laplacian_dtype(
|
||||
A_copy,
|
||||
L,
|
||||
d,
|
||||
normed=normed,
|
||||
use_out_degree=use_out_degree,
|
||||
copy=copy,
|
||||
dtype=dtype,
|
||||
arr_type=arr_type,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fmt", ['csr', 'csc', 'coo', 'lil',
|
||||
'dok', 'dia', 'bsr'])
|
||||
@pytest.mark.parametrize("normed", [True, False])
|
||||
@pytest.mark.parametrize("copy", [True, False])
|
||||
def test_sparse_formats(fmt, normed, copy):
|
||||
mat = sparse.diags([1, 1], [-1, 1], shape=(4, 4), format=fmt)
|
||||
_check_symmetric_graph_laplacian(mat, normed, copy)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arr_type", [np.asarray,
|
||||
sparse.csr_matrix,
|
||||
sparse.coo_matrix,
|
||||
sparse.csr_array,
|
||||
sparse.coo_array]
|
||||
)
|
||||
@pytest.mark.parametrize("form", ["array", "function", "lo"])
|
||||
def test_laplacian_symmetrized(arr_type, form):
|
||||
# adjacency matrix
|
||||
n = 3
|
||||
mat = arr_type(np.arange(n * n).reshape(n, n))
|
||||
L_in, d_in = csgraph.laplacian(
|
||||
mat,
|
||||
return_diag=True,
|
||||
form=form,
|
||||
)
|
||||
L_out, d_out = csgraph.laplacian(
|
||||
mat,
|
||||
return_diag=True,
|
||||
use_out_degree=True,
|
||||
form=form,
|
||||
)
|
||||
Ls, ds = csgraph.laplacian(
|
||||
mat,
|
||||
return_diag=True,
|
||||
symmetrized=True,
|
||||
form=form,
|
||||
)
|
||||
Ls_normed, ds_normed = csgraph.laplacian(
|
||||
mat,
|
||||
return_diag=True,
|
||||
symmetrized=True,
|
||||
normed=True,
|
||||
form=form,
|
||||
)
|
||||
mat += mat.T
|
||||
Lss, dss = csgraph.laplacian(mat, return_diag=True, form=form)
|
||||
Lss_normed, dss_normed = csgraph.laplacian(
|
||||
mat,
|
||||
return_diag=True,
|
||||
normed=True,
|
||||
form=form,
|
||||
)
|
||||
|
||||
assert_allclose(ds, d_in + d_out)
|
||||
assert_allclose(ds, dss)
|
||||
assert_allclose(ds_normed, dss_normed)
|
||||
|
||||
d = {}
|
||||
for L in ["L_in", "L_out", "Ls", "Ls_normed", "Lss", "Lss_normed"]:
|
||||
if form == "array":
|
||||
d[L] = eval(L)
|
||||
else:
|
||||
d[L] = eval(L)(np.eye(n, dtype=mat.dtype))
|
||||
|
||||
_assert_allclose_sparse(d["Ls"], d["L_in"] + d["L_out"].T)
|
||||
_assert_allclose_sparse(d["Ls"], d["Lss"])
|
||||
_assert_allclose_sparse(d["Ls_normed"], d["Lss_normed"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arr_type", [np.asarray,
|
||||
sparse.csr_matrix,
|
||||
sparse.coo_matrix,
|
||||
sparse.csr_array,
|
||||
sparse.coo_array]
|
||||
)
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("normed", [True, False])
|
||||
@pytest.mark.parametrize("symmetrized", [True, False])
|
||||
@pytest.mark.parametrize("use_out_degree", [True, False])
|
||||
@pytest.mark.parametrize("form", ["function", "lo"])
|
||||
def test_format(dtype, arr_type, normed, symmetrized, use_out_degree, form):
|
||||
n = 3
|
||||
mat = [[0, 1, 0], [4, 2, 0], [0, 0, 0]]
|
||||
mat = arr_type(np.array(mat), dtype=dtype)
|
||||
Lo, do = csgraph.laplacian(
|
||||
mat,
|
||||
return_diag=True,
|
||||
normed=normed,
|
||||
symmetrized=symmetrized,
|
||||
use_out_degree=use_out_degree,
|
||||
dtype=dtype,
|
||||
)
|
||||
La, da = csgraph.laplacian(
|
||||
mat,
|
||||
return_diag=True,
|
||||
normed=normed,
|
||||
symmetrized=symmetrized,
|
||||
use_out_degree=use_out_degree,
|
||||
dtype=dtype,
|
||||
form="array",
|
||||
)
|
||||
assert_allclose(do, da)
|
||||
_assert_allclose_sparse(Lo, La)
|
||||
|
||||
L, d = csgraph.laplacian(
|
||||
mat,
|
||||
return_diag=True,
|
||||
normed=normed,
|
||||
symmetrized=symmetrized,
|
||||
use_out_degree=use_out_degree,
|
||||
dtype=dtype,
|
||||
form=form,
|
||||
)
|
||||
assert_allclose(d, do)
|
||||
assert d.dtype == dtype
|
||||
Lm = L(np.eye(n, dtype=mat.dtype)).astype(dtype)
|
||||
_assert_allclose_sparse(Lm, Lo, rtol=2e-7, atol=2e-7)
|
||||
x = np.arange(6).reshape(3, 2)
|
||||
if not (normed and dtype in INT_DTYPES):
|
||||
assert_allclose(L(x), Lo @ x)
|
||||
else:
|
||||
# Normalized Lo is casted to integer, but L() is not
|
||||
pass
|
||||
|
||||
|
||||
def test_format_error_message():
|
||||
with pytest.raises(ValueError, match="Invalid form: 'toto'"):
|
||||
_ = csgraph.laplacian(np.eye(1), form='toto')
|
||||
@ -0,0 +1,294 @@
|
||||
from itertools import product
|
||||
|
||||
import numpy as np
|
||||
from numpy.testing import assert_array_equal, assert_equal
|
||||
import pytest
|
||||
|
||||
from scipy.sparse import csr_matrix, coo_matrix, diags
|
||||
from scipy.sparse.csgraph import (
|
||||
maximum_bipartite_matching, min_weight_full_bipartite_matching
|
||||
)
|
||||
|
||||
|
||||
def test_maximum_bipartite_matching_raises_on_dense_input():
|
||||
with pytest.raises(TypeError):
|
||||
graph = np.array([[0, 1], [0, 0]])
|
||||
maximum_bipartite_matching(graph)
|
||||
|
||||
|
||||
def test_maximum_bipartite_matching_empty_graph():
|
||||
graph = csr_matrix((0, 0))
|
||||
x = maximum_bipartite_matching(graph, perm_type='row')
|
||||
y = maximum_bipartite_matching(graph, perm_type='column')
|
||||
expected_matching = np.array([])
|
||||
assert_array_equal(expected_matching, x)
|
||||
assert_array_equal(expected_matching, y)
|
||||
|
||||
|
||||
def test_maximum_bipartite_matching_empty_left_partition():
|
||||
graph = csr_matrix((2, 0))
|
||||
x = maximum_bipartite_matching(graph, perm_type='row')
|
||||
y = maximum_bipartite_matching(graph, perm_type='column')
|
||||
assert_array_equal(np.array([]), x)
|
||||
assert_array_equal(np.array([-1, -1]), y)
|
||||
|
||||
|
||||
def test_maximum_bipartite_matching_empty_right_partition():
|
||||
graph = csr_matrix((0, 3))
|
||||
x = maximum_bipartite_matching(graph, perm_type='row')
|
||||
y = maximum_bipartite_matching(graph, perm_type='column')
|
||||
assert_array_equal(np.array([-1, -1, -1]), x)
|
||||
assert_array_equal(np.array([]), y)
|
||||
|
||||
|
||||
def test_maximum_bipartite_matching_graph_with_no_edges():
|
||||
graph = csr_matrix((2, 2))
|
||||
x = maximum_bipartite_matching(graph, perm_type='row')
|
||||
y = maximum_bipartite_matching(graph, perm_type='column')
|
||||
assert_array_equal(np.array([-1, -1]), x)
|
||||
assert_array_equal(np.array([-1, -1]), y)
|
||||
|
||||
|
||||
def test_maximum_bipartite_matching_graph_that_causes_augmentation():
|
||||
# In this graph, column 1 is initially assigned to row 1, but it should be
|
||||
# reassigned to make room for row 2.
|
||||
graph = csr_matrix([[1, 1], [1, 0]])
|
||||
x = maximum_bipartite_matching(graph, perm_type='column')
|
||||
y = maximum_bipartite_matching(graph, perm_type='row')
|
||||
expected_matching = np.array([1, 0])
|
||||
assert_array_equal(expected_matching, x)
|
||||
assert_array_equal(expected_matching, y)
|
||||
|
||||
|
||||
def test_maximum_bipartite_matching_graph_with_more_rows_than_columns():
|
||||
graph = csr_matrix([[1, 1], [1, 0], [0, 1]])
|
||||
x = maximum_bipartite_matching(graph, perm_type='column')
|
||||
y = maximum_bipartite_matching(graph, perm_type='row')
|
||||
assert_array_equal(np.array([0, -1, 1]), x)
|
||||
assert_array_equal(np.array([0, 2]), y)
|
||||
|
||||
|
||||
def test_maximum_bipartite_matching_graph_with_more_columns_than_rows():
|
||||
graph = csr_matrix([[1, 1, 0], [0, 0, 1]])
|
||||
x = maximum_bipartite_matching(graph, perm_type='column')
|
||||
y = maximum_bipartite_matching(graph, perm_type='row')
|
||||
assert_array_equal(np.array([0, 2]), x)
|
||||
assert_array_equal(np.array([0, -1, 1]), y)
|
||||
|
||||
|
||||
def test_maximum_bipartite_matching_explicit_zeros_count_as_edges():
|
||||
data = [0, 0]
|
||||
indices = [1, 0]
|
||||
indptr = [0, 1, 2]
|
||||
graph = csr_matrix((data, indices, indptr), shape=(2, 2))
|
||||
x = maximum_bipartite_matching(graph, perm_type='row')
|
||||
y = maximum_bipartite_matching(graph, perm_type='column')
|
||||
expected_matching = np.array([1, 0])
|
||||
assert_array_equal(expected_matching, x)
|
||||
assert_array_equal(expected_matching, y)
|
||||
|
||||
|
||||
def test_maximum_bipartite_matching_feasibility_of_result():
|
||||
# This is a regression test for GitHub issue #11458
|
||||
data = np.ones(50, dtype=int)
|
||||
indices = [11, 12, 19, 22, 23, 5, 22, 3, 8, 10, 5, 6, 11, 12, 13, 5, 13,
|
||||
14, 20, 22, 3, 15, 3, 13, 14, 11, 12, 19, 22, 23, 5, 22, 3, 8,
|
||||
10, 5, 6, 11, 12, 13, 5, 13, 14, 20, 22, 3, 15, 3, 13, 14]
|
||||
indptr = [0, 5, 7, 10, 10, 15, 20, 22, 22, 23, 25, 30, 32, 35, 35, 40, 45,
|
||||
47, 47, 48, 50]
|
||||
graph = csr_matrix((data, indices, indptr), shape=(20, 25))
|
||||
x = maximum_bipartite_matching(graph, perm_type='row')
|
||||
y = maximum_bipartite_matching(graph, perm_type='column')
|
||||
assert (x != -1).sum() == 13
|
||||
assert (y != -1).sum() == 13
|
||||
# Ensure that each element of the matching is in fact an edge in the graph.
|
||||
for u, v in zip(range(graph.shape[0]), y):
|
||||
if v != -1:
|
||||
assert graph[u, v]
|
||||
for u, v in zip(x, range(graph.shape[1])):
|
||||
if u != -1:
|
||||
assert graph[u, v]
|
||||
|
||||
|
||||
def test_matching_large_random_graph_with_one_edge_incident_to_each_vertex():
|
||||
np.random.seed(42)
|
||||
A = diags(np.ones(25), offsets=0, format='csr')
|
||||
rand_perm = np.random.permutation(25)
|
||||
rand_perm2 = np.random.permutation(25)
|
||||
|
||||
Rrow = np.arange(25)
|
||||
Rcol = rand_perm
|
||||
Rdata = np.ones(25, dtype=int)
|
||||
Rmat = coo_matrix((Rdata, (Rrow, Rcol))).tocsr()
|
||||
|
||||
Crow = rand_perm2
|
||||
Ccol = np.arange(25)
|
||||
Cdata = np.ones(25, dtype=int)
|
||||
Cmat = coo_matrix((Cdata, (Crow, Ccol))).tocsr()
|
||||
# Randomly permute identity matrix
|
||||
B = Rmat * A * Cmat
|
||||
|
||||
# Row permute
|
||||
perm = maximum_bipartite_matching(B, perm_type='row')
|
||||
Rrow = np.arange(25)
|
||||
Rcol = perm
|
||||
Rdata = np.ones(25, dtype=int)
|
||||
Rmat = coo_matrix((Rdata, (Rrow, Rcol))).tocsr()
|
||||
C1 = Rmat * B
|
||||
|
||||
# Column permute
|
||||
perm2 = maximum_bipartite_matching(B, perm_type='column')
|
||||
Crow = perm2
|
||||
Ccol = np.arange(25)
|
||||
Cdata = np.ones(25, dtype=int)
|
||||
Cmat = coo_matrix((Cdata, (Crow, Ccol))).tocsr()
|
||||
C2 = B * Cmat
|
||||
|
||||
# Should get identity matrix back
|
||||
assert_equal(any(C1.diagonal() == 0), False)
|
||||
assert_equal(any(C2.diagonal() == 0), False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('num_rows,num_cols', [(0, 0), (2, 0), (0, 3)])
|
||||
def test_min_weight_full_matching_trivial_graph(num_rows, num_cols):
|
||||
biadjacency_matrix = csr_matrix((num_cols, num_rows))
|
||||
row_ind, col_ind = min_weight_full_bipartite_matching(biadjacency_matrix)
|
||||
assert len(row_ind) == 0
|
||||
assert len(col_ind) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('biadjacency_matrix',
|
||||
[
|
||||
[[1, 1, 1], [1, 0, 0], [1, 0, 0]],
|
||||
[[1, 1, 1], [0, 0, 1], [0, 0, 1]],
|
||||
[[1, 0, 0, 1], [1, 1, 0, 1], [0, 0, 0, 0]],
|
||||
[[1, 0, 0], [2, 0, 0]],
|
||||
[[0, 1, 0], [0, 2, 0]],
|
||||
[[1, 0], [2, 0], [5, 0]]
|
||||
])
|
||||
def test_min_weight_full_matching_infeasible_problems(biadjacency_matrix):
|
||||
with pytest.raises(ValueError):
|
||||
min_weight_full_bipartite_matching(csr_matrix(biadjacency_matrix))
|
||||
|
||||
|
||||
def test_min_weight_full_matching_large_infeasible():
|
||||
# Regression test for GitHub issue #17269
|
||||
a = np.asarray([
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001],
|
||||
[0.0, 0.11687445, 0.0, 0.0, 0.01319788, 0.07509257, 0.0,
|
||||
0.0, 0.0, 0.74228317, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.81087935, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.0, 0.8408466, 0.0, 0.0, 0.0, 0.0, 0.01194389,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.82994211, 0.0, 0.0, 0.0, 0.11468516, 0.0, 0.0, 0.0,
|
||||
0.11173505, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0],
|
||||
[0.18796507, 0.0, 0.04002318, 0.0, 0.0, 0.0, 0.0, 0.0, 0.75883335,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.71545464, 0.0, 0.0, 0.0, 0.0, 0.0, 0.02748488,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.78470564, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.14829198,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.10870609, 0.0, 0.0, 0.0, 0.8918677, 0.0, 0.0, 0.0, 0.06306644,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
0.63844085, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7442354, 0.0, 0.0, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.09850549, 0.0, 0.0, 0.18638258,
|
||||
0.2769244, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.73182464, 0.0, 0.0, 0.46443561,
|
||||
0.38589284, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
[0.29510278, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.09666032, 0.0,
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
])
|
||||
with pytest.raises(ValueError, match='no full matching exists'):
|
||||
min_weight_full_bipartite_matching(csr_matrix(a))
|
||||
|
||||
|
||||
def test_explicit_zero_causes_warning():
|
||||
with pytest.warns(UserWarning):
|
||||
biadjacency_matrix = csr_matrix(((2, 0, 3), (0, 1, 1), (0, 2, 3)))
|
||||
min_weight_full_bipartite_matching(biadjacency_matrix)
|
||||
|
||||
|
||||
# General test for linear sum assignment solvers to make it possible to rely
|
||||
# on the same tests for scipy.optimize.linear_sum_assignment.
|
||||
def linear_sum_assignment_assertions(
|
||||
solver, array_type, sign, test_case
|
||||
):
|
||||
cost_matrix, expected_cost = test_case
|
||||
maximize = sign == -1
|
||||
cost_matrix = sign * array_type(cost_matrix)
|
||||
expected_cost = sign * np.array(expected_cost)
|
||||
|
||||
row_ind, col_ind = solver(cost_matrix, maximize=maximize)
|
||||
assert_array_equal(row_ind, np.sort(row_ind))
|
||||
assert_array_equal(expected_cost,
|
||||
np.array(cost_matrix[row_ind, col_ind]).flatten())
|
||||
|
||||
cost_matrix = cost_matrix.T
|
||||
row_ind, col_ind = solver(cost_matrix, maximize=maximize)
|
||||
assert_array_equal(row_ind, np.sort(row_ind))
|
||||
assert_array_equal(np.sort(expected_cost),
|
||||
np.sort(np.array(
|
||||
cost_matrix[row_ind, col_ind])).flatten())
|
||||
|
||||
|
||||
linear_sum_assignment_test_cases = product(
|
||||
[-1, 1],
|
||||
[
|
||||
# Square
|
||||
([[400, 150, 400],
|
||||
[400, 450, 600],
|
||||
[300, 225, 300]],
|
||||
[150, 400, 300]),
|
||||
|
||||
# Rectangular variant
|
||||
([[400, 150, 400, 1],
|
||||
[400, 450, 600, 2],
|
||||
[300, 225, 300, 3]],
|
||||
[150, 2, 300]),
|
||||
|
||||
([[10, 10, 8],
|
||||
[9, 8, 1],
|
||||
[9, 7, 4]],
|
||||
[10, 1, 7]),
|
||||
|
||||
# Square
|
||||
([[10, 10, 8, 11],
|
||||
[9, 8, 1, 1],
|
||||
[9, 7, 4, 10]],
|
||||
[10, 1, 4]),
|
||||
|
||||
# Rectangular variant
|
||||
([[10, float("inf"), float("inf")],
|
||||
[float("inf"), float("inf"), 1],
|
||||
[float("inf"), 7, float("inf")]],
|
||||
[10, 1, 7])
|
||||
])
|
||||
|
||||
|
||||
@pytest.mark.parametrize('sign,test_case', linear_sum_assignment_test_cases)
|
||||
def test_min_weight_full_matching_small_inputs(sign, test_case):
|
||||
linear_sum_assignment_assertions(
|
||||
min_weight_full_bipartite_matching, csr_matrix, sign, test_case)
|
||||
@ -0,0 +1,149 @@
|
||||
import pytest
|
||||
|
||||
import numpy as np
|
||||
import scipy.sparse as sp
|
||||
import scipy.sparse.csgraph as spgraph
|
||||
|
||||
from numpy.testing import assert_equal
|
||||
|
||||
try:
|
||||
import sparse
|
||||
except Exception:
|
||||
sparse = None
|
||||
|
||||
pytestmark = pytest.mark.skipif(sparse is None,
|
||||
reason="pydata/sparse not installed")
|
||||
|
||||
|
||||
msg = "pydata/sparse (0.15.1) does not implement necessary operations"
|
||||
|
||||
|
||||
sparse_params = (pytest.param("COO"),
|
||||
pytest.param("DOK", marks=[pytest.mark.xfail(reason=msg)]))
|
||||
|
||||
|
||||
@pytest.fixture(params=sparse_params)
|
||||
def sparse_cls(request):
|
||||
return getattr(sparse, request.param)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def graphs(sparse_cls):
|
||||
graph = [
|
||||
[0, 1, 1, 0, 0],
|
||||
[0, 0, 1, 0, 0],
|
||||
[0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 1],
|
||||
[0, 0, 0, 0, 0],
|
||||
]
|
||||
A_dense = np.array(graph)
|
||||
A_sparse = sparse_cls(A_dense)
|
||||
return A_dense, A_sparse
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"func",
|
||||
[
|
||||
spgraph.shortest_path,
|
||||
spgraph.dijkstra,
|
||||
spgraph.floyd_warshall,
|
||||
spgraph.bellman_ford,
|
||||
spgraph.johnson,
|
||||
spgraph.reverse_cuthill_mckee,
|
||||
spgraph.maximum_bipartite_matching,
|
||||
spgraph.structural_rank,
|
||||
]
|
||||
)
|
||||
def test_csgraph_equiv(func, graphs):
|
||||
A_dense, A_sparse = graphs
|
||||
actual = func(A_sparse)
|
||||
desired = func(sp.csc_matrix(A_dense))
|
||||
assert_equal(actual, desired)
|
||||
|
||||
|
||||
def test_connected_components(graphs):
|
||||
A_dense, A_sparse = graphs
|
||||
func = spgraph.connected_components
|
||||
|
||||
actual_comp, actual_labels = func(A_sparse)
|
||||
desired_comp, desired_labels, = func(sp.csc_matrix(A_dense))
|
||||
|
||||
assert actual_comp == desired_comp
|
||||
assert_equal(actual_labels, desired_labels)
|
||||
|
||||
|
||||
def test_laplacian(graphs):
|
||||
A_dense, A_sparse = graphs
|
||||
sparse_cls = type(A_sparse)
|
||||
func = spgraph.laplacian
|
||||
|
||||
actual = func(A_sparse)
|
||||
desired = func(sp.csc_matrix(A_dense))
|
||||
|
||||
assert isinstance(actual, sparse_cls)
|
||||
|
||||
assert_equal(actual.todense(), desired.todense())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"func", [spgraph.breadth_first_order, spgraph.depth_first_order]
|
||||
)
|
||||
def test_order_search(graphs, func):
|
||||
A_dense, A_sparse = graphs
|
||||
|
||||
actual = func(A_sparse, 0)
|
||||
desired = func(sp.csc_matrix(A_dense), 0)
|
||||
|
||||
assert_equal(actual, desired)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"func", [spgraph.breadth_first_tree, spgraph.depth_first_tree]
|
||||
)
|
||||
def test_tree_search(graphs, func):
|
||||
A_dense, A_sparse = graphs
|
||||
sparse_cls = type(A_sparse)
|
||||
|
||||
actual = func(A_sparse, 0)
|
||||
desired = func(sp.csc_matrix(A_dense), 0)
|
||||
|
||||
assert isinstance(actual, sparse_cls)
|
||||
|
||||
assert_equal(actual.todense(), desired.todense())
|
||||
|
||||
|
||||
def test_minimum_spanning_tree(graphs):
|
||||
A_dense, A_sparse = graphs
|
||||
sparse_cls = type(A_sparse)
|
||||
func = spgraph.minimum_spanning_tree
|
||||
|
||||
actual = func(A_sparse)
|
||||
desired = func(sp.csc_matrix(A_dense))
|
||||
|
||||
assert isinstance(actual, sparse_cls)
|
||||
|
||||
assert_equal(actual.todense(), desired.todense())
|
||||
|
||||
|
||||
def test_maximum_flow(graphs):
|
||||
A_dense, A_sparse = graphs
|
||||
sparse_cls = type(A_sparse)
|
||||
func = spgraph.maximum_flow
|
||||
|
||||
actual = func(A_sparse, 0, 2)
|
||||
desired = func(sp.csr_matrix(A_dense), 0, 2)
|
||||
|
||||
assert actual.flow_value == desired.flow_value
|
||||
assert isinstance(actual.flow, sparse_cls)
|
||||
|
||||
assert_equal(actual.flow.todense(), desired.flow.todense())
|
||||
|
||||
|
||||
def test_min_weight_full_bipartite_matching(graphs):
|
||||
A_dense, A_sparse = graphs
|
||||
func = spgraph.min_weight_full_bipartite_matching
|
||||
|
||||
actual = func(A_sparse[0:2, 1:3])
|
||||
desired = func(sp.csc_matrix(A_dense)[0:2, 1:3])
|
||||
|
||||
assert_equal(actual, desired)
|
||||
@ -0,0 +1,70 @@
|
||||
import numpy as np
|
||||
from numpy.testing import assert_equal
|
||||
from scipy.sparse.csgraph import reverse_cuthill_mckee, structural_rank
|
||||
from scipy.sparse import csc_matrix, csr_matrix, coo_matrix
|
||||
|
||||
|
||||
def test_graph_reverse_cuthill_mckee():
|
||||
A = np.array([[1, 0, 0, 0, 1, 0, 0, 0],
|
||||
[0, 1, 1, 0, 0, 1, 0, 1],
|
||||
[0, 1, 1, 0, 1, 0, 0, 0],
|
||||
[0, 0, 0, 1, 0, 0, 1, 0],
|
||||
[1, 0, 1, 0, 1, 0, 0, 0],
|
||||
[0, 1, 0, 0, 0, 1, 0, 1],
|
||||
[0, 0, 0, 1, 0, 0, 1, 0],
|
||||
[0, 1, 0, 0, 0, 1, 0, 1]], dtype=int)
|
||||
|
||||
graph = csr_matrix(A)
|
||||
perm = reverse_cuthill_mckee(graph)
|
||||
correct_perm = np.array([6, 3, 7, 5, 1, 2, 4, 0])
|
||||
assert_equal(perm, correct_perm)
|
||||
|
||||
# Test int64 indices input
|
||||
graph.indices = graph.indices.astype('int64')
|
||||
graph.indptr = graph.indptr.astype('int64')
|
||||
perm = reverse_cuthill_mckee(graph, True)
|
||||
assert_equal(perm, correct_perm)
|
||||
|
||||
|
||||
def test_graph_reverse_cuthill_mckee_ordering():
|
||||
data = np.ones(63,dtype=int)
|
||||
rows = np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2,
|
||||
2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5,
|
||||
6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9,
|
||||
9, 10, 10, 10, 10, 10, 11, 11, 11, 11,
|
||||
12, 12, 12, 13, 13, 13, 13, 14, 14, 14,
|
||||
14, 15, 15, 15, 15, 15])
|
||||
cols = np.array([0, 2, 5, 8, 10, 1, 3, 9, 11, 0, 2,
|
||||
7, 10, 1, 3, 11, 4, 6, 12, 14, 0, 7, 13,
|
||||
15, 4, 6, 14, 2, 5, 7, 15, 0, 8, 10, 13,
|
||||
1, 9, 11, 0, 2, 8, 10, 15, 1, 3, 9, 11,
|
||||
4, 12, 14, 5, 8, 13, 15, 4, 6, 12, 14,
|
||||
5, 7, 10, 13, 15])
|
||||
graph = coo_matrix((data, (rows,cols))).tocsr()
|
||||
perm = reverse_cuthill_mckee(graph)
|
||||
correct_perm = np.array([12, 14, 4, 6, 10, 8, 2, 15,
|
||||
0, 13, 7, 5, 9, 11, 1, 3])
|
||||
assert_equal(perm, correct_perm)
|
||||
|
||||
|
||||
def test_graph_structural_rank():
|
||||
# Test square matrix #1
|
||||
A = csc_matrix([[1, 1, 0],
|
||||
[1, 0, 1],
|
||||
[0, 1, 0]])
|
||||
assert_equal(structural_rank(A), 3)
|
||||
|
||||
# Test square matrix #2
|
||||
rows = np.array([0,0,0,0,0,1,1,2,2,3,3,3,3,3,3,4,4,5,5,6,6,7,7])
|
||||
cols = np.array([0,1,2,3,4,2,5,2,6,0,1,3,5,6,7,4,5,5,6,2,6,2,4])
|
||||
data = np.ones_like(rows)
|
||||
B = coo_matrix((data,(rows,cols)), shape=(8,8))
|
||||
assert_equal(structural_rank(B), 6)
|
||||
|
||||
#Test non-square matrix
|
||||
C = csc_matrix([[1, 0, 2, 0],
|
||||
[2, 0, 4, 0]])
|
||||
assert_equal(structural_rank(C), 2)
|
||||
|
||||
#Test tall matrix
|
||||
assert_equal(structural_rank(C.T), 2)
|
||||
@ -0,0 +1,454 @@
|
||||
from io import StringIO
|
||||
import warnings
|
||||
import numpy as np
|
||||
from numpy.testing import assert_array_almost_equal, assert_array_equal, assert_allclose
|
||||
from pytest import raises as assert_raises
|
||||
from scipy.sparse.csgraph import (shortest_path, dijkstra, johnson,
|
||||
bellman_ford, construct_dist_matrix, yen,
|
||||
NegativeCycleError)
|
||||
import scipy.sparse
|
||||
from scipy.io import mmread
|
||||
import pytest
|
||||
|
||||
directed_G = np.array([[0, 3, 3, 0, 0],
|
||||
[0, 0, 0, 2, 4],
|
||||
[0, 0, 0, 0, 0],
|
||||
[1, 0, 0, 0, 0],
|
||||
[2, 0, 0, 2, 0]], dtype=float)
|
||||
|
||||
undirected_G = np.array([[0, 3, 3, 1, 2],
|
||||
[3, 0, 0, 2, 4],
|
||||
[3, 0, 0, 0, 0],
|
||||
[1, 2, 0, 0, 2],
|
||||
[2, 4, 0, 2, 0]], dtype=float)
|
||||
|
||||
unweighted_G = (directed_G > 0).astype(float)
|
||||
|
||||
directed_SP = [[0, 3, 3, 5, 7],
|
||||
[3, 0, 6, 2, 4],
|
||||
[np.inf, np.inf, 0, np.inf, np.inf],
|
||||
[1, 4, 4, 0, 8],
|
||||
[2, 5, 5, 2, 0]]
|
||||
|
||||
directed_2SP_0_to_3 = [[-9999, 0, -9999, 1, -9999],
|
||||
[-9999, 0, -9999, 4, 1]]
|
||||
|
||||
directed_sparse_zero_G = scipy.sparse.csr_matrix(([0, 1, 2, 3, 1],
|
||||
([0, 1, 2, 3, 4],
|
||||
[1, 2, 0, 4, 3])),
|
||||
shape = (5, 5))
|
||||
|
||||
directed_sparse_zero_SP = [[0, 0, 1, np.inf, np.inf],
|
||||
[3, 0, 1, np.inf, np.inf],
|
||||
[2, 2, 0, np.inf, np.inf],
|
||||
[np.inf, np.inf, np.inf, 0, 3],
|
||||
[np.inf, np.inf, np.inf, 1, 0]]
|
||||
|
||||
undirected_sparse_zero_G = scipy.sparse.csr_matrix(([0, 0, 1, 1, 2, 2, 1, 1],
|
||||
([0, 1, 1, 2, 2, 0, 3, 4],
|
||||
[1, 0, 2, 1, 0, 2, 4, 3])),
|
||||
shape = (5, 5))
|
||||
|
||||
undirected_sparse_zero_SP = [[0, 0, 1, np.inf, np.inf],
|
||||
[0, 0, 1, np.inf, np.inf],
|
||||
[1, 1, 0, np.inf, np.inf],
|
||||
[np.inf, np.inf, np.inf, 0, 1],
|
||||
[np.inf, np.inf, np.inf, 1, 0]]
|
||||
|
||||
directed_pred = np.array([[-9999, 0, 0, 1, 1],
|
||||
[3, -9999, 0, 1, 1],
|
||||
[-9999, -9999, -9999, -9999, -9999],
|
||||
[3, 0, 0, -9999, 1],
|
||||
[4, 0, 0, 4, -9999]], dtype=float)
|
||||
|
||||
undirected_SP = np.array([[0, 3, 3, 1, 2],
|
||||
[3, 0, 6, 2, 4],
|
||||
[3, 6, 0, 4, 5],
|
||||
[1, 2, 4, 0, 2],
|
||||
[2, 4, 5, 2, 0]], dtype=float)
|
||||
|
||||
undirected_SP_limit_2 = np.array([[0, np.inf, np.inf, 1, 2],
|
||||
[np.inf, 0, np.inf, 2, np.inf],
|
||||
[np.inf, np.inf, 0, np.inf, np.inf],
|
||||
[1, 2, np.inf, 0, 2],
|
||||
[2, np.inf, np.inf, 2, 0]], dtype=float)
|
||||
|
||||
undirected_SP_limit_0 = np.ones((5, 5), dtype=float) - np.eye(5)
|
||||
undirected_SP_limit_0[undirected_SP_limit_0 > 0] = np.inf
|
||||
|
||||
undirected_pred = np.array([[-9999, 0, 0, 0, 0],
|
||||
[1, -9999, 0, 1, 1],
|
||||
[2, 0, -9999, 0, 0],
|
||||
[3, 3, 0, -9999, 3],
|
||||
[4, 4, 0, 4, -9999]], dtype=float)
|
||||
|
||||
directed_negative_weighted_G = np.array([[0, 0, 0],
|
||||
[-1, 0, 0],
|
||||
[0, -1, 0]], dtype=float)
|
||||
|
||||
directed_negative_weighted_SP = np.array([[0, np.inf, np.inf],
|
||||
[-1, 0, np.inf],
|
||||
[-2, -1, 0]], dtype=float)
|
||||
|
||||
methods = ['auto', 'FW', 'D', 'BF', 'J']
|
||||
|
||||
|
||||
def test_dijkstra_limit():
|
||||
limits = [0, 2, np.inf]
|
||||
results = [undirected_SP_limit_0,
|
||||
undirected_SP_limit_2,
|
||||
undirected_SP]
|
||||
|
||||
def check(limit, result):
|
||||
SP = dijkstra(undirected_G, directed=False, limit=limit)
|
||||
assert_array_almost_equal(SP, result)
|
||||
|
||||
for limit, result in zip(limits, results):
|
||||
check(limit, result)
|
||||
|
||||
|
||||
def test_directed():
|
||||
def check(method):
|
||||
SP = shortest_path(directed_G, method=method, directed=True,
|
||||
overwrite=False)
|
||||
assert_array_almost_equal(SP, directed_SP)
|
||||
|
||||
for method in methods:
|
||||
check(method)
|
||||
|
||||
|
||||
def test_undirected():
|
||||
def check(method, directed_in):
|
||||
if directed_in:
|
||||
SP1 = shortest_path(directed_G, method=method, directed=False,
|
||||
overwrite=False)
|
||||
assert_array_almost_equal(SP1, undirected_SP)
|
||||
else:
|
||||
SP2 = shortest_path(undirected_G, method=method, directed=True,
|
||||
overwrite=False)
|
||||
assert_array_almost_equal(SP2, undirected_SP)
|
||||
|
||||
for method in methods:
|
||||
for directed_in in (True, False):
|
||||
check(method, directed_in)
|
||||
|
||||
|
||||
def test_directed_sparse_zero():
|
||||
# test directed sparse graph with zero-weight edge and two connected components
|
||||
def check(method):
|
||||
SP = shortest_path(directed_sparse_zero_G, method=method, directed=True,
|
||||
overwrite=False)
|
||||
assert_array_almost_equal(SP, directed_sparse_zero_SP)
|
||||
|
||||
for method in methods:
|
||||
check(method)
|
||||
|
||||
|
||||
def test_undirected_sparse_zero():
|
||||
def check(method, directed_in):
|
||||
if directed_in:
|
||||
SP1 = shortest_path(directed_sparse_zero_G, method=method, directed=False,
|
||||
overwrite=False)
|
||||
assert_array_almost_equal(SP1, undirected_sparse_zero_SP)
|
||||
else:
|
||||
SP2 = shortest_path(undirected_sparse_zero_G, method=method, directed=True,
|
||||
overwrite=False)
|
||||
assert_array_almost_equal(SP2, undirected_sparse_zero_SP)
|
||||
|
||||
for method in methods:
|
||||
for directed_in in (True, False):
|
||||
check(method, directed_in)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('directed, SP_ans',
|
||||
((True, directed_SP),
|
||||
(False, undirected_SP)))
|
||||
@pytest.mark.parametrize('indices', ([0, 2, 4], [0, 4], [3, 4], [0, 0]))
|
||||
def test_dijkstra_indices_min_only(directed, SP_ans, indices):
|
||||
SP_ans = np.array(SP_ans)
|
||||
indices = np.array(indices, dtype=np.int64)
|
||||
min_ind_ans = indices[np.argmin(SP_ans[indices, :], axis=0)]
|
||||
min_d_ans = np.zeros(SP_ans.shape[0], SP_ans.dtype)
|
||||
for k in range(SP_ans.shape[0]):
|
||||
min_d_ans[k] = SP_ans[min_ind_ans[k], k]
|
||||
min_ind_ans[np.isinf(min_d_ans)] = -9999
|
||||
|
||||
SP, pred, sources = dijkstra(directed_G,
|
||||
directed=directed,
|
||||
indices=indices,
|
||||
min_only=True,
|
||||
return_predecessors=True)
|
||||
assert_array_almost_equal(SP, min_d_ans)
|
||||
assert_array_equal(min_ind_ans, sources)
|
||||
SP = dijkstra(directed_G,
|
||||
directed=directed,
|
||||
indices=indices,
|
||||
min_only=True,
|
||||
return_predecessors=False)
|
||||
assert_array_almost_equal(SP, min_d_ans)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('n', (10, 100, 1000))
|
||||
def test_dijkstra_min_only_random(n):
|
||||
np.random.seed(1234)
|
||||
data = scipy.sparse.rand(n, n, density=0.5, format='lil',
|
||||
random_state=42, dtype=np.float64)
|
||||
data.setdiag(np.zeros(n, dtype=np.bool_))
|
||||
# choose some random vertices
|
||||
v = np.arange(n)
|
||||
np.random.shuffle(v)
|
||||
indices = v[:int(n*.1)]
|
||||
ds, pred, sources = dijkstra(data,
|
||||
directed=True,
|
||||
indices=indices,
|
||||
min_only=True,
|
||||
return_predecessors=True)
|
||||
for k in range(n):
|
||||
p = pred[k]
|
||||
s = sources[k]
|
||||
while p != -9999:
|
||||
assert sources[p] == s
|
||||
p = pred[p]
|
||||
|
||||
|
||||
def test_dijkstra_random():
|
||||
# reproduces the hang observed in gh-17782
|
||||
n = 10
|
||||
indices = [0, 4, 4, 5, 7, 9, 0, 6, 2, 3, 7, 9, 1, 2, 9, 2, 5, 6]
|
||||
indptr = [0, 0, 2, 5, 6, 7, 8, 12, 15, 18, 18]
|
||||
data = [0.33629, 0.40458, 0.47493, 0.42757, 0.11497, 0.91653, 0.69084,
|
||||
0.64979, 0.62555, 0.743, 0.01724, 0.99945, 0.31095, 0.15557,
|
||||
0.02439, 0.65814, 0.23478, 0.24072]
|
||||
graph = scipy.sparse.csr_matrix((data, indices, indptr), shape=(n, n))
|
||||
dijkstra(graph, directed=True, return_predecessors=True)
|
||||
|
||||
|
||||
def test_gh_17782_segfault():
|
||||
text = """%%MatrixMarket matrix coordinate real general
|
||||
84 84 22
|
||||
2 1 4.699999809265137e+00
|
||||
6 14 1.199999973177910e-01
|
||||
9 6 1.199999973177910e-01
|
||||
10 16 2.012000083923340e+01
|
||||
11 10 1.422000026702881e+01
|
||||
12 1 9.645999908447266e+01
|
||||
13 18 2.012000083923340e+01
|
||||
14 13 4.679999828338623e+00
|
||||
15 11 1.199999973177910e-01
|
||||
16 12 1.199999973177910e-01
|
||||
18 15 1.199999973177910e-01
|
||||
32 2 2.299999952316284e+00
|
||||
33 20 6.000000000000000e+00
|
||||
33 32 5.000000000000000e+00
|
||||
36 9 3.720000028610229e+00
|
||||
36 37 3.720000028610229e+00
|
||||
36 38 3.720000028610229e+00
|
||||
37 44 8.159999847412109e+00
|
||||
38 32 7.903999328613281e+01
|
||||
43 20 2.400000000000000e+01
|
||||
43 33 4.000000000000000e+00
|
||||
44 43 6.028000259399414e+01
|
||||
"""
|
||||
data = mmread(StringIO(text))
|
||||
dijkstra(data, directed=True, return_predecessors=True)
|
||||
|
||||
|
||||
def test_shortest_path_indices():
|
||||
indices = np.arange(4)
|
||||
|
||||
def check(func, indshape):
|
||||
outshape = indshape + (5,)
|
||||
SP = func(directed_G, directed=False,
|
||||
indices=indices.reshape(indshape))
|
||||
assert_array_almost_equal(SP, undirected_SP[indices].reshape(outshape))
|
||||
|
||||
for indshape in [(4,), (4, 1), (2, 2)]:
|
||||
for func in (dijkstra, bellman_ford, johnson, shortest_path):
|
||||
check(func, indshape)
|
||||
|
||||
assert_raises(ValueError, shortest_path, directed_G, method='FW',
|
||||
indices=indices)
|
||||
|
||||
|
||||
def test_predecessors():
|
||||
SP_res = {True: directed_SP,
|
||||
False: undirected_SP}
|
||||
pred_res = {True: directed_pred,
|
||||
False: undirected_pred}
|
||||
|
||||
def check(method, directed):
|
||||
SP, pred = shortest_path(directed_G, method, directed=directed,
|
||||
overwrite=False,
|
||||
return_predecessors=True)
|
||||
assert_array_almost_equal(SP, SP_res[directed])
|
||||
assert_array_almost_equal(pred, pred_res[directed])
|
||||
|
||||
for method in methods:
|
||||
for directed in (True, False):
|
||||
check(method, directed)
|
||||
|
||||
|
||||
def test_construct_shortest_path():
|
||||
def check(method, directed):
|
||||
SP1, pred = shortest_path(directed_G,
|
||||
directed=directed,
|
||||
overwrite=False,
|
||||
return_predecessors=True)
|
||||
SP2 = construct_dist_matrix(directed_G, pred, directed=directed)
|
||||
assert_array_almost_equal(SP1, SP2)
|
||||
|
||||
for method in methods:
|
||||
for directed in (True, False):
|
||||
check(method, directed)
|
||||
|
||||
|
||||
def test_unweighted_path():
|
||||
def check(method, directed):
|
||||
SP1 = shortest_path(directed_G,
|
||||
directed=directed,
|
||||
overwrite=False,
|
||||
unweighted=True)
|
||||
SP2 = shortest_path(unweighted_G,
|
||||
directed=directed,
|
||||
overwrite=False,
|
||||
unweighted=False)
|
||||
assert_array_almost_equal(SP1, SP2)
|
||||
|
||||
for method in methods:
|
||||
for directed in (True, False):
|
||||
check(method, directed)
|
||||
|
||||
|
||||
def test_negative_cycles():
|
||||
# create a small graph with a negative cycle
|
||||
graph = np.ones([5, 5])
|
||||
graph.flat[::6] = 0
|
||||
graph[1, 2] = -2
|
||||
|
||||
def check(method, directed):
|
||||
assert_raises(NegativeCycleError, shortest_path, graph, method,
|
||||
directed)
|
||||
|
||||
for directed in (True, False):
|
||||
for method in ['FW', 'J', 'BF']:
|
||||
check(method, directed)
|
||||
|
||||
assert_raises(NegativeCycleError, yen, graph, 0, 1, 1,
|
||||
directed=directed)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", ['FW', 'J', 'BF'])
|
||||
def test_negative_weights(method):
|
||||
SP = shortest_path(directed_negative_weighted_G, method, directed=True)
|
||||
assert_allclose(SP, directed_negative_weighted_SP, atol=1e-10)
|
||||
|
||||
|
||||
def test_masked_input():
|
||||
np.ma.masked_equal(directed_G, 0)
|
||||
|
||||
def check(method):
|
||||
SP = shortest_path(directed_G, method=method, directed=True,
|
||||
overwrite=False)
|
||||
assert_array_almost_equal(SP, directed_SP)
|
||||
|
||||
for method in methods:
|
||||
check(method)
|
||||
|
||||
|
||||
def test_overwrite():
|
||||
G = np.array([[0, 3, 3, 1, 2],
|
||||
[3, 0, 0, 2, 4],
|
||||
[3, 0, 0, 0, 0],
|
||||
[1, 2, 0, 0, 2],
|
||||
[2, 4, 0, 2, 0]], dtype=float)
|
||||
foo = G.copy()
|
||||
shortest_path(foo, overwrite=False)
|
||||
assert_array_equal(foo, G)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('method', methods)
|
||||
def test_buffer(method):
|
||||
# Smoke test that sparse matrices with read-only buffers (e.g., those from
|
||||
# joblib workers) do not cause::
|
||||
#
|
||||
# ValueError: buffer source array is read-only
|
||||
#
|
||||
G = scipy.sparse.csr_matrix([[1.]])
|
||||
G.data.flags['WRITEABLE'] = False
|
||||
shortest_path(G, method=method)
|
||||
|
||||
|
||||
def test_NaN_warnings():
|
||||
with warnings.catch_warnings(record=True) as record:
|
||||
shortest_path(np.array([[0, 1], [np.nan, 0]]))
|
||||
for r in record:
|
||||
assert r.category is not RuntimeWarning
|
||||
|
||||
|
||||
def test_sparse_matrices():
|
||||
# Test that using lil,csr and csc sparse matrix do not cause error
|
||||
G_dense = np.array([[0, 3, 0, 0, 0],
|
||||
[0, 0, -1, 0, 0],
|
||||
[0, 0, 0, 2, 0],
|
||||
[0, 0, 0, 0, 4],
|
||||
[0, 0, 0, 0, 0]], dtype=float)
|
||||
SP = shortest_path(G_dense)
|
||||
G_csr = scipy.sparse.csr_matrix(G_dense)
|
||||
G_csc = scipy.sparse.csc_matrix(G_dense)
|
||||
G_lil = scipy.sparse.lil_matrix(G_dense)
|
||||
assert_array_almost_equal(SP, shortest_path(G_csr))
|
||||
assert_array_almost_equal(SP, shortest_path(G_csc))
|
||||
assert_array_almost_equal(SP, shortest_path(G_lil))
|
||||
|
||||
|
||||
def test_yen_directed():
|
||||
distances, predecessors = yen(
|
||||
directed_G,
|
||||
source=0,
|
||||
sink=3,
|
||||
K=2,
|
||||
return_predecessors=True
|
||||
)
|
||||
assert_allclose(distances, [5., 9.])
|
||||
assert_allclose(predecessors, directed_2SP_0_to_3)
|
||||
|
||||
|
||||
def test_yen_undirected():
|
||||
distances = yen(
|
||||
undirected_G,
|
||||
source=0,
|
||||
sink=3,
|
||||
K=4,
|
||||
)
|
||||
assert_allclose(distances, [1., 4., 5., 8.])
|
||||
|
||||
def test_yen_unweighted():
|
||||
# Ask for more paths than there are, verify only the available paths are returned
|
||||
distances, predecessors = yen(
|
||||
directed_G,
|
||||
source=0,
|
||||
sink=3,
|
||||
K=4,
|
||||
unweighted=True,
|
||||
return_predecessors=True,
|
||||
)
|
||||
assert_allclose(distances, [2., 3.])
|
||||
assert_allclose(predecessors, directed_2SP_0_to_3)
|
||||
|
||||
def test_yen_no_paths():
|
||||
distances = yen(
|
||||
directed_G,
|
||||
source=2,
|
||||
sink=3,
|
||||
K=1,
|
||||
)
|
||||
assert distances.size == 0
|
||||
|
||||
def test_yen_negative_weights():
|
||||
distances = yen(
|
||||
directed_negative_weighted_G,
|
||||
source=2,
|
||||
sink=0,
|
||||
K=1,
|
||||
)
|
||||
assert_allclose(distances, [-2.])
|
||||
@ -0,0 +1,66 @@
|
||||
"""Test the minimum spanning tree function"""
|
||||
import numpy as np
|
||||
from numpy.testing import assert_
|
||||
import numpy.testing as npt
|
||||
from scipy.sparse import csr_matrix
|
||||
from scipy.sparse.csgraph import minimum_spanning_tree
|
||||
|
||||
|
||||
def test_minimum_spanning_tree():
|
||||
|
||||
# Create a graph with two connected components.
|
||||
graph = [[0,1,0,0,0],
|
||||
[1,0,0,0,0],
|
||||
[0,0,0,8,5],
|
||||
[0,0,8,0,1],
|
||||
[0,0,5,1,0]]
|
||||
graph = np.asarray(graph)
|
||||
|
||||
# Create the expected spanning tree.
|
||||
expected = [[0,1,0,0,0],
|
||||
[0,0,0,0,0],
|
||||
[0,0,0,0,5],
|
||||
[0,0,0,0,1],
|
||||
[0,0,0,0,0]]
|
||||
expected = np.asarray(expected)
|
||||
|
||||
# Ensure minimum spanning tree code gives this expected output.
|
||||
csgraph = csr_matrix(graph)
|
||||
mintree = minimum_spanning_tree(csgraph)
|
||||
mintree_array = mintree.toarray()
|
||||
npt.assert_array_equal(mintree_array, expected,
|
||||
'Incorrect spanning tree found.')
|
||||
|
||||
# Ensure that the original graph was not modified.
|
||||
npt.assert_array_equal(csgraph.toarray(), graph,
|
||||
'Original graph was modified.')
|
||||
|
||||
# Now let the algorithm modify the csgraph in place.
|
||||
mintree = minimum_spanning_tree(csgraph, overwrite=True)
|
||||
npt.assert_array_equal(mintree.toarray(), expected,
|
||||
'Graph was not properly modified to contain MST.')
|
||||
|
||||
np.random.seed(1234)
|
||||
for N in (5, 10, 15, 20):
|
||||
|
||||
# Create a random graph.
|
||||
graph = 3 + np.random.random((N, N))
|
||||
csgraph = csr_matrix(graph)
|
||||
|
||||
# The spanning tree has at most N - 1 edges.
|
||||
mintree = minimum_spanning_tree(csgraph)
|
||||
assert_(mintree.nnz < N)
|
||||
|
||||
# Set the sub diagonal to 1 to create a known spanning tree.
|
||||
idx = np.arange(N-1)
|
||||
graph[idx,idx+1] = 1
|
||||
csgraph = csr_matrix(graph)
|
||||
mintree = minimum_spanning_tree(csgraph)
|
||||
|
||||
# We expect to see this pattern in the spanning tree and otherwise
|
||||
# have this zero.
|
||||
expected = np.zeros((N, N))
|
||||
expected[idx, idx+1] = 1
|
||||
|
||||
npt.assert_array_equal(mintree.toarray(), expected,
|
||||
'Incorrect spanning tree found.')
|
||||
@ -0,0 +1,81 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from numpy.testing import assert_array_almost_equal
|
||||
from scipy.sparse import csr_array
|
||||
from scipy.sparse.csgraph import (breadth_first_tree, depth_first_tree,
|
||||
csgraph_to_dense, csgraph_from_dense)
|
||||
|
||||
|
||||
def test_graph_breadth_first():
|
||||
csgraph = np.array([[0, 1, 2, 0, 0],
|
||||
[1, 0, 0, 0, 3],
|
||||
[2, 0, 0, 7, 0],
|
||||
[0, 0, 7, 0, 1],
|
||||
[0, 3, 0, 1, 0]])
|
||||
csgraph = csgraph_from_dense(csgraph, null_value=0)
|
||||
|
||||
bfirst = np.array([[0, 1, 2, 0, 0],
|
||||
[0, 0, 0, 0, 3],
|
||||
[0, 0, 0, 7, 0],
|
||||
[0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0]])
|
||||
|
||||
for directed in [True, False]:
|
||||
bfirst_test = breadth_first_tree(csgraph, 0, directed)
|
||||
assert_array_almost_equal(csgraph_to_dense(bfirst_test),
|
||||
bfirst)
|
||||
|
||||
|
||||
def test_graph_depth_first():
|
||||
csgraph = np.array([[0, 1, 2, 0, 0],
|
||||
[1, 0, 0, 0, 3],
|
||||
[2, 0, 0, 7, 0],
|
||||
[0, 0, 7, 0, 1],
|
||||
[0, 3, 0, 1, 0]])
|
||||
csgraph = csgraph_from_dense(csgraph, null_value=0)
|
||||
|
||||
dfirst = np.array([[0, 1, 0, 0, 0],
|
||||
[0, 0, 0, 0, 3],
|
||||
[0, 0, 0, 0, 0],
|
||||
[0, 0, 7, 0, 0],
|
||||
[0, 0, 0, 1, 0]])
|
||||
|
||||
for directed in [True, False]:
|
||||
dfirst_test = depth_first_tree(csgraph, 0, directed)
|
||||
assert_array_almost_equal(csgraph_to_dense(dfirst_test),
|
||||
dfirst)
|
||||
|
||||
|
||||
def test_graph_breadth_first_trivial_graph():
|
||||
csgraph = np.array([[0]])
|
||||
csgraph = csgraph_from_dense(csgraph, null_value=0)
|
||||
|
||||
bfirst = np.array([[0]])
|
||||
|
||||
for directed in [True, False]:
|
||||
bfirst_test = breadth_first_tree(csgraph, 0, directed)
|
||||
assert_array_almost_equal(csgraph_to_dense(bfirst_test),
|
||||
bfirst)
|
||||
|
||||
|
||||
def test_graph_depth_first_trivial_graph():
|
||||
csgraph = np.array([[0]])
|
||||
csgraph = csgraph_from_dense(csgraph, null_value=0)
|
||||
|
||||
bfirst = np.array([[0]])
|
||||
|
||||
for directed in [True, False]:
|
||||
bfirst_test = depth_first_tree(csgraph, 0, directed)
|
||||
assert_array_almost_equal(csgraph_to_dense(bfirst_test),
|
||||
bfirst)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('directed', [True, False])
|
||||
@pytest.mark.parametrize('tree_func', [breadth_first_tree, depth_first_tree])
|
||||
def test_int64_indices(tree_func, directed):
|
||||
# See https://github.com/scipy/scipy/issues/18716
|
||||
g = csr_array(([1], np.array([[0], [1]], dtype=np.int64)), shape=(2, 2))
|
||||
assert g.indices.dtype == np.int64
|
||||
tree = tree_func(g, 0, directed=directed)
|
||||
assert_array_almost_equal(csgraph_to_dense(tree), [[0, 1], [0, 0]])
|
||||
|
||||
27
venv/lib/python3.12/site-packages/scipy/sparse/csr.py
Normal file
27
venv/lib/python3.12/site-packages/scipy/sparse/csr.py
Normal file
@ -0,0 +1,27 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.sparse` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'csr_count_blocks',
|
||||
'csr_matrix',
|
||||
'csr_tobsr',
|
||||
'csr_tocsc',
|
||||
'get_csr_submatrix',
|
||||
'isspmatrix_csr',
|
||||
'spmatrix',
|
||||
'upcast',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="sparse", module="csr",
|
||||
private_modules=["_csr"], all=__all__,
|
||||
attribute=name)
|
||||
23
venv/lib/python3.12/site-packages/scipy/sparse/data.py
Normal file
23
venv/lib/python3.12/site-packages/scipy/sparse/data.py
Normal file
@ -0,0 +1,23 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.sparse` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'isscalarlike',
|
||||
'name',
|
||||
'npfunc',
|
||||
'validateaxis',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="sparse", module="data",
|
||||
private_modules=["_data"], all=__all__,
|
||||
attribute=name)
|
||||
29
venv/lib/python3.12/site-packages/scipy/sparse/dia.py
Normal file
29
venv/lib/python3.12/site-packages/scipy/sparse/dia.py
Normal file
@ -0,0 +1,29 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.sparse` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'check_shape',
|
||||
'dia_matrix',
|
||||
'dia_matvec',
|
||||
'get_sum_dtype',
|
||||
'getdtype',
|
||||
'isshape',
|
||||
'isspmatrix_dia',
|
||||
'spmatrix',
|
||||
'upcast_char',
|
||||
'validateaxis',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="sparse", module="dia",
|
||||
private_modules=["_dia"], all=__all__,
|
||||
attribute=name)
|
||||
32
venv/lib/python3.12/site-packages/scipy/sparse/dok.py
Normal file
32
venv/lib/python3.12/site-packages/scipy/sparse/dok.py
Normal file
@ -0,0 +1,32 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.sparse` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'IndexMixin',
|
||||
'check_shape',
|
||||
'dok_matrix',
|
||||
'getdtype',
|
||||
'isdense',
|
||||
'isintlike',
|
||||
'isscalarlike',
|
||||
'isshape',
|
||||
'isspmatrix_dok',
|
||||
'itertools',
|
||||
'spmatrix',
|
||||
'upcast',
|
||||
'upcast_scalar',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="sparse", module="dok",
|
||||
private_modules=["_dok"], all=__all__,
|
||||
attribute=name)
|
||||
23
venv/lib/python3.12/site-packages/scipy/sparse/extract.py
Normal file
23
venv/lib/python3.12/site-packages/scipy/sparse/extract.py
Normal file
@ -0,0 +1,23 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.sparse` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'coo_matrix',
|
||||
'find',
|
||||
'tril',
|
||||
'triu',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="sparse", module="extract",
|
||||
private_modules=["_extract"], all=__all__,
|
||||
attribute=name)
|
||||
22
venv/lib/python3.12/site-packages/scipy/sparse/lil.py
Normal file
22
venv/lib/python3.12/site-packages/scipy/sparse/lil.py
Normal file
@ -0,0 +1,22 @@
|
||||
# This file is not meant for public use and will be removed in SciPy v2.0.0.
|
||||
# Use the `scipy.sparse` namespace for importing the functions
|
||||
# included below.
|
||||
|
||||
from scipy._lib.deprecation import _sub_module_deprecation
|
||||
|
||||
|
||||
__all__ = [ # noqa: F822
|
||||
'isspmatrix_lil',
|
||||
'lil_array',
|
||||
'lil_matrix',
|
||||
]
|
||||
|
||||
|
||||
def __dir__():
|
||||
return __all__
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
return _sub_module_deprecation(sub_package="sparse", module="lil",
|
||||
private_modules=["_lil"], all=__all__,
|
||||
attribute=name)
|
||||
@ -0,0 +1,146 @@
|
||||
"""
|
||||
Sparse linear algebra (:mod:`scipy.sparse.linalg`)
|
||||
==================================================
|
||||
|
||||
.. currentmodule:: scipy.sparse.linalg
|
||||
|
||||
Abstract linear operators
|
||||
-------------------------
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
LinearOperator -- abstract representation of a linear operator
|
||||
aslinearoperator -- convert an object to an abstract linear operator
|
||||
|
||||
Matrix Operations
|
||||
-----------------
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
inv -- compute the sparse matrix inverse
|
||||
expm -- compute the sparse matrix exponential
|
||||
expm_multiply -- compute the product of a matrix exponential and a matrix
|
||||
matrix_power -- compute the matrix power by raising a matrix to an exponent
|
||||
|
||||
Matrix norms
|
||||
------------
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
norm -- Norm of a sparse matrix
|
||||
onenormest -- Estimate the 1-norm of a sparse matrix
|
||||
|
||||
Solving linear problems
|
||||
-----------------------
|
||||
|
||||
Direct methods for linear equation systems:
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
spsolve -- Solve the sparse linear system Ax=b
|
||||
spsolve_triangular -- Solve sparse linear system Ax=b for a triangular A.
|
||||
factorized -- Pre-factorize matrix to a function solving a linear system
|
||||
MatrixRankWarning -- Warning on exactly singular matrices
|
||||
use_solver -- Select direct solver to use
|
||||
|
||||
Iterative methods for linear equation systems:
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
bicg -- Use BIConjugate Gradient iteration to solve Ax = b
|
||||
bicgstab -- Use BIConjugate Gradient STABilized iteration to solve Ax = b
|
||||
cg -- Use Conjugate Gradient iteration to solve Ax = b
|
||||
cgs -- Use Conjugate Gradient Squared iteration to solve Ax = b
|
||||
gmres -- Use Generalized Minimal RESidual iteration to solve Ax = b
|
||||
lgmres -- Solve a matrix equation using the LGMRES algorithm
|
||||
minres -- Use MINimum RESidual iteration to solve Ax = b
|
||||
qmr -- Use Quasi-Minimal Residual iteration to solve Ax = b
|
||||
gcrotmk -- Solve a matrix equation using the GCROT(m,k) algorithm
|
||||
tfqmr -- Use Transpose-Free Quasi-Minimal Residual iteration to solve Ax = b
|
||||
|
||||
Iterative methods for least-squares problems:
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
lsqr -- Find the least-squares solution to a sparse linear equation system
|
||||
lsmr -- Find the least-squares solution to a sparse linear equation system
|
||||
|
||||
Matrix factorizations
|
||||
---------------------
|
||||
|
||||
Eigenvalue problems:
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
eigs -- Find k eigenvalues and eigenvectors of the square matrix A
|
||||
eigsh -- Find k eigenvalues and eigenvectors of a symmetric matrix
|
||||
lobpcg -- Solve symmetric partial eigenproblems with optional preconditioning
|
||||
|
||||
Singular values problems:
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
svds -- Compute k singular values/vectors for a sparse matrix
|
||||
|
||||
The `svds` function supports the following solvers:
|
||||
|
||||
.. toctree::
|
||||
|
||||
sparse.linalg.svds-arpack
|
||||
sparse.linalg.svds-lobpcg
|
||||
sparse.linalg.svds-propack
|
||||
|
||||
Complete or incomplete LU factorizations
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
splu -- Compute a LU decomposition for a sparse matrix
|
||||
spilu -- Compute an incomplete LU decomposition for a sparse matrix
|
||||
SuperLU -- Object representing an LU factorization
|
||||
|
||||
Sparse arrays with structure
|
||||
----------------------------
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
LaplacianNd -- Laplacian on a uniform rectangular grid in ``N`` dimensions
|
||||
|
||||
Exceptions
|
||||
----------
|
||||
|
||||
.. autosummary::
|
||||
:toctree: generated/
|
||||
|
||||
ArpackNoConvergence
|
||||
ArpackError
|
||||
|
||||
"""
|
||||
|
||||
from ._isolve import *
|
||||
from ._dsolve import *
|
||||
from ._interface import *
|
||||
from ._eigen import *
|
||||
from ._matfuncs import *
|
||||
from ._onenormest import *
|
||||
from ._norm import *
|
||||
from ._expm_multiply import *
|
||||
from ._special_sparse_arrays import *
|
||||
|
||||
# Deprecated namespaces, to be removed in v2.0.0
|
||||
from . import isolve, dsolve, interface, eigen, matfuncs
|
||||
|
||||
__all__ = [s for s in dir() if not s.startswith('_')]
|
||||
|
||||
from scipy._lib._testutils import PytestTester
|
||||
test = PytestTester(__name__)
|
||||
del PytestTester
|
||||
@ -0,0 +1,71 @@
|
||||
"""
|
||||
Linear Solvers
|
||||
==============
|
||||
|
||||
The default solver is SuperLU (included in the scipy distribution),
|
||||
which can solve real or complex linear systems in both single and
|
||||
double precisions. It is automatically replaced by UMFPACK, if
|
||||
available. Note that UMFPACK works in double precision only, so
|
||||
switch it off by::
|
||||
|
||||
>>> from scipy.sparse.linalg import spsolve, use_solver
|
||||
>>> use_solver(useUmfpack=False)
|
||||
|
||||
to solve in the single precision. See also use_solver documentation.
|
||||
|
||||
Example session::
|
||||
|
||||
>>> from scipy.sparse import csc_matrix, spdiags
|
||||
>>> from numpy import array
|
||||
>>>
|
||||
>>> print("Inverting a sparse linear system:")
|
||||
>>> print("The sparse matrix (constructed from diagonals):")
|
||||
>>> a = spdiags([[1, 2, 3, 4, 5], [6, 5, 8, 9, 10]], [0, 1], 5, 5)
|
||||
>>> b = array([1, 2, 3, 4, 5])
|
||||
>>> print("Solve: single precision complex:")
|
||||
>>> use_solver( useUmfpack = False )
|
||||
>>> a = a.astype('F')
|
||||
>>> x = spsolve(a, b)
|
||||
>>> print(x)
|
||||
>>> print("Error: ", a@x-b)
|
||||
>>>
|
||||
>>> print("Solve: double precision complex:")
|
||||
>>> use_solver( useUmfpack = True )
|
||||
>>> a = a.astype('D')
|
||||
>>> x = spsolve(a, b)
|
||||
>>> print(x)
|
||||
>>> print("Error: ", a@x-b)
|
||||
>>>
|
||||
>>> print("Solve: double precision:")
|
||||
>>> a = a.astype('d')
|
||||
>>> x = spsolve(a, b)
|
||||
>>> print(x)
|
||||
>>> print("Error: ", a@x-b)
|
||||
>>>
|
||||
>>> print("Solve: single precision:")
|
||||
>>> use_solver( useUmfpack = False )
|
||||
>>> a = a.astype('f')
|
||||
>>> x = spsolve(a, b.astype('f'))
|
||||
>>> print(x)
|
||||
>>> print("Error: ", a@x-b)
|
||||
|
||||
"""
|
||||
|
||||
#import umfpack
|
||||
#__doc__ = '\n\n'.join( (__doc__, umfpack.__doc__) )
|
||||
#del umfpack
|
||||
|
||||
from .linsolve import *
|
||||
from ._superlu import SuperLU
|
||||
from . import _add_newdocs
|
||||
from . import linsolve
|
||||
|
||||
__all__ = [
|
||||
'MatrixRankWarning', 'SuperLU', 'factorized',
|
||||
'spilu', 'splu', 'spsolve',
|
||||
'spsolve_triangular', 'use_solver'
|
||||
]
|
||||
|
||||
from scipy._lib._testutils import PytestTester
|
||||
test = PytestTester(__name__)
|
||||
del PytestTester
|
||||
@ -0,0 +1,153 @@
|
||||
from numpy.lib import add_newdoc
|
||||
|
||||
add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU',
|
||||
"""
|
||||
LU factorization of a sparse matrix.
|
||||
|
||||
Factorization is represented as::
|
||||
|
||||
Pr @ A @ Pc = L @ U
|
||||
|
||||
To construct these `SuperLU` objects, call the `splu` and `spilu`
|
||||
functions.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
shape
|
||||
nnz
|
||||
perm_c
|
||||
perm_r
|
||||
L
|
||||
U
|
||||
|
||||
Methods
|
||||
-------
|
||||
solve
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
.. versionadded:: 0.14.0
|
||||
|
||||
Examples
|
||||
--------
|
||||
The LU decomposition can be used to solve matrix equations. Consider:
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> from scipy.sparse.linalg import splu
|
||||
>>> A = csc_matrix([[1,2,0,4], [1,0,0,1], [1,0,2,1], [2,2,1,0.]])
|
||||
|
||||
This can be solved for a given right-hand side:
|
||||
|
||||
>>> lu = splu(A)
|
||||
>>> b = np.array([1, 2, 3, 4])
|
||||
>>> x = lu.solve(b)
|
||||
>>> A.dot(x)
|
||||
array([ 1., 2., 3., 4.])
|
||||
|
||||
The ``lu`` object also contains an explicit representation of the
|
||||
decomposition. The permutations are represented as mappings of
|
||||
indices:
|
||||
|
||||
>>> lu.perm_r
|
||||
array([2, 1, 3, 0], dtype=int32) # may vary
|
||||
>>> lu.perm_c
|
||||
array([0, 1, 3, 2], dtype=int32) # may vary
|
||||
|
||||
The L and U factors are sparse matrices in CSC format:
|
||||
|
||||
>>> lu.L.toarray()
|
||||
array([[ 1. , 0. , 0. , 0. ], # may vary
|
||||
[ 0.5, 1. , 0. , 0. ],
|
||||
[ 0.5, -1. , 1. , 0. ],
|
||||
[ 0.5, 1. , 0. , 1. ]])
|
||||
>>> lu.U.toarray()
|
||||
array([[ 2. , 2. , 0. , 1. ], # may vary
|
||||
[ 0. , -1. , 1. , -0.5],
|
||||
[ 0. , 0. , 5. , -1. ],
|
||||
[ 0. , 0. , 0. , 2. ]])
|
||||
|
||||
The permutation matrices can be constructed:
|
||||
|
||||
>>> Pr = csc_matrix((np.ones(4), (lu.perm_r, np.arange(4))))
|
||||
>>> Pc = csc_matrix((np.ones(4), (np.arange(4), lu.perm_c)))
|
||||
|
||||
We can reassemble the original matrix:
|
||||
|
||||
>>> (Pr.T @ (lu.L @ lu.U) @ Pc.T).toarray()
|
||||
array([[ 1., 2., 0., 4.],
|
||||
[ 1., 0., 0., 1.],
|
||||
[ 1., 0., 2., 1.],
|
||||
[ 2., 2., 1., 0.]])
|
||||
""")
|
||||
|
||||
add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU', ('solve',
|
||||
"""
|
||||
solve(rhs[, trans])
|
||||
|
||||
Solves linear system of equations with one or several right-hand sides.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
rhs : ndarray, shape (n,) or (n, k)
|
||||
Right hand side(s) of equation
|
||||
trans : {'N', 'T', 'H'}, optional
|
||||
Type of system to solve::
|
||||
|
||||
'N': A @ x == rhs (default)
|
||||
'T': A^T @ x == rhs
|
||||
'H': A^H @ x == rhs
|
||||
|
||||
i.e., normal, transposed, and hermitian conjugate.
|
||||
|
||||
Returns
|
||||
-------
|
||||
x : ndarray, shape ``rhs.shape``
|
||||
Solution vector(s)
|
||||
"""))
|
||||
|
||||
add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU', ('L',
|
||||
"""
|
||||
Lower triangular factor with unit diagonal as a
|
||||
`scipy.sparse.csc_matrix`.
|
||||
|
||||
.. versionadded:: 0.14.0
|
||||
"""))
|
||||
|
||||
add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU', ('U',
|
||||
"""
|
||||
Upper triangular factor as a `scipy.sparse.csc_matrix`.
|
||||
|
||||
.. versionadded:: 0.14.0
|
||||
"""))
|
||||
|
||||
add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU', ('shape',
|
||||
"""
|
||||
Shape of the original matrix as a tuple of ints.
|
||||
"""))
|
||||
|
||||
add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU', ('nnz',
|
||||
"""
|
||||
Number of nonzero elements in the matrix.
|
||||
"""))
|
||||
|
||||
add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU', ('perm_c',
|
||||
"""
|
||||
Permutation Pc represented as an array of indices.
|
||||
|
||||
The column permutation matrix can be reconstructed via:
|
||||
|
||||
>>> Pc = np.zeros((n, n))
|
||||
>>> Pc[np.arange(n), perm_c] = 1
|
||||
"""))
|
||||
|
||||
add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU', ('perm_r',
|
||||
"""
|
||||
Permutation Pr represented as an array of indices.
|
||||
|
||||
The row permutation matrix can be reconstructed via:
|
||||
|
||||
>>> Pr = np.zeros((n, n))
|
||||
>>> Pr[perm_r, np.arange(n)] = 1
|
||||
"""))
|
||||
Binary file not shown.
@ -0,0 +1,742 @@
|
||||
from warnings import warn, catch_warnings, simplefilter
|
||||
|
||||
import numpy as np
|
||||
from numpy import asarray
|
||||
from scipy.sparse import (issparse,
|
||||
SparseEfficiencyWarning, csc_matrix, eye, diags)
|
||||
from scipy.sparse._sputils import is_pydata_spmatrix, convert_pydata_sparse_to_scipy
|
||||
from scipy.linalg import LinAlgError
|
||||
import copy
|
||||
|
||||
from . import _superlu
|
||||
|
||||
noScikit = False
|
||||
try:
|
||||
import scikits.umfpack as umfpack
|
||||
except ImportError:
|
||||
noScikit = True
|
||||
|
||||
useUmfpack = not noScikit
|
||||
|
||||
__all__ = ['use_solver', 'spsolve', 'splu', 'spilu', 'factorized',
|
||||
'MatrixRankWarning', 'spsolve_triangular']
|
||||
|
||||
|
||||
class MatrixRankWarning(UserWarning):
|
||||
pass
|
||||
|
||||
|
||||
def use_solver(**kwargs):
|
||||
"""
|
||||
Select default sparse direct solver to be used.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
useUmfpack : bool, optional
|
||||
Use UMFPACK [1]_, [2]_, [3]_, [4]_. over SuperLU. Has effect only
|
||||
if ``scikits.umfpack`` is installed. Default: True
|
||||
assumeSortedIndices : bool, optional
|
||||
Allow UMFPACK to skip the step of sorting indices for a CSR/CSC matrix.
|
||||
Has effect only if useUmfpack is True and ``scikits.umfpack`` is
|
||||
installed. Default: False
|
||||
|
||||
Notes
|
||||
-----
|
||||
The default sparse solver is UMFPACK when available
|
||||
(``scikits.umfpack`` is installed). This can be changed by passing
|
||||
useUmfpack = False, which then causes the always present SuperLU
|
||||
based solver to be used.
|
||||
|
||||
UMFPACK requires a CSR/CSC matrix to have sorted column/row indices. If
|
||||
sure that the matrix fulfills this, pass ``assumeSortedIndices=True``
|
||||
to gain some speed.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] T. A. Davis, Algorithm 832: UMFPACK - an unsymmetric-pattern
|
||||
multifrontal method with a column pre-ordering strategy, ACM
|
||||
Trans. on Mathematical Software, 30(2), 2004, pp. 196--199.
|
||||
https://dl.acm.org/doi/abs/10.1145/992200.992206
|
||||
|
||||
.. [2] T. A. Davis, A column pre-ordering strategy for the
|
||||
unsymmetric-pattern multifrontal method, ACM Trans.
|
||||
on Mathematical Software, 30(2), 2004, pp. 165--195.
|
||||
https://dl.acm.org/doi/abs/10.1145/992200.992205
|
||||
|
||||
.. [3] T. A. Davis and I. S. Duff, A combined unifrontal/multifrontal
|
||||
method for unsymmetric sparse matrices, ACM Trans. on
|
||||
Mathematical Software, 25(1), 1999, pp. 1--19.
|
||||
https://doi.org/10.1145/305658.287640
|
||||
|
||||
.. [4] T. A. Davis and I. S. Duff, An unsymmetric-pattern multifrontal
|
||||
method for sparse LU factorization, SIAM J. Matrix Analysis and
|
||||
Computations, 18(1), 1997, pp. 140--158.
|
||||
https://doi.org/10.1137/S0895479894246905T.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse.linalg import use_solver, spsolve
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> R = np.random.randn(5, 5)
|
||||
>>> A = csc_matrix(R)
|
||||
>>> b = np.random.randn(5)
|
||||
>>> use_solver(useUmfpack=False) # enforce superLU over UMFPACK
|
||||
>>> x = spsolve(A, b)
|
||||
>>> np.allclose(A.dot(x), b)
|
||||
True
|
||||
>>> use_solver(useUmfpack=True) # reset umfPack usage to default
|
||||
"""
|
||||
if 'useUmfpack' in kwargs:
|
||||
globals()['useUmfpack'] = kwargs['useUmfpack']
|
||||
if useUmfpack and 'assumeSortedIndices' in kwargs:
|
||||
umfpack.configure(assumeSortedIndices=kwargs['assumeSortedIndices'])
|
||||
|
||||
def _get_umf_family(A):
|
||||
"""Get umfpack family string given the sparse matrix dtype."""
|
||||
_families = {
|
||||
(np.float64, np.int32): 'di',
|
||||
(np.complex128, np.int32): 'zi',
|
||||
(np.float64, np.int64): 'dl',
|
||||
(np.complex128, np.int64): 'zl'
|
||||
}
|
||||
|
||||
# A.dtype.name can only be "float64" or
|
||||
# "complex128" in control flow
|
||||
f_type = getattr(np, A.dtype.name)
|
||||
# control flow may allow for more index
|
||||
# types to get through here
|
||||
i_type = getattr(np, A.indices.dtype.name)
|
||||
|
||||
try:
|
||||
family = _families[(f_type, i_type)]
|
||||
|
||||
except KeyError as e:
|
||||
msg = ('only float64 or complex128 matrices with int32 or int64 '
|
||||
f'indices are supported! (got: matrix: {f_type}, indices: {i_type})')
|
||||
raise ValueError(msg) from e
|
||||
|
||||
# See gh-8278. Considered converting only if
|
||||
# A.shape[0]*A.shape[1] > np.iinfo(np.int32).max,
|
||||
# but that didn't always fix the issue.
|
||||
family = family[0] + "l"
|
||||
A_new = copy.copy(A)
|
||||
A_new.indptr = np.asarray(A.indptr, dtype=np.int64)
|
||||
A_new.indices = np.asarray(A.indices, dtype=np.int64)
|
||||
|
||||
return family, A_new
|
||||
|
||||
def _safe_downcast_indices(A):
|
||||
# check for safe downcasting
|
||||
max_value = np.iinfo(np.intc).max
|
||||
|
||||
if A.indptr[-1] > max_value: # indptr[-1] is max b/c indptr always sorted
|
||||
raise ValueError("indptr values too large for SuperLU")
|
||||
|
||||
if max(*A.shape) > max_value: # only check large enough arrays
|
||||
if np.any(A.indices > max_value):
|
||||
raise ValueError("indices values too large for SuperLU")
|
||||
|
||||
indices = A.indices.astype(np.intc, copy=False)
|
||||
indptr = A.indptr.astype(np.intc, copy=False)
|
||||
return indices, indptr
|
||||
|
||||
def spsolve(A, b, permc_spec=None, use_umfpack=True):
|
||||
"""Solve the sparse linear system Ax=b, where b may be a vector or a matrix.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : ndarray or sparse matrix
|
||||
The square matrix A will be converted into CSC or CSR form
|
||||
b : ndarray or sparse matrix
|
||||
The matrix or vector representing the right hand side of the equation.
|
||||
If a vector, b.shape must be (n,) or (n, 1).
|
||||
permc_spec : str, optional
|
||||
How to permute the columns of the matrix for sparsity preservation.
|
||||
(default: 'COLAMD')
|
||||
|
||||
- ``NATURAL``: natural ordering.
|
||||
- ``MMD_ATA``: minimum degree ordering on the structure of A^T A.
|
||||
- ``MMD_AT_PLUS_A``: minimum degree ordering on the structure of A^T+A.
|
||||
- ``COLAMD``: approximate minimum degree column ordering [1]_, [2]_.
|
||||
|
||||
use_umfpack : bool, optional
|
||||
if True (default) then use UMFPACK for the solution [3]_, [4]_, [5]_,
|
||||
[6]_ . This is only referenced if b is a vector and
|
||||
``scikits.umfpack`` is installed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
x : ndarray or sparse matrix
|
||||
the solution of the sparse linear equation.
|
||||
If b is a vector, then x is a vector of size A.shape[1]
|
||||
If b is a matrix, then x is a matrix of size (A.shape[1], b.shape[1])
|
||||
|
||||
Notes
|
||||
-----
|
||||
For solving the matrix expression AX = B, this solver assumes the resulting
|
||||
matrix X is sparse, as is often the case for very sparse inputs. If the
|
||||
resulting X is dense, the construction of this sparse result will be
|
||||
relatively expensive. In that case, consider converting A to a dense
|
||||
matrix and using scipy.linalg.solve or its variants.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] T. A. Davis, J. R. Gilbert, S. Larimore, E. Ng, Algorithm 836:
|
||||
COLAMD, an approximate column minimum degree ordering algorithm,
|
||||
ACM Trans. on Mathematical Software, 30(3), 2004, pp. 377--380.
|
||||
:doi:`10.1145/1024074.1024080`
|
||||
|
||||
.. [2] T. A. Davis, J. R. Gilbert, S. Larimore, E. Ng, A column approximate
|
||||
minimum degree ordering algorithm, ACM Trans. on Mathematical
|
||||
Software, 30(3), 2004, pp. 353--376. :doi:`10.1145/1024074.1024079`
|
||||
|
||||
.. [3] T. A. Davis, Algorithm 832: UMFPACK - an unsymmetric-pattern
|
||||
multifrontal method with a column pre-ordering strategy, ACM
|
||||
Trans. on Mathematical Software, 30(2), 2004, pp. 196--199.
|
||||
https://dl.acm.org/doi/abs/10.1145/992200.992206
|
||||
|
||||
.. [4] T. A. Davis, A column pre-ordering strategy for the
|
||||
unsymmetric-pattern multifrontal method, ACM Trans.
|
||||
on Mathematical Software, 30(2), 2004, pp. 165--195.
|
||||
https://dl.acm.org/doi/abs/10.1145/992200.992205
|
||||
|
||||
.. [5] T. A. Davis and I. S. Duff, A combined unifrontal/multifrontal
|
||||
method for unsymmetric sparse matrices, ACM Trans. on
|
||||
Mathematical Software, 25(1), 1999, pp. 1--19.
|
||||
https://doi.org/10.1145/305658.287640
|
||||
|
||||
.. [6] T. A. Davis and I. S. Duff, An unsymmetric-pattern multifrontal
|
||||
method for sparse LU factorization, SIAM J. Matrix Analysis and
|
||||
Computations, 18(1), 1997, pp. 140--158.
|
||||
https://doi.org/10.1137/S0895479894246905T.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> from scipy.sparse.linalg import spsolve
|
||||
>>> A = csc_matrix([[3, 2, 0], [1, -1, 0], [0, 5, 1]], dtype=float)
|
||||
>>> B = csc_matrix([[2, 0], [-1, 0], [2, 0]], dtype=float)
|
||||
>>> x = spsolve(A, B)
|
||||
>>> np.allclose(A.dot(x).toarray(), B.toarray())
|
||||
True
|
||||
"""
|
||||
is_pydata_sparse = is_pydata_spmatrix(b)
|
||||
pydata_sparse_cls = b.__class__ if is_pydata_sparse else None
|
||||
A = convert_pydata_sparse_to_scipy(A)
|
||||
b = convert_pydata_sparse_to_scipy(b)
|
||||
|
||||
if not (issparse(A) and A.format in ("csc", "csr")):
|
||||
A = csc_matrix(A)
|
||||
warn('spsolve requires A be CSC or CSR matrix format',
|
||||
SparseEfficiencyWarning, stacklevel=2)
|
||||
|
||||
# b is a vector only if b have shape (n,) or (n, 1)
|
||||
b_is_sparse = issparse(b)
|
||||
if not b_is_sparse:
|
||||
b = asarray(b)
|
||||
b_is_vector = ((b.ndim == 1) or (b.ndim == 2 and b.shape[1] == 1))
|
||||
|
||||
# sum duplicates for non-canonical format
|
||||
A.sum_duplicates()
|
||||
A = A._asfptype() # upcast to a floating point format
|
||||
result_dtype = np.promote_types(A.dtype, b.dtype)
|
||||
if A.dtype != result_dtype:
|
||||
A = A.astype(result_dtype)
|
||||
if b.dtype != result_dtype:
|
||||
b = b.astype(result_dtype)
|
||||
|
||||
# validate input shapes
|
||||
M, N = A.shape
|
||||
if (M != N):
|
||||
raise ValueError(f"matrix must be square (has shape {(M, N)})")
|
||||
|
||||
if M != b.shape[0]:
|
||||
raise ValueError(f"matrix - rhs dimension mismatch ({A.shape} - {b.shape[0]})")
|
||||
|
||||
use_umfpack = use_umfpack and useUmfpack
|
||||
|
||||
if b_is_vector and use_umfpack:
|
||||
if b_is_sparse:
|
||||
b_vec = b.toarray()
|
||||
else:
|
||||
b_vec = b
|
||||
b_vec = asarray(b_vec, dtype=A.dtype).ravel()
|
||||
|
||||
if noScikit:
|
||||
raise RuntimeError('Scikits.umfpack not installed.')
|
||||
|
||||
if A.dtype.char not in 'dD':
|
||||
raise ValueError("convert matrix data to double, please, using"
|
||||
" .astype(), or set linsolve.useUmfpack = False")
|
||||
|
||||
umf_family, A = _get_umf_family(A)
|
||||
umf = umfpack.UmfpackContext(umf_family)
|
||||
x = umf.linsolve(umfpack.UMFPACK_A, A, b_vec,
|
||||
autoTranspose=True)
|
||||
else:
|
||||
if b_is_vector and b_is_sparse:
|
||||
b = b.toarray()
|
||||
b_is_sparse = False
|
||||
|
||||
if not b_is_sparse:
|
||||
if A.format == "csc":
|
||||
flag = 1 # CSC format
|
||||
else:
|
||||
flag = 0 # CSR format
|
||||
|
||||
indices = A.indices.astype(np.intc, copy=False)
|
||||
indptr = A.indptr.astype(np.intc, copy=False)
|
||||
options = dict(ColPerm=permc_spec)
|
||||
x, info = _superlu.gssv(N, A.nnz, A.data, indices, indptr,
|
||||
b, flag, options=options)
|
||||
if info != 0:
|
||||
warn("Matrix is exactly singular", MatrixRankWarning, stacklevel=2)
|
||||
x.fill(np.nan)
|
||||
if b_is_vector:
|
||||
x = x.ravel()
|
||||
else:
|
||||
# b is sparse
|
||||
Afactsolve = factorized(A)
|
||||
|
||||
if not (b.format == "csc" or is_pydata_spmatrix(b)):
|
||||
warn('spsolve is more efficient when sparse b '
|
||||
'is in the CSC matrix format',
|
||||
SparseEfficiencyWarning, stacklevel=2)
|
||||
b = csc_matrix(b)
|
||||
|
||||
# Create a sparse output matrix by repeatedly applying
|
||||
# the sparse factorization to solve columns of b.
|
||||
data_segs = []
|
||||
row_segs = []
|
||||
col_segs = []
|
||||
for j in range(b.shape[1]):
|
||||
# TODO: replace this with
|
||||
# bj = b[:, j].toarray().ravel()
|
||||
# once 1D sparse arrays are supported.
|
||||
# That is a slightly faster code path.
|
||||
bj = b[:, [j]].toarray().ravel()
|
||||
xj = Afactsolve(bj)
|
||||
w = np.flatnonzero(xj)
|
||||
segment_length = w.shape[0]
|
||||
row_segs.append(w)
|
||||
col_segs.append(np.full(segment_length, j, dtype=int))
|
||||
data_segs.append(np.asarray(xj[w], dtype=A.dtype))
|
||||
sparse_data = np.concatenate(data_segs)
|
||||
sparse_row = np.concatenate(row_segs)
|
||||
sparse_col = np.concatenate(col_segs)
|
||||
x = A.__class__((sparse_data, (sparse_row, sparse_col)),
|
||||
shape=b.shape, dtype=A.dtype)
|
||||
|
||||
if is_pydata_sparse:
|
||||
x = pydata_sparse_cls.from_scipy_sparse(x)
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def splu(A, permc_spec=None, diag_pivot_thresh=None,
|
||||
relax=None, panel_size=None, options=dict()):
|
||||
"""
|
||||
Compute the LU decomposition of a sparse, square matrix.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : sparse matrix
|
||||
Sparse matrix to factorize. Most efficient when provided in CSC
|
||||
format. Other formats will be converted to CSC before factorization.
|
||||
permc_spec : str, optional
|
||||
How to permute the columns of the matrix for sparsity preservation.
|
||||
(default: 'COLAMD')
|
||||
|
||||
- ``NATURAL``: natural ordering.
|
||||
- ``MMD_ATA``: minimum degree ordering on the structure of A^T A.
|
||||
- ``MMD_AT_PLUS_A``: minimum degree ordering on the structure of A^T+A.
|
||||
- ``COLAMD``: approximate minimum degree column ordering
|
||||
|
||||
diag_pivot_thresh : float, optional
|
||||
Threshold used for a diagonal entry to be an acceptable pivot.
|
||||
See SuperLU user's guide for details [1]_
|
||||
relax : int, optional
|
||||
Expert option for customizing the degree of relaxing supernodes.
|
||||
See SuperLU user's guide for details [1]_
|
||||
panel_size : int, optional
|
||||
Expert option for customizing the panel size.
|
||||
See SuperLU user's guide for details [1]_
|
||||
options : dict, optional
|
||||
Dictionary containing additional expert options to SuperLU.
|
||||
See SuperLU user guide [1]_ (section 2.4 on the 'Options' argument)
|
||||
for more details. For example, you can specify
|
||||
``options=dict(Equil=False, IterRefine='SINGLE'))``
|
||||
to turn equilibration off and perform a single iterative refinement.
|
||||
|
||||
Returns
|
||||
-------
|
||||
invA : scipy.sparse.linalg.SuperLU
|
||||
Object, which has a ``solve`` method.
|
||||
|
||||
See also
|
||||
--------
|
||||
spilu : incomplete LU decomposition
|
||||
|
||||
Notes
|
||||
-----
|
||||
This function uses the SuperLU library.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] SuperLU https://portal.nersc.gov/project/sparse/superlu/
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> from scipy.sparse.linalg import splu
|
||||
>>> A = csc_matrix([[1., 0., 0.], [5., 0., 2.], [0., -1., 0.]], dtype=float)
|
||||
>>> B = splu(A)
|
||||
>>> x = np.array([1., 2., 3.], dtype=float)
|
||||
>>> B.solve(x)
|
||||
array([ 1. , -3. , -1.5])
|
||||
>>> A.dot(B.solve(x))
|
||||
array([ 1., 2., 3.])
|
||||
>>> B.solve(A.dot(x))
|
||||
array([ 1., 2., 3.])
|
||||
"""
|
||||
|
||||
if is_pydata_spmatrix(A):
|
||||
def csc_construct_func(*a, cls=type(A)):
|
||||
return cls.from_scipy_sparse(csc_matrix(*a))
|
||||
A = A.to_scipy_sparse().tocsc()
|
||||
else:
|
||||
csc_construct_func = csc_matrix
|
||||
|
||||
if not (issparse(A) and A.format == "csc"):
|
||||
A = csc_matrix(A)
|
||||
warn('splu converted its input to CSC format',
|
||||
SparseEfficiencyWarning, stacklevel=2)
|
||||
|
||||
# sum duplicates for non-canonical format
|
||||
A.sum_duplicates()
|
||||
A = A._asfptype() # upcast to a floating point format
|
||||
|
||||
M, N = A.shape
|
||||
if (M != N):
|
||||
raise ValueError("can only factor square matrices") # is this true?
|
||||
|
||||
indices, indptr = _safe_downcast_indices(A)
|
||||
|
||||
_options = dict(DiagPivotThresh=diag_pivot_thresh, ColPerm=permc_spec,
|
||||
PanelSize=panel_size, Relax=relax)
|
||||
if options is not None:
|
||||
_options.update(options)
|
||||
|
||||
# Ensure that no column permutations are applied
|
||||
if (_options["ColPerm"] == "NATURAL"):
|
||||
_options["SymmetricMode"] = True
|
||||
|
||||
return _superlu.gstrf(N, A.nnz, A.data, indices, indptr,
|
||||
csc_construct_func=csc_construct_func,
|
||||
ilu=False, options=_options)
|
||||
|
||||
|
||||
def spilu(A, drop_tol=None, fill_factor=None, drop_rule=None, permc_spec=None,
|
||||
diag_pivot_thresh=None, relax=None, panel_size=None, options=None):
|
||||
"""
|
||||
Compute an incomplete LU decomposition for a sparse, square matrix.
|
||||
|
||||
The resulting object is an approximation to the inverse of `A`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : (N, N) array_like
|
||||
Sparse matrix to factorize. Most efficient when provided in CSC format.
|
||||
Other formats will be converted to CSC before factorization.
|
||||
drop_tol : float, optional
|
||||
Drop tolerance (0 <= tol <= 1) for an incomplete LU decomposition.
|
||||
(default: 1e-4)
|
||||
fill_factor : float, optional
|
||||
Specifies the fill ratio upper bound (>= 1.0) for ILU. (default: 10)
|
||||
drop_rule : str, optional
|
||||
Comma-separated string of drop rules to use.
|
||||
Available rules: ``basic``, ``prows``, ``column``, ``area``,
|
||||
``secondary``, ``dynamic``, ``interp``. (Default: ``basic,area``)
|
||||
|
||||
See SuperLU documentation for details.
|
||||
|
||||
Remaining other options
|
||||
Same as for `splu`
|
||||
|
||||
Returns
|
||||
-------
|
||||
invA_approx : scipy.sparse.linalg.SuperLU
|
||||
Object, which has a ``solve`` method.
|
||||
|
||||
See also
|
||||
--------
|
||||
splu : complete LU decomposition
|
||||
|
||||
Notes
|
||||
-----
|
||||
To improve the better approximation to the inverse, you may need to
|
||||
increase `fill_factor` AND decrease `drop_tol`.
|
||||
|
||||
This function uses the SuperLU library.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> from scipy.sparse.linalg import spilu
|
||||
>>> A = csc_matrix([[1., 0., 0.], [5., 0., 2.], [0., -1., 0.]], dtype=float)
|
||||
>>> B = spilu(A)
|
||||
>>> x = np.array([1., 2., 3.], dtype=float)
|
||||
>>> B.solve(x)
|
||||
array([ 1. , -3. , -1.5])
|
||||
>>> A.dot(B.solve(x))
|
||||
array([ 1., 2., 3.])
|
||||
>>> B.solve(A.dot(x))
|
||||
array([ 1., 2., 3.])
|
||||
"""
|
||||
|
||||
if is_pydata_spmatrix(A):
|
||||
def csc_construct_func(*a, cls=type(A)):
|
||||
return cls.from_scipy_sparse(csc_matrix(*a))
|
||||
A = A.to_scipy_sparse().tocsc()
|
||||
else:
|
||||
csc_construct_func = csc_matrix
|
||||
|
||||
if not (issparse(A) and A.format == "csc"):
|
||||
A = csc_matrix(A)
|
||||
warn('spilu converted its input to CSC format',
|
||||
SparseEfficiencyWarning, stacklevel=2)
|
||||
|
||||
# sum duplicates for non-canonical format
|
||||
A.sum_duplicates()
|
||||
A = A._asfptype() # upcast to a floating point format
|
||||
|
||||
M, N = A.shape
|
||||
if (M != N):
|
||||
raise ValueError("can only factor square matrices") # is this true?
|
||||
|
||||
indices, indptr = _safe_downcast_indices(A)
|
||||
|
||||
_options = dict(ILU_DropRule=drop_rule, ILU_DropTol=drop_tol,
|
||||
ILU_FillFactor=fill_factor,
|
||||
DiagPivotThresh=diag_pivot_thresh, ColPerm=permc_spec,
|
||||
PanelSize=panel_size, Relax=relax)
|
||||
if options is not None:
|
||||
_options.update(options)
|
||||
|
||||
# Ensure that no column permutations are applied
|
||||
if (_options["ColPerm"] == "NATURAL"):
|
||||
_options["SymmetricMode"] = True
|
||||
|
||||
return _superlu.gstrf(N, A.nnz, A.data, indices, indptr,
|
||||
csc_construct_func=csc_construct_func,
|
||||
ilu=True, options=_options)
|
||||
|
||||
|
||||
def factorized(A):
|
||||
"""
|
||||
Return a function for solving a sparse linear system, with A pre-factorized.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : (N, N) array_like
|
||||
Input. A in CSC format is most efficient. A CSR format matrix will
|
||||
be converted to CSC before factorization.
|
||||
|
||||
Returns
|
||||
-------
|
||||
solve : callable
|
||||
To solve the linear system of equations given in `A`, the `solve`
|
||||
callable should be passed an ndarray of shape (N,).
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse.linalg import factorized
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> A = np.array([[ 3. , 2. , -1. ],
|
||||
... [ 2. , -2. , 4. ],
|
||||
... [-1. , 0.5, -1. ]])
|
||||
>>> solve = factorized(csc_matrix(A)) # Makes LU decomposition.
|
||||
>>> rhs1 = np.array([1, -2, 0])
|
||||
>>> solve(rhs1) # Uses the LU factors.
|
||||
array([ 1., -2., -2.])
|
||||
|
||||
"""
|
||||
if is_pydata_spmatrix(A):
|
||||
A = A.to_scipy_sparse().tocsc()
|
||||
|
||||
if useUmfpack:
|
||||
if noScikit:
|
||||
raise RuntimeError('Scikits.umfpack not installed.')
|
||||
|
||||
if not (issparse(A) and A.format == "csc"):
|
||||
A = csc_matrix(A)
|
||||
warn('splu converted its input to CSC format',
|
||||
SparseEfficiencyWarning, stacklevel=2)
|
||||
|
||||
A = A._asfptype() # upcast to a floating point format
|
||||
|
||||
if A.dtype.char not in 'dD':
|
||||
raise ValueError("convert matrix data to double, please, using"
|
||||
" .astype(), or set linsolve.useUmfpack = False")
|
||||
|
||||
umf_family, A = _get_umf_family(A)
|
||||
umf = umfpack.UmfpackContext(umf_family)
|
||||
|
||||
# Make LU decomposition.
|
||||
umf.numeric(A)
|
||||
|
||||
def solve(b):
|
||||
with np.errstate(divide="ignore", invalid="ignore"):
|
||||
# Ignoring warnings with numpy >= 1.23.0, see gh-16523
|
||||
result = umf.solve(umfpack.UMFPACK_A, A, b, autoTranspose=True)
|
||||
|
||||
return result
|
||||
|
||||
return solve
|
||||
else:
|
||||
return splu(A).solve
|
||||
|
||||
|
||||
def spsolve_triangular(A, b, lower=True, overwrite_A=False, overwrite_b=False,
|
||||
unit_diagonal=False):
|
||||
"""
|
||||
Solve the equation ``A x = b`` for `x`, assuming A is a triangular matrix.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : (M, M) sparse matrix
|
||||
A sparse square triangular matrix. Should be in CSR or CSC format.
|
||||
b : (M,) or (M, N) array_like
|
||||
Right-hand side matrix in ``A x = b``
|
||||
lower : bool, optional
|
||||
Whether `A` is a lower or upper triangular matrix.
|
||||
Default is lower triangular matrix.
|
||||
overwrite_A : bool, optional
|
||||
Allow changing `A`.
|
||||
Enabling gives a performance gain. Default is False.
|
||||
overwrite_b : bool, optional
|
||||
Allow overwriting data in `b`.
|
||||
Enabling gives a performance gain. Default is False.
|
||||
If `overwrite_b` is True, it should be ensured that
|
||||
`b` has an appropriate dtype to be able to store the result.
|
||||
unit_diagonal : bool, optional
|
||||
If True, diagonal elements of `a` are assumed to be 1.
|
||||
|
||||
.. versionadded:: 1.4.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
x : (M,) or (M, N) ndarray
|
||||
Solution to the system ``A x = b``. Shape of return matches shape
|
||||
of `b`.
|
||||
|
||||
Raises
|
||||
------
|
||||
LinAlgError
|
||||
If `A` is singular or not triangular.
|
||||
ValueError
|
||||
If shape of `A` or shape of `b` do not match the requirements.
|
||||
|
||||
Notes
|
||||
-----
|
||||
.. versionadded:: 0.19.0
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csc_array
|
||||
>>> from scipy.sparse.linalg import spsolve_triangular
|
||||
>>> A = csc_array([[3, 0, 0], [1, -1, 0], [2, 0, 1]], dtype=float)
|
||||
>>> B = np.array([[2, 0], [-1, 0], [2, 0]], dtype=float)
|
||||
>>> x = spsolve_triangular(A, B)
|
||||
>>> np.allclose(A.dot(x), B)
|
||||
True
|
||||
"""
|
||||
|
||||
if is_pydata_spmatrix(A):
|
||||
A = A.to_scipy_sparse().tocsc()
|
||||
|
||||
trans = "N"
|
||||
if issparse(A) and A.format == "csr":
|
||||
A = A.T
|
||||
trans = "T"
|
||||
lower = not lower
|
||||
|
||||
if not (issparse(A) and A.format == "csc"):
|
||||
warn('CSC or CSR matrix format is required. Converting to CSC matrix.',
|
||||
SparseEfficiencyWarning, stacklevel=2)
|
||||
A = csc_matrix(A)
|
||||
elif not overwrite_A:
|
||||
A = A.copy()
|
||||
|
||||
|
||||
M, N = A.shape
|
||||
if M != N:
|
||||
raise ValueError(
|
||||
f'A must be a square matrix but its shape is {A.shape}.')
|
||||
|
||||
if unit_diagonal:
|
||||
with catch_warnings():
|
||||
simplefilter('ignore', SparseEfficiencyWarning)
|
||||
A.setdiag(1)
|
||||
else:
|
||||
diag = A.diagonal()
|
||||
if np.any(diag == 0):
|
||||
raise LinAlgError(
|
||||
'A is singular: zero entry on diagonal.')
|
||||
invdiag = 1/diag
|
||||
if trans == "N":
|
||||
A = A @ diags(invdiag)
|
||||
else:
|
||||
A = (A.T @ diags(invdiag)).T
|
||||
|
||||
# sum duplicates for non-canonical format
|
||||
A.sum_duplicates()
|
||||
|
||||
b = np.asanyarray(b)
|
||||
|
||||
if b.ndim not in [1, 2]:
|
||||
raise ValueError(
|
||||
f'b must have 1 or 2 dims but its shape is {b.shape}.')
|
||||
if M != b.shape[0]:
|
||||
raise ValueError(
|
||||
'The size of the dimensions of A must be equal to '
|
||||
'the size of the first dimension of b but the shape of A is '
|
||||
f'{A.shape} and the shape of b is {b.shape}.'
|
||||
)
|
||||
|
||||
result_dtype = np.promote_types(np.promote_types(A.dtype, np.float32), b.dtype)
|
||||
if A.dtype != result_dtype:
|
||||
A = A.astype(result_dtype)
|
||||
if b.dtype != result_dtype:
|
||||
b = b.astype(result_dtype)
|
||||
elif not overwrite_b:
|
||||
b = b.copy()
|
||||
|
||||
if lower:
|
||||
L = A
|
||||
U = csc_matrix((N, N), dtype=result_dtype)
|
||||
else:
|
||||
L = eye(N, dtype=result_dtype, format='csc')
|
||||
U = A
|
||||
U.setdiag(0)
|
||||
|
||||
x, info = _superlu.gstrs(trans,
|
||||
N, L.nnz, L.data, L.indices, L.indptr,
|
||||
N, U.nnz, U.data, U.indices, U.indptr,
|
||||
b)
|
||||
if info:
|
||||
raise LinAlgError('A is singular.')
|
||||
|
||||
if not unit_diagonal:
|
||||
invdiag = invdiag.reshape(-1, *([1] * (len(x.shape) - 1)))
|
||||
x = x * invdiag
|
||||
|
||||
return x
|
||||
|
||||
@ -0,0 +1,883 @@
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import numpy as np
|
||||
from numpy import array, finfo, arange, eye, all, unique, ones, dot
|
||||
import numpy.random as random
|
||||
from numpy.testing import (
|
||||
assert_array_almost_equal, assert_almost_equal,
|
||||
assert_equal, assert_array_equal, assert_, assert_allclose,
|
||||
assert_warns, suppress_warnings)
|
||||
import pytest
|
||||
from pytest import raises as assert_raises
|
||||
|
||||
import scipy.linalg
|
||||
from scipy.linalg import norm, inv
|
||||
from scipy.sparse import (spdiags, SparseEfficiencyWarning, csc_matrix,
|
||||
csr_matrix, identity, issparse, dok_matrix, lil_matrix, bsr_matrix)
|
||||
from scipy.sparse.linalg import SuperLU
|
||||
from scipy.sparse.linalg._dsolve import (spsolve, use_solver, splu, spilu,
|
||||
MatrixRankWarning, _superlu, spsolve_triangular, factorized)
|
||||
import scipy.sparse
|
||||
|
||||
from scipy._lib._testutils import check_free_memory
|
||||
from scipy._lib._util import ComplexWarning
|
||||
|
||||
|
||||
sup_sparse_efficiency = suppress_warnings()
|
||||
sup_sparse_efficiency.filter(SparseEfficiencyWarning)
|
||||
|
||||
# scikits.umfpack is not a SciPy dependency but it is optionally used in
|
||||
# dsolve, so check whether it's available
|
||||
try:
|
||||
import scikits.umfpack as umfpack
|
||||
has_umfpack = True
|
||||
except ImportError:
|
||||
has_umfpack = False
|
||||
|
||||
def toarray(a):
|
||||
if issparse(a):
|
||||
return a.toarray()
|
||||
else:
|
||||
return a
|
||||
|
||||
|
||||
def setup_bug_8278():
|
||||
N = 2 ** 6
|
||||
h = 1/N
|
||||
Ah1D = scipy.sparse.diags([-1, 2, -1], [-1, 0, 1],
|
||||
shape=(N-1, N-1))/(h**2)
|
||||
eyeN = scipy.sparse.eye(N - 1)
|
||||
A = (scipy.sparse.kron(eyeN, scipy.sparse.kron(eyeN, Ah1D))
|
||||
+ scipy.sparse.kron(eyeN, scipy.sparse.kron(Ah1D, eyeN))
|
||||
+ scipy.sparse.kron(Ah1D, scipy.sparse.kron(eyeN, eyeN)))
|
||||
b = np.random.rand((N-1)**3)
|
||||
return A, b
|
||||
|
||||
|
||||
class TestFactorized:
|
||||
def setup_method(self):
|
||||
n = 5
|
||||
d = arange(n) + 1
|
||||
self.n = n
|
||||
self.A = spdiags((d, 2*d, d[::-1]), (-3, 0, 5), n, n).tocsc()
|
||||
random.seed(1234)
|
||||
|
||||
def _check_singular(self):
|
||||
A = csc_matrix((5,5), dtype='d')
|
||||
b = ones(5)
|
||||
assert_array_almost_equal(0. * b, factorized(A)(b))
|
||||
|
||||
def _check_non_singular(self):
|
||||
# Make a diagonal dominant, to make sure it is not singular
|
||||
n = 5
|
||||
a = csc_matrix(random.rand(n, n))
|
||||
b = ones(n)
|
||||
|
||||
expected = splu(a).solve(b)
|
||||
assert_array_almost_equal(factorized(a)(b), expected)
|
||||
|
||||
def test_singular_without_umfpack(self):
|
||||
use_solver(useUmfpack=False)
|
||||
with assert_raises(RuntimeError, match="Factor is exactly singular"):
|
||||
self._check_singular()
|
||||
|
||||
@pytest.mark.skipif(not has_umfpack, reason="umfpack not available")
|
||||
def test_singular_with_umfpack(self):
|
||||
use_solver(useUmfpack=True)
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(RuntimeWarning, "divide by zero encountered in double_scalars")
|
||||
assert_warns(umfpack.UmfpackWarning, self._check_singular)
|
||||
|
||||
def test_non_singular_without_umfpack(self):
|
||||
use_solver(useUmfpack=False)
|
||||
self._check_non_singular()
|
||||
|
||||
@pytest.mark.skipif(not has_umfpack, reason="umfpack not available")
|
||||
def test_non_singular_with_umfpack(self):
|
||||
use_solver(useUmfpack=True)
|
||||
self._check_non_singular()
|
||||
|
||||
def test_cannot_factorize_nonsquare_matrix_without_umfpack(self):
|
||||
use_solver(useUmfpack=False)
|
||||
msg = "can only factor square matrices"
|
||||
with assert_raises(ValueError, match=msg):
|
||||
factorized(self.A[:, :4])
|
||||
|
||||
@pytest.mark.skipif(not has_umfpack, reason="umfpack not available")
|
||||
def test_factorizes_nonsquare_matrix_with_umfpack(self):
|
||||
use_solver(useUmfpack=True)
|
||||
# does not raise
|
||||
factorized(self.A[:,:4])
|
||||
|
||||
def test_call_with_incorrectly_sized_matrix_without_umfpack(self):
|
||||
use_solver(useUmfpack=False)
|
||||
solve = factorized(self.A)
|
||||
b = random.rand(4)
|
||||
B = random.rand(4, 3)
|
||||
BB = random.rand(self.n, 3, 9)
|
||||
|
||||
with assert_raises(ValueError, match="is of incompatible size"):
|
||||
solve(b)
|
||||
with assert_raises(ValueError, match="is of incompatible size"):
|
||||
solve(B)
|
||||
with assert_raises(ValueError,
|
||||
match="object too deep for desired array"):
|
||||
solve(BB)
|
||||
|
||||
@pytest.mark.skipif(not has_umfpack, reason="umfpack not available")
|
||||
def test_call_with_incorrectly_sized_matrix_with_umfpack(self):
|
||||
use_solver(useUmfpack=True)
|
||||
solve = factorized(self.A)
|
||||
b = random.rand(4)
|
||||
B = random.rand(4, 3)
|
||||
BB = random.rand(self.n, 3, 9)
|
||||
|
||||
# does not raise
|
||||
solve(b)
|
||||
msg = "object too deep for desired array"
|
||||
with assert_raises(ValueError, match=msg):
|
||||
solve(B)
|
||||
with assert_raises(ValueError, match=msg):
|
||||
solve(BB)
|
||||
|
||||
def test_call_with_cast_to_complex_without_umfpack(self):
|
||||
use_solver(useUmfpack=False)
|
||||
solve = factorized(self.A)
|
||||
b = random.rand(4)
|
||||
for t in [np.complex64, np.complex128]:
|
||||
with assert_raises(TypeError, match="Cannot cast array data"):
|
||||
solve(b.astype(t))
|
||||
|
||||
@pytest.mark.skipif(not has_umfpack, reason="umfpack not available")
|
||||
def test_call_with_cast_to_complex_with_umfpack(self):
|
||||
use_solver(useUmfpack=True)
|
||||
solve = factorized(self.A)
|
||||
b = random.rand(4)
|
||||
for t in [np.complex64, np.complex128]:
|
||||
assert_warns(ComplexWarning, solve, b.astype(t))
|
||||
|
||||
@pytest.mark.skipif(not has_umfpack, reason="umfpack not available")
|
||||
def test_assume_sorted_indices_flag(self):
|
||||
# a sparse matrix with unsorted indices
|
||||
unsorted_inds = np.array([2, 0, 1, 0])
|
||||
data = np.array([10, 16, 5, 0.4])
|
||||
indptr = np.array([0, 1, 2, 4])
|
||||
A = csc_matrix((data, unsorted_inds, indptr), (3, 3))
|
||||
b = ones(3)
|
||||
|
||||
# should raise when incorrectly assuming indices are sorted
|
||||
use_solver(useUmfpack=True, assumeSortedIndices=True)
|
||||
with assert_raises(RuntimeError,
|
||||
match="UMFPACK_ERROR_invalid_matrix"):
|
||||
factorized(A)
|
||||
|
||||
# should sort indices and succeed when not assuming indices are sorted
|
||||
use_solver(useUmfpack=True, assumeSortedIndices=False)
|
||||
expected = splu(A.copy()).solve(b)
|
||||
|
||||
assert_equal(A.has_sorted_indices, 0)
|
||||
assert_array_almost_equal(factorized(A)(b), expected)
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.skipif(not has_umfpack, reason="umfpack not available")
|
||||
def test_bug_8278(self):
|
||||
check_free_memory(8000)
|
||||
use_solver(useUmfpack=True)
|
||||
A, b = setup_bug_8278()
|
||||
A = A.tocsc()
|
||||
f = factorized(A)
|
||||
x = f(b)
|
||||
assert_array_almost_equal(A @ x, b)
|
||||
|
||||
|
||||
class TestLinsolve:
|
||||
def setup_method(self):
|
||||
use_solver(useUmfpack=False)
|
||||
|
||||
def test_singular(self):
|
||||
A = csc_matrix((5,5), dtype='d')
|
||||
b = array([1, 2, 3, 4, 5],dtype='d')
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(MatrixRankWarning, "Matrix is exactly singular")
|
||||
x = spsolve(A, b)
|
||||
assert_(not np.isfinite(x).any())
|
||||
|
||||
def test_singular_gh_3312(self):
|
||||
# "Bad" test case that leads SuperLU to call LAPACK with invalid
|
||||
# arguments. Check that it fails moderately gracefully.
|
||||
ij = np.array([(17, 0), (17, 6), (17, 12), (10, 13)], dtype=np.int32)
|
||||
v = np.array([0.284213, 0.94933781, 0.15767017, 0.38797296])
|
||||
A = csc_matrix((v, ij.T), shape=(20, 20))
|
||||
b = np.arange(20)
|
||||
|
||||
try:
|
||||
# should either raise a runtime error or return value
|
||||
# appropriate for singular input (which yields the warning)
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(MatrixRankWarning, "Matrix is exactly singular")
|
||||
x = spsolve(A, b)
|
||||
assert not np.isfinite(x).any()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
@pytest.mark.parametrize('format', ['csc', 'csr'])
|
||||
@pytest.mark.parametrize('idx_dtype', [np.int32, np.int64])
|
||||
def test_twodiags(self, format: str, idx_dtype: np.dtype):
|
||||
A = spdiags([[1, 2, 3, 4, 5], [6, 5, 8, 9, 10]], [0, 1], 5, 5,
|
||||
format=format)
|
||||
b = array([1, 2, 3, 4, 5])
|
||||
|
||||
# condition number of A
|
||||
cond_A = norm(A.toarray(), 2) * norm(inv(A.toarray()), 2)
|
||||
|
||||
for t in ['f','d','F','D']:
|
||||
eps = finfo(t).eps # floating point epsilon
|
||||
b = b.astype(t)
|
||||
Asp = A.astype(t)
|
||||
Asp.indices = Asp.indices.astype(idx_dtype, copy=False)
|
||||
Asp.indptr = Asp.indptr.astype(idx_dtype, copy=False)
|
||||
|
||||
x = spsolve(Asp, b)
|
||||
assert_(norm(b - Asp@x) < 10 * cond_A * eps)
|
||||
|
||||
def test_bvector_smoketest(self):
|
||||
Adense = array([[0., 1., 1.],
|
||||
[1., 0., 1.],
|
||||
[0., 0., 1.]])
|
||||
As = csc_matrix(Adense)
|
||||
random.seed(1234)
|
||||
x = random.randn(3)
|
||||
b = As@x
|
||||
x2 = spsolve(As, b)
|
||||
|
||||
assert_array_almost_equal(x, x2)
|
||||
|
||||
def test_bmatrix_smoketest(self):
|
||||
Adense = array([[0., 1., 1.],
|
||||
[1., 0., 1.],
|
||||
[0., 0., 1.]])
|
||||
As = csc_matrix(Adense)
|
||||
random.seed(1234)
|
||||
x = random.randn(3, 4)
|
||||
Bdense = As.dot(x)
|
||||
Bs = csc_matrix(Bdense)
|
||||
x2 = spsolve(As, Bs)
|
||||
assert_array_almost_equal(x, x2.toarray())
|
||||
|
||||
@sup_sparse_efficiency
|
||||
def test_non_square(self):
|
||||
# A is not square.
|
||||
A = ones((3, 4))
|
||||
b = ones((4, 1))
|
||||
assert_raises(ValueError, spsolve, A, b)
|
||||
# A2 and b2 have incompatible shapes.
|
||||
A2 = csc_matrix(eye(3))
|
||||
b2 = array([1.0, 2.0])
|
||||
assert_raises(ValueError, spsolve, A2, b2)
|
||||
|
||||
@sup_sparse_efficiency
|
||||
def test_example_comparison(self):
|
||||
row = array([0,0,1,2,2,2])
|
||||
col = array([0,2,2,0,1,2])
|
||||
data = array([1,2,3,-4,5,6])
|
||||
sM = csr_matrix((data,(row,col)), shape=(3,3), dtype=float)
|
||||
M = sM.toarray()
|
||||
|
||||
row = array([0,0,1,1,0,0])
|
||||
col = array([0,2,1,1,0,0])
|
||||
data = array([1,1,1,1,1,1])
|
||||
sN = csr_matrix((data, (row,col)), shape=(3,3), dtype=float)
|
||||
N = sN.toarray()
|
||||
|
||||
sX = spsolve(sM, sN)
|
||||
X = scipy.linalg.solve(M, N)
|
||||
|
||||
assert_array_almost_equal(X, sX.toarray())
|
||||
|
||||
@sup_sparse_efficiency
|
||||
@pytest.mark.skipif(not has_umfpack, reason="umfpack not available")
|
||||
def test_shape_compatibility(self):
|
||||
use_solver(useUmfpack=True)
|
||||
A = csc_matrix([[1., 0], [0, 2]])
|
||||
bs = [
|
||||
[1, 6],
|
||||
array([1, 6]),
|
||||
[[1], [6]],
|
||||
array([[1], [6]]),
|
||||
csc_matrix([[1], [6]]),
|
||||
csr_matrix([[1], [6]]),
|
||||
dok_matrix([[1], [6]]),
|
||||
bsr_matrix([[1], [6]]),
|
||||
array([[1., 2., 3.], [6., 8., 10.]]),
|
||||
csc_matrix([[1., 2., 3.], [6., 8., 10.]]),
|
||||
csr_matrix([[1., 2., 3.], [6., 8., 10.]]),
|
||||
dok_matrix([[1., 2., 3.], [6., 8., 10.]]),
|
||||
bsr_matrix([[1., 2., 3.], [6., 8., 10.]]),
|
||||
]
|
||||
|
||||
for b in bs:
|
||||
x = np.linalg.solve(A.toarray(), toarray(b))
|
||||
for spmattype in [csc_matrix, csr_matrix, dok_matrix, lil_matrix]:
|
||||
x1 = spsolve(spmattype(A), b, use_umfpack=True)
|
||||
x2 = spsolve(spmattype(A), b, use_umfpack=False)
|
||||
|
||||
# check solution
|
||||
if x.ndim == 2 and x.shape[1] == 1:
|
||||
# interprets also these as "vectors"
|
||||
x = x.ravel()
|
||||
|
||||
assert_array_almost_equal(toarray(x1), x,
|
||||
err_msg=repr((b, spmattype, 1)))
|
||||
assert_array_almost_equal(toarray(x2), x,
|
||||
err_msg=repr((b, spmattype, 2)))
|
||||
|
||||
# dense vs. sparse output ("vectors" are always dense)
|
||||
if issparse(b) and x.ndim > 1:
|
||||
assert_(issparse(x1), repr((b, spmattype, 1)))
|
||||
assert_(issparse(x2), repr((b, spmattype, 2)))
|
||||
else:
|
||||
assert_(isinstance(x1, np.ndarray), repr((b, spmattype, 1)))
|
||||
assert_(isinstance(x2, np.ndarray), repr((b, spmattype, 2)))
|
||||
|
||||
# check output shape
|
||||
if x.ndim == 1:
|
||||
# "vector"
|
||||
assert_equal(x1.shape, (A.shape[1],))
|
||||
assert_equal(x2.shape, (A.shape[1],))
|
||||
else:
|
||||
# "matrix"
|
||||
assert_equal(x1.shape, x.shape)
|
||||
assert_equal(x2.shape, x.shape)
|
||||
|
||||
A = csc_matrix((3, 3))
|
||||
b = csc_matrix((1, 3))
|
||||
assert_raises(ValueError, spsolve, A, b)
|
||||
|
||||
@sup_sparse_efficiency
|
||||
def test_ndarray_support(self):
|
||||
A = array([[1., 2.], [2., 0.]])
|
||||
x = array([[1., 1.], [0.5, -0.5]])
|
||||
b = array([[2., 0.], [2., 2.]])
|
||||
|
||||
assert_array_almost_equal(x, spsolve(A, b))
|
||||
|
||||
def test_gssv_badinput(self):
|
||||
N = 10
|
||||
d = arange(N) + 1.0
|
||||
A = spdiags((d, 2*d, d[::-1]), (-3, 0, 5), N, N)
|
||||
|
||||
for spmatrix in (csc_matrix, csr_matrix):
|
||||
A = spmatrix(A)
|
||||
b = np.arange(N)
|
||||
|
||||
def not_c_contig(x):
|
||||
return x.repeat(2)[::2]
|
||||
|
||||
def not_1dim(x):
|
||||
return x[:,None]
|
||||
|
||||
def bad_type(x):
|
||||
return x.astype(bool)
|
||||
|
||||
def too_short(x):
|
||||
return x[:-1]
|
||||
|
||||
badops = [not_c_contig, not_1dim, bad_type, too_short]
|
||||
|
||||
for badop in badops:
|
||||
msg = f"{spmatrix!r} {badop!r}"
|
||||
# Not C-contiguous
|
||||
assert_raises((ValueError, TypeError), _superlu.gssv,
|
||||
N, A.nnz, badop(A.data), A.indices, A.indptr,
|
||||
b, int(spmatrix == csc_matrix), err_msg=msg)
|
||||
assert_raises((ValueError, TypeError), _superlu.gssv,
|
||||
N, A.nnz, A.data, badop(A.indices), A.indptr,
|
||||
b, int(spmatrix == csc_matrix), err_msg=msg)
|
||||
assert_raises((ValueError, TypeError), _superlu.gssv,
|
||||
N, A.nnz, A.data, A.indices, badop(A.indptr),
|
||||
b, int(spmatrix == csc_matrix), err_msg=msg)
|
||||
|
||||
def test_sparsity_preservation(self):
|
||||
ident = csc_matrix([
|
||||
[1, 0, 0],
|
||||
[0, 1, 0],
|
||||
[0, 0, 1]])
|
||||
b = csc_matrix([
|
||||
[0, 1],
|
||||
[1, 0],
|
||||
[0, 0]])
|
||||
x = spsolve(ident, b)
|
||||
assert_equal(ident.nnz, 3)
|
||||
assert_equal(b.nnz, 2)
|
||||
assert_equal(x.nnz, 2)
|
||||
assert_allclose(x.toarray(), b.toarray(), atol=1e-12, rtol=1e-12)
|
||||
|
||||
def test_dtype_cast(self):
|
||||
A_real = scipy.sparse.csr_matrix([[1, 2, 0],
|
||||
[0, 0, 3],
|
||||
[4, 0, 5]])
|
||||
A_complex = scipy.sparse.csr_matrix([[1, 2, 0],
|
||||
[0, 0, 3],
|
||||
[4, 0, 5 + 1j]])
|
||||
b_real = np.array([1,1,1])
|
||||
b_complex = np.array([1,1,1]) + 1j*np.array([1,1,1])
|
||||
x = spsolve(A_real, b_real)
|
||||
assert_(np.issubdtype(x.dtype, np.floating))
|
||||
x = spsolve(A_real, b_complex)
|
||||
assert_(np.issubdtype(x.dtype, np.complexfloating))
|
||||
x = spsolve(A_complex, b_real)
|
||||
assert_(np.issubdtype(x.dtype, np.complexfloating))
|
||||
x = spsolve(A_complex, b_complex)
|
||||
assert_(np.issubdtype(x.dtype, np.complexfloating))
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.skipif(not has_umfpack, reason="umfpack not available")
|
||||
def test_bug_8278(self):
|
||||
check_free_memory(8000)
|
||||
use_solver(useUmfpack=True)
|
||||
A, b = setup_bug_8278()
|
||||
x = spsolve(A, b)
|
||||
assert_array_almost_equal(A @ x, b)
|
||||
|
||||
|
||||
class TestSplu:
|
||||
def setup_method(self):
|
||||
use_solver(useUmfpack=False)
|
||||
n = 40
|
||||
d = arange(n) + 1
|
||||
self.n = n
|
||||
self.A = spdiags((d, 2*d, d[::-1]), (-3, 0, 5), n, n, format='csc')
|
||||
random.seed(1234)
|
||||
|
||||
def _smoketest(self, spxlu, check, dtype, idx_dtype):
|
||||
if np.issubdtype(dtype, np.complexfloating):
|
||||
A = self.A + 1j*self.A.T
|
||||
else:
|
||||
A = self.A
|
||||
|
||||
A = A.astype(dtype)
|
||||
A.indices = A.indices.astype(idx_dtype, copy=False)
|
||||
A.indptr = A.indptr.astype(idx_dtype, copy=False)
|
||||
lu = spxlu(A)
|
||||
|
||||
rng = random.RandomState(1234)
|
||||
|
||||
# Input shapes
|
||||
for k in [None, 1, 2, self.n, self.n+2]:
|
||||
msg = f"k={k!r}"
|
||||
|
||||
if k is None:
|
||||
b = rng.rand(self.n)
|
||||
else:
|
||||
b = rng.rand(self.n, k)
|
||||
|
||||
if np.issubdtype(dtype, np.complexfloating):
|
||||
b = b + 1j*rng.rand(*b.shape)
|
||||
b = b.astype(dtype)
|
||||
|
||||
x = lu.solve(b)
|
||||
check(A, b, x, msg)
|
||||
|
||||
x = lu.solve(b, 'T')
|
||||
check(A.T, b, x, msg)
|
||||
|
||||
x = lu.solve(b, 'H')
|
||||
check(A.T.conj(), b, x, msg)
|
||||
|
||||
@sup_sparse_efficiency
|
||||
def test_splu_smoketest(self):
|
||||
self._internal_test_splu_smoketest()
|
||||
|
||||
def _internal_test_splu_smoketest(self):
|
||||
# Check that splu works at all
|
||||
def check(A, b, x, msg=""):
|
||||
eps = np.finfo(A.dtype).eps
|
||||
r = A @ x
|
||||
assert_(abs(r - b).max() < 1e3*eps, msg)
|
||||
|
||||
for dtype in [np.float32, np.float64, np.complex64, np.complex128]:
|
||||
for idx_dtype in [np.int32, np.int64]:
|
||||
self._smoketest(splu, check, dtype, idx_dtype)
|
||||
|
||||
@sup_sparse_efficiency
|
||||
def test_spilu_smoketest(self):
|
||||
self._internal_test_spilu_smoketest()
|
||||
|
||||
def _internal_test_spilu_smoketest(self):
|
||||
errors = []
|
||||
|
||||
def check(A, b, x, msg=""):
|
||||
r = A @ x
|
||||
err = abs(r - b).max()
|
||||
assert_(err < 1e-2, msg)
|
||||
if b.dtype in (np.float64, np.complex128):
|
||||
errors.append(err)
|
||||
|
||||
for dtype in [np.float32, np.float64, np.complex64, np.complex128]:
|
||||
for idx_dtype in [np.int32, np.int64]:
|
||||
self._smoketest(spilu, check, dtype, idx_dtype)
|
||||
|
||||
assert_(max(errors) > 1e-5)
|
||||
|
||||
@sup_sparse_efficiency
|
||||
def test_spilu_drop_rule(self):
|
||||
# Test passing in the drop_rule argument to spilu.
|
||||
A = identity(2)
|
||||
|
||||
rules = [
|
||||
b'basic,area'.decode('ascii'), # unicode
|
||||
b'basic,area', # ascii
|
||||
[b'basic', b'area'.decode('ascii')]
|
||||
]
|
||||
for rule in rules:
|
||||
# Argument should be accepted
|
||||
assert_(isinstance(spilu(A, drop_rule=rule), SuperLU))
|
||||
|
||||
def test_splu_nnz0(self):
|
||||
A = csc_matrix((5,5), dtype='d')
|
||||
assert_raises(RuntimeError, splu, A)
|
||||
|
||||
def test_spilu_nnz0(self):
|
||||
A = csc_matrix((5,5), dtype='d')
|
||||
assert_raises(RuntimeError, spilu, A)
|
||||
|
||||
def test_splu_basic(self):
|
||||
# Test basic splu functionality.
|
||||
n = 30
|
||||
rng = random.RandomState(12)
|
||||
a = rng.rand(n, n)
|
||||
a[a < 0.95] = 0
|
||||
# First test with a singular matrix
|
||||
a[:, 0] = 0
|
||||
a_ = csc_matrix(a)
|
||||
# Matrix is exactly singular
|
||||
assert_raises(RuntimeError, splu, a_)
|
||||
|
||||
# Make a diagonal dominant, to make sure it is not singular
|
||||
a += 4*eye(n)
|
||||
a_ = csc_matrix(a)
|
||||
lu = splu(a_)
|
||||
b = ones(n)
|
||||
x = lu.solve(b)
|
||||
assert_almost_equal(dot(a, x), b)
|
||||
|
||||
def test_splu_perm(self):
|
||||
# Test the permutation vectors exposed by splu.
|
||||
n = 30
|
||||
a = random.random((n, n))
|
||||
a[a < 0.95] = 0
|
||||
# Make a diagonal dominant, to make sure it is not singular
|
||||
a += 4*eye(n)
|
||||
a_ = csc_matrix(a)
|
||||
lu = splu(a_)
|
||||
# Check that the permutation indices do belong to [0, n-1].
|
||||
for perm in (lu.perm_r, lu.perm_c):
|
||||
assert_(all(perm > -1))
|
||||
assert_(all(perm < n))
|
||||
assert_equal(len(unique(perm)), len(perm))
|
||||
|
||||
# Now make a symmetric, and test that the two permutation vectors are
|
||||
# the same
|
||||
# Note: a += a.T relies on undefined behavior.
|
||||
a = a + a.T
|
||||
a_ = csc_matrix(a)
|
||||
lu = splu(a_)
|
||||
assert_array_equal(lu.perm_r, lu.perm_c)
|
||||
|
||||
@pytest.mark.parametrize("splu_fun, rtol", [(splu, 1e-7), (spilu, 1e-1)])
|
||||
def test_natural_permc(self, splu_fun, rtol):
|
||||
# Test that the "NATURAL" permc_spec does not permute the matrix
|
||||
np.random.seed(42)
|
||||
n = 500
|
||||
p = 0.01
|
||||
A = scipy.sparse.random(n, n, p)
|
||||
x = np.random.rand(n)
|
||||
# Make A diagonal dominant to make sure it is not singular
|
||||
A += (n+1)*scipy.sparse.identity(n)
|
||||
A_ = csc_matrix(A)
|
||||
b = A_ @ x
|
||||
|
||||
# without permc_spec, permutation is not identity
|
||||
lu = splu_fun(A_)
|
||||
assert_(np.any(lu.perm_c != np.arange(n)))
|
||||
|
||||
# with permc_spec="NATURAL", permutation is identity
|
||||
lu = splu_fun(A_, permc_spec="NATURAL")
|
||||
assert_array_equal(lu.perm_c, np.arange(n))
|
||||
|
||||
# Also, lu decomposition is valid
|
||||
x2 = lu.solve(b)
|
||||
assert_allclose(x, x2, rtol=rtol)
|
||||
|
||||
@pytest.mark.skipif(not hasattr(sys, 'getrefcount'), reason="no sys.getrefcount")
|
||||
def test_lu_refcount(self):
|
||||
# Test that we are keeping track of the reference count with splu.
|
||||
n = 30
|
||||
a = random.random((n, n))
|
||||
a[a < 0.95] = 0
|
||||
# Make a diagonal dominant, to make sure it is not singular
|
||||
a += 4*eye(n)
|
||||
a_ = csc_matrix(a)
|
||||
lu = splu(a_)
|
||||
|
||||
# And now test that we don't have a refcount bug
|
||||
rc = sys.getrefcount(lu)
|
||||
for attr in ('perm_r', 'perm_c'):
|
||||
perm = getattr(lu, attr)
|
||||
assert_equal(sys.getrefcount(lu), rc + 1)
|
||||
del perm
|
||||
assert_equal(sys.getrefcount(lu), rc)
|
||||
|
||||
def test_bad_inputs(self):
|
||||
A = self.A.tocsc()
|
||||
|
||||
assert_raises(ValueError, splu, A[:,:4])
|
||||
assert_raises(ValueError, spilu, A[:,:4])
|
||||
|
||||
for lu in [splu(A), spilu(A)]:
|
||||
b = random.rand(42)
|
||||
B = random.rand(42, 3)
|
||||
BB = random.rand(self.n, 3, 9)
|
||||
assert_raises(ValueError, lu.solve, b)
|
||||
assert_raises(ValueError, lu.solve, B)
|
||||
assert_raises(ValueError, lu.solve, BB)
|
||||
assert_raises(TypeError, lu.solve,
|
||||
b.astype(np.complex64))
|
||||
assert_raises(TypeError, lu.solve,
|
||||
b.astype(np.complex128))
|
||||
|
||||
@sup_sparse_efficiency
|
||||
def test_superlu_dlamch_i386_nan(self):
|
||||
# SuperLU 4.3 calls some functions returning floats without
|
||||
# declaring them. On i386@linux call convention, this fails to
|
||||
# clear floating point registers after call. As a result, NaN
|
||||
# can appear in the next floating point operation made.
|
||||
#
|
||||
# Here's a test case that triggered the issue.
|
||||
n = 8
|
||||
d = np.arange(n) + 1
|
||||
A = spdiags((d, 2*d, d[::-1]), (-3, 0, 5), n, n)
|
||||
A = A.astype(np.float32)
|
||||
spilu(A)
|
||||
A = A + 1j*A
|
||||
B = A.toarray()
|
||||
assert_(not np.isnan(B).any())
|
||||
|
||||
@sup_sparse_efficiency
|
||||
def test_lu_attr(self):
|
||||
|
||||
def check(dtype, complex_2=False):
|
||||
A = self.A.astype(dtype)
|
||||
|
||||
if complex_2:
|
||||
A = A + 1j*A.T
|
||||
|
||||
n = A.shape[0]
|
||||
lu = splu(A)
|
||||
|
||||
# Check that the decomposition is as advertised
|
||||
|
||||
Pc = np.zeros((n, n))
|
||||
Pc[np.arange(n), lu.perm_c] = 1
|
||||
|
||||
Pr = np.zeros((n, n))
|
||||
Pr[lu.perm_r, np.arange(n)] = 1
|
||||
|
||||
Ad = A.toarray()
|
||||
lhs = Pr.dot(Ad).dot(Pc)
|
||||
rhs = (lu.L @ lu.U).toarray()
|
||||
|
||||
eps = np.finfo(dtype).eps
|
||||
|
||||
assert_allclose(lhs, rhs, atol=100*eps)
|
||||
|
||||
check(np.float32)
|
||||
check(np.float64)
|
||||
check(np.complex64)
|
||||
check(np.complex128)
|
||||
check(np.complex64, True)
|
||||
check(np.complex128, True)
|
||||
|
||||
@pytest.mark.slow
|
||||
@sup_sparse_efficiency
|
||||
def test_threads_parallel(self):
|
||||
oks = []
|
||||
|
||||
def worker():
|
||||
try:
|
||||
self.test_splu_basic()
|
||||
self._internal_test_splu_smoketest()
|
||||
self._internal_test_spilu_smoketest()
|
||||
oks.append(True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
threads = [threading.Thread(target=worker)
|
||||
for k in range(20)]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
assert_equal(len(oks), 20)
|
||||
|
||||
def test_singular_matrix(self):
|
||||
# Test that SuperLU does not print to stdout when a singular matrix is
|
||||
# passed. See gh-20993.
|
||||
A = identity(10, format='csr').tocsr()
|
||||
A[-1, -1] = 0
|
||||
b = np.zeros(10)
|
||||
with pytest.warns(MatrixRankWarning):
|
||||
res = spsolve(A, b)
|
||||
assert np.isnan(res).all()
|
||||
|
||||
|
||||
class TestGstrsErrors:
|
||||
def setup_method(self):
|
||||
self.A = array([[1.0,2.0,3.0],[4.0,5.0,6.0],[7.0,8.0,9.0]], dtype=np.float64)
|
||||
self.b = np.array([[1.0],[2.0],[3.0]], dtype=np.float64)
|
||||
|
||||
def test_trans(self):
|
||||
L = scipy.sparse.tril(self.A, format='csc')
|
||||
U = scipy.sparse.triu(self.A, k=1, format='csc')
|
||||
with assert_raises(ValueError, match="trans must be N, T, or H"):
|
||||
_superlu.gstrs('X', L.shape[0], L.nnz, L.data, L.indices, L.indptr,
|
||||
U.shape[0], U.nnz, U.data, U.indices, U.indptr, self.b)
|
||||
|
||||
def test_shape_LU(self):
|
||||
L = scipy.sparse.tril(self.A[0:2,0:2], format='csc')
|
||||
U = scipy.sparse.triu(self.A, k=1, format='csc')
|
||||
with assert_raises(ValueError, match="L and U must have the same dimension"):
|
||||
_superlu.gstrs('N', L.shape[0], L.nnz, L.data, L.indices, L.indptr,
|
||||
U.shape[0], U.nnz, U.data, U.indices, U.indptr, self.b)
|
||||
|
||||
def test_shape_b(self):
|
||||
L = scipy.sparse.tril(self.A, format='csc')
|
||||
U = scipy.sparse.triu(self.A, k=1, format='csc')
|
||||
with assert_raises(ValueError, match="right hand side array has invalid shape"):
|
||||
_superlu.gstrs('N', L.shape[0], L.nnz, L.data, L.indices, L.indptr,
|
||||
U.shape[0], U.nnz, U.data, U.indices, U.indptr,
|
||||
self.b[0:2])
|
||||
|
||||
def test_types_differ(self):
|
||||
L = scipy.sparse.tril(self.A.astype(np.float32), format='csc')
|
||||
U = scipy.sparse.triu(self.A, k=1, format='csc')
|
||||
with assert_raises(TypeError, match="nzvals types of L and U differ"):
|
||||
_superlu.gstrs('N', L.shape[0], L.nnz, L.data, L.indices, L.indptr,
|
||||
U.shape[0], U.nnz, U.data, U.indices, U.indptr, self.b)
|
||||
|
||||
def test_types_unsupported(self):
|
||||
L = scipy.sparse.tril(self.A.astype(np.uint8), format='csc')
|
||||
U = scipy.sparse.triu(self.A.astype(np.uint8), k=1, format='csc')
|
||||
with assert_raises(TypeError, match="nzvals is not of a type supported"):
|
||||
_superlu.gstrs('N', L.shape[0], L.nnz, L.data, L.indices, L.indptr,
|
||||
U.shape[0], U.nnz, U.data, U.indices, U.indptr,
|
||||
self.b.astype(np.uint8))
|
||||
|
||||
class TestSpsolveTriangular:
|
||||
def setup_method(self):
|
||||
use_solver(useUmfpack=False)
|
||||
|
||||
@pytest.mark.parametrize("fmt",["csr","csc"])
|
||||
def test_zero_diagonal(self,fmt):
|
||||
n = 5
|
||||
rng = np.random.default_rng(43876432987)
|
||||
A = rng.standard_normal((n, n))
|
||||
b = np.arange(n)
|
||||
A = scipy.sparse.tril(A, k=0, format=fmt)
|
||||
|
||||
x = spsolve_triangular(A, b, unit_diagonal=True, lower=True)
|
||||
|
||||
A.setdiag(1)
|
||||
assert_allclose(A.dot(x), b)
|
||||
|
||||
# Regression test from gh-15199
|
||||
A = np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0]], dtype=np.float64)
|
||||
b = np.array([1., 2., 3.])
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(SparseEfficiencyWarning, "CSC or CSR matrix format is")
|
||||
spsolve_triangular(A, b, unit_diagonal=True)
|
||||
|
||||
@pytest.mark.parametrize("fmt",["csr","csc"])
|
||||
def test_singular(self,fmt):
|
||||
n = 5
|
||||
if fmt == "csr":
|
||||
A = csr_matrix((n, n))
|
||||
else:
|
||||
A = csc_matrix((n, n))
|
||||
b = np.arange(n)
|
||||
for lower in (True, False):
|
||||
assert_raises(scipy.linalg.LinAlgError,
|
||||
spsolve_triangular, A, b, lower=lower)
|
||||
|
||||
@sup_sparse_efficiency
|
||||
def test_bad_shape(self):
|
||||
# A is not square.
|
||||
A = np.zeros((3, 4))
|
||||
b = ones((4, 1))
|
||||
assert_raises(ValueError, spsolve_triangular, A, b)
|
||||
# A2 and b2 have incompatible shapes.
|
||||
A2 = csr_matrix(eye(3))
|
||||
b2 = array([1.0, 2.0])
|
||||
assert_raises(ValueError, spsolve_triangular, A2, b2)
|
||||
|
||||
@sup_sparse_efficiency
|
||||
def test_input_types(self):
|
||||
A = array([[1., 0.], [1., 2.]])
|
||||
b = array([[2., 0.], [2., 2.]])
|
||||
for matrix_type in (array, csc_matrix, csr_matrix):
|
||||
x = spsolve_triangular(matrix_type(A), b, lower=True)
|
||||
assert_array_almost_equal(A.dot(x), b)
|
||||
|
||||
@pytest.mark.slow
|
||||
@sup_sparse_efficiency
|
||||
@pytest.mark.parametrize("n", [10, 10**2, 10**3])
|
||||
@pytest.mark.parametrize("m", [1, 10])
|
||||
@pytest.mark.parametrize("lower", [True, False])
|
||||
@pytest.mark.parametrize("format", ["csr", "csc"])
|
||||
@pytest.mark.parametrize("unit_diagonal", [False, True])
|
||||
@pytest.mark.parametrize("choice_of_A", ["real", "complex"])
|
||||
@pytest.mark.parametrize("choice_of_b", ["floats", "ints", "complexints"])
|
||||
def test_random(self, n, m, lower, format, unit_diagonal, choice_of_A, choice_of_b):
|
||||
def random_triangle_matrix(n, lower=True, format="csr", choice_of_A="real"):
|
||||
if choice_of_A == "real":
|
||||
dtype = np.float64
|
||||
elif choice_of_A == "complex":
|
||||
dtype = np.complex128
|
||||
else:
|
||||
raise ValueError("choice_of_A must be 'real' or 'complex'.")
|
||||
rng = np.random.default_rng(789002319)
|
||||
rvs = rng.random
|
||||
A = scipy.sparse.random(n, n, density=0.1, format='lil', dtype=dtype,
|
||||
random_state=rng, data_rvs=rvs)
|
||||
if lower:
|
||||
A = scipy.sparse.tril(A, format="lil")
|
||||
else:
|
||||
A = scipy.sparse.triu(A, format="lil")
|
||||
for i in range(n):
|
||||
A[i, i] = np.random.rand() + 1
|
||||
if format == "csc":
|
||||
A = A.tocsc(copy=False)
|
||||
else:
|
||||
A = A.tocsr(copy=False)
|
||||
return A
|
||||
|
||||
np.random.seed(1234)
|
||||
A = random_triangle_matrix(n, lower=lower)
|
||||
if choice_of_b == "floats":
|
||||
b = np.random.rand(n, m)
|
||||
elif choice_of_b == "ints":
|
||||
b = np.random.randint(-9, 9, (n, m))
|
||||
elif choice_of_b == "complexints":
|
||||
b = np.random.randint(-9, 9, (n, m)) + np.random.randint(-9, 9, (n, m)) * 1j
|
||||
else:
|
||||
raise ValueError(
|
||||
"choice_of_b must be 'floats', 'ints', or 'complexints'.")
|
||||
x = spsolve_triangular(A, b, lower=lower, unit_diagonal=unit_diagonal)
|
||||
if unit_diagonal:
|
||||
A.setdiag(1)
|
||||
assert_allclose(A.dot(x), b, atol=1.5e-6)
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
"""
|
||||
Sparse Eigenvalue Solvers
|
||||
-------------------------
|
||||
|
||||
The submodules of sparse.linalg._eigen:
|
||||
1. lobpcg: Locally Optimal Block Preconditioned Conjugate Gradient Method
|
||||
|
||||
"""
|
||||
from .arpack import *
|
||||
from .lobpcg import *
|
||||
from ._svds import svds
|
||||
|
||||
from . import arpack
|
||||
|
||||
__all__ = [
|
||||
'ArpackError', 'ArpackNoConvergence',
|
||||
'eigs', 'eigsh', 'lobpcg', 'svds'
|
||||
]
|
||||
|
||||
from scipy._lib._testutils import PytestTester
|
||||
test = PytestTester(__name__)
|
||||
del PytestTester
|
||||
@ -0,0 +1,546 @@
|
||||
import math
|
||||
import numpy as np
|
||||
|
||||
from .arpack import _arpack # type: ignore[attr-defined]
|
||||
from . import eigsh
|
||||
|
||||
from scipy._lib._util import check_random_state
|
||||
from scipy.sparse.linalg._interface import LinearOperator, aslinearoperator
|
||||
from scipy.sparse.linalg._eigen.lobpcg import lobpcg # type: ignore[no-redef]
|
||||
from scipy.sparse.linalg._svdp import _svdp
|
||||
from scipy.linalg import svd
|
||||
|
||||
arpack_int = _arpack.timing.nbx.dtype
|
||||
__all__ = ['svds']
|
||||
|
||||
|
||||
def _herm(x):
|
||||
return x.T.conj()
|
||||
|
||||
|
||||
def _iv(A, k, ncv, tol, which, v0, maxiter,
|
||||
return_singular, solver, random_state):
|
||||
|
||||
# input validation/standardization for `solver`
|
||||
# out of order because it's needed for other parameters
|
||||
solver = str(solver).lower()
|
||||
solvers = {"arpack", "lobpcg", "propack"}
|
||||
if solver not in solvers:
|
||||
raise ValueError(f"solver must be one of {solvers}.")
|
||||
|
||||
# input validation/standardization for `A`
|
||||
A = aslinearoperator(A) # this takes care of some input validation
|
||||
if not (np.issubdtype(A.dtype, np.complexfloating)
|
||||
or np.issubdtype(A.dtype, np.floating)):
|
||||
message = "`A` must be of floating or complex floating data type."
|
||||
raise ValueError(message)
|
||||
if math.prod(A.shape) == 0:
|
||||
message = "`A` must not be empty."
|
||||
raise ValueError(message)
|
||||
|
||||
# input validation/standardization for `k`
|
||||
kmax = min(A.shape) if solver == 'propack' else min(A.shape) - 1
|
||||
if int(k) != k or not (0 < k <= kmax):
|
||||
message = "`k` must be an integer satisfying `0 < k < min(A.shape)`."
|
||||
raise ValueError(message)
|
||||
k = int(k)
|
||||
|
||||
# input validation/standardization for `ncv`
|
||||
if solver == "arpack" and ncv is not None:
|
||||
if int(ncv) != ncv or not (k < ncv < min(A.shape)):
|
||||
message = ("`ncv` must be an integer satisfying "
|
||||
"`k < ncv < min(A.shape)`.")
|
||||
raise ValueError(message)
|
||||
ncv = int(ncv)
|
||||
|
||||
# input validation/standardization for `tol`
|
||||
if tol < 0 or not np.isfinite(tol):
|
||||
message = "`tol` must be a non-negative floating point value."
|
||||
raise ValueError(message)
|
||||
tol = float(tol)
|
||||
|
||||
# input validation/standardization for `which`
|
||||
which = str(which).upper()
|
||||
whichs = {'LM', 'SM'}
|
||||
if which not in whichs:
|
||||
raise ValueError(f"`which` must be in {whichs}.")
|
||||
|
||||
# input validation/standardization for `v0`
|
||||
if v0 is not None:
|
||||
v0 = np.atleast_1d(v0)
|
||||
if not (np.issubdtype(v0.dtype, np.complexfloating)
|
||||
or np.issubdtype(v0.dtype, np.floating)):
|
||||
message = ("`v0` must be of floating or complex floating "
|
||||
"data type.")
|
||||
raise ValueError(message)
|
||||
|
||||
shape = (A.shape[0],) if solver == 'propack' else (min(A.shape),)
|
||||
if v0.shape != shape:
|
||||
message = f"`v0` must have shape {shape}."
|
||||
raise ValueError(message)
|
||||
|
||||
# input validation/standardization for `maxiter`
|
||||
if maxiter is not None and (int(maxiter) != maxiter or maxiter <= 0):
|
||||
message = "`maxiter` must be a positive integer."
|
||||
raise ValueError(message)
|
||||
maxiter = int(maxiter) if maxiter is not None else maxiter
|
||||
|
||||
# input validation/standardization for `return_singular_vectors`
|
||||
# not going to be flexible with this; too complicated for little gain
|
||||
rs_options = {True, False, "vh", "u"}
|
||||
if return_singular not in rs_options:
|
||||
raise ValueError(f"`return_singular_vectors` must be in {rs_options}.")
|
||||
|
||||
random_state = check_random_state(random_state)
|
||||
|
||||
return (A, k, ncv, tol, which, v0, maxiter,
|
||||
return_singular, solver, random_state)
|
||||
|
||||
|
||||
def svds(A, k=6, ncv=None, tol=0, which='LM', v0=None,
|
||||
maxiter=None, return_singular_vectors=True,
|
||||
solver='arpack', random_state=None, options=None):
|
||||
"""
|
||||
Partial singular value decomposition of a sparse matrix.
|
||||
|
||||
Compute the largest or smallest `k` singular values and corresponding
|
||||
singular vectors of a sparse matrix `A`. The order in which the singular
|
||||
values are returned is not guaranteed.
|
||||
|
||||
In the descriptions below, let ``M, N = A.shape``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : ndarray, sparse matrix, or LinearOperator
|
||||
Matrix to decompose of a floating point numeric dtype.
|
||||
k : int, default: 6
|
||||
Number of singular values and singular vectors to compute.
|
||||
Must satisfy ``1 <= k <= kmax``, where ``kmax=min(M, N)`` for
|
||||
``solver='propack'`` and ``kmax=min(M, N) - 1`` otherwise.
|
||||
ncv : int, optional
|
||||
When ``solver='arpack'``, this is the number of Lanczos vectors
|
||||
generated. See :ref:`'arpack' <sparse.linalg.svds-arpack>` for details.
|
||||
When ``solver='lobpcg'`` or ``solver='propack'``, this parameter is
|
||||
ignored.
|
||||
tol : float, optional
|
||||
Tolerance for singular values. Zero (default) means machine precision.
|
||||
which : {'LM', 'SM'}
|
||||
Which `k` singular values to find: either the largest magnitude ('LM')
|
||||
or smallest magnitude ('SM') singular values.
|
||||
v0 : ndarray, optional
|
||||
The starting vector for iteration; see method-specific
|
||||
documentation (:ref:`'arpack' <sparse.linalg.svds-arpack>`,
|
||||
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`), or
|
||||
:ref:`'propack' <sparse.linalg.svds-propack>` for details.
|
||||
maxiter : int, optional
|
||||
Maximum number of iterations; see method-specific
|
||||
documentation (:ref:`'arpack' <sparse.linalg.svds-arpack>`,
|
||||
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`), or
|
||||
:ref:`'propack' <sparse.linalg.svds-propack>` for details.
|
||||
return_singular_vectors : {True, False, "u", "vh"}
|
||||
Singular values are always computed and returned; this parameter
|
||||
controls the computation and return of singular vectors.
|
||||
|
||||
- ``True``: return singular vectors.
|
||||
- ``False``: do not return singular vectors.
|
||||
- ``"u"``: if ``M <= N``, compute only the left singular vectors and
|
||||
return ``None`` for the right singular vectors. Otherwise, compute
|
||||
all singular vectors.
|
||||
- ``"vh"``: if ``M > N``, compute only the right singular vectors and
|
||||
return ``None`` for the left singular vectors. Otherwise, compute
|
||||
all singular vectors.
|
||||
|
||||
If ``solver='propack'``, the option is respected regardless of the
|
||||
matrix shape.
|
||||
|
||||
solver : {'arpack', 'propack', 'lobpcg'}, optional
|
||||
The solver used.
|
||||
:ref:`'arpack' <sparse.linalg.svds-arpack>`,
|
||||
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`, and
|
||||
:ref:`'propack' <sparse.linalg.svds-propack>` are supported.
|
||||
Default: `'arpack'`.
|
||||
random_state : {None, int, `numpy.random.Generator`,
|
||||
`numpy.random.RandomState`}, optional
|
||||
|
||||
Pseudorandom number generator state used to generate resamples.
|
||||
|
||||
If `random_state` is ``None`` (or `np.random`), the
|
||||
`numpy.random.RandomState` singleton is used.
|
||||
If `random_state` is an int, a new ``RandomState`` instance is used,
|
||||
seeded with `random_state`.
|
||||
If `random_state` is already a ``Generator`` or ``RandomState``
|
||||
instance then that instance is used.
|
||||
options : dict, optional
|
||||
A dictionary of solver-specific options. No solver-specific options
|
||||
are currently supported; this parameter is reserved for future use.
|
||||
|
||||
Returns
|
||||
-------
|
||||
u : ndarray, shape=(M, k)
|
||||
Unitary matrix having left singular vectors as columns.
|
||||
s : ndarray, shape=(k,)
|
||||
The singular values.
|
||||
vh : ndarray, shape=(k, N)
|
||||
Unitary matrix having right singular vectors as rows.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a naive implementation using ARPACK or LOBPCG as an eigensolver
|
||||
on the matrix ``A.conj().T @ A`` or ``A @ A.conj().T``, depending on
|
||||
which one is smaller size, followed by the Rayleigh-Ritz method
|
||||
as postprocessing; see
|
||||
Using the normal matrix, in Rayleigh-Ritz method, (2022, Nov. 19),
|
||||
Wikipedia, https://w.wiki/4zms.
|
||||
|
||||
Alternatively, the PROPACK solver can be called.
|
||||
|
||||
Choices of the input matrix `A` numeric dtype may be limited.
|
||||
Only ``solver="lobpcg"`` supports all floating point dtypes
|
||||
real: 'np.float32', 'np.float64', 'np.longdouble' and
|
||||
complex: 'np.complex64', 'np.complex128', 'np.clongdouble'.
|
||||
The ``solver="arpack"`` supports only
|
||||
'np.float32', 'np.float64', and 'np.complex128'.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Construct a matrix `A` from singular values and vectors.
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy import sparse, linalg, stats
|
||||
>>> from scipy.sparse.linalg import svds, aslinearoperator, LinearOperator
|
||||
|
||||
Construct a dense matrix `A` from singular values and vectors.
|
||||
|
||||
>>> rng = np.random.default_rng(258265244568965474821194062361901728911)
|
||||
>>> orthogonal = stats.ortho_group.rvs(10, random_state=rng)
|
||||
>>> s = [1e-3, 1, 2, 3, 4] # non-zero singular values
|
||||
>>> u = orthogonal[:, :5] # left singular vectors
|
||||
>>> vT = orthogonal[:, 5:].T # right singular vectors
|
||||
>>> A = u @ np.diag(s) @ vT
|
||||
|
||||
With only four singular values/vectors, the SVD approximates the original
|
||||
matrix.
|
||||
|
||||
>>> u4, s4, vT4 = svds(A, k=4)
|
||||
>>> A4 = u4 @ np.diag(s4) @ vT4
|
||||
>>> np.allclose(A4, A, atol=1e-3)
|
||||
True
|
||||
|
||||
With all five non-zero singular values/vectors, we can reproduce
|
||||
the original matrix more accurately.
|
||||
|
||||
>>> u5, s5, vT5 = svds(A, k=5)
|
||||
>>> A5 = u5 @ np.diag(s5) @ vT5
|
||||
>>> np.allclose(A5, A)
|
||||
True
|
||||
|
||||
The singular values match the expected singular values.
|
||||
|
||||
>>> np.allclose(s5, s)
|
||||
True
|
||||
|
||||
Since the singular values are not close to each other in this example,
|
||||
every singular vector matches as expected up to a difference in sign.
|
||||
|
||||
>>> (np.allclose(np.abs(u5), np.abs(u)) and
|
||||
... np.allclose(np.abs(vT5), np.abs(vT)))
|
||||
True
|
||||
|
||||
The singular vectors are also orthogonal.
|
||||
|
||||
>>> (np.allclose(u5.T @ u5, np.eye(5)) and
|
||||
... np.allclose(vT5 @ vT5.T, np.eye(5)))
|
||||
True
|
||||
|
||||
If there are (nearly) multiple singular values, the corresponding
|
||||
individual singular vectors may be unstable, but the whole invariant
|
||||
subspace containing all such singular vectors is computed accurately
|
||||
as can be measured by angles between subspaces via 'subspace_angles'.
|
||||
|
||||
>>> rng = np.random.default_rng(178686584221410808734965903901790843963)
|
||||
>>> s = [1, 1 + 1e-6] # non-zero singular values
|
||||
>>> u, _ = np.linalg.qr(rng.standard_normal((99, 2)))
|
||||
>>> v, _ = np.linalg.qr(rng.standard_normal((99, 2)))
|
||||
>>> vT = v.T
|
||||
>>> A = u @ np.diag(s) @ vT
|
||||
>>> A = A.astype(np.float32)
|
||||
>>> u2, s2, vT2 = svds(A, k=2, random_state=rng)
|
||||
>>> np.allclose(s2, s)
|
||||
True
|
||||
|
||||
The angles between the individual exact and computed singular vectors
|
||||
may not be so small. To check use:
|
||||
|
||||
>>> (linalg.subspace_angles(u2[:, :1], u[:, :1]) +
|
||||
... linalg.subspace_angles(u2[:, 1:], u[:, 1:]))
|
||||
array([0.06562513]) # may vary
|
||||
>>> (linalg.subspace_angles(vT2[:1, :].T, vT[:1, :].T) +
|
||||
... linalg.subspace_angles(vT2[1:, :].T, vT[1:, :].T))
|
||||
array([0.06562507]) # may vary
|
||||
|
||||
As opposed to the angles between the 2-dimensional invariant subspaces
|
||||
that these vectors span, which are small for rights singular vectors
|
||||
|
||||
>>> linalg.subspace_angles(u2, u).sum() < 1e-6
|
||||
True
|
||||
|
||||
as well as for left singular vectors.
|
||||
|
||||
>>> linalg.subspace_angles(vT2.T, vT.T).sum() < 1e-6
|
||||
True
|
||||
|
||||
The next example follows that of 'sklearn.decomposition.TruncatedSVD'.
|
||||
|
||||
>>> rng = np.random.RandomState(0)
|
||||
>>> X_dense = rng.random(size=(100, 100))
|
||||
>>> X_dense[:, 2 * np.arange(50)] = 0
|
||||
>>> X = sparse.csr_matrix(X_dense)
|
||||
>>> _, singular_values, _ = svds(X, k=5, random_state=rng)
|
||||
>>> print(singular_values)
|
||||
[ 4.3293... 4.4491... 4.5420... 4.5987... 35.2410...]
|
||||
|
||||
The function can be called without the transpose of the input matrix
|
||||
ever explicitly constructed.
|
||||
|
||||
>>> rng = np.random.default_rng(102524723947864966825913730119128190974)
|
||||
>>> G = sparse.rand(8, 9, density=0.5, random_state=rng)
|
||||
>>> Glo = aslinearoperator(G)
|
||||
>>> _, singular_values_svds, _ = svds(Glo, k=5, random_state=rng)
|
||||
>>> _, singular_values_svd, _ = linalg.svd(G.toarray())
|
||||
>>> np.allclose(singular_values_svds, singular_values_svd[-4::-1])
|
||||
True
|
||||
|
||||
The most memory efficient scenario is where neither
|
||||
the original matrix, nor its transpose, is explicitly constructed.
|
||||
Our example computes the smallest singular values and vectors
|
||||
of 'LinearOperator' constructed from the numpy function 'np.diff' used
|
||||
column-wise to be consistent with 'LinearOperator' operating on columns.
|
||||
|
||||
>>> diff0 = lambda a: np.diff(a, axis=0)
|
||||
|
||||
Let us create the matrix from 'diff0' to be used for validation only.
|
||||
|
||||
>>> n = 5 # The dimension of the space.
|
||||
>>> M_from_diff0 = diff0(np.eye(n))
|
||||
>>> print(M_from_diff0.astype(int))
|
||||
[[-1 1 0 0 0]
|
||||
[ 0 -1 1 0 0]
|
||||
[ 0 0 -1 1 0]
|
||||
[ 0 0 0 -1 1]]
|
||||
|
||||
The matrix 'M_from_diff0' is bi-diagonal and could be alternatively
|
||||
created directly by
|
||||
|
||||
>>> M = - np.eye(n - 1, n, dtype=int)
|
||||
>>> np.fill_diagonal(M[:,1:], 1)
|
||||
>>> np.allclose(M, M_from_diff0)
|
||||
True
|
||||
|
||||
Its transpose
|
||||
|
||||
>>> print(M.T)
|
||||
[[-1 0 0 0]
|
||||
[ 1 -1 0 0]
|
||||
[ 0 1 -1 0]
|
||||
[ 0 0 1 -1]
|
||||
[ 0 0 0 1]]
|
||||
|
||||
can be viewed as the incidence matrix; see
|
||||
Incidence matrix, (2022, Nov. 19), Wikipedia, https://w.wiki/5YXU,
|
||||
of a linear graph with 5 vertices and 4 edges. The 5x5 normal matrix
|
||||
``M.T @ M`` thus is
|
||||
|
||||
>>> print(M.T @ M)
|
||||
[[ 1 -1 0 0 0]
|
||||
[-1 2 -1 0 0]
|
||||
[ 0 -1 2 -1 0]
|
||||
[ 0 0 -1 2 -1]
|
||||
[ 0 0 0 -1 1]]
|
||||
|
||||
the graph Laplacian, while the actually used in 'svds' smaller size
|
||||
4x4 normal matrix ``M @ M.T``
|
||||
|
||||
>>> print(M @ M.T)
|
||||
[[ 2 -1 0 0]
|
||||
[-1 2 -1 0]
|
||||
[ 0 -1 2 -1]
|
||||
[ 0 0 -1 2]]
|
||||
|
||||
is the so-called edge-based Laplacian; see
|
||||
Symmetric Laplacian via the incidence matrix, in Laplacian matrix,
|
||||
(2022, Nov. 19), Wikipedia, https://w.wiki/5YXW.
|
||||
|
||||
The 'LinearOperator' setup needs the options 'rmatvec' and 'rmatmat'
|
||||
of multiplication by the matrix transpose ``M.T``, but we want to be
|
||||
matrix-free to save memory, so knowing how ``M.T`` looks like, we
|
||||
manually construct the following function to be
|
||||
used in ``rmatmat=diff0t``.
|
||||
|
||||
>>> def diff0t(a):
|
||||
... if a.ndim == 1:
|
||||
... a = a[:,np.newaxis] # Turn 1D into 2D array
|
||||
... d = np.zeros((a.shape[0] + 1, a.shape[1]), dtype=a.dtype)
|
||||
... d[0, :] = - a[0, :]
|
||||
... d[1:-1, :] = a[0:-1, :] - a[1:, :]
|
||||
... d[-1, :] = a[-1, :]
|
||||
... return d
|
||||
|
||||
We check that our function 'diff0t' for the matrix transpose is valid.
|
||||
|
||||
>>> np.allclose(M.T, diff0t(np.eye(n-1)))
|
||||
True
|
||||
|
||||
Now we setup our matrix-free 'LinearOperator' called 'diff0_func_aslo'
|
||||
and for validation the matrix-based 'diff0_matrix_aslo'.
|
||||
|
||||
>>> def diff0_func_aslo_def(n):
|
||||
... return LinearOperator(matvec=diff0,
|
||||
... matmat=diff0,
|
||||
... rmatvec=diff0t,
|
||||
... rmatmat=diff0t,
|
||||
... shape=(n - 1, n))
|
||||
>>> diff0_func_aslo = diff0_func_aslo_def(n)
|
||||
>>> diff0_matrix_aslo = aslinearoperator(M_from_diff0)
|
||||
|
||||
And validate both the matrix and its transpose in 'LinearOperator'.
|
||||
|
||||
>>> np.allclose(diff0_func_aslo(np.eye(n)),
|
||||
... diff0_matrix_aslo(np.eye(n)))
|
||||
True
|
||||
>>> np.allclose(diff0_func_aslo.T(np.eye(n-1)),
|
||||
... diff0_matrix_aslo.T(np.eye(n-1)))
|
||||
True
|
||||
|
||||
Having the 'LinearOperator' setup validated, we run the solver.
|
||||
|
||||
>>> n = 100
|
||||
>>> diff0_func_aslo = diff0_func_aslo_def(n)
|
||||
>>> u, s, vT = svds(diff0_func_aslo, k=3, which='SM')
|
||||
|
||||
The singular values squared and the singular vectors are known
|
||||
explicitly; see
|
||||
Pure Dirichlet boundary conditions, in
|
||||
Eigenvalues and eigenvectors of the second derivative,
|
||||
(2022, Nov. 19), Wikipedia, https://w.wiki/5YX6,
|
||||
since 'diff' corresponds to first
|
||||
derivative, and its smaller size n-1 x n-1 normal matrix
|
||||
``M @ M.T`` represent the discrete second derivative with the Dirichlet
|
||||
boundary conditions. We use these analytic expressions for validation.
|
||||
|
||||
>>> se = 2. * np.sin(np.pi * np.arange(1, 4) / (2. * n))
|
||||
>>> ue = np.sqrt(2 / n) * np.sin(np.pi * np.outer(np.arange(1, n),
|
||||
... np.arange(1, 4)) / n)
|
||||
>>> np.allclose(s, se, atol=1e-3)
|
||||
True
|
||||
>>> print(np.allclose(np.abs(u), np.abs(ue), atol=1e-6))
|
||||
True
|
||||
|
||||
"""
|
||||
args = _iv(A, k, ncv, tol, which, v0, maxiter, return_singular_vectors,
|
||||
solver, random_state)
|
||||
(A, k, ncv, tol, which, v0, maxiter,
|
||||
return_singular_vectors, solver, random_state) = args
|
||||
|
||||
largest = (which == 'LM')
|
||||
n, m = A.shape
|
||||
|
||||
if n >= m:
|
||||
X_dot = A.matvec
|
||||
X_matmat = A.matmat
|
||||
XH_dot = A.rmatvec
|
||||
XH_mat = A.rmatmat
|
||||
transpose = False
|
||||
else:
|
||||
X_dot = A.rmatvec
|
||||
X_matmat = A.rmatmat
|
||||
XH_dot = A.matvec
|
||||
XH_mat = A.matmat
|
||||
transpose = True
|
||||
|
||||
dtype = getattr(A, 'dtype', None)
|
||||
if dtype is None:
|
||||
dtype = A.dot(np.zeros([m, 1])).dtype
|
||||
|
||||
def matvec_XH_X(x):
|
||||
return XH_dot(X_dot(x))
|
||||
|
||||
def matmat_XH_X(x):
|
||||
return XH_mat(X_matmat(x))
|
||||
|
||||
XH_X = LinearOperator(matvec=matvec_XH_X, dtype=A.dtype,
|
||||
matmat=matmat_XH_X,
|
||||
shape=(min(A.shape), min(A.shape)))
|
||||
|
||||
# Get a low rank approximation of the implicitly defined gramian matrix.
|
||||
# This is not a stable way to approach the problem.
|
||||
if solver == 'lobpcg':
|
||||
|
||||
if k == 1 and v0 is not None:
|
||||
X = np.reshape(v0, (-1, 1))
|
||||
else:
|
||||
X = random_state.standard_normal(size=(min(A.shape), k))
|
||||
|
||||
_, eigvec = lobpcg(XH_X, X, tol=tol ** 2, maxiter=maxiter,
|
||||
largest=largest)
|
||||
|
||||
elif solver == 'propack':
|
||||
jobu = return_singular_vectors in {True, 'u'}
|
||||
jobv = return_singular_vectors in {True, 'vh'}
|
||||
irl_mode = (which == 'SM')
|
||||
res = _svdp(A, k=k, tol=tol**2, which=which, maxiter=None,
|
||||
compute_u=jobu, compute_v=jobv, irl_mode=irl_mode,
|
||||
kmax=maxiter, v0=v0, random_state=random_state)
|
||||
|
||||
u, s, vh, _ = res # but we'll ignore bnd, the last output
|
||||
|
||||
# PROPACK order appears to be largest first. `svds` output order is not
|
||||
# guaranteed, according to documentation, but for ARPACK and LOBPCG
|
||||
# they actually are ordered smallest to largest, so reverse for
|
||||
# consistency.
|
||||
s = s[::-1]
|
||||
u = u[:, ::-1]
|
||||
vh = vh[::-1]
|
||||
|
||||
u = u if jobu else None
|
||||
vh = vh if jobv else None
|
||||
|
||||
if return_singular_vectors:
|
||||
return u, s, vh
|
||||
else:
|
||||
return s
|
||||
|
||||
elif solver == 'arpack' or solver is None:
|
||||
if v0 is None:
|
||||
v0 = random_state.standard_normal(size=(min(A.shape),))
|
||||
_, eigvec = eigsh(XH_X, k=k, tol=tol ** 2, maxiter=maxiter,
|
||||
ncv=ncv, which=which, v0=v0)
|
||||
# arpack do not guarantee exactly orthonormal eigenvectors
|
||||
# for clustered eigenvalues, especially in complex arithmetic
|
||||
eigvec, _ = np.linalg.qr(eigvec)
|
||||
|
||||
# the eigenvectors eigvec must be orthonomal here; see gh-16712
|
||||
Av = X_matmat(eigvec)
|
||||
if not return_singular_vectors:
|
||||
s = svd(Av, compute_uv=False, overwrite_a=True)
|
||||
return s[::-1]
|
||||
|
||||
# compute the left singular vectors of X and update the right ones
|
||||
# accordingly
|
||||
u, s, vh = svd(Av, full_matrices=False, overwrite_a=True)
|
||||
u = u[:, ::-1]
|
||||
s = s[::-1]
|
||||
vh = vh[::-1]
|
||||
|
||||
jobu = return_singular_vectors in {True, 'u'}
|
||||
jobv = return_singular_vectors in {True, 'vh'}
|
||||
|
||||
if transpose:
|
||||
u_tmp = eigvec @ _herm(vh) if jobu else None
|
||||
vh = _herm(u) if jobv else None
|
||||
u = u_tmp
|
||||
else:
|
||||
if not jobu:
|
||||
u = None
|
||||
vh = vh @ _herm(eigvec) if jobv else None
|
||||
|
||||
return u, s, vh
|
||||
@ -0,0 +1,400 @@
|
||||
def _svds_arpack_doc(A, k=6, ncv=None, tol=0, which='LM', v0=None,
|
||||
maxiter=None, return_singular_vectors=True,
|
||||
solver='arpack', random_state=None):
|
||||
"""
|
||||
Partial singular value decomposition of a sparse matrix using ARPACK.
|
||||
|
||||
Compute the largest or smallest `k` singular values and corresponding
|
||||
singular vectors of a sparse matrix `A`. The order in which the singular
|
||||
values are returned is not guaranteed.
|
||||
|
||||
In the descriptions below, let ``M, N = A.shape``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : sparse matrix or LinearOperator
|
||||
Matrix to decompose.
|
||||
k : int, optional
|
||||
Number of singular values and singular vectors to compute.
|
||||
Must satisfy ``1 <= k <= min(M, N) - 1``.
|
||||
Default is 6.
|
||||
ncv : int, optional
|
||||
The number of Lanczos vectors generated.
|
||||
The default is ``min(n, max(2*k + 1, 20))``.
|
||||
If specified, must satistify ``k + 1 < ncv < min(M, N)``; ``ncv > 2*k``
|
||||
is recommended.
|
||||
tol : float, optional
|
||||
Tolerance for singular values. Zero (default) means machine precision.
|
||||
which : {'LM', 'SM'}
|
||||
Which `k` singular values to find: either the largest magnitude ('LM')
|
||||
or smallest magnitude ('SM') singular values.
|
||||
v0 : ndarray, optional
|
||||
The starting vector for iteration:
|
||||
an (approximate) left singular vector if ``N > M`` and a right singular
|
||||
vector otherwise. Must be of length ``min(M, N)``.
|
||||
Default: random
|
||||
maxiter : int, optional
|
||||
Maximum number of Arnoldi update iterations allowed;
|
||||
default is ``min(M, N) * 10``.
|
||||
return_singular_vectors : {True, False, "u", "vh"}
|
||||
Singular values are always computed and returned; this parameter
|
||||
controls the computation and return of singular vectors.
|
||||
|
||||
- ``True``: return singular vectors.
|
||||
- ``False``: do not return singular vectors.
|
||||
- ``"u"``: if ``M <= N``, compute only the left singular vectors and
|
||||
return ``None`` for the right singular vectors. Otherwise, compute
|
||||
all singular vectors.
|
||||
- ``"vh"``: if ``M > N``, compute only the right singular vectors and
|
||||
return ``None`` for the left singular vectors. Otherwise, compute
|
||||
all singular vectors.
|
||||
|
||||
solver : {'arpack', 'propack', 'lobpcg'}, optional
|
||||
This is the solver-specific documentation for ``solver='arpack'``.
|
||||
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>` and
|
||||
:ref:`'propack' <sparse.linalg.svds-propack>`
|
||||
are also supported.
|
||||
random_state : {None, int, `numpy.random.Generator`,
|
||||
`numpy.random.RandomState`}, optional
|
||||
|
||||
Pseudorandom number generator state used to generate resamples.
|
||||
|
||||
If `random_state` is ``None`` (or `np.random`), the
|
||||
`numpy.random.RandomState` singleton is used.
|
||||
If `random_state` is an int, a new ``RandomState`` instance is used,
|
||||
seeded with `random_state`.
|
||||
If `random_state` is already a ``Generator`` or ``RandomState``
|
||||
instance then that instance is used.
|
||||
options : dict, optional
|
||||
A dictionary of solver-specific options. No solver-specific options
|
||||
are currently supported; this parameter is reserved for future use.
|
||||
|
||||
Returns
|
||||
-------
|
||||
u : ndarray, shape=(M, k)
|
||||
Unitary matrix having left singular vectors as columns.
|
||||
s : ndarray, shape=(k,)
|
||||
The singular values.
|
||||
vh : ndarray, shape=(k, N)
|
||||
Unitary matrix having right singular vectors as rows.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a naive implementation using ARPACK as an eigensolver
|
||||
on ``A.conj().T @ A`` or ``A @ A.conj().T``, depending on which one is more
|
||||
efficient.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Construct a matrix ``A`` from singular values and vectors.
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.stats import ortho_group
|
||||
>>> from scipy.sparse import csc_matrix, diags
|
||||
>>> from scipy.sparse.linalg import svds
|
||||
>>> rng = np.random.default_rng()
|
||||
>>> orthogonal = csc_matrix(ortho_group.rvs(10, random_state=rng))
|
||||
>>> s = [0.0001, 0.001, 3, 4, 5] # singular values
|
||||
>>> u = orthogonal[:, :5] # left singular vectors
|
||||
>>> vT = orthogonal[:, 5:].T # right singular vectors
|
||||
>>> A = u @ diags(s) @ vT
|
||||
|
||||
With only three singular values/vectors, the SVD approximates the original
|
||||
matrix.
|
||||
|
||||
>>> u2, s2, vT2 = svds(A, k=3, solver='arpack')
|
||||
>>> A2 = u2 @ np.diag(s2) @ vT2
|
||||
>>> np.allclose(A2, A.toarray(), atol=1e-3)
|
||||
True
|
||||
|
||||
With all five singular values/vectors, we can reproduce the original
|
||||
matrix.
|
||||
|
||||
>>> u3, s3, vT3 = svds(A, k=5, solver='arpack')
|
||||
>>> A3 = u3 @ np.diag(s3) @ vT3
|
||||
>>> np.allclose(A3, A.toarray())
|
||||
True
|
||||
|
||||
The singular values match the expected singular values, and the singular
|
||||
vectors are as expected up to a difference in sign.
|
||||
|
||||
>>> (np.allclose(s3, s) and
|
||||
... np.allclose(np.abs(u3), np.abs(u.toarray())) and
|
||||
... np.allclose(np.abs(vT3), np.abs(vT.toarray())))
|
||||
True
|
||||
|
||||
The singular vectors are also orthogonal.
|
||||
|
||||
>>> (np.allclose(u3.T @ u3, np.eye(5)) and
|
||||
... np.allclose(vT3 @ vT3.T, np.eye(5)))
|
||||
True
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def _svds_lobpcg_doc(A, k=6, ncv=None, tol=0, which='LM', v0=None,
|
||||
maxiter=None, return_singular_vectors=True,
|
||||
solver='lobpcg', random_state=None):
|
||||
"""
|
||||
Partial singular value decomposition of a sparse matrix using LOBPCG.
|
||||
|
||||
Compute the largest or smallest `k` singular values and corresponding
|
||||
singular vectors of a sparse matrix `A`. The order in which the singular
|
||||
values are returned is not guaranteed.
|
||||
|
||||
In the descriptions below, let ``M, N = A.shape``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : sparse matrix or LinearOperator
|
||||
Matrix to decompose.
|
||||
k : int, default: 6
|
||||
Number of singular values and singular vectors to compute.
|
||||
Must satisfy ``1 <= k <= min(M, N) - 1``.
|
||||
ncv : int, optional
|
||||
Ignored.
|
||||
tol : float, optional
|
||||
Tolerance for singular values. Zero (default) means machine precision.
|
||||
which : {'LM', 'SM'}
|
||||
Which `k` singular values to find: either the largest magnitude ('LM')
|
||||
or smallest magnitude ('SM') singular values.
|
||||
v0 : ndarray, optional
|
||||
If `k` is 1, the starting vector for iteration:
|
||||
an (approximate) left singular vector if ``N > M`` and a right singular
|
||||
vector otherwise. Must be of length ``min(M, N)``.
|
||||
Ignored otherwise.
|
||||
Default: random
|
||||
maxiter : int, default: 20
|
||||
Maximum number of iterations.
|
||||
return_singular_vectors : {True, False, "u", "vh"}
|
||||
Singular values are always computed and returned; this parameter
|
||||
controls the computation and return of singular vectors.
|
||||
|
||||
- ``True``: return singular vectors.
|
||||
- ``False``: do not return singular vectors.
|
||||
- ``"u"``: if ``M <= N``, compute only the left singular vectors and
|
||||
return ``None`` for the right singular vectors. Otherwise, compute
|
||||
all singular vectors.
|
||||
- ``"vh"``: if ``M > N``, compute only the right singular vectors and
|
||||
return ``None`` for the left singular vectors. Otherwise, compute
|
||||
all singular vectors.
|
||||
|
||||
solver : {'arpack', 'propack', 'lobpcg'}, optional
|
||||
This is the solver-specific documentation for ``solver='lobpcg'``.
|
||||
:ref:`'arpack' <sparse.linalg.svds-arpack>` and
|
||||
:ref:`'propack' <sparse.linalg.svds-propack>`
|
||||
are also supported.
|
||||
random_state : {None, int, `numpy.random.Generator`,
|
||||
`numpy.random.RandomState`}, optional
|
||||
|
||||
Pseudorandom number generator state used to generate resamples.
|
||||
|
||||
If `random_state` is ``None`` (or `np.random`), the
|
||||
`numpy.random.RandomState` singleton is used.
|
||||
If `random_state` is an int, a new ``RandomState`` instance is used,
|
||||
seeded with `random_state`.
|
||||
If `random_state` is already a ``Generator`` or ``RandomState``
|
||||
instance then that instance is used.
|
||||
options : dict, optional
|
||||
A dictionary of solver-specific options. No solver-specific options
|
||||
are currently supported; this parameter is reserved for future use.
|
||||
|
||||
Returns
|
||||
-------
|
||||
u : ndarray, shape=(M, k)
|
||||
Unitary matrix having left singular vectors as columns.
|
||||
s : ndarray, shape=(k,)
|
||||
The singular values.
|
||||
vh : ndarray, shape=(k, N)
|
||||
Unitary matrix having right singular vectors as rows.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a naive implementation using LOBPCG as an eigensolver
|
||||
on ``A.conj().T @ A`` or ``A @ A.conj().T``, depending on which one is more
|
||||
efficient.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Construct a matrix ``A`` from singular values and vectors.
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.stats import ortho_group
|
||||
>>> from scipy.sparse import csc_matrix, diags
|
||||
>>> from scipy.sparse.linalg import svds
|
||||
>>> rng = np.random.default_rng()
|
||||
>>> orthogonal = csc_matrix(ortho_group.rvs(10, random_state=rng))
|
||||
>>> s = [0.0001, 0.001, 3, 4, 5] # singular values
|
||||
>>> u = orthogonal[:, :5] # left singular vectors
|
||||
>>> vT = orthogonal[:, 5:].T # right singular vectors
|
||||
>>> A = u @ diags(s) @ vT
|
||||
|
||||
With only three singular values/vectors, the SVD approximates the original
|
||||
matrix.
|
||||
|
||||
>>> u2, s2, vT2 = svds(A, k=3, solver='lobpcg')
|
||||
>>> A2 = u2 @ np.diag(s2) @ vT2
|
||||
>>> np.allclose(A2, A.toarray(), atol=1e-3)
|
||||
True
|
||||
|
||||
With all five singular values/vectors, we can reproduce the original
|
||||
matrix.
|
||||
|
||||
>>> u3, s3, vT3 = svds(A, k=5, solver='lobpcg')
|
||||
>>> A3 = u3 @ np.diag(s3) @ vT3
|
||||
>>> np.allclose(A3, A.toarray())
|
||||
True
|
||||
|
||||
The singular values match the expected singular values, and the singular
|
||||
vectors are as expected up to a difference in sign.
|
||||
|
||||
>>> (np.allclose(s3, s) and
|
||||
... np.allclose(np.abs(u3), np.abs(u.todense())) and
|
||||
... np.allclose(np.abs(vT3), np.abs(vT.todense())))
|
||||
True
|
||||
|
||||
The singular vectors are also orthogonal.
|
||||
|
||||
>>> (np.allclose(u3.T @ u3, np.eye(5)) and
|
||||
... np.allclose(vT3 @ vT3.T, np.eye(5)))
|
||||
True
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def _svds_propack_doc(A, k=6, ncv=None, tol=0, which='LM', v0=None,
|
||||
maxiter=None, return_singular_vectors=True,
|
||||
solver='propack', random_state=None):
|
||||
"""
|
||||
Partial singular value decomposition of a sparse matrix using PROPACK.
|
||||
|
||||
Compute the largest or smallest `k` singular values and corresponding
|
||||
singular vectors of a sparse matrix `A`. The order in which the singular
|
||||
values are returned is not guaranteed.
|
||||
|
||||
In the descriptions below, let ``M, N = A.shape``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : sparse matrix or LinearOperator
|
||||
Matrix to decompose. If `A` is a ``LinearOperator``
|
||||
object, it must define both ``matvec`` and ``rmatvec`` methods.
|
||||
k : int, default: 6
|
||||
Number of singular values and singular vectors to compute.
|
||||
Must satisfy ``1 <= k <= min(M, N)``.
|
||||
ncv : int, optional
|
||||
Ignored.
|
||||
tol : float, optional
|
||||
The desired relative accuracy for computed singular values.
|
||||
Zero (default) means machine precision.
|
||||
which : {'LM', 'SM'}
|
||||
Which `k` singular values to find: either the largest magnitude ('LM')
|
||||
or smallest magnitude ('SM') singular values. Note that choosing
|
||||
``which='SM'`` will force the ``irl`` option to be set ``True``.
|
||||
v0 : ndarray, optional
|
||||
Starting vector for iterations: must be of length ``A.shape[0]``.
|
||||
If not specified, PROPACK will generate a starting vector.
|
||||
maxiter : int, optional
|
||||
Maximum number of iterations / maximal dimension of the Krylov
|
||||
subspace. Default is ``10 * k``.
|
||||
return_singular_vectors : {True, False, "u", "vh"}
|
||||
Singular values are always computed and returned; this parameter
|
||||
controls the computation and return of singular vectors.
|
||||
|
||||
- ``True``: return singular vectors.
|
||||
- ``False``: do not return singular vectors.
|
||||
- ``"u"``: compute only the left singular vectors; return ``None`` for
|
||||
the right singular vectors.
|
||||
- ``"vh"``: compute only the right singular vectors; return ``None``
|
||||
for the left singular vectors.
|
||||
|
||||
solver : {'arpack', 'propack', 'lobpcg'}, optional
|
||||
This is the solver-specific documentation for ``solver='propack'``.
|
||||
:ref:`'arpack' <sparse.linalg.svds-arpack>` and
|
||||
:ref:`'lobpcg' <sparse.linalg.svds-lobpcg>`
|
||||
are also supported.
|
||||
random_state : {None, int, `numpy.random.Generator`,
|
||||
`numpy.random.RandomState`}, optional
|
||||
|
||||
Pseudorandom number generator state used to generate resamples.
|
||||
|
||||
If `random_state` is ``None`` (or `np.random`), the
|
||||
`numpy.random.RandomState` singleton is used.
|
||||
If `random_state` is an int, a new ``RandomState`` instance is used,
|
||||
seeded with `random_state`.
|
||||
If `random_state` is already a ``Generator`` or ``RandomState``
|
||||
instance then that instance is used.
|
||||
options : dict, optional
|
||||
A dictionary of solver-specific options. No solver-specific options
|
||||
are currently supported; this parameter is reserved for future use.
|
||||
|
||||
Returns
|
||||
-------
|
||||
u : ndarray, shape=(M, k)
|
||||
Unitary matrix having left singular vectors as columns.
|
||||
s : ndarray, shape=(k,)
|
||||
The singular values.
|
||||
vh : ndarray, shape=(k, N)
|
||||
Unitary matrix having right singular vectors as rows.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is an interface to the Fortran library PROPACK [1]_.
|
||||
The current default is to run with IRL mode disabled unless seeking the
|
||||
smallest singular values/vectors (``which='SM'``).
|
||||
|
||||
References
|
||||
----------
|
||||
|
||||
.. [1] Larsen, Rasmus Munk. "PROPACK-Software for large and sparse SVD
|
||||
calculations." Available online. URL
|
||||
http://sun.stanford.edu/~rmunk/PROPACK (2004): 2008-2009.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Construct a matrix ``A`` from singular values and vectors.
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.stats import ortho_group
|
||||
>>> from scipy.sparse import csc_matrix, diags
|
||||
>>> from scipy.sparse.linalg import svds
|
||||
>>> rng = np.random.default_rng()
|
||||
>>> orthogonal = csc_matrix(ortho_group.rvs(10, random_state=rng))
|
||||
>>> s = [0.0001, 0.001, 3, 4, 5] # singular values
|
||||
>>> u = orthogonal[:, :5] # left singular vectors
|
||||
>>> vT = orthogonal[:, 5:].T # right singular vectors
|
||||
>>> A = u @ diags(s) @ vT
|
||||
|
||||
With only three singular values/vectors, the SVD approximates the original
|
||||
matrix.
|
||||
|
||||
>>> u2, s2, vT2 = svds(A, k=3, solver='propack')
|
||||
>>> A2 = u2 @ np.diag(s2) @ vT2
|
||||
>>> np.allclose(A2, A.todense(), atol=1e-3)
|
||||
True
|
||||
|
||||
With all five singular values/vectors, we can reproduce the original
|
||||
matrix.
|
||||
|
||||
>>> u3, s3, vT3 = svds(A, k=5, solver='propack')
|
||||
>>> A3 = u3 @ np.diag(s3) @ vT3
|
||||
>>> np.allclose(A3, A.todense())
|
||||
True
|
||||
|
||||
The singular values match the expected singular values, and the singular
|
||||
vectors are as expected up to a difference in sign.
|
||||
|
||||
>>> (np.allclose(s3, s) and
|
||||
... np.allclose(np.abs(u3), np.abs(u.toarray())) and
|
||||
... np.allclose(np.abs(vT3), np.abs(vT.toarray())))
|
||||
True
|
||||
|
||||
The singular vectors are also orthogonal.
|
||||
|
||||
>>> (np.allclose(u3.T @ u3, np.eye(5)) and
|
||||
... np.allclose(vT3 @ vT3.T, np.eye(5)))
|
||||
True
|
||||
|
||||
"""
|
||||
pass
|
||||
@ -0,0 +1,45 @@
|
||||
|
||||
BSD Software License
|
||||
|
||||
Pertains to ARPACK and P_ARPACK
|
||||
|
||||
Copyright (c) 1996-2008 Rice University.
|
||||
Developed by D.C. Sorensen, R.B. Lehoucq, C. Yang, and K. Maschhoff.
|
||||
All rights reserved.
|
||||
|
||||
Arpack has been renamed to arpack-ng.
|
||||
|
||||
Copyright (c) 2001-2011 - Scilab Enterprises
|
||||
Updated by Allan Cornet, Sylvestre Ledru.
|
||||
|
||||
Copyright (c) 2010 - Jordi Gutiérrez Hermoso (Octave patch)
|
||||
|
||||
Copyright (c) 2007 - Sébastien Fabbro (gentoo patch)
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer listed
|
||||
in this license in the documentation and/or other materials
|
||||
provided with the distribution.
|
||||
|
||||
- Neither the name of the copyright holders nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@ -0,0 +1,20 @@
|
||||
"""
|
||||
Eigenvalue solver using iterative methods.
|
||||
|
||||
Find k eigenvectors and eigenvalues of a matrix A using the
|
||||
Arnoldi/Lanczos iterative methods from ARPACK [1]_,[2]_.
|
||||
|
||||
These methods are most useful for large sparse matrices.
|
||||
|
||||
- eigs(A,k)
|
||||
- eigsh(A,k)
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] ARPACK Software, http://www.caam.rice.edu/software/ARPACK/
|
||||
.. [2] R. B. Lehoucq, D. C. Sorensen, and C. Yang, ARPACK USERS GUIDE:
|
||||
Solution of Large Scale Eigenvalue Problems by Implicitly Restarted
|
||||
Arnoldi Methods. SIAM, Philadelphia, PA, 1998.
|
||||
|
||||
"""
|
||||
from .arpack import *
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,718 @@
|
||||
__usage__ = """
|
||||
To run tests locally:
|
||||
python tests/test_arpack.py [-l<int>] [-v<int>]
|
||||
|
||||
"""
|
||||
|
||||
import threading
|
||||
import itertools
|
||||
|
||||
import numpy as np
|
||||
|
||||
from numpy.testing import assert_allclose, assert_equal, suppress_warnings
|
||||
from pytest import raises as assert_raises
|
||||
import pytest
|
||||
|
||||
from numpy import dot, conj, random
|
||||
from scipy.linalg import eig, eigh
|
||||
from scipy.sparse import csc_matrix, csr_matrix, diags, rand
|
||||
from scipy.sparse.linalg import LinearOperator, aslinearoperator
|
||||
from scipy.sparse.linalg._eigen.arpack import (eigs, eigsh, arpack,
|
||||
ArpackNoConvergence)
|
||||
|
||||
|
||||
from scipy._lib._gcutils import assert_deallocated, IS_PYPY
|
||||
|
||||
|
||||
# precision for tests
|
||||
_ndigits = {'f': 3, 'd': 11, 'F': 3, 'D': 11}
|
||||
|
||||
|
||||
def _get_test_tolerance(type_char, mattype=None, D_type=None, which=None):
|
||||
"""
|
||||
Return tolerance values suitable for a given test:
|
||||
|
||||
Parameters
|
||||
----------
|
||||
type_char : {'f', 'd', 'F', 'D'}
|
||||
Data type in ARPACK eigenvalue problem
|
||||
mattype : {csr_matrix, aslinearoperator, asarray}, optional
|
||||
Linear operator type
|
||||
|
||||
Returns
|
||||
-------
|
||||
tol
|
||||
Tolerance to pass to the ARPACK routine
|
||||
rtol
|
||||
Relative tolerance for outputs
|
||||
atol
|
||||
Absolute tolerance for outputs
|
||||
|
||||
"""
|
||||
|
||||
rtol = {'f': 3000 * np.finfo(np.float32).eps,
|
||||
'F': 3000 * np.finfo(np.float32).eps,
|
||||
'd': 2000 * np.finfo(np.float64).eps,
|
||||
'D': 2000 * np.finfo(np.float64).eps}[type_char]
|
||||
atol = rtol
|
||||
tol = 0
|
||||
|
||||
if mattype is aslinearoperator and type_char in ('f', 'F'):
|
||||
# iterative methods in single precision: worse errors
|
||||
# also: bump ARPACK tolerance so that the iterative method converges
|
||||
tol = 30 * np.finfo(np.float32).eps
|
||||
rtol *= 5
|
||||
|
||||
if mattype is csr_matrix and type_char in ('f', 'F'):
|
||||
# sparse in single precision: worse errors
|
||||
rtol *= 5
|
||||
|
||||
if (
|
||||
which in ('LM', 'SM', 'LA')
|
||||
and D_type.name == "gen-hermitian-Mc"
|
||||
):
|
||||
if type_char == 'F':
|
||||
# missing case 1, 2, and more, from PR 14798
|
||||
rtol *= 5
|
||||
|
||||
if type_char == 'D':
|
||||
# missing more cases, from PR 14798
|
||||
rtol *= 10
|
||||
atol *= 10
|
||||
|
||||
return tol, rtol, atol
|
||||
|
||||
|
||||
def generate_matrix(N, complex_=False, hermitian=False,
|
||||
pos_definite=False, sparse=False):
|
||||
M = np.random.random((N, N))
|
||||
if complex_:
|
||||
M = M + 1j * np.random.random((N, N))
|
||||
|
||||
if hermitian:
|
||||
if pos_definite:
|
||||
if sparse:
|
||||
i = np.arange(N)
|
||||
j = np.random.randint(N, size=N-2)
|
||||
i, j = np.meshgrid(i, j)
|
||||
M[i, j] = 0
|
||||
M = np.dot(M.conj(), M.T)
|
||||
else:
|
||||
M = np.dot(M.conj(), M.T)
|
||||
if sparse:
|
||||
i = np.random.randint(N, size=N * N // 4)
|
||||
j = np.random.randint(N, size=N * N // 4)
|
||||
ind = np.nonzero(i == j)
|
||||
j[ind] = (j[ind] + 1) % N
|
||||
M[i, j] = 0
|
||||
M[j, i] = 0
|
||||
else:
|
||||
if sparse:
|
||||
i = np.random.randint(N, size=N * N // 2)
|
||||
j = np.random.randint(N, size=N * N // 2)
|
||||
M[i, j] = 0
|
||||
return M
|
||||
|
||||
|
||||
def generate_matrix_symmetric(N, pos_definite=False, sparse=False):
|
||||
M = np.random.random((N, N))
|
||||
|
||||
M = 0.5 * (M + M.T) # Make M symmetric
|
||||
|
||||
if pos_definite:
|
||||
Id = N * np.eye(N)
|
||||
if sparse:
|
||||
M = csr_matrix(M)
|
||||
M += Id
|
||||
else:
|
||||
if sparse:
|
||||
M = csr_matrix(M)
|
||||
|
||||
return M
|
||||
|
||||
|
||||
def assert_allclose_cc(actual, desired, **kw):
|
||||
"""Almost equal or complex conjugates almost equal"""
|
||||
try:
|
||||
assert_allclose(actual, desired, **kw)
|
||||
except AssertionError:
|
||||
assert_allclose(actual, conj(desired), **kw)
|
||||
|
||||
|
||||
def argsort_which(eigenvalues, typ, k, which,
|
||||
sigma=None, OPpart=None, mode=None):
|
||||
"""Return sorted indices of eigenvalues using the "which" keyword
|
||||
from eigs and eigsh"""
|
||||
if sigma is None:
|
||||
reval = np.round(eigenvalues, decimals=_ndigits[typ])
|
||||
else:
|
||||
if mode is None or mode == 'normal':
|
||||
if OPpart is None:
|
||||
reval = 1. / (eigenvalues - sigma)
|
||||
elif OPpart == 'r':
|
||||
reval = 0.5 * (1. / (eigenvalues - sigma)
|
||||
+ 1. / (eigenvalues - np.conj(sigma)))
|
||||
elif OPpart == 'i':
|
||||
reval = -0.5j * (1. / (eigenvalues - sigma)
|
||||
- 1. / (eigenvalues - np.conj(sigma)))
|
||||
elif mode == 'cayley':
|
||||
reval = (eigenvalues + sigma) / (eigenvalues - sigma)
|
||||
elif mode == 'buckling':
|
||||
reval = eigenvalues / (eigenvalues - sigma)
|
||||
else:
|
||||
raise ValueError("mode='%s' not recognized" % mode)
|
||||
|
||||
reval = np.round(reval, decimals=_ndigits[typ])
|
||||
|
||||
if which in ['LM', 'SM']:
|
||||
ind = np.argsort(abs(reval))
|
||||
elif which in ['LR', 'SR', 'LA', 'SA', 'BE']:
|
||||
ind = np.argsort(np.real(reval))
|
||||
elif which in ['LI', 'SI']:
|
||||
# for LI,SI ARPACK returns largest,smallest abs(imaginary) why?
|
||||
if typ.islower():
|
||||
ind = np.argsort(abs(np.imag(reval)))
|
||||
else:
|
||||
ind = np.argsort(np.imag(reval))
|
||||
else:
|
||||
raise ValueError("which='%s' is unrecognized" % which)
|
||||
|
||||
if which in ['LM', 'LA', 'LR', 'LI']:
|
||||
return ind[-k:]
|
||||
elif which in ['SM', 'SA', 'SR', 'SI']:
|
||||
return ind[:k]
|
||||
elif which == 'BE':
|
||||
return np.concatenate((ind[:k//2], ind[k//2-k:]))
|
||||
|
||||
|
||||
def eval_evec(symmetric, d, typ, k, which, v0=None, sigma=None,
|
||||
mattype=np.asarray, OPpart=None, mode='normal'):
|
||||
general = ('bmat' in d)
|
||||
|
||||
if symmetric:
|
||||
eigs_func = eigsh
|
||||
else:
|
||||
eigs_func = eigs
|
||||
|
||||
if general:
|
||||
err = ("error for {}:general, typ={}, which={}, sigma={}, "
|
||||
"mattype={}, OPpart={}, mode={}".format(eigs_func.__name__,
|
||||
typ, which, sigma,
|
||||
mattype.__name__,
|
||||
OPpart, mode))
|
||||
else:
|
||||
err = ("error for {}:standard, typ={}, which={}, sigma={}, "
|
||||
"mattype={}, OPpart={}, mode={}".format(eigs_func.__name__,
|
||||
typ, which, sigma,
|
||||
mattype.__name__,
|
||||
OPpart, mode))
|
||||
|
||||
a = d['mat'].astype(typ)
|
||||
ac = mattype(a)
|
||||
|
||||
if general:
|
||||
b = d['bmat'].astype(typ)
|
||||
bc = mattype(b)
|
||||
|
||||
# get exact eigenvalues
|
||||
exact_eval = d['eval'].astype(typ.upper())
|
||||
ind = argsort_which(exact_eval, typ, k, which,
|
||||
sigma, OPpart, mode)
|
||||
exact_eval = exact_eval[ind]
|
||||
|
||||
# compute arpack eigenvalues
|
||||
kwargs = dict(which=which, v0=v0, sigma=sigma)
|
||||
if eigs_func is eigsh:
|
||||
kwargs['mode'] = mode
|
||||
else:
|
||||
kwargs['OPpart'] = OPpart
|
||||
|
||||
# compute suitable tolerances
|
||||
kwargs['tol'], rtol, atol = _get_test_tolerance(typ, mattype, d, which)
|
||||
# on rare occasions, ARPACK routines return results that are proper
|
||||
# eigenvalues and -vectors, but not necessarily the ones requested in
|
||||
# the parameter which. This is inherent to the Krylov methods, and
|
||||
# should not be treated as a failure. If such a rare situation
|
||||
# occurs, the calculation is tried again (but at most a few times).
|
||||
ntries = 0
|
||||
while ntries < 5:
|
||||
# solve
|
||||
if general:
|
||||
try:
|
||||
eigenvalues, evec = eigs_func(ac, k, bc, **kwargs)
|
||||
except ArpackNoConvergence:
|
||||
kwargs['maxiter'] = 20*a.shape[0]
|
||||
eigenvalues, evec = eigs_func(ac, k, bc, **kwargs)
|
||||
else:
|
||||
try:
|
||||
eigenvalues, evec = eigs_func(ac, k, **kwargs)
|
||||
except ArpackNoConvergence:
|
||||
kwargs['maxiter'] = 20*a.shape[0]
|
||||
eigenvalues, evec = eigs_func(ac, k, **kwargs)
|
||||
|
||||
ind = argsort_which(eigenvalues, typ, k, which,
|
||||
sigma, OPpart, mode)
|
||||
eigenvalues = eigenvalues[ind]
|
||||
evec = evec[:, ind]
|
||||
|
||||
try:
|
||||
# check eigenvalues
|
||||
assert_allclose_cc(eigenvalues, exact_eval, rtol=rtol, atol=atol,
|
||||
err_msg=err)
|
||||
check_evecs = True
|
||||
except AssertionError:
|
||||
check_evecs = False
|
||||
ntries += 1
|
||||
|
||||
if check_evecs:
|
||||
# check eigenvectors
|
||||
LHS = np.dot(a, evec)
|
||||
if general:
|
||||
RHS = eigenvalues * np.dot(b, evec)
|
||||
else:
|
||||
RHS = eigenvalues * evec
|
||||
|
||||
assert_allclose(LHS, RHS, rtol=rtol, atol=atol, err_msg=err)
|
||||
break
|
||||
|
||||
# check eigenvalues
|
||||
assert_allclose_cc(eigenvalues, exact_eval, rtol=rtol, atol=atol, err_msg=err)
|
||||
|
||||
|
||||
class DictWithRepr(dict):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s>" % self.name
|
||||
|
||||
|
||||
class SymmetricParams:
|
||||
def __init__(self):
|
||||
self.eigs = eigsh
|
||||
self.which = ['LM', 'SM', 'LA', 'SA', 'BE']
|
||||
self.mattypes = [csr_matrix, aslinearoperator, np.asarray]
|
||||
self.sigmas_modes = {None: ['normal'],
|
||||
0.5: ['normal', 'buckling', 'cayley']}
|
||||
|
||||
# generate matrices
|
||||
# these should all be float32 so that the eigenvalues
|
||||
# are the same in float32 and float64
|
||||
N = 6
|
||||
np.random.seed(2300)
|
||||
Ar = generate_matrix(N, hermitian=True,
|
||||
pos_definite=True).astype('f').astype('d')
|
||||
M = generate_matrix(N, hermitian=True,
|
||||
pos_definite=True).astype('f').astype('d')
|
||||
Ac = generate_matrix(N, hermitian=True, pos_definite=True,
|
||||
complex_=True).astype('F').astype('D')
|
||||
Mc = generate_matrix(N, hermitian=True, pos_definite=True,
|
||||
complex_=True).astype('F').astype('D')
|
||||
v0 = np.random.random(N)
|
||||
|
||||
# standard symmetric problem
|
||||
SS = DictWithRepr("std-symmetric")
|
||||
SS['mat'] = Ar
|
||||
SS['v0'] = v0
|
||||
SS['eval'] = eigh(SS['mat'], eigvals_only=True)
|
||||
|
||||
# general symmetric problem
|
||||
GS = DictWithRepr("gen-symmetric")
|
||||
GS['mat'] = Ar
|
||||
GS['bmat'] = M
|
||||
GS['v0'] = v0
|
||||
GS['eval'] = eigh(GS['mat'], GS['bmat'], eigvals_only=True)
|
||||
|
||||
# standard hermitian problem
|
||||
SH = DictWithRepr("std-hermitian")
|
||||
SH['mat'] = Ac
|
||||
SH['v0'] = v0
|
||||
SH['eval'] = eigh(SH['mat'], eigvals_only=True)
|
||||
|
||||
# general hermitian problem
|
||||
GH = DictWithRepr("gen-hermitian")
|
||||
GH['mat'] = Ac
|
||||
GH['bmat'] = M
|
||||
GH['v0'] = v0
|
||||
GH['eval'] = eigh(GH['mat'], GH['bmat'], eigvals_only=True)
|
||||
|
||||
# general hermitian problem with hermitian M
|
||||
GHc = DictWithRepr("gen-hermitian-Mc")
|
||||
GHc['mat'] = Ac
|
||||
GHc['bmat'] = Mc
|
||||
GHc['v0'] = v0
|
||||
GHc['eval'] = eigh(GHc['mat'], GHc['bmat'], eigvals_only=True)
|
||||
|
||||
self.real_test_cases = [SS, GS]
|
||||
self.complex_test_cases = [SH, GH, GHc]
|
||||
|
||||
|
||||
class NonSymmetricParams:
|
||||
def __init__(self):
|
||||
self.eigs = eigs
|
||||
self.which = ['LM', 'LR', 'LI'] # , 'SM', 'LR', 'SR', 'LI', 'SI']
|
||||
self.mattypes = [csr_matrix, aslinearoperator, np.asarray]
|
||||
self.sigmas_OPparts = {None: [None],
|
||||
0.1: ['r'],
|
||||
0.1 + 0.1j: ['r', 'i']}
|
||||
|
||||
# generate matrices
|
||||
# these should all be float32 so that the eigenvalues
|
||||
# are the same in float32 and float64
|
||||
N = 6
|
||||
np.random.seed(2300)
|
||||
Ar = generate_matrix(N).astype('f').astype('d')
|
||||
M = generate_matrix(N, hermitian=True,
|
||||
pos_definite=True).astype('f').astype('d')
|
||||
Ac = generate_matrix(N, complex_=True).astype('F').astype('D')
|
||||
v0 = np.random.random(N)
|
||||
|
||||
# standard real nonsymmetric problem
|
||||
SNR = DictWithRepr("std-real-nonsym")
|
||||
SNR['mat'] = Ar
|
||||
SNR['v0'] = v0
|
||||
SNR['eval'] = eig(SNR['mat'], left=False, right=False)
|
||||
|
||||
# general real nonsymmetric problem
|
||||
GNR = DictWithRepr("gen-real-nonsym")
|
||||
GNR['mat'] = Ar
|
||||
GNR['bmat'] = M
|
||||
GNR['v0'] = v0
|
||||
GNR['eval'] = eig(GNR['mat'], GNR['bmat'], left=False, right=False)
|
||||
|
||||
# standard complex nonsymmetric problem
|
||||
SNC = DictWithRepr("std-cmplx-nonsym")
|
||||
SNC['mat'] = Ac
|
||||
SNC['v0'] = v0
|
||||
SNC['eval'] = eig(SNC['mat'], left=False, right=False)
|
||||
|
||||
# general complex nonsymmetric problem
|
||||
GNC = DictWithRepr("gen-cmplx-nonsym")
|
||||
GNC['mat'] = Ac
|
||||
GNC['bmat'] = M
|
||||
GNC['v0'] = v0
|
||||
GNC['eval'] = eig(GNC['mat'], GNC['bmat'], left=False, right=False)
|
||||
|
||||
self.real_test_cases = [SNR, GNR]
|
||||
self.complex_test_cases = [SNC, GNC]
|
||||
|
||||
|
||||
def test_symmetric_modes():
|
||||
params = SymmetricParams()
|
||||
k = 2
|
||||
symmetric = True
|
||||
for D in params.real_test_cases:
|
||||
for typ in 'fd':
|
||||
for which in params.which:
|
||||
for mattype in params.mattypes:
|
||||
for (sigma, modes) in params.sigmas_modes.items():
|
||||
for mode in modes:
|
||||
eval_evec(symmetric, D, typ, k, which,
|
||||
None, sigma, mattype, None, mode)
|
||||
|
||||
|
||||
def test_hermitian_modes():
|
||||
params = SymmetricParams()
|
||||
k = 2
|
||||
symmetric = True
|
||||
for D in params.complex_test_cases:
|
||||
for typ in 'FD':
|
||||
for which in params.which:
|
||||
if which == 'BE':
|
||||
continue # BE invalid for complex
|
||||
for mattype in params.mattypes:
|
||||
for sigma in params.sigmas_modes:
|
||||
eval_evec(symmetric, D, typ, k, which,
|
||||
None, sigma, mattype)
|
||||
|
||||
|
||||
def test_symmetric_starting_vector():
|
||||
params = SymmetricParams()
|
||||
symmetric = True
|
||||
for k in [1, 2, 3, 4, 5]:
|
||||
for D in params.real_test_cases:
|
||||
for typ in 'fd':
|
||||
v0 = random.rand(len(D['v0'])).astype(typ)
|
||||
eval_evec(symmetric, D, typ, k, 'LM', v0)
|
||||
|
||||
|
||||
def test_symmetric_no_convergence():
|
||||
np.random.seed(1234)
|
||||
m = generate_matrix(30, hermitian=True, pos_definite=True)
|
||||
tol, rtol, atol = _get_test_tolerance('d')
|
||||
try:
|
||||
w, v = eigsh(m, 4, which='LM', v0=m[:, 0], maxiter=5, tol=tol, ncv=9)
|
||||
raise AssertionError("Spurious no-error exit")
|
||||
except ArpackNoConvergence as err:
|
||||
k = len(err.eigenvalues)
|
||||
if k <= 0:
|
||||
raise AssertionError("Spurious no-eigenvalues-found case") from err
|
||||
w, v = err.eigenvalues, err.eigenvectors
|
||||
assert_allclose(dot(m, v), w * v, rtol=rtol, atol=atol)
|
||||
|
||||
|
||||
def test_real_nonsymmetric_modes():
|
||||
params = NonSymmetricParams()
|
||||
k = 2
|
||||
symmetric = False
|
||||
for D in params.real_test_cases:
|
||||
for typ in 'fd':
|
||||
for which in params.which:
|
||||
for mattype in params.mattypes:
|
||||
for sigma, OPparts in params.sigmas_OPparts.items():
|
||||
for OPpart in OPparts:
|
||||
eval_evec(symmetric, D, typ, k, which,
|
||||
None, sigma, mattype, OPpart)
|
||||
|
||||
|
||||
def test_complex_nonsymmetric_modes():
|
||||
params = NonSymmetricParams()
|
||||
k = 2
|
||||
symmetric = False
|
||||
for D in params.complex_test_cases:
|
||||
for typ in 'DF':
|
||||
for which in params.which:
|
||||
for mattype in params.mattypes:
|
||||
for sigma in params.sigmas_OPparts:
|
||||
eval_evec(symmetric, D, typ, k, which,
|
||||
None, sigma, mattype)
|
||||
|
||||
|
||||
def test_standard_nonsymmetric_starting_vector():
|
||||
params = NonSymmetricParams()
|
||||
sigma = None
|
||||
symmetric = False
|
||||
for k in [1, 2, 3, 4]:
|
||||
for d in params.complex_test_cases:
|
||||
for typ in 'FD':
|
||||
A = d['mat']
|
||||
n = A.shape[0]
|
||||
v0 = random.rand(n).astype(typ)
|
||||
eval_evec(symmetric, d, typ, k, "LM", v0, sigma)
|
||||
|
||||
|
||||
def test_general_nonsymmetric_starting_vector():
|
||||
params = NonSymmetricParams()
|
||||
sigma = None
|
||||
symmetric = False
|
||||
for k in [1, 2, 3, 4]:
|
||||
for d in params.complex_test_cases:
|
||||
for typ in 'FD':
|
||||
A = d['mat']
|
||||
n = A.shape[0]
|
||||
v0 = random.rand(n).astype(typ)
|
||||
eval_evec(symmetric, d, typ, k, "LM", v0, sigma)
|
||||
|
||||
|
||||
def test_standard_nonsymmetric_no_convergence():
|
||||
np.random.seed(1234)
|
||||
m = generate_matrix(30, complex_=True)
|
||||
tol, rtol, atol = _get_test_tolerance('d')
|
||||
try:
|
||||
w, v = eigs(m, 4, which='LM', v0=m[:, 0], maxiter=5, tol=tol)
|
||||
raise AssertionError("Spurious no-error exit")
|
||||
except ArpackNoConvergence as err:
|
||||
k = len(err.eigenvalues)
|
||||
if k <= 0:
|
||||
raise AssertionError("Spurious no-eigenvalues-found case") from err
|
||||
w, v = err.eigenvalues, err.eigenvectors
|
||||
for ww, vv in zip(w, v.T):
|
||||
assert_allclose(dot(m, vv), ww * vv, rtol=rtol, atol=atol)
|
||||
|
||||
|
||||
def test_eigen_bad_shapes():
|
||||
# A is not square.
|
||||
A = csc_matrix(np.zeros((2, 3)))
|
||||
assert_raises(ValueError, eigs, A)
|
||||
|
||||
|
||||
def test_eigen_bad_kwargs():
|
||||
# Test eigen on wrong keyword argument
|
||||
A = csc_matrix(np.zeros((8, 8)))
|
||||
assert_raises(ValueError, eigs, A, which='XX')
|
||||
|
||||
|
||||
def test_ticket_1459_arpack_crash():
|
||||
for dtype in [np.float32, np.float64]:
|
||||
# This test does not seem to catch the issue for float32,
|
||||
# but we made the same fix there, just to be sure
|
||||
|
||||
N = 6
|
||||
k = 2
|
||||
|
||||
np.random.seed(2301)
|
||||
A = np.random.random((N, N)).astype(dtype)
|
||||
v0 = np.array([-0.71063568258907849895, -0.83185111795729227424,
|
||||
-0.34365925382227402451, 0.46122533684552280420,
|
||||
-0.58001341115969040629, -0.78844877570084292984e-01],
|
||||
dtype=dtype)
|
||||
|
||||
# Should not crash:
|
||||
evals, evecs = eigs(A, k, v0=v0)
|
||||
|
||||
|
||||
@pytest.mark.skipif(IS_PYPY, reason="Test not meaningful on PyPy")
|
||||
def test_linearoperator_deallocation():
|
||||
# Check that the linear operators used by the Arpack wrappers are
|
||||
# deallocatable by reference counting -- they are big objects, so
|
||||
# Python's cyclic GC may not collect them fast enough before
|
||||
# running out of memory if eigs/eigsh are called in a tight loop.
|
||||
|
||||
M_d = np.eye(10)
|
||||
M_s = csc_matrix(M_d)
|
||||
M_o = aslinearoperator(M_d)
|
||||
|
||||
with assert_deallocated(lambda: arpack.SpLuInv(M_s)):
|
||||
pass
|
||||
with assert_deallocated(lambda: arpack.LuInv(M_d)):
|
||||
pass
|
||||
with assert_deallocated(lambda: arpack.IterInv(M_s)):
|
||||
pass
|
||||
with assert_deallocated(lambda: arpack.IterOpInv(M_o, None, 0.3)):
|
||||
pass
|
||||
with assert_deallocated(lambda: arpack.IterOpInv(M_o, M_o, 0.3)):
|
||||
pass
|
||||
|
||||
def test_parallel_threads():
|
||||
results = []
|
||||
v0 = np.random.rand(50)
|
||||
|
||||
def worker():
|
||||
x = diags([1, -2, 1], [-1, 0, 1], shape=(50, 50))
|
||||
w, v = eigs(x, k=3, v0=v0)
|
||||
results.append(w)
|
||||
|
||||
w, v = eigsh(x, k=3, v0=v0)
|
||||
results.append(w)
|
||||
|
||||
threads = [threading.Thread(target=worker) for k in range(10)]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
worker()
|
||||
|
||||
for r in results:
|
||||
assert_allclose(r, results[-1])
|
||||
|
||||
|
||||
def test_reentering():
|
||||
# Just some linear operator that calls eigs recursively
|
||||
def A_matvec(x):
|
||||
x = diags([1, -2, 1], [-1, 0, 1], shape=(50, 50))
|
||||
w, v = eigs(x, k=1)
|
||||
return v / w[0]
|
||||
A = LinearOperator(matvec=A_matvec, dtype=float, shape=(50, 50))
|
||||
|
||||
# The Fortran code is not reentrant, so this fails (gracefully, not crashing)
|
||||
assert_raises(RuntimeError, eigs, A, k=1)
|
||||
assert_raises(RuntimeError, eigsh, A, k=1)
|
||||
|
||||
|
||||
def test_regression_arpackng_1315():
|
||||
# Check that issue arpack-ng/#1315 is not present.
|
||||
# Adapted from arpack-ng/TESTS/bug_1315_single.c
|
||||
# If this fails, then the installed ARPACK library is faulty.
|
||||
|
||||
for dtype in [np.float32, np.float64]:
|
||||
np.random.seed(1234)
|
||||
|
||||
w0 = np.arange(1, 1000+1).astype(dtype)
|
||||
A = diags([w0], [0], shape=(1000, 1000))
|
||||
|
||||
v0 = np.random.rand(1000).astype(dtype)
|
||||
w, v = eigs(A, k=9, ncv=2*9+1, which="LM", v0=v0)
|
||||
|
||||
assert_allclose(np.sort(w), np.sort(w0[-9:]),
|
||||
rtol=1e-4)
|
||||
|
||||
|
||||
def test_eigs_for_k_greater():
|
||||
# Test eigs() for k beyond limits.
|
||||
A_sparse = diags([1, -2, 1], [-1, 0, 1], shape=(4, 4)) # sparse
|
||||
A = generate_matrix(4, sparse=False)
|
||||
M_dense = np.random.random((4, 4))
|
||||
M_sparse = generate_matrix(4, sparse=True)
|
||||
M_linop = aslinearoperator(M_dense)
|
||||
eig_tuple1 = eig(A, b=M_dense)
|
||||
eig_tuple2 = eig(A, b=M_sparse)
|
||||
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(RuntimeWarning)
|
||||
|
||||
assert_equal(eigs(A, M=M_dense, k=3), eig_tuple1)
|
||||
assert_equal(eigs(A, M=M_dense, k=4), eig_tuple1)
|
||||
assert_equal(eigs(A, M=M_dense, k=5), eig_tuple1)
|
||||
assert_equal(eigs(A, M=M_sparse, k=5), eig_tuple2)
|
||||
|
||||
# M as LinearOperator
|
||||
assert_raises(TypeError, eigs, A, M=M_linop, k=3)
|
||||
|
||||
# Test 'A' for different types
|
||||
assert_raises(TypeError, eigs, aslinearoperator(A), k=3)
|
||||
assert_raises(TypeError, eigs, A_sparse, k=3)
|
||||
|
||||
|
||||
def test_eigsh_for_k_greater():
|
||||
# Test eigsh() for k beyond limits.
|
||||
A_sparse = diags([1, -2, 1], [-1, 0, 1], shape=(4, 4)) # sparse
|
||||
A = generate_matrix(4, sparse=False)
|
||||
M_dense = generate_matrix_symmetric(4, pos_definite=True)
|
||||
M_sparse = generate_matrix_symmetric(4, pos_definite=True, sparse=True)
|
||||
M_linop = aslinearoperator(M_dense)
|
||||
eig_tuple1 = eigh(A, b=M_dense)
|
||||
eig_tuple2 = eigh(A, b=M_sparse)
|
||||
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(RuntimeWarning)
|
||||
|
||||
assert_equal(eigsh(A, M=M_dense, k=4), eig_tuple1)
|
||||
assert_equal(eigsh(A, M=M_dense, k=5), eig_tuple1)
|
||||
assert_equal(eigsh(A, M=M_sparse, k=5), eig_tuple2)
|
||||
|
||||
# M as LinearOperator
|
||||
assert_raises(TypeError, eigsh, A, M=M_linop, k=4)
|
||||
|
||||
# Test 'A' for different types
|
||||
assert_raises(TypeError, eigsh, aslinearoperator(A), k=4)
|
||||
assert_raises(TypeError, eigsh, A_sparse, M=M_dense, k=4)
|
||||
|
||||
|
||||
def test_real_eigs_real_k_subset():
|
||||
np.random.seed(1)
|
||||
|
||||
n = 10
|
||||
A = rand(n, n, density=0.5)
|
||||
A.data *= 2
|
||||
A.data -= 1
|
||||
|
||||
v0 = np.ones(n)
|
||||
|
||||
whichs = ['LM', 'SM', 'LR', 'SR', 'LI', 'SI']
|
||||
dtypes = [np.float32, np.float64]
|
||||
|
||||
for which, sigma, dtype in itertools.product(whichs, [None, 0, 5], dtypes):
|
||||
prev_w = np.array([], dtype=dtype)
|
||||
eps = np.finfo(dtype).eps
|
||||
for k in range(1, 9):
|
||||
w, z = eigs(A.astype(dtype), k=k, which=which, sigma=sigma,
|
||||
v0=v0.astype(dtype), tol=0)
|
||||
assert_allclose(np.linalg.norm(A.dot(z) - z * w), 0, atol=np.sqrt(eps))
|
||||
|
||||
# Check that the set of eigenvalues for `k` is a subset of that for `k+1`
|
||||
dist = abs(prev_w[:,None] - w).min(axis=1)
|
||||
assert_allclose(dist, 0, atol=np.sqrt(eps))
|
||||
|
||||
prev_w = w
|
||||
|
||||
# Check sort order
|
||||
if sigma is None:
|
||||
d = w
|
||||
else:
|
||||
d = 1 / (w - sigma)
|
||||
|
||||
if which == 'LM':
|
||||
# ARPACK is systematic for 'LM', but sort order
|
||||
# appears not well defined for other modes
|
||||
assert np.all(np.diff(abs(d)) <= 1e-6)
|
||||
@ -0,0 +1,16 @@
|
||||
"""
|
||||
Locally Optimal Block Preconditioned Conjugate Gradient Method (LOBPCG)
|
||||
|
||||
LOBPCG is a preconditioned eigensolver for large symmetric positive definite
|
||||
(SPD) generalized eigenproblems.
|
||||
|
||||
Call the function lobpcg - see help for lobpcg.lobpcg.
|
||||
|
||||
"""
|
||||
from .lobpcg import *
|
||||
|
||||
__all__ = [s for s in dir() if not s.startswith('_')]
|
||||
|
||||
from scipy._lib._testutils import PytestTester
|
||||
test = PytestTester(__name__)
|
||||
del PytestTester
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,641 @@
|
||||
""" Test functions for the sparse.linalg._eigen.lobpcg module
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import platform
|
||||
import sys
|
||||
import pytest
|
||||
import numpy as np
|
||||
from numpy import ones, r_, diag
|
||||
from numpy.testing import (assert_almost_equal, assert_equal,
|
||||
assert_allclose, assert_array_less)
|
||||
|
||||
from scipy import sparse
|
||||
from scipy.linalg import eig, eigh, toeplitz, orth
|
||||
from scipy.sparse import spdiags, diags, eye, csr_matrix
|
||||
from scipy.sparse.linalg import eigs, LinearOperator
|
||||
from scipy.sparse.linalg._eigen.lobpcg import lobpcg
|
||||
from scipy.sparse.linalg._eigen.lobpcg.lobpcg import _b_orthonormalize
|
||||
from scipy._lib._util import np_long, np_ulong
|
||||
|
||||
_IS_32BIT = (sys.maxsize < 2**32)
|
||||
|
||||
INT_DTYPES = {np.intc, np_long, np.longlong, np.uintc, np_ulong, np.ulonglong}
|
||||
# np.half is unsupported on many test systems so excluded
|
||||
REAL_DTYPES = {np.float32, np.float64, np.longdouble}
|
||||
COMPLEX_DTYPES = {np.complex64, np.complex128, np.clongdouble}
|
||||
# use sorted list to ensure fixed order of tests
|
||||
VDTYPES = sorted(REAL_DTYPES ^ COMPLEX_DTYPES, key=str)
|
||||
MDTYPES = sorted(INT_DTYPES ^ REAL_DTYPES ^ COMPLEX_DTYPES, key=str)
|
||||
|
||||
|
||||
def sign_align(A, B):
|
||||
"""Align signs of columns of A match those of B: column-wise remove
|
||||
sign of A by multiplying with its sign then multiply in sign of B.
|
||||
"""
|
||||
return np.array([col_A * np.sign(col_A[0]) * np.sign(col_B[0])
|
||||
for col_A, col_B in zip(A.T, B.T)]).T
|
||||
|
||||
def ElasticRod(n):
|
||||
"""Build the matrices for the generalized eigenvalue problem of the
|
||||
fixed-free elastic rod vibration model.
|
||||
"""
|
||||
L = 1.0
|
||||
le = L/n
|
||||
rho = 7.85e3
|
||||
S = 1.e-4
|
||||
E = 2.1e11
|
||||
mass = rho*S*le/6.
|
||||
k = E*S/le
|
||||
A = k*(diag(r_[2.*ones(n-1), 1])-diag(ones(n-1), 1)-diag(ones(n-1), -1))
|
||||
B = mass*(diag(r_[4.*ones(n-1), 2])+diag(ones(n-1), 1)+diag(ones(n-1), -1))
|
||||
return A, B
|
||||
|
||||
|
||||
def MikotaPair(n):
|
||||
"""Build a pair of full diagonal matrices for the generalized eigenvalue
|
||||
problem. The Mikota pair acts as a nice test since the eigenvalues are the
|
||||
squares of the integers n, n=1,2,...
|
||||
"""
|
||||
x = np.arange(1, n+1)
|
||||
B = diag(1./x)
|
||||
y = np.arange(n-1, 0, -1)
|
||||
z = np.arange(2*n-1, 0, -2)
|
||||
A = diag(z)-diag(y, -1)-diag(y, 1)
|
||||
return A, B
|
||||
|
||||
|
||||
def compare_solutions(A, B, m):
|
||||
"""Check eig vs. lobpcg consistency.
|
||||
"""
|
||||
n = A.shape[0]
|
||||
rnd = np.random.RandomState(0)
|
||||
V = rnd.random((n, m))
|
||||
X = orth(V)
|
||||
eigvals, _ = lobpcg(A, X, B=B, tol=1e-2, maxiter=50, largest=False)
|
||||
eigvals.sort()
|
||||
w, _ = eig(A, b=B)
|
||||
w.sort()
|
||||
assert_almost_equal(w[:int(m/2)], eigvals[:int(m/2)], decimal=2)
|
||||
|
||||
|
||||
def test_Small():
|
||||
A, B = ElasticRod(10)
|
||||
with pytest.warns(UserWarning, match="The problem size"):
|
||||
compare_solutions(A, B, 10)
|
||||
A, B = MikotaPair(10)
|
||||
with pytest.warns(UserWarning, match="The problem size"):
|
||||
compare_solutions(A, B, 10)
|
||||
|
||||
|
||||
def test_ElasticRod():
|
||||
A, B = ElasticRod(20)
|
||||
msg = "Exited at iteration.*|Exited postprocessing with accuracies.*"
|
||||
with pytest.warns(UserWarning, match=msg):
|
||||
compare_solutions(A, B, 2)
|
||||
|
||||
|
||||
def test_MikotaPair():
|
||||
A, B = MikotaPair(20)
|
||||
compare_solutions(A, B, 2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n", [50])
|
||||
@pytest.mark.parametrize("m", [1, 2, 10])
|
||||
@pytest.mark.parametrize("Vdtype", sorted(REAL_DTYPES, key=str))
|
||||
@pytest.mark.parametrize("Bdtype", sorted(REAL_DTYPES, key=str))
|
||||
@pytest.mark.parametrize("BVdtype", sorted(REAL_DTYPES, key=str))
|
||||
def test_b_orthonormalize(n, m, Vdtype, Bdtype, BVdtype):
|
||||
"""Test B-orthonormalization by Cholesky with callable 'B'.
|
||||
The function '_b_orthonormalize' is key in LOBPCG but may
|
||||
lead to numerical instabilities. The input vectors are often
|
||||
badly scaled, so the function needs scale-invariant Cholesky;
|
||||
see https://netlib.org/lapack/lawnspdf/lawn14.pdf.
|
||||
"""
|
||||
rnd = np.random.RandomState(0)
|
||||
X = rnd.standard_normal((n, m)).astype(Vdtype)
|
||||
Xcopy = np.copy(X)
|
||||
vals = np.arange(1, n+1, dtype=float)
|
||||
B = diags([vals], [0], (n, n)).astype(Bdtype)
|
||||
BX = B @ X
|
||||
BX = BX.astype(BVdtype)
|
||||
dtype = min(X.dtype, B.dtype, BX.dtype)
|
||||
# np.longdouble tol cannot be achieved on most systems
|
||||
atol = m * n * max(np.finfo(dtype).eps, np.finfo(np.float64).eps)
|
||||
|
||||
Xo, BXo, _ = _b_orthonormalize(lambda v: B @ v, X, BX)
|
||||
# Check in-place.
|
||||
assert_equal(X, Xo)
|
||||
assert_equal(id(X), id(Xo))
|
||||
assert_equal(BX, BXo)
|
||||
assert_equal(id(BX), id(BXo))
|
||||
# Check BXo.
|
||||
assert_allclose(B @ Xo, BXo, atol=atol, rtol=atol)
|
||||
# Check B-orthonormality
|
||||
assert_allclose(Xo.T.conj() @ B @ Xo, np.identity(m),
|
||||
atol=atol, rtol=atol)
|
||||
# Repeat without BX in outputs
|
||||
X = np.copy(Xcopy)
|
||||
Xo1, BXo1, _ = _b_orthonormalize(lambda v: B @ v, X)
|
||||
assert_allclose(Xo, Xo1, atol=atol, rtol=atol)
|
||||
assert_allclose(BXo, BXo1, atol=atol, rtol=atol)
|
||||
# Check in-place.
|
||||
assert_equal(X, Xo1)
|
||||
assert_equal(id(X), id(Xo1))
|
||||
# Check BXo1.
|
||||
assert_allclose(B @ Xo1, BXo1, atol=atol, rtol=atol)
|
||||
|
||||
# Introduce column-scaling in X.
|
||||
scaling = 1.0 / np.geomspace(10, 1e10, num=m)
|
||||
X = Xcopy * scaling
|
||||
X = X.astype(Vdtype)
|
||||
BX = B @ X
|
||||
BX = BX.astype(BVdtype)
|
||||
# Check scaling-invariance of Cholesky-based orthonormalization
|
||||
Xo1, BXo1, _ = _b_orthonormalize(lambda v: B @ v, X, BX)
|
||||
# The output should be the same, up the signs of the columns.
|
||||
Xo1 = sign_align(Xo1, Xo)
|
||||
assert_allclose(Xo, Xo1, atol=atol, rtol=atol)
|
||||
BXo1 = sign_align(BXo1, BXo)
|
||||
assert_allclose(BXo, BXo1, atol=atol, rtol=atol)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:Exited at iteration 0")
|
||||
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
|
||||
def test_nonhermitian_warning(capsys):
|
||||
"""Check the warning of a Ritz matrix being not Hermitian
|
||||
by feeding a non-Hermitian input matrix.
|
||||
Also check stdout since verbosityLevel=1 and lack of stderr.
|
||||
"""
|
||||
n = 10
|
||||
X = np.arange(n * 2).reshape(n, 2).astype(np.float32)
|
||||
A = np.arange(n * n).reshape(n, n).astype(np.float32)
|
||||
with pytest.warns(UserWarning, match="Matrix gramA"):
|
||||
_, _ = lobpcg(A, X, verbosityLevel=1, maxiter=0)
|
||||
out, err = capsys.readouterr() # Capture output
|
||||
assert out.startswith("Solving standard eigenvalue") # Test stdout
|
||||
assert err == '' # Test empty stderr
|
||||
# Make the matrix symmetric and the UserWarning disappears.
|
||||
A += A.T
|
||||
_, _ = lobpcg(A, X, verbosityLevel=1, maxiter=0)
|
||||
out, err = capsys.readouterr() # Capture output
|
||||
assert out.startswith("Solving standard eigenvalue") # Test stdout
|
||||
assert err == '' # Test empty stderr
|
||||
|
||||
|
||||
def test_regression():
|
||||
"""Check the eigenvalue of the identity matrix is one.
|
||||
"""
|
||||
# https://mail.python.org/pipermail/scipy-user/2010-October/026944.html
|
||||
n = 10
|
||||
X = np.ones((n, 1))
|
||||
A = np.identity(n)
|
||||
w, _ = lobpcg(A, X)
|
||||
assert_allclose(w, [1])
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:The problem size")
|
||||
@pytest.mark.parametrize('n, m, m_excluded', [(30, 4, 3), (4, 2, 0)])
|
||||
def test_diagonal(n, m, m_excluded):
|
||||
"""Test ``m - m_excluded`` eigenvalues and eigenvectors of
|
||||
diagonal matrices of the size ``n`` varying matrix formats:
|
||||
dense array, spare matrix, and ``LinearOperator`` for both
|
||||
matrixes in the generalized eigenvalue problem ``Av = cBv``
|
||||
and for the preconditioner.
|
||||
"""
|
||||
rnd = np.random.RandomState(0)
|
||||
|
||||
# Define the generalized eigenvalue problem Av = cBv
|
||||
# where (c, v) is a generalized eigenpair,
|
||||
# A is the diagonal matrix whose entries are 1,...n,
|
||||
# B is the identity matrix.
|
||||
vals = np.arange(1, n+1, dtype=float)
|
||||
A_s = diags([vals], [0], (n, n))
|
||||
A_a = A_s.toarray()
|
||||
|
||||
def A_f(x):
|
||||
return A_s @ x
|
||||
|
||||
A_lo = LinearOperator(matvec=A_f,
|
||||
matmat=A_f,
|
||||
shape=(n, n), dtype=float)
|
||||
|
||||
B_a = eye(n)
|
||||
B_s = csr_matrix(B_a)
|
||||
|
||||
def B_f(x):
|
||||
return B_a @ x
|
||||
|
||||
B_lo = LinearOperator(matvec=B_f,
|
||||
matmat=B_f,
|
||||
shape=(n, n), dtype=float)
|
||||
|
||||
# Let the preconditioner M be the inverse of A.
|
||||
M_s = diags([1./vals], [0], (n, n))
|
||||
M_a = M_s.toarray()
|
||||
|
||||
def M_f(x):
|
||||
return M_s @ x
|
||||
|
||||
M_lo = LinearOperator(matvec=M_f,
|
||||
matmat=M_f,
|
||||
shape=(n, n), dtype=float)
|
||||
|
||||
# Pick random initial vectors.
|
||||
X = rnd.normal(size=(n, m))
|
||||
|
||||
# Require that the returned eigenvectors be in the orthogonal complement
|
||||
# of the first few standard basis vectors.
|
||||
if m_excluded > 0:
|
||||
Y = np.eye(n, m_excluded)
|
||||
else:
|
||||
Y = None
|
||||
|
||||
for A in [A_a, A_s, A_lo]:
|
||||
for B in [B_a, B_s, B_lo]:
|
||||
for M in [M_a, M_s, M_lo]:
|
||||
eigvals, vecs = lobpcg(A, X, B, M=M, Y=Y,
|
||||
maxiter=40, largest=False)
|
||||
|
||||
assert_allclose(eigvals, np.arange(1+m_excluded,
|
||||
1+m_excluded+m))
|
||||
_check_eigen(A, eigvals, vecs, rtol=1e-3, atol=1e-3)
|
||||
|
||||
|
||||
def _check_eigen(M, w, V, rtol=1e-8, atol=1e-14):
|
||||
"""Check if the eigenvalue residual is small.
|
||||
"""
|
||||
mult_wV = np.multiply(w, V)
|
||||
dot_MV = M.dot(V)
|
||||
assert_allclose(mult_wV, dot_MV, rtol=rtol, atol=atol)
|
||||
|
||||
|
||||
def _check_fiedler(n, p):
|
||||
"""Check the Fiedler vector computation.
|
||||
"""
|
||||
# This is not necessarily the recommended way to find the Fiedler vector.
|
||||
col = np.zeros(n)
|
||||
col[1] = 1
|
||||
A = toeplitz(col)
|
||||
D = np.diag(A.sum(axis=1))
|
||||
L = D - A
|
||||
# Compute the full eigendecomposition using tricks, e.g.
|
||||
# http://www.cs.yale.edu/homes/spielman/561/2009/lect02-09.pdf
|
||||
tmp = np.pi * np.arange(n) / n
|
||||
analytic_w = 2 * (1 - np.cos(tmp))
|
||||
analytic_V = np.cos(np.outer(np.arange(n) + 1/2, tmp))
|
||||
_check_eigen(L, analytic_w, analytic_V)
|
||||
# Compute the full eigendecomposition using eigh.
|
||||
eigh_w, eigh_V = eigh(L)
|
||||
_check_eigen(L, eigh_w, eigh_V)
|
||||
# Check that the first eigenvalue is near zero and that the rest agree.
|
||||
assert_array_less(np.abs([eigh_w[0], analytic_w[0]]), 1e-14)
|
||||
assert_allclose(eigh_w[1:], analytic_w[1:])
|
||||
|
||||
# Check small lobpcg eigenvalues.
|
||||
X = analytic_V[:, :p]
|
||||
lobpcg_w, lobpcg_V = lobpcg(L, X, largest=False)
|
||||
assert_equal(lobpcg_w.shape, (p,))
|
||||
assert_equal(lobpcg_V.shape, (n, p))
|
||||
_check_eigen(L, lobpcg_w, lobpcg_V)
|
||||
assert_array_less(np.abs(np.min(lobpcg_w)), 1e-14)
|
||||
assert_allclose(np.sort(lobpcg_w)[1:], analytic_w[1:p])
|
||||
|
||||
# Check large lobpcg eigenvalues.
|
||||
X = analytic_V[:, -p:]
|
||||
lobpcg_w, lobpcg_V = lobpcg(L, X, largest=True)
|
||||
assert_equal(lobpcg_w.shape, (p,))
|
||||
assert_equal(lobpcg_V.shape, (n, p))
|
||||
_check_eigen(L, lobpcg_w, lobpcg_V)
|
||||
assert_allclose(np.sort(lobpcg_w), analytic_w[-p:])
|
||||
|
||||
# Look for the Fiedler vector using good but not exactly correct guesses.
|
||||
fiedler_guess = np.concatenate((np.ones(n//2), -np.ones(n-n//2)))
|
||||
X = np.vstack((np.ones(n), fiedler_guess)).T
|
||||
lobpcg_w, _ = lobpcg(L, X, largest=False)
|
||||
# Mathematically, the smaller eigenvalue should be zero
|
||||
# and the larger should be the algebraic connectivity.
|
||||
lobpcg_w = np.sort(lobpcg_w)
|
||||
assert_allclose(lobpcg_w, analytic_w[:2], atol=1e-14)
|
||||
|
||||
|
||||
def test_fiedler_small_8():
|
||||
"""Check the dense workaround path for small matrices.
|
||||
"""
|
||||
# This triggers the dense path because 8 < 2*5.
|
||||
with pytest.warns(UserWarning, match="The problem size"):
|
||||
_check_fiedler(8, 2)
|
||||
|
||||
|
||||
def test_fiedler_large_12():
|
||||
"""Check the dense workaround path avoided for non-small matrices.
|
||||
"""
|
||||
# This does not trigger the dense path, because 2*5 <= 12.
|
||||
_check_fiedler(12, 2)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:Failed at iteration")
|
||||
@pytest.mark.filterwarnings("ignore:Exited at iteration")
|
||||
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
|
||||
def test_failure_to_run_iterations():
|
||||
"""Check that the code exits gracefully without breaking. Issue #10974.
|
||||
The code may or not issue a warning, filtered out. Issue #15935, #17954.
|
||||
"""
|
||||
rnd = np.random.RandomState(0)
|
||||
X = rnd.standard_normal((100, 10))
|
||||
A = X @ X.T
|
||||
Q = rnd.standard_normal((X.shape[0], 4))
|
||||
eigenvalues, _ = lobpcg(A, Q, maxiter=40, tol=1e-12)
|
||||
assert np.max(eigenvalues) > 0
|
||||
|
||||
|
||||
def test_failure_to_run_iterations_nonsymmetric():
|
||||
"""Check that the code exists gracefully without breaking
|
||||
if the matrix in not symmetric.
|
||||
"""
|
||||
A = np.zeros((10, 10))
|
||||
A[0, 1] = 1
|
||||
Q = np.ones((10, 1))
|
||||
msg = "Exited at iteration 2|Exited postprocessing with accuracies.*"
|
||||
with pytest.warns(UserWarning, match=msg):
|
||||
eigenvalues, _ = lobpcg(A, Q, maxiter=20)
|
||||
assert np.max(eigenvalues) > 0
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:The problem size")
|
||||
def test_hermitian():
|
||||
"""Check complex-value Hermitian cases.
|
||||
"""
|
||||
rnd = np.random.RandomState(0)
|
||||
|
||||
sizes = [3, 12]
|
||||
ks = [1, 2]
|
||||
gens = [True, False]
|
||||
|
||||
for s, k, gen, dh, dx, db in (
|
||||
itertools.product(sizes, ks, gens, gens, gens, gens)
|
||||
):
|
||||
H = rnd.random((s, s)) + 1.j * rnd.random((s, s))
|
||||
H = 10 * np.eye(s) + H + H.T.conj()
|
||||
H = H.astype(np.complex128) if dh else H.astype(np.complex64)
|
||||
|
||||
X = rnd.standard_normal((s, k))
|
||||
X = X + 1.j * rnd.standard_normal((s, k))
|
||||
X = X.astype(np.complex128) if dx else X.astype(np.complex64)
|
||||
|
||||
if not gen:
|
||||
B = np.eye(s)
|
||||
w, v = lobpcg(H, X, maxiter=99, verbosityLevel=0)
|
||||
# Also test mixing complex H with real B.
|
||||
wb, _ = lobpcg(H, X, B, maxiter=99, verbosityLevel=0)
|
||||
assert_allclose(w, wb, rtol=1e-6)
|
||||
w0, _ = eigh(H)
|
||||
else:
|
||||
B = rnd.random((s, s)) + 1.j * rnd.random((s, s))
|
||||
B = 10 * np.eye(s) + B.dot(B.T.conj())
|
||||
B = B.astype(np.complex128) if db else B.astype(np.complex64)
|
||||
w, v = lobpcg(H, X, B, maxiter=99, verbosityLevel=0)
|
||||
w0, _ = eigh(H, B)
|
||||
|
||||
for wx, vx in zip(w, v.T):
|
||||
# Check eigenvector
|
||||
assert_allclose(np.linalg.norm(H.dot(vx) - B.dot(vx) * wx)
|
||||
/ np.linalg.norm(H.dot(vx)),
|
||||
0, atol=5e-2, rtol=0)
|
||||
|
||||
# Compare eigenvalues
|
||||
j = np.argmin(abs(w0 - wx))
|
||||
assert_allclose(wx, w0[j], rtol=1e-4)
|
||||
|
||||
|
||||
# The n=5 case tests the alternative small matrix code path that uses eigh().
|
||||
@pytest.mark.filterwarnings("ignore:The problem size")
|
||||
@pytest.mark.parametrize('n, atol', [(20, 1e-3), (5, 1e-8)])
|
||||
def test_eigs_consistency(n, atol):
|
||||
"""Check eigs vs. lobpcg consistency.
|
||||
"""
|
||||
vals = np.arange(1, n+1, dtype=np.float64)
|
||||
A = spdiags(vals, 0, n, n)
|
||||
rnd = np.random.RandomState(0)
|
||||
X = rnd.standard_normal((n, 2))
|
||||
lvals, lvecs = lobpcg(A, X, largest=True, maxiter=100)
|
||||
vals, _ = eigs(A, k=2)
|
||||
|
||||
_check_eigen(A, lvals, lvecs, atol=atol, rtol=0)
|
||||
assert_allclose(np.sort(vals), np.sort(lvals), atol=1e-14)
|
||||
|
||||
|
||||
def test_verbosity():
|
||||
"""Check that nonzero verbosity level code runs.
|
||||
"""
|
||||
rnd = np.random.RandomState(0)
|
||||
X = rnd.standard_normal((10, 10))
|
||||
A = X @ X.T
|
||||
Q = rnd.standard_normal((X.shape[0], 1))
|
||||
msg = "Exited at iteration.*|Exited postprocessing with accuracies.*"
|
||||
with pytest.warns(UserWarning, match=msg):
|
||||
_, _ = lobpcg(A, Q, maxiter=3, verbosityLevel=9)
|
||||
|
||||
|
||||
@pytest.mark.xfail(_IS_32BIT and sys.platform == 'win32',
|
||||
reason="tolerance violation on windows")
|
||||
@pytest.mark.xfail(platform.machine() == 'ppc64le',
|
||||
reason="fails on ppc64le")
|
||||
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
|
||||
def test_tolerance_float32():
|
||||
"""Check lobpcg for attainable tolerance in float32.
|
||||
"""
|
||||
rnd = np.random.RandomState(0)
|
||||
n = 50
|
||||
m = 3
|
||||
vals = -np.arange(1, n + 1)
|
||||
A = diags([vals], [0], (n, n))
|
||||
A = A.astype(np.float32)
|
||||
X = rnd.standard_normal((n, m))
|
||||
X = X.astype(np.float32)
|
||||
eigvals, _ = lobpcg(A, X, tol=1.25e-5, maxiter=50, verbosityLevel=0)
|
||||
assert_allclose(eigvals, -np.arange(1, 1 + m), atol=2e-5, rtol=1e-5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("vdtype", VDTYPES)
|
||||
@pytest.mark.parametrize("mdtype", MDTYPES)
|
||||
@pytest.mark.parametrize("arr_type", [np.array,
|
||||
sparse.csr_matrix,
|
||||
sparse.coo_matrix])
|
||||
def test_dtypes(vdtype, mdtype, arr_type):
|
||||
"""Test lobpcg in various dtypes.
|
||||
"""
|
||||
rnd = np.random.RandomState(0)
|
||||
n = 12
|
||||
m = 2
|
||||
A = arr_type(np.diag(np.arange(1, n + 1)).astype(mdtype))
|
||||
X = rnd.random((n, m))
|
||||
X = X.astype(vdtype)
|
||||
eigvals, eigvecs = lobpcg(A, X, tol=1e-2, largest=False)
|
||||
assert_allclose(eigvals, np.arange(1, 1 + m), atol=1e-1)
|
||||
# eigenvectors must be nearly real in any case
|
||||
assert_allclose(np.sum(np.abs(eigvecs - eigvecs.conj())), 0, atol=1e-2)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:Exited at iteration")
|
||||
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
|
||||
def test_inplace_warning():
|
||||
"""Check lobpcg gives a warning in '_b_orthonormalize'
|
||||
that in-place orthogonalization is impossible due to dtype mismatch.
|
||||
"""
|
||||
rnd = np.random.RandomState(0)
|
||||
n = 6
|
||||
m = 1
|
||||
vals = -np.arange(1, n + 1)
|
||||
A = diags([vals], [0], (n, n))
|
||||
A = A.astype(np.cdouble)
|
||||
X = rnd.standard_normal((n, m))
|
||||
with pytest.warns(UserWarning, match="Inplace update"):
|
||||
eigvals, _ = lobpcg(A, X, maxiter=2, verbosityLevel=1)
|
||||
|
||||
|
||||
def test_maxit():
|
||||
"""Check lobpcg if maxit=maxiter runs maxiter iterations and
|
||||
if maxit=None runs 20 iterations (the default)
|
||||
by checking the size of the iteration history output, which should
|
||||
be the number of iterations plus 3 (initial, final, and postprocessing)
|
||||
typically when maxiter is small and the choice of the best is passive.
|
||||
"""
|
||||
rnd = np.random.RandomState(0)
|
||||
n = 50
|
||||
m = 4
|
||||
vals = -np.arange(1, n + 1)
|
||||
A = diags([vals], [0], (n, n))
|
||||
A = A.astype(np.float32)
|
||||
X = rnd.standard_normal((n, m))
|
||||
X = X.astype(np.float64)
|
||||
msg = "Exited at iteration.*|Exited postprocessing with accuracies.*"
|
||||
for maxiter in range(1, 4):
|
||||
with pytest.warns(UserWarning, match=msg):
|
||||
_, _, l_h, r_h = lobpcg(A, X, tol=1e-8, maxiter=maxiter,
|
||||
retLambdaHistory=True,
|
||||
retResidualNormsHistory=True)
|
||||
assert_allclose(np.shape(l_h)[0], maxiter+3)
|
||||
assert_allclose(np.shape(r_h)[0], maxiter+3)
|
||||
with pytest.warns(UserWarning, match=msg):
|
||||
l, _, l_h, r_h = lobpcg(A, X, tol=1e-8,
|
||||
retLambdaHistory=True,
|
||||
retResidualNormsHistory=True)
|
||||
assert_allclose(np.shape(l_h)[0], 20+3)
|
||||
assert_allclose(np.shape(r_h)[0], 20+3)
|
||||
# Check that eigenvalue output is the last one in history
|
||||
assert_allclose(l, l_h[-1])
|
||||
# Make sure that both history outputs are lists
|
||||
assert isinstance(l_h, list)
|
||||
assert isinstance(r_h, list)
|
||||
# Make sure that both history lists are arrays-like
|
||||
assert_allclose(np.shape(l_h), np.shape(np.asarray(l_h)))
|
||||
assert_allclose(np.shape(r_h), np.shape(np.asarray(r_h)))
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.parametrize("n", [15])
|
||||
@pytest.mark.parametrize("m", [1, 2])
|
||||
@pytest.mark.filterwarnings("ignore:Exited at iteration")
|
||||
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
|
||||
def test_diagonal_data_types(n, m):
|
||||
"""Check lobpcg for diagonal matrices for all matrix types.
|
||||
Constraints are imposed, so a dense eigensolver eig cannot run.
|
||||
"""
|
||||
rnd = np.random.RandomState(0)
|
||||
# Define the generalized eigenvalue problem Av = cBv
|
||||
# where (c, v) is a generalized eigenpair,
|
||||
# and where we choose A and B to be diagonal.
|
||||
vals = np.arange(1, n + 1)
|
||||
|
||||
list_sparse_format = ['bsr', 'coo', 'csc', 'csr', 'dia', 'dok', 'lil']
|
||||
for s_f_i, s_f in enumerate(list_sparse_format):
|
||||
|
||||
As64 = diags([vals * vals], [0], (n, n), format=s_f)
|
||||
As32 = As64.astype(np.float32)
|
||||
Af64 = As64.toarray()
|
||||
Af32 = Af64.astype(np.float32)
|
||||
|
||||
def As32f(x):
|
||||
return As32 @ x
|
||||
As32LO = LinearOperator(matvec=As32f,
|
||||
matmat=As32f,
|
||||
shape=(n, n),
|
||||
dtype=As32.dtype)
|
||||
|
||||
listA = [Af64, As64, Af32, As32, As32f, As32LO, lambda v: As32 @ v]
|
||||
|
||||
Bs64 = diags([vals], [0], (n, n), format=s_f)
|
||||
Bf64 = Bs64.toarray()
|
||||
Bs32 = Bs64.astype(np.float32)
|
||||
|
||||
def Bs32f(x):
|
||||
return Bs32 @ x
|
||||
Bs32LO = LinearOperator(matvec=Bs32f,
|
||||
matmat=Bs32f,
|
||||
shape=(n, n),
|
||||
dtype=Bs32.dtype)
|
||||
listB = [Bf64, Bs64, Bs32, Bs32f, Bs32LO, lambda v: Bs32 @ v]
|
||||
|
||||
# Define the preconditioner function as LinearOperator.
|
||||
Ms64 = diags([1./vals], [0], (n, n), format=s_f)
|
||||
|
||||
def Ms64precond(x):
|
||||
return Ms64 @ x
|
||||
Ms64precondLO = LinearOperator(matvec=Ms64precond,
|
||||
matmat=Ms64precond,
|
||||
shape=(n, n),
|
||||
dtype=Ms64.dtype)
|
||||
Mf64 = Ms64.toarray()
|
||||
|
||||
def Mf64precond(x):
|
||||
return Mf64 @ x
|
||||
Mf64precondLO = LinearOperator(matvec=Mf64precond,
|
||||
matmat=Mf64precond,
|
||||
shape=(n, n),
|
||||
dtype=Mf64.dtype)
|
||||
Ms32 = Ms64.astype(np.float32)
|
||||
|
||||
def Ms32precond(x):
|
||||
return Ms32 @ x
|
||||
Ms32precondLO = LinearOperator(matvec=Ms32precond,
|
||||
matmat=Ms32precond,
|
||||
shape=(n, n),
|
||||
dtype=Ms32.dtype)
|
||||
Mf32 = Ms32.toarray()
|
||||
|
||||
def Mf32precond(x):
|
||||
return Mf32 @ x
|
||||
Mf32precondLO = LinearOperator(matvec=Mf32precond,
|
||||
matmat=Mf32precond,
|
||||
shape=(n, n),
|
||||
dtype=Mf32.dtype)
|
||||
listM = [None, Ms64, Ms64precondLO, Mf64precondLO, Ms64precond,
|
||||
Ms32, Ms32precondLO, Mf32precondLO, Ms32precond]
|
||||
|
||||
# Setup matrix of the initial approximation to the eigenvectors
|
||||
# (cannot be sparse array).
|
||||
Xf64 = rnd.random((n, m))
|
||||
Xf32 = Xf64.astype(np.float32)
|
||||
listX = [Xf64, Xf32]
|
||||
|
||||
# Require that the returned eigenvectors be in the orthogonal complement
|
||||
# of the first few standard basis vectors (cannot be sparse array).
|
||||
m_excluded = 3
|
||||
Yf64 = np.eye(n, m_excluded, dtype=float)
|
||||
Yf32 = np.eye(n, m_excluded, dtype=np.float32)
|
||||
listY = [Yf64, Yf32]
|
||||
|
||||
tests = list(itertools.product(listA, listB, listM, listX, listY))
|
||||
|
||||
for A, B, M, X, Y in tests:
|
||||
# This is one of the slower tests because there are >1,000 configs
|
||||
# to test here. Flip a biased coin to decide whether to run each
|
||||
# test to get decent coverage in less time.
|
||||
if rnd.random() < 0.98:
|
||||
continue # too many tests
|
||||
eigvals, _ = lobpcg(A, X, B=B, M=M, Y=Y, tol=1e-4,
|
||||
maxiter=100, largest=False)
|
||||
assert_allclose(eigvals,
|
||||
np.arange(1 + m_excluded, 1 + m_excluded + m),
|
||||
atol=1e-5)
|
||||
@ -0,0 +1,862 @@
|
||||
import re
|
||||
import copy
|
||||
import numpy as np
|
||||
|
||||
from numpy.testing import assert_allclose, assert_equal, assert_array_equal
|
||||
import pytest
|
||||
|
||||
from scipy.linalg import svd, null_space
|
||||
from scipy.sparse import csc_matrix, issparse, spdiags, random
|
||||
from scipy.sparse.linalg import LinearOperator, aslinearoperator
|
||||
from scipy.sparse.linalg import svds
|
||||
from scipy.sparse.linalg._eigen.arpack import ArpackNoConvergence
|
||||
|
||||
|
||||
# --- Helper Functions / Classes ---
|
||||
|
||||
|
||||
def sorted_svd(m, k, which='LM'):
|
||||
# Compute svd of a dense matrix m, and return singular vectors/values
|
||||
# sorted.
|
||||
if issparse(m):
|
||||
m = m.toarray()
|
||||
u, s, vh = svd(m)
|
||||
if which == 'LM':
|
||||
ii = np.argsort(s)[-k:]
|
||||
elif which == 'SM':
|
||||
ii = np.argsort(s)[:k]
|
||||
else:
|
||||
raise ValueError(f"unknown which={which!r}")
|
||||
|
||||
return u[:, ii], s[ii], vh[ii]
|
||||
|
||||
|
||||
def _check_svds(A, k, u, s, vh, which="LM", check_usvh_A=False,
|
||||
check_svd=True, atol=1e-10, rtol=1e-7):
|
||||
n, m = A.shape
|
||||
|
||||
# Check shapes.
|
||||
assert_equal(u.shape, (n, k))
|
||||
assert_equal(s.shape, (k,))
|
||||
assert_equal(vh.shape, (k, m))
|
||||
|
||||
# Check that the original matrix can be reconstituted.
|
||||
A_rebuilt = (u*s).dot(vh)
|
||||
assert_equal(A_rebuilt.shape, A.shape)
|
||||
if check_usvh_A:
|
||||
assert_allclose(A_rebuilt, A, atol=atol, rtol=rtol)
|
||||
|
||||
# Check that u is a semi-orthogonal matrix.
|
||||
uh_u = np.dot(u.T.conj(), u)
|
||||
assert_equal(uh_u.shape, (k, k))
|
||||
assert_allclose(uh_u, np.identity(k), atol=atol, rtol=rtol)
|
||||
|
||||
# Check that vh is a semi-orthogonal matrix.
|
||||
vh_v = np.dot(vh, vh.T.conj())
|
||||
assert_equal(vh_v.shape, (k, k))
|
||||
assert_allclose(vh_v, np.identity(k), atol=atol, rtol=rtol)
|
||||
|
||||
# Check that scipy.sparse.linalg.svds ~ scipy.linalg.svd
|
||||
if check_svd:
|
||||
u2, s2, vh2 = sorted_svd(A, k, which)
|
||||
assert_allclose(np.abs(u), np.abs(u2), atol=atol, rtol=rtol)
|
||||
assert_allclose(s, s2, atol=atol, rtol=rtol)
|
||||
assert_allclose(np.abs(vh), np.abs(vh2), atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
def _check_svds_n(A, k, u, s, vh, which="LM", check_res=True,
|
||||
check_svd=True, atol=1e-10, rtol=1e-7):
|
||||
n, m = A.shape
|
||||
|
||||
# Check shapes.
|
||||
assert_equal(u.shape, (n, k))
|
||||
assert_equal(s.shape, (k,))
|
||||
assert_equal(vh.shape, (k, m))
|
||||
|
||||
# Check that u is a semi-orthogonal matrix.
|
||||
uh_u = np.dot(u.T.conj(), u)
|
||||
assert_equal(uh_u.shape, (k, k))
|
||||
error = np.sum(np.abs(uh_u - np.identity(k))) / (k * k)
|
||||
assert_allclose(error, 0.0, atol=atol, rtol=rtol)
|
||||
|
||||
# Check that vh is a semi-orthogonal matrix.
|
||||
vh_v = np.dot(vh, vh.T.conj())
|
||||
assert_equal(vh_v.shape, (k, k))
|
||||
error = np.sum(np.abs(vh_v - np.identity(k))) / (k * k)
|
||||
assert_allclose(error, 0.0, atol=atol, rtol=rtol)
|
||||
|
||||
# Check residuals
|
||||
if check_res:
|
||||
ru = A.T.conj() @ u - vh.T.conj() * s
|
||||
rus = np.sum(np.abs(ru)) / (n * k)
|
||||
rvh = A @ vh.T.conj() - u * s
|
||||
rvhs = np.sum(np.abs(rvh)) / (m * k)
|
||||
assert_allclose(rus, 0.0, atol=atol, rtol=rtol)
|
||||
assert_allclose(rvhs, 0.0, atol=atol, rtol=rtol)
|
||||
|
||||
# Check that scipy.sparse.linalg.svds ~ scipy.linalg.svd
|
||||
if check_svd:
|
||||
u2, s2, vh2 = sorted_svd(A, k, which)
|
||||
assert_allclose(s, s2, atol=atol, rtol=rtol)
|
||||
A_rebuilt_svd = (u2*s2).dot(vh2)
|
||||
A_rebuilt = (u*s).dot(vh)
|
||||
assert_equal(A_rebuilt.shape, A.shape)
|
||||
error = np.sum(np.abs(A_rebuilt_svd - A_rebuilt)) / (k * k)
|
||||
assert_allclose(error, 0.0, atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
class CheckingLinearOperator(LinearOperator):
|
||||
def __init__(self, A):
|
||||
self.A = A
|
||||
self.dtype = A.dtype
|
||||
self.shape = A.shape
|
||||
|
||||
def _matvec(self, x):
|
||||
assert_equal(max(x.shape), np.size(x))
|
||||
return self.A.dot(x)
|
||||
|
||||
def _rmatvec(self, x):
|
||||
assert_equal(max(x.shape), np.size(x))
|
||||
return self.A.T.conjugate().dot(x)
|
||||
|
||||
|
||||
# --- Test Input Validation ---
|
||||
# Tests input validation on parameters `k` and `which`.
|
||||
# Needs better input validation checks for all other parameters.
|
||||
|
||||
class SVDSCommonTests:
|
||||
|
||||
solver = None
|
||||
|
||||
# some of these IV tests could run only once, say with solver=None
|
||||
|
||||
_A_empty_msg = "`A` must not be empty."
|
||||
_A_dtype_msg = "`A` must be of floating or complex floating data type"
|
||||
_A_type_msg = "type not understood"
|
||||
_A_ndim_msg = "array must have ndim <= 2"
|
||||
_A_validation_inputs = [
|
||||
(np.asarray([[]]), ValueError, _A_empty_msg),
|
||||
(np.asarray([[1, 2], [3, 4]]), ValueError, _A_dtype_msg),
|
||||
("hi", TypeError, _A_type_msg),
|
||||
(np.asarray([[[1., 2.], [3., 4.]]]), ValueError, _A_ndim_msg)]
|
||||
|
||||
@pytest.mark.parametrize("args", _A_validation_inputs)
|
||||
def test_svds_input_validation_A(self, args):
|
||||
A, error_type, message = args
|
||||
with pytest.raises(error_type, match=message):
|
||||
svds(A, k=1, solver=self.solver)
|
||||
|
||||
@pytest.mark.parametrize("k", [-1, 0, 3, 4, 5, 1.5, "1"])
|
||||
def test_svds_input_validation_k_1(self, k):
|
||||
rng = np.random.default_rng(0)
|
||||
A = rng.random((4, 3))
|
||||
|
||||
# propack can do complete SVD
|
||||
if self.solver == 'propack' and k == 3:
|
||||
res = svds(A, k=k, solver=self.solver, random_state=0)
|
||||
_check_svds(A, k, *res, check_usvh_A=True, check_svd=True)
|
||||
return
|
||||
|
||||
message = ("`k` must be an integer satisfying")
|
||||
with pytest.raises(ValueError, match=message):
|
||||
svds(A, k=k, solver=self.solver)
|
||||
|
||||
def test_svds_input_validation_k_2(self):
|
||||
# I think the stack trace is reasonable when `k` can't be converted
|
||||
# to an int.
|
||||
message = "int() argument must be a"
|
||||
with pytest.raises(TypeError, match=re.escape(message)):
|
||||
svds(np.eye(10), k=[], solver=self.solver)
|
||||
|
||||
message = "invalid literal for int()"
|
||||
with pytest.raises(ValueError, match=message):
|
||||
svds(np.eye(10), k="hi", solver=self.solver)
|
||||
|
||||
@pytest.mark.parametrize("tol", (-1, np.inf, np.nan))
|
||||
def test_svds_input_validation_tol_1(self, tol):
|
||||
message = "`tol` must be a non-negative floating point value."
|
||||
with pytest.raises(ValueError, match=message):
|
||||
svds(np.eye(10), tol=tol, solver=self.solver)
|
||||
|
||||
@pytest.mark.parametrize("tol", ([], 'hi'))
|
||||
def test_svds_input_validation_tol_2(self, tol):
|
||||
# I think the stack trace is reasonable here
|
||||
message = "'<' not supported between instances"
|
||||
with pytest.raises(TypeError, match=message):
|
||||
svds(np.eye(10), tol=tol, solver=self.solver)
|
||||
|
||||
@pytest.mark.parametrize("which", ('LA', 'SA', 'ekki', 0))
|
||||
def test_svds_input_validation_which(self, which):
|
||||
# Regression test for a github issue.
|
||||
# https://github.com/scipy/scipy/issues/4590
|
||||
# Function was not checking for eigenvalue type and unintended
|
||||
# values could be returned.
|
||||
with pytest.raises(ValueError, match="`which` must be in"):
|
||||
svds(np.eye(10), which=which, solver=self.solver)
|
||||
|
||||
@pytest.mark.parametrize("transpose", (True, False))
|
||||
@pytest.mark.parametrize("n", range(4, 9))
|
||||
def test_svds_input_validation_v0_1(self, transpose, n):
|
||||
rng = np.random.default_rng(0)
|
||||
A = rng.random((5, 7))
|
||||
v0 = rng.random(n)
|
||||
if transpose:
|
||||
A = A.T
|
||||
k = 2
|
||||
message = "`v0` must have shape"
|
||||
|
||||
required_length = (A.shape[0] if self.solver == 'propack'
|
||||
else min(A.shape))
|
||||
if n != required_length:
|
||||
with pytest.raises(ValueError, match=message):
|
||||
svds(A, k=k, v0=v0, solver=self.solver)
|
||||
|
||||
def test_svds_input_validation_v0_2(self):
|
||||
A = np.ones((10, 10))
|
||||
v0 = np.ones((1, 10))
|
||||
message = "`v0` must have shape"
|
||||
with pytest.raises(ValueError, match=message):
|
||||
svds(A, k=1, v0=v0, solver=self.solver)
|
||||
|
||||
@pytest.mark.parametrize("v0", ("hi", 1, np.ones(10, dtype=int)))
|
||||
def test_svds_input_validation_v0_3(self, v0):
|
||||
A = np.ones((10, 10))
|
||||
message = "`v0` must be of floating or complex floating data type."
|
||||
with pytest.raises(ValueError, match=message):
|
||||
svds(A, k=1, v0=v0, solver=self.solver)
|
||||
|
||||
@pytest.mark.parametrize("maxiter", (-1, 0, 5.5))
|
||||
def test_svds_input_validation_maxiter_1(self, maxiter):
|
||||
message = ("`maxiter` must be a positive integer.")
|
||||
with pytest.raises(ValueError, match=message):
|
||||
svds(np.eye(10), maxiter=maxiter, solver=self.solver)
|
||||
|
||||
def test_svds_input_validation_maxiter_2(self):
|
||||
# I think the stack trace is reasonable when `k` can't be converted
|
||||
# to an int.
|
||||
message = "int() argument must be a"
|
||||
with pytest.raises(TypeError, match=re.escape(message)):
|
||||
svds(np.eye(10), maxiter=[], solver=self.solver)
|
||||
|
||||
message = "invalid literal for int()"
|
||||
with pytest.raises(ValueError, match=message):
|
||||
svds(np.eye(10), maxiter="hi", solver=self.solver)
|
||||
|
||||
@pytest.mark.parametrize("rsv", ('ekki', 10))
|
||||
def test_svds_input_validation_return_singular_vectors(self, rsv):
|
||||
message = "`return_singular_vectors` must be in"
|
||||
with pytest.raises(ValueError, match=message):
|
||||
svds(np.eye(10), return_singular_vectors=rsv, solver=self.solver)
|
||||
|
||||
# --- Test Parameters ---
|
||||
|
||||
@pytest.mark.parametrize("k", [3, 5])
|
||||
@pytest.mark.parametrize("which", ["LM", "SM"])
|
||||
def test_svds_parameter_k_which(self, k, which):
|
||||
# check that the `k` parameter sets the number of eigenvalues/
|
||||
# eigenvectors returned.
|
||||
# Also check that the `which` parameter sets whether the largest or
|
||||
# smallest eigenvalues are returned
|
||||
rng = np.random.default_rng(0)
|
||||
A = rng.random((10, 10))
|
||||
if self.solver == 'lobpcg':
|
||||
with pytest.warns(UserWarning, match="The problem size"):
|
||||
res = svds(A, k=k, which=which, solver=self.solver,
|
||||
random_state=0)
|
||||
else:
|
||||
res = svds(A, k=k, which=which, solver=self.solver,
|
||||
random_state=0)
|
||||
_check_svds(A, k, *res, which=which, atol=1e-9, rtol=2e-13)
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:Exited",
|
||||
reason="Ignore LOBPCG early exit.")
|
||||
# loop instead of parametrize for simplicity
|
||||
def test_svds_parameter_tol(self):
|
||||
# check the effect of the `tol` parameter on solver accuracy by solving
|
||||
# the same problem with varying `tol` and comparing the eigenvalues
|
||||
# against ground truth computed
|
||||
n = 100 # matrix size
|
||||
k = 3 # number of eigenvalues to check
|
||||
|
||||
# generate a random, sparse-ish matrix
|
||||
# effect isn't apparent for matrices that are too small
|
||||
rng = np.random.default_rng(0)
|
||||
A = rng.random((n, n))
|
||||
A[A > .1] = 0
|
||||
A = A @ A.T
|
||||
|
||||
_, s, _ = svd(A) # calculate ground truth
|
||||
|
||||
# calculate the error as a function of `tol`
|
||||
A = csc_matrix(A)
|
||||
|
||||
def err(tol):
|
||||
_, s2, _ = svds(A, k=k, v0=np.ones(n), maxiter=1000,
|
||||
solver=self.solver, tol=tol, random_state=0)
|
||||
return np.linalg.norm((s2 - s[k-1::-1])/s[k-1::-1])
|
||||
|
||||
tols = [1e-4, 1e-2, 1e0] # tolerance levels to check
|
||||
# for 'arpack' and 'propack', accuracies make discrete steps
|
||||
accuracies = {'propack': [1e-12, 1e-6, 1e-4],
|
||||
'arpack': [2.5e-15, 1e-10, 1e-10],
|
||||
'lobpcg': [2e-12, 4e-2, 2]}
|
||||
|
||||
for tol, accuracy in zip(tols, accuracies[self.solver]):
|
||||
error = err(tol)
|
||||
assert error < accuracy
|
||||
|
||||
def test_svd_v0(self):
|
||||
# check that the `v0` parameter affects the solution
|
||||
n = 100
|
||||
k = 1
|
||||
# If k != 1, LOBPCG needs more initial vectors, which are generated
|
||||
# with random_state, so it does not pass w/ k >= 2.
|
||||
# For some other values of `n`, the AssertionErrors are not raised
|
||||
# with different v0s, which is reasonable.
|
||||
|
||||
rng = np.random.default_rng(0)
|
||||
A = rng.random((n, n))
|
||||
|
||||
# with the same v0, solutions are the same, and they are accurate
|
||||
# v0 takes precedence over random_state
|
||||
v0a = rng.random(n)
|
||||
res1a = svds(A, k, v0=v0a, solver=self.solver, random_state=0)
|
||||
res2a = svds(A, k, v0=v0a, solver=self.solver, random_state=1)
|
||||
for idx in range(3):
|
||||
assert_allclose(res1a[idx], res2a[idx], rtol=1e-15, atol=2e-16)
|
||||
_check_svds(A, k, *res1a)
|
||||
|
||||
# with the same v0, solutions are the same, and they are accurate
|
||||
v0b = rng.random(n)
|
||||
res1b = svds(A, k, v0=v0b, solver=self.solver, random_state=2)
|
||||
res2b = svds(A, k, v0=v0b, solver=self.solver, random_state=3)
|
||||
for idx in range(3):
|
||||
assert_allclose(res1b[idx], res2b[idx], rtol=1e-15, atol=2e-16)
|
||||
_check_svds(A, k, *res1b)
|
||||
|
||||
# with different v0, solutions can be numerically different
|
||||
message = "Arrays are not equal"
|
||||
with pytest.raises(AssertionError, match=message):
|
||||
assert_equal(res1a, res1b)
|
||||
|
||||
def test_svd_random_state(self):
|
||||
# check that the `random_state` parameter affects the solution
|
||||
# Admittedly, `n` and `k` are chosen so that all solver pass all
|
||||
# these checks. That's a tall order, since LOBPCG doesn't want to
|
||||
# achieve the desired accuracy and ARPACK often returns the same
|
||||
# singular values/vectors for different v0.
|
||||
n = 100
|
||||
k = 1
|
||||
|
||||
rng = np.random.default_rng(0)
|
||||
A = rng.random((n, n))
|
||||
|
||||
# with the same random_state, solutions are the same and accurate
|
||||
res1a = svds(A, k, solver=self.solver, random_state=0)
|
||||
res2a = svds(A, k, solver=self.solver, random_state=0)
|
||||
for idx in range(3):
|
||||
assert_allclose(res1a[idx], res2a[idx], rtol=1e-15, atol=2e-16)
|
||||
_check_svds(A, k, *res1a)
|
||||
|
||||
# with the same random_state, solutions are the same and accurate
|
||||
res1b = svds(A, k, solver=self.solver, random_state=1)
|
||||
res2b = svds(A, k, solver=self.solver, random_state=1)
|
||||
for idx in range(3):
|
||||
assert_allclose(res1b[idx], res2b[idx], rtol=1e-15, atol=2e-16)
|
||||
_check_svds(A, k, *res1b)
|
||||
|
||||
# with different random_state, solutions can be numerically different
|
||||
message = "Arrays are not equal"
|
||||
with pytest.raises(AssertionError, match=message):
|
||||
assert_equal(res1a, res1b)
|
||||
|
||||
@pytest.mark.parametrize("random_state", (0, 1,
|
||||
np.random.RandomState(0),
|
||||
np.random.default_rng(0)))
|
||||
def test_svd_random_state_2(self, random_state):
|
||||
n = 100
|
||||
k = 1
|
||||
|
||||
rng = np.random.default_rng(0)
|
||||
A = rng.random((n, n))
|
||||
|
||||
random_state_2 = copy.deepcopy(random_state)
|
||||
|
||||
# with the same random_state, solutions are the same and accurate
|
||||
res1a = svds(A, k, solver=self.solver, random_state=random_state)
|
||||
res2a = svds(A, k, solver=self.solver, random_state=random_state_2)
|
||||
for idx in range(3):
|
||||
assert_allclose(res1a[idx], res2a[idx], rtol=1e-15, atol=2e-16)
|
||||
_check_svds(A, k, *res1a)
|
||||
|
||||
@pytest.mark.parametrize("random_state", (None,
|
||||
np.random.RandomState(0),
|
||||
np.random.default_rng(0)))
|
||||
@pytest.mark.filterwarnings("ignore:Exited",
|
||||
reason="Ignore LOBPCG early exit.")
|
||||
def test_svd_random_state_3(self, random_state):
|
||||
n = 100
|
||||
k = 5
|
||||
|
||||
rng = np.random.default_rng(0)
|
||||
A = rng.random((n, n))
|
||||
|
||||
random_state = copy.deepcopy(random_state)
|
||||
|
||||
# random_state in different state produces accurate - but not
|
||||
# not necessarily identical - results
|
||||
res1a = svds(A, k, solver=self.solver, random_state=random_state, maxiter=1000)
|
||||
res2a = svds(A, k, solver=self.solver, random_state=random_state, maxiter=1000)
|
||||
_check_svds(A, k, *res1a, atol=2e-7)
|
||||
_check_svds(A, k, *res2a, atol=2e-7)
|
||||
|
||||
message = "Arrays are not equal"
|
||||
with pytest.raises(AssertionError, match=message):
|
||||
assert_equal(res1a, res2a)
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
|
||||
def test_svd_maxiter(self):
|
||||
# check that maxiter works as expected: should not return accurate
|
||||
# solution after 1 iteration, but should with default `maxiter`
|
||||
A = np.diag(np.arange(9)).astype(np.float64)
|
||||
k = 1
|
||||
u, s, vh = sorted_svd(A, k)
|
||||
# Use default maxiter by default
|
||||
maxiter = None
|
||||
|
||||
if self.solver == 'arpack':
|
||||
message = "ARPACK error -1: No convergence"
|
||||
with pytest.raises(ArpackNoConvergence, match=message):
|
||||
svds(A, k, ncv=3, maxiter=1, solver=self.solver)
|
||||
elif self.solver == 'lobpcg':
|
||||
# Set maxiter higher so test passes without changing
|
||||
# default and breaking backward compatibility (gh-20221)
|
||||
maxiter = 30
|
||||
with pytest.warns(UserWarning, match="Exited at iteration"):
|
||||
svds(A, k, maxiter=1, solver=self.solver)
|
||||
elif self.solver == 'propack':
|
||||
message = "k=1 singular triplets did not converge within"
|
||||
with pytest.raises(np.linalg.LinAlgError, match=message):
|
||||
svds(A, k, maxiter=1, solver=self.solver)
|
||||
|
||||
ud, sd, vhd = svds(A, k, solver=self.solver, maxiter=maxiter,
|
||||
random_state=0)
|
||||
_check_svds(A, k, ud, sd, vhd, atol=1e-8)
|
||||
assert_allclose(np.abs(ud), np.abs(u), atol=1e-8)
|
||||
assert_allclose(np.abs(vhd), np.abs(vh), atol=1e-8)
|
||||
assert_allclose(np.abs(sd), np.abs(s), atol=1e-9)
|
||||
|
||||
@pytest.mark.parametrize("rsv", (True, False, 'u', 'vh'))
|
||||
@pytest.mark.parametrize("shape", ((5, 7), (6, 6), (7, 5)))
|
||||
def test_svd_return_singular_vectors(self, rsv, shape):
|
||||
# check that the return_singular_vectors parameter works as expected
|
||||
rng = np.random.default_rng(0)
|
||||
A = rng.random(shape)
|
||||
k = 2
|
||||
M, N = shape
|
||||
u, s, vh = sorted_svd(A, k)
|
||||
|
||||
respect_u = True if self.solver == 'propack' else M <= N
|
||||
respect_vh = True if self.solver == 'propack' else M > N
|
||||
|
||||
if self.solver == 'lobpcg':
|
||||
with pytest.warns(UserWarning, match="The problem size"):
|
||||
if rsv is False:
|
||||
s2 = svds(A, k, return_singular_vectors=rsv,
|
||||
solver=self.solver, random_state=rng)
|
||||
assert_allclose(s2, s)
|
||||
elif rsv == 'u' and respect_u:
|
||||
u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
|
||||
solver=self.solver, random_state=rng)
|
||||
assert_allclose(np.abs(u2), np.abs(u))
|
||||
assert_allclose(s2, s)
|
||||
assert vh2 is None
|
||||
elif rsv == 'vh' and respect_vh:
|
||||
u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
|
||||
solver=self.solver, random_state=rng)
|
||||
assert u2 is None
|
||||
assert_allclose(s2, s)
|
||||
assert_allclose(np.abs(vh2), np.abs(vh))
|
||||
else:
|
||||
u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
|
||||
solver=self.solver, random_state=rng)
|
||||
if u2 is not None:
|
||||
assert_allclose(np.abs(u2), np.abs(u))
|
||||
assert_allclose(s2, s)
|
||||
if vh2 is not None:
|
||||
assert_allclose(np.abs(vh2), np.abs(vh))
|
||||
else:
|
||||
if rsv is False:
|
||||
s2 = svds(A, k, return_singular_vectors=rsv,
|
||||
solver=self.solver, random_state=rng)
|
||||
assert_allclose(s2, s)
|
||||
elif rsv == 'u' and respect_u:
|
||||
u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
|
||||
solver=self.solver, random_state=rng)
|
||||
assert_allclose(np.abs(u2), np.abs(u))
|
||||
assert_allclose(s2, s)
|
||||
assert vh2 is None
|
||||
elif rsv == 'vh' and respect_vh:
|
||||
u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
|
||||
solver=self.solver, random_state=rng)
|
||||
assert u2 is None
|
||||
assert_allclose(s2, s)
|
||||
assert_allclose(np.abs(vh2), np.abs(vh))
|
||||
else:
|
||||
u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv,
|
||||
solver=self.solver, random_state=rng)
|
||||
if u2 is not None:
|
||||
assert_allclose(np.abs(u2), np.abs(u))
|
||||
assert_allclose(s2, s)
|
||||
if vh2 is not None:
|
||||
assert_allclose(np.abs(vh2), np.abs(vh))
|
||||
|
||||
# --- Test Basic Functionality ---
|
||||
# Tests the accuracy of each solver for real and complex matrices provided
|
||||
# as list, dense array, sparse matrix, and LinearOperator.
|
||||
|
||||
A1 = [[1, 2, 3], [3, 4, 3], [1 + 1j, 0, 2], [0, 0, 1]]
|
||||
A2 = [[1, 2, 3, 8 + 5j], [3 - 2j, 4, 3, 5], [1, 0, 2, 3], [0, 0, 1, 0]]
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:k >= N - 1",
|
||||
reason="needed to demonstrate #16725")
|
||||
@pytest.mark.parametrize('A', (A1, A2))
|
||||
@pytest.mark.parametrize('k', range(1, 5))
|
||||
# PROPACK fails a lot if @pytest.mark.parametrize('which', ("SM", "LM"))
|
||||
@pytest.mark.parametrize('real', (True, False))
|
||||
@pytest.mark.parametrize('transpose', (False, True))
|
||||
# In gh-14299, it was suggested the `svds` should _not_ work with lists
|
||||
@pytest.mark.parametrize('lo_type', (np.asarray, csc_matrix,
|
||||
aslinearoperator))
|
||||
def test_svd_simple(self, A, k, real, transpose, lo_type):
|
||||
|
||||
A = np.asarray(A)
|
||||
A = np.real(A) if real else A
|
||||
A = A.T if transpose else A
|
||||
A2 = lo_type(A)
|
||||
|
||||
# could check for the appropriate errors, but that is tested above
|
||||
if k > min(A.shape):
|
||||
pytest.skip("`k` cannot be greater than `min(A.shape)`")
|
||||
if self.solver != 'propack' and k >= min(A.shape):
|
||||
pytest.skip("Only PROPACK supports complete SVD")
|
||||
if self.solver == 'arpack' and not real and k == min(A.shape) - 1:
|
||||
pytest.skip("#16725")
|
||||
|
||||
atol = 3e-10
|
||||
if self.solver == 'propack':
|
||||
atol = 3e-9 # otherwise test fails on Linux aarch64 (see gh-19855)
|
||||
|
||||
if self.solver == 'lobpcg':
|
||||
with pytest.warns(UserWarning, match="The problem size"):
|
||||
u, s, vh = svds(A2, k, solver=self.solver, random_state=0)
|
||||
else:
|
||||
u, s, vh = svds(A2, k, solver=self.solver, random_state=0)
|
||||
_check_svds(A, k, u, s, vh, atol=atol)
|
||||
|
||||
def test_svd_linop(self):
|
||||
solver = self.solver
|
||||
|
||||
nmks = [(6, 7, 3),
|
||||
(9, 5, 4),
|
||||
(10, 8, 5)]
|
||||
|
||||
def reorder(args):
|
||||
U, s, VH = args
|
||||
j = np.argsort(s)
|
||||
return U[:, j], s[j], VH[j, :]
|
||||
|
||||
for n, m, k in nmks:
|
||||
# Test svds on a LinearOperator.
|
||||
A = np.random.RandomState(52).randn(n, m)
|
||||
L = CheckingLinearOperator(A)
|
||||
|
||||
if solver == 'propack':
|
||||
v0 = np.ones(n)
|
||||
else:
|
||||
v0 = np.ones(min(A.shape))
|
||||
if solver == 'lobpcg':
|
||||
with pytest.warns(UserWarning, match="The problem size"):
|
||||
U1, s1, VH1 = reorder(svds(A, k, v0=v0, solver=solver,
|
||||
random_state=0))
|
||||
U2, s2, VH2 = reorder(svds(L, k, v0=v0, solver=solver,
|
||||
random_state=0))
|
||||
else:
|
||||
U1, s1, VH1 = reorder(svds(A, k, v0=v0, solver=solver,
|
||||
random_state=0))
|
||||
U2, s2, VH2 = reorder(svds(L, k, v0=v0, solver=solver,
|
||||
random_state=0))
|
||||
|
||||
assert_allclose(np.abs(U1), np.abs(U2))
|
||||
assert_allclose(s1, s2)
|
||||
assert_allclose(np.abs(VH1), np.abs(VH2))
|
||||
assert_allclose(np.dot(U1, np.dot(np.diag(s1), VH1)),
|
||||
np.dot(U2, np.dot(np.diag(s2), VH2)))
|
||||
|
||||
# Try again with which="SM".
|
||||
A = np.random.RandomState(1909).randn(n, m)
|
||||
L = CheckingLinearOperator(A)
|
||||
|
||||
# TODO: arpack crashes when v0=v0, which="SM"
|
||||
kwargs = {'v0': v0} if solver not in {None, 'arpack'} else {}
|
||||
if self.solver == 'lobpcg':
|
||||
with pytest.warns(UserWarning, match="The problem size"):
|
||||
U1, s1, VH1 = reorder(svds(A, k, which="SM", solver=solver,
|
||||
random_state=0, **kwargs))
|
||||
U2, s2, VH2 = reorder(svds(L, k, which="SM", solver=solver,
|
||||
random_state=0, **kwargs))
|
||||
else:
|
||||
U1, s1, VH1 = reorder(svds(A, k, which="SM", solver=solver,
|
||||
random_state=0, **kwargs))
|
||||
U2, s2, VH2 = reorder(svds(L, k, which="SM", solver=solver,
|
||||
random_state=0, **kwargs))
|
||||
|
||||
assert_allclose(np.abs(U1), np.abs(U2))
|
||||
assert_allclose(s1 + 1, s2 + 1)
|
||||
assert_allclose(np.abs(VH1), np.abs(VH2))
|
||||
assert_allclose(np.dot(U1, np.dot(np.diag(s1), VH1)),
|
||||
np.dot(U2, np.dot(np.diag(s2), VH2)))
|
||||
|
||||
if k < min(n, m) - 1:
|
||||
# Complex input and explicit which="LM".
|
||||
for (dt, eps) in [(complex, 1e-7), (np.complex64, 3e-3)]:
|
||||
rng = np.random.RandomState(1648)
|
||||
A = (rng.randn(n, m) + 1j * rng.randn(n, m)).astype(dt)
|
||||
L = CheckingLinearOperator(A)
|
||||
|
||||
if self.solver == 'lobpcg':
|
||||
with pytest.warns(UserWarning,
|
||||
match="The problem size"):
|
||||
U1, s1, VH1 = reorder(svds(A, k, which="LM",
|
||||
solver=solver,
|
||||
random_state=0))
|
||||
U2, s2, VH2 = reorder(svds(L, k, which="LM",
|
||||
solver=solver,
|
||||
random_state=0))
|
||||
else:
|
||||
U1, s1, VH1 = reorder(svds(A, k, which="LM",
|
||||
solver=solver,
|
||||
random_state=0))
|
||||
U2, s2, VH2 = reorder(svds(L, k, which="LM",
|
||||
solver=solver,
|
||||
random_state=0))
|
||||
|
||||
assert_allclose(np.abs(U1), np.abs(U2), rtol=eps)
|
||||
assert_allclose(s1, s2, rtol=eps)
|
||||
assert_allclose(np.abs(VH1), np.abs(VH2), rtol=eps)
|
||||
assert_allclose(np.dot(U1, np.dot(np.diag(s1), VH1)),
|
||||
np.dot(U2, np.dot(np.diag(s2), VH2)),
|
||||
rtol=eps)
|
||||
|
||||
SHAPES = ((100, 100), (100, 101), (101, 100))
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:Exited at iteration")
|
||||
@pytest.mark.filterwarnings("ignore:Exited postprocessing")
|
||||
@pytest.mark.parametrize("shape", SHAPES)
|
||||
# ARPACK supports only dtype float, complex, or np.float32
|
||||
@pytest.mark.parametrize("dtype", (float, complex, np.float32))
|
||||
def test_small_sigma_sparse(self, shape, dtype):
|
||||
# https://github.com/scipy/scipy/pull/11829
|
||||
solver = self.solver
|
||||
# 2do: PROPACK fails orthogonality of singular vectors
|
||||
# if dtype == complex and self.solver == 'propack':
|
||||
# pytest.skip("PROPACK unsupported for complex dtype")
|
||||
rng = np.random.default_rng(0)
|
||||
k = 5
|
||||
(m, n) = shape
|
||||
S = random(m, n, density=0.1, random_state=rng)
|
||||
if dtype == complex:
|
||||
S = + 1j * random(m, n, density=0.1, random_state=rng)
|
||||
e = np.ones(m)
|
||||
e[0:5] *= 1e1 ** np.arange(-5, 0, 1)
|
||||
S = spdiags(e, 0, m, m) @ S
|
||||
S = S.astype(dtype)
|
||||
u, s, vh = svds(S, k, which='SM', solver=solver, maxiter=1000,
|
||||
random_state=0)
|
||||
c_svd = False # partial SVD can be different from full SVD
|
||||
_check_svds_n(S, k, u, s, vh, which="SM", check_svd=c_svd, atol=2e-1)
|
||||
|
||||
# --- Test Edge Cases ---
|
||||
# Checks a few edge cases.
|
||||
|
||||
@pytest.mark.parametrize("shape", ((6, 5), (5, 5), (5, 6)))
|
||||
@pytest.mark.parametrize("dtype", (float, complex))
|
||||
def test_svd_LM_ones_matrix(self, shape, dtype):
|
||||
# Check that svds can deal with matrix_rank less than k in LM mode.
|
||||
k = 3
|
||||
n, m = shape
|
||||
A = np.ones((n, m), dtype=dtype)
|
||||
|
||||
if self.solver == 'lobpcg':
|
||||
with pytest.warns(UserWarning, match="The problem size"):
|
||||
U, s, VH = svds(A, k, solver=self.solver, random_state=0)
|
||||
else:
|
||||
U, s, VH = svds(A, k, solver=self.solver, random_state=0)
|
||||
|
||||
_check_svds(A, k, U, s, VH, check_usvh_A=True, check_svd=False)
|
||||
|
||||
# Check that the largest singular value is near sqrt(n*m)
|
||||
# and the other singular values have been forced to zero.
|
||||
assert_allclose(np.max(s), np.sqrt(n*m))
|
||||
s = np.array(sorted(s)[:-1]) + 1
|
||||
z = np.ones_like(s)
|
||||
assert_allclose(s, z)
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:k >= N - 1",
|
||||
reason="needed to demonstrate #16725")
|
||||
@pytest.mark.parametrize("shape", ((3, 4), (4, 4), (4, 3), (4, 2)))
|
||||
@pytest.mark.parametrize("dtype", (float, complex))
|
||||
def test_zero_matrix(self, shape, dtype):
|
||||
# Check that svds can deal with matrices containing only zeros;
|
||||
# see https://github.com/scipy/scipy/issues/3452/
|
||||
# shape = (4, 2) is included because it is the particular case
|
||||
# reported in the issue
|
||||
k = 1
|
||||
n, m = shape
|
||||
A = np.zeros((n, m), dtype=dtype)
|
||||
|
||||
if (self.solver == 'arpack' and dtype is complex
|
||||
and k == min(A.shape) - 1):
|
||||
pytest.skip("#16725")
|
||||
|
||||
if self.solver == 'propack':
|
||||
pytest.skip("PROPACK failures unrelated to PR #16712")
|
||||
|
||||
if self.solver == 'lobpcg':
|
||||
with pytest.warns(UserWarning, match="The problem size"):
|
||||
U, s, VH = svds(A, k, solver=self.solver, random_state=0)
|
||||
else:
|
||||
U, s, VH = svds(A, k, solver=self.solver, random_state=0)
|
||||
|
||||
# Check some generic properties of svd.
|
||||
_check_svds(A, k, U, s, VH, check_usvh_A=True, check_svd=False)
|
||||
|
||||
# Check that the singular values are zero.
|
||||
assert_array_equal(s, 0)
|
||||
|
||||
@pytest.mark.parametrize("shape", ((20, 20), (20, 21), (21, 20)))
|
||||
# ARPACK supports only dtype float, complex, or np.float32
|
||||
@pytest.mark.parametrize("dtype", (float, complex, np.float32))
|
||||
@pytest.mark.filterwarnings("ignore:Exited",
|
||||
reason="Ignore LOBPCG early exit.")
|
||||
def test_small_sigma(self, shape, dtype):
|
||||
rng = np.random.default_rng(179847540)
|
||||
A = rng.random(shape).astype(dtype)
|
||||
u, _, vh = svd(A, full_matrices=False)
|
||||
if dtype == np.float32:
|
||||
e = 10.0
|
||||
else:
|
||||
e = 100.0
|
||||
t = e**(-np.arange(len(vh))).astype(dtype)
|
||||
A = (u*t).dot(vh)
|
||||
k = 4
|
||||
u, s, vh = svds(A, k, solver=self.solver, maxiter=100, random_state=0)
|
||||
t = np.sum(s > 0)
|
||||
assert_equal(t, k)
|
||||
# LOBPCG needs larger atol and rtol to pass
|
||||
_check_svds_n(A, k, u, s, vh, atol=1e-3, rtol=1e0, check_svd=False)
|
||||
|
||||
# ARPACK supports only dtype float, complex, or np.float32
|
||||
@pytest.mark.filterwarnings("ignore:The problem size")
|
||||
@pytest.mark.parametrize("dtype", (float, complex, np.float32))
|
||||
def test_small_sigma2(self, dtype):
|
||||
rng = np.random.default_rng(179847540)
|
||||
# create a 10x10 singular matrix with a 4-dim null space
|
||||
dim = 4
|
||||
size = 10
|
||||
x = rng.random((size, size-dim))
|
||||
y = x[:, :dim] * rng.random(dim)
|
||||
mat = np.hstack((x, y))
|
||||
mat = mat.astype(dtype)
|
||||
|
||||
nz = null_space(mat)
|
||||
assert_equal(nz.shape[1], dim)
|
||||
|
||||
# Tolerances atol and rtol adjusted to pass np.float32
|
||||
# Use non-sparse svd
|
||||
u, s, vh = svd(mat)
|
||||
# Singular values are 0:
|
||||
assert_allclose(s[-dim:], 0, atol=1e-6, rtol=1e0)
|
||||
# Smallest right singular vectors in null space:
|
||||
assert_allclose(mat @ vh[-dim:, :].T, 0, atol=1e-6, rtol=1e0)
|
||||
|
||||
# Smallest singular values should be 0
|
||||
sp_mat = csc_matrix(mat)
|
||||
su, ss, svh = svds(sp_mat, k=dim, which='SM', solver=self.solver,
|
||||
random_state=0)
|
||||
# Smallest dim singular values are 0:
|
||||
assert_allclose(ss, 0, atol=1e-5, rtol=1e0)
|
||||
# Smallest singular vectors via svds in null space:
|
||||
n, m = mat.shape
|
||||
if n < m: # else the assert fails with some libraries unclear why
|
||||
assert_allclose(sp_mat.transpose() @ su, 0, atol=1e-5, rtol=1e0)
|
||||
assert_allclose(sp_mat @ svh.T, 0, atol=1e-5, rtol=1e0)
|
||||
|
||||
# --- Perform tests with each solver ---
|
||||
|
||||
|
||||
class Test_SVDS_once:
|
||||
@pytest.mark.parametrize("solver", ['ekki', object])
|
||||
def test_svds_input_validation_solver(self, solver):
|
||||
message = "solver must be one of"
|
||||
with pytest.raises(ValueError, match=message):
|
||||
svds(np.ones((3, 4)), k=2, solver=solver)
|
||||
|
||||
|
||||
class Test_SVDS_ARPACK(SVDSCommonTests):
|
||||
|
||||
def setup_method(self):
|
||||
self.solver = 'arpack'
|
||||
|
||||
@pytest.mark.parametrize("ncv", list(range(-1, 8)) + [4.5, "5"])
|
||||
def test_svds_input_validation_ncv_1(self, ncv):
|
||||
rng = np.random.default_rng(0)
|
||||
A = rng.random((6, 7))
|
||||
k = 3
|
||||
if ncv in {4, 5}:
|
||||
u, s, vh = svds(A, k=k, ncv=ncv, solver=self.solver, random_state=0)
|
||||
# partial decomposition, so don't check that u@diag(s)@vh=A;
|
||||
# do check that scipy.sparse.linalg.svds ~ scipy.linalg.svd
|
||||
_check_svds(A, k, u, s, vh)
|
||||
else:
|
||||
message = ("`ncv` must be an integer satisfying")
|
||||
with pytest.raises(ValueError, match=message):
|
||||
svds(A, k=k, ncv=ncv, solver=self.solver)
|
||||
|
||||
def test_svds_input_validation_ncv_2(self):
|
||||
# I think the stack trace is reasonable when `ncv` can't be converted
|
||||
# to an int.
|
||||
message = "int() argument must be a"
|
||||
with pytest.raises(TypeError, match=re.escape(message)):
|
||||
svds(np.eye(10), ncv=[], solver=self.solver)
|
||||
|
||||
message = "invalid literal for int()"
|
||||
with pytest.raises(ValueError, match=message):
|
||||
svds(np.eye(10), ncv="hi", solver=self.solver)
|
||||
|
||||
# I can't see a robust relationship between `ncv` and relevant outputs
|
||||
# (e.g. accuracy, time), so no test of the parameter.
|
||||
|
||||
|
||||
class Test_SVDS_LOBPCG(SVDSCommonTests):
|
||||
|
||||
def setup_method(self):
|
||||
self.solver = 'lobpcg'
|
||||
|
||||
|
||||
class Test_SVDS_PROPACK(SVDSCommonTests):
|
||||
|
||||
def setup_method(self):
|
||||
self.solver = 'propack'
|
||||
|
||||
def test_svd_LM_ones_matrix(self):
|
||||
message = ("PROPACK does not return orthonormal singular vectors "
|
||||
"associated with zero singular values.")
|
||||
# There are some other issues with this matrix of all ones, e.g.
|
||||
# `which='sm'` and `k=1` returns the largest singular value
|
||||
pytest.xfail(message)
|
||||
|
||||
def test_svd_LM_zeros_matrix(self):
|
||||
message = ("PROPACK does not return orthonormal singular vectors "
|
||||
"associated with zero singular values.")
|
||||
pytest.xfail(message)
|
||||
@ -0,0 +1,810 @@
|
||||
"""Compute the action of the matrix exponential."""
|
||||
from warnings import warn
|
||||
|
||||
import numpy as np
|
||||
|
||||
import scipy.linalg
|
||||
import scipy.sparse.linalg
|
||||
from scipy.linalg._decomp_qr import qr
|
||||
from scipy.sparse._sputils import is_pydata_spmatrix
|
||||
from scipy.sparse.linalg import aslinearoperator
|
||||
from scipy.sparse.linalg._interface import IdentityOperator
|
||||
from scipy.sparse.linalg._onenormest import onenormest
|
||||
|
||||
__all__ = ['expm_multiply']
|
||||
|
||||
|
||||
def _exact_inf_norm(A):
|
||||
# A compatibility function which should eventually disappear.
|
||||
if scipy.sparse.issparse(A):
|
||||
return max(abs(A).sum(axis=1).flat)
|
||||
elif is_pydata_spmatrix(A):
|
||||
return max(abs(A).sum(axis=1))
|
||||
else:
|
||||
return np.linalg.norm(A, np.inf)
|
||||
|
||||
|
||||
def _exact_1_norm(A):
|
||||
# A compatibility function which should eventually disappear.
|
||||
if scipy.sparse.issparse(A):
|
||||
return max(abs(A).sum(axis=0).flat)
|
||||
elif is_pydata_spmatrix(A):
|
||||
return max(abs(A).sum(axis=0))
|
||||
else:
|
||||
return np.linalg.norm(A, 1)
|
||||
|
||||
|
||||
def _trace(A):
|
||||
# A compatibility function which should eventually disappear.
|
||||
if is_pydata_spmatrix(A):
|
||||
return A.to_scipy_sparse().trace()
|
||||
else:
|
||||
return A.trace()
|
||||
|
||||
|
||||
def traceest(A, m3, seed=None):
|
||||
"""Estimate `np.trace(A)` using `3*m3` matrix-vector products.
|
||||
|
||||
The result is not deterministic.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : LinearOperator
|
||||
Linear operator whose trace will be estimated. Has to be square.
|
||||
m3 : int
|
||||
Number of matrix-vector products divided by 3 used to estimate the
|
||||
trace.
|
||||
seed : optional
|
||||
Seed for `numpy.random.default_rng`.
|
||||
Can be provided to obtain deterministic results.
|
||||
|
||||
Returns
|
||||
-------
|
||||
trace : LinearOperator.dtype
|
||||
Estimate of the trace
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is the Hutch++ algorithm given in [1]_.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Meyer, Raphael A., Cameron Musco, Christopher Musco, and David P.
|
||||
Woodruff. "Hutch++: Optimal Stochastic Trace Estimation." In Symposium
|
||||
on Simplicity in Algorithms (SOSA), pp. 142-155. Society for Industrial
|
||||
and Applied Mathematics, 2021
|
||||
https://doi.org/10.1137/1.9781611976496.16
|
||||
|
||||
"""
|
||||
rng = np.random.default_rng(seed)
|
||||
if len(A.shape) != 2 or A.shape[-1] != A.shape[-2]:
|
||||
raise ValueError("Expected A to be like a square matrix.")
|
||||
n = A.shape[-1]
|
||||
S = rng.choice([-1.0, +1.0], [n, m3])
|
||||
Q, _ = qr(A.matmat(S), overwrite_a=True, mode='economic')
|
||||
trQAQ = np.trace(Q.conj().T @ A.matmat(Q))
|
||||
G = rng.choice([-1, +1], [n, m3])
|
||||
right = G - Q@(Q.conj().T @ G)
|
||||
trGAG = np.trace(right.conj().T @ A.matmat(right))
|
||||
return trQAQ + trGAG/m3
|
||||
|
||||
|
||||
def _ident_like(A):
|
||||
# A compatibility function which should eventually disappear.
|
||||
if scipy.sparse.issparse(A):
|
||||
# Creates a sparse matrix in dia format
|
||||
out = scipy.sparse.eye(A.shape[0], A.shape[1], dtype=A.dtype)
|
||||
if isinstance(A, scipy.sparse.spmatrix):
|
||||
return out.asformat(A.format)
|
||||
return scipy.sparse.dia_array(out).asformat(A.format)
|
||||
elif is_pydata_spmatrix(A):
|
||||
import sparse
|
||||
return sparse.eye(A.shape[0], A.shape[1], dtype=A.dtype)
|
||||
elif isinstance(A, scipy.sparse.linalg.LinearOperator):
|
||||
return IdentityOperator(A.shape, dtype=A.dtype)
|
||||
else:
|
||||
return np.eye(A.shape[0], A.shape[1], dtype=A.dtype)
|
||||
|
||||
|
||||
def expm_multiply(A, B, start=None, stop=None, num=None,
|
||||
endpoint=None, traceA=None):
|
||||
"""
|
||||
Compute the action of the matrix exponential of A on B.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : transposable linear operator
|
||||
The operator whose exponential is of interest.
|
||||
B : ndarray
|
||||
The matrix or vector to be multiplied by the matrix exponential of A.
|
||||
start : scalar, optional
|
||||
The starting time point of the sequence.
|
||||
stop : scalar, optional
|
||||
The end time point of the sequence, unless `endpoint` is set to False.
|
||||
In that case, the sequence consists of all but the last of ``num + 1``
|
||||
evenly spaced time points, so that `stop` is excluded.
|
||||
Note that the step size changes when `endpoint` is False.
|
||||
num : int, optional
|
||||
Number of time points to use.
|
||||
endpoint : bool, optional
|
||||
If True, `stop` is the last time point. Otherwise, it is not included.
|
||||
traceA : scalar, optional
|
||||
Trace of `A`. If not given the trace is estimated for linear operators,
|
||||
or calculated exactly for sparse matrices. It is used to precondition
|
||||
`A`, thus an approximate trace is acceptable.
|
||||
For linear operators, `traceA` should be provided to ensure performance
|
||||
as the estimation is not guaranteed to be reliable for all cases.
|
||||
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
expm_A_B : ndarray
|
||||
The result of the action :math:`e^{t_k A} B`.
|
||||
|
||||
Warns
|
||||
-----
|
||||
UserWarning
|
||||
If `A` is a linear operator and ``traceA=None`` (default).
|
||||
|
||||
Notes
|
||||
-----
|
||||
The optional arguments defining the sequence of evenly spaced time points
|
||||
are compatible with the arguments of `numpy.linspace`.
|
||||
|
||||
The output ndarray shape is somewhat complicated so I explain it here.
|
||||
The ndim of the output could be either 1, 2, or 3.
|
||||
It would be 1 if you are computing the expm action on a single vector
|
||||
at a single time point.
|
||||
It would be 2 if you are computing the expm action on a vector
|
||||
at multiple time points, or if you are computing the expm action
|
||||
on a matrix at a single time point.
|
||||
It would be 3 if you want the action on a matrix with multiple
|
||||
columns at multiple time points.
|
||||
If multiple time points are requested, expm_A_B[0] will always
|
||||
be the action of the expm at the first time point,
|
||||
regardless of whether the action is on a vector or a matrix.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Awad H. Al-Mohy and Nicholas J. Higham (2011)
|
||||
"Computing the Action of the Matrix Exponential,
|
||||
with an Application to Exponential Integrators."
|
||||
SIAM Journal on Scientific Computing,
|
||||
33 (2). pp. 488-511. ISSN 1064-8275
|
||||
http://eprints.ma.man.ac.uk/1591/
|
||||
|
||||
.. [2] Nicholas J. Higham and Awad H. Al-Mohy (2010)
|
||||
"Computing Matrix Functions."
|
||||
Acta Numerica,
|
||||
19. 159-208. ISSN 0962-4929
|
||||
http://eprints.ma.man.ac.uk/1451/
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> from scipy.sparse.linalg import expm, expm_multiply
|
||||
>>> A = csc_matrix([[1, 0], [0, 1]])
|
||||
>>> A.toarray()
|
||||
array([[1, 0],
|
||||
[0, 1]], dtype=int64)
|
||||
>>> B = np.array([np.exp(-1.), np.exp(-2.)])
|
||||
>>> B
|
||||
array([ 0.36787944, 0.13533528])
|
||||
>>> expm_multiply(A, B, start=1, stop=2, num=3, endpoint=True)
|
||||
array([[ 1. , 0.36787944],
|
||||
[ 1.64872127, 0.60653066],
|
||||
[ 2.71828183, 1. ]])
|
||||
>>> expm(A).dot(B) # Verify 1st timestep
|
||||
array([ 1. , 0.36787944])
|
||||
>>> expm(1.5*A).dot(B) # Verify 2nd timestep
|
||||
array([ 1.64872127, 0.60653066])
|
||||
>>> expm(2*A).dot(B) # Verify 3rd timestep
|
||||
array([ 2.71828183, 1. ])
|
||||
"""
|
||||
if all(arg is None for arg in (start, stop, num, endpoint)):
|
||||
X = _expm_multiply_simple(A, B, traceA=traceA)
|
||||
else:
|
||||
X, status = _expm_multiply_interval(A, B, start, stop, num,
|
||||
endpoint, traceA=traceA)
|
||||
return X
|
||||
|
||||
|
||||
def _expm_multiply_simple(A, B, t=1.0, traceA=None, balance=False):
|
||||
"""
|
||||
Compute the action of the matrix exponential at a single time point.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : transposable linear operator
|
||||
The operator whose exponential is of interest.
|
||||
B : ndarray
|
||||
The matrix to be multiplied by the matrix exponential of A.
|
||||
t : float
|
||||
A time point.
|
||||
traceA : scalar, optional
|
||||
Trace of `A`. If not given the trace is estimated for linear operators,
|
||||
or calculated exactly for sparse matrices. It is used to precondition
|
||||
`A`, thus an approximate trace is acceptable
|
||||
balance : bool
|
||||
Indicates whether or not to apply balancing.
|
||||
|
||||
Returns
|
||||
-------
|
||||
F : ndarray
|
||||
:math:`e^{t A} B`
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is algorithm (3.2) in Al-Mohy and Higham (2011).
|
||||
|
||||
"""
|
||||
if balance:
|
||||
raise NotImplementedError
|
||||
if len(A.shape) != 2 or A.shape[0] != A.shape[1]:
|
||||
raise ValueError('expected A to be like a square matrix')
|
||||
if A.shape[1] != B.shape[0]:
|
||||
raise ValueError('shapes of matrices A {} and B {} are incompatible'
|
||||
.format(A.shape, B.shape))
|
||||
ident = _ident_like(A)
|
||||
is_linear_operator = isinstance(A, scipy.sparse.linalg.LinearOperator)
|
||||
n = A.shape[0]
|
||||
if len(B.shape) == 1:
|
||||
n0 = 1
|
||||
elif len(B.shape) == 2:
|
||||
n0 = B.shape[1]
|
||||
else:
|
||||
raise ValueError('expected B to be like a matrix or a vector')
|
||||
u_d = 2**-53
|
||||
tol = u_d
|
||||
if traceA is None:
|
||||
if is_linear_operator:
|
||||
warn("Trace of LinearOperator not available, it will be estimated."
|
||||
" Provide `traceA` to ensure performance.", stacklevel=3)
|
||||
# m3=1 is bit arbitrary choice, a more accurate trace (larger m3) might
|
||||
# speed up exponential calculation, but trace estimation is more costly
|
||||
traceA = traceest(A, m3=1) if is_linear_operator else _trace(A)
|
||||
mu = traceA / float(n)
|
||||
A = A - mu * ident
|
||||
A_1_norm = onenormest(A) if is_linear_operator else _exact_1_norm(A)
|
||||
if t*A_1_norm == 0:
|
||||
m_star, s = 0, 1
|
||||
else:
|
||||
ell = 2
|
||||
norm_info = LazyOperatorNormInfo(t*A, A_1_norm=t*A_1_norm, ell=ell)
|
||||
m_star, s = _fragment_3_1(norm_info, n0, tol, ell=ell)
|
||||
return _expm_multiply_simple_core(A, B, t, mu, m_star, s, tol, balance)
|
||||
|
||||
|
||||
def _expm_multiply_simple_core(A, B, t, mu, m_star, s, tol=None, balance=False):
|
||||
"""
|
||||
A helper function.
|
||||
"""
|
||||
if balance:
|
||||
raise NotImplementedError
|
||||
if tol is None:
|
||||
u_d = 2 ** -53
|
||||
tol = u_d
|
||||
F = B
|
||||
eta = np.exp(t*mu / float(s))
|
||||
for i in range(s):
|
||||
c1 = _exact_inf_norm(B)
|
||||
for j in range(m_star):
|
||||
coeff = t / float(s*(j+1))
|
||||
B = coeff * A.dot(B)
|
||||
c2 = _exact_inf_norm(B)
|
||||
F = F + B
|
||||
if c1 + c2 <= tol * _exact_inf_norm(F):
|
||||
break
|
||||
c1 = c2
|
||||
F = eta * F
|
||||
B = F
|
||||
return F
|
||||
|
||||
|
||||
# This table helps to compute bounds.
|
||||
# They seem to have been difficult to calculate, involving symbolic
|
||||
# manipulation of equations, followed by numerical root finding.
|
||||
_theta = {
|
||||
# The first 30 values are from table A.3 of Computing Matrix Functions.
|
||||
1: 2.29e-16,
|
||||
2: 2.58e-8,
|
||||
3: 1.39e-5,
|
||||
4: 3.40e-4,
|
||||
5: 2.40e-3,
|
||||
6: 9.07e-3,
|
||||
7: 2.38e-2,
|
||||
8: 5.00e-2,
|
||||
9: 8.96e-2,
|
||||
10: 1.44e-1,
|
||||
# 11
|
||||
11: 2.14e-1,
|
||||
12: 3.00e-1,
|
||||
13: 4.00e-1,
|
||||
14: 5.14e-1,
|
||||
15: 6.41e-1,
|
||||
16: 7.81e-1,
|
||||
17: 9.31e-1,
|
||||
18: 1.09,
|
||||
19: 1.26,
|
||||
20: 1.44,
|
||||
# 21
|
||||
21: 1.62,
|
||||
22: 1.82,
|
||||
23: 2.01,
|
||||
24: 2.22,
|
||||
25: 2.43,
|
||||
26: 2.64,
|
||||
27: 2.86,
|
||||
28: 3.08,
|
||||
29: 3.31,
|
||||
30: 3.54,
|
||||
# The rest are from table 3.1 of
|
||||
# Computing the Action of the Matrix Exponential.
|
||||
35: 4.7,
|
||||
40: 6.0,
|
||||
45: 7.2,
|
||||
50: 8.5,
|
||||
55: 9.9,
|
||||
}
|
||||
|
||||
|
||||
def _onenormest_matrix_power(A, p,
|
||||
t=2, itmax=5, compute_v=False, compute_w=False):
|
||||
"""
|
||||
Efficiently estimate the 1-norm of A^p.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : ndarray
|
||||
Matrix whose 1-norm of a power is to be computed.
|
||||
p : int
|
||||
Non-negative integer power.
|
||||
t : int, optional
|
||||
A positive parameter controlling the tradeoff between
|
||||
accuracy versus time and memory usage.
|
||||
Larger values take longer and use more memory
|
||||
but give more accurate output.
|
||||
itmax : int, optional
|
||||
Use at most this many iterations.
|
||||
compute_v : bool, optional
|
||||
Request a norm-maximizing linear operator input vector if True.
|
||||
compute_w : bool, optional
|
||||
Request a norm-maximizing linear operator output vector if True.
|
||||
|
||||
Returns
|
||||
-------
|
||||
est : float
|
||||
An underestimate of the 1-norm of the sparse matrix.
|
||||
v : ndarray, optional
|
||||
The vector such that ||Av||_1 == est*||v||_1.
|
||||
It can be thought of as an input to the linear operator
|
||||
that gives an output with particularly large norm.
|
||||
w : ndarray, optional
|
||||
The vector Av which has relatively large 1-norm.
|
||||
It can be thought of as an output of the linear operator
|
||||
that is relatively large in norm compared to the input.
|
||||
|
||||
"""
|
||||
#XXX Eventually turn this into an API function in the _onenormest module,
|
||||
#XXX and remove its underscore,
|
||||
#XXX but wait until expm_multiply goes into scipy.
|
||||
from scipy.sparse.linalg._onenormest import onenormest
|
||||
return onenormest(aslinearoperator(A) ** p)
|
||||
|
||||
class LazyOperatorNormInfo:
|
||||
"""
|
||||
Information about an operator is lazily computed.
|
||||
|
||||
The information includes the exact 1-norm of the operator,
|
||||
in addition to estimates of 1-norms of powers of the operator.
|
||||
This uses the notation of Computing the Action (2011).
|
||||
This class is specialized enough to probably not be of general interest
|
||||
outside of this module.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, A, A_1_norm=None, ell=2, scale=1):
|
||||
"""
|
||||
Provide the operator and some norm-related information.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : linear operator
|
||||
The operator of interest.
|
||||
A_1_norm : float, optional
|
||||
The exact 1-norm of A.
|
||||
ell : int, optional
|
||||
A technical parameter controlling norm estimation quality.
|
||||
scale : int, optional
|
||||
If specified, return the norms of scale*A instead of A.
|
||||
|
||||
"""
|
||||
self._A = A
|
||||
self._A_1_norm = A_1_norm
|
||||
self._ell = ell
|
||||
self._d = {}
|
||||
self._scale = scale
|
||||
|
||||
def set_scale(self,scale):
|
||||
"""
|
||||
Set the scale parameter.
|
||||
"""
|
||||
self._scale = scale
|
||||
|
||||
def onenorm(self):
|
||||
"""
|
||||
Compute the exact 1-norm.
|
||||
"""
|
||||
if self._A_1_norm is None:
|
||||
self._A_1_norm = _exact_1_norm(self._A)
|
||||
return self._scale*self._A_1_norm
|
||||
|
||||
def d(self, p):
|
||||
"""
|
||||
Lazily estimate :math:`d_p(A) ~= || A^p ||^(1/p)` where :math:`||.||` is the 1-norm.
|
||||
"""
|
||||
if p not in self._d:
|
||||
est = _onenormest_matrix_power(self._A, p, self._ell)
|
||||
self._d[p] = est ** (1.0 / p)
|
||||
return self._scale*self._d[p]
|
||||
|
||||
def alpha(self, p):
|
||||
"""
|
||||
Lazily compute max(d(p), d(p+1)).
|
||||
"""
|
||||
return max(self.d(p), self.d(p+1))
|
||||
|
||||
def _compute_cost_div_m(m, p, norm_info):
|
||||
"""
|
||||
A helper function for computing bounds.
|
||||
|
||||
This is equation (3.10).
|
||||
It measures cost in terms of the number of required matrix products.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
m : int
|
||||
A valid key of _theta.
|
||||
p : int
|
||||
A matrix power.
|
||||
norm_info : LazyOperatorNormInfo
|
||||
Information about 1-norms of related operators.
|
||||
|
||||
Returns
|
||||
-------
|
||||
cost_div_m : int
|
||||
Required number of matrix products divided by m.
|
||||
|
||||
"""
|
||||
return int(np.ceil(norm_info.alpha(p) / _theta[m]))
|
||||
|
||||
|
||||
def _compute_p_max(m_max):
|
||||
"""
|
||||
Compute the largest positive integer p such that p*(p-1) <= m_max + 1.
|
||||
|
||||
Do this in a slightly dumb way, but safe and not too slow.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
m_max : int
|
||||
A count related to bounds.
|
||||
|
||||
"""
|
||||
sqrt_m_max = np.sqrt(m_max)
|
||||
p_low = int(np.floor(sqrt_m_max))
|
||||
p_high = int(np.ceil(sqrt_m_max + 1))
|
||||
return max(p for p in range(p_low, p_high+1) if p*(p-1) <= m_max + 1)
|
||||
|
||||
|
||||
def _fragment_3_1(norm_info, n0, tol, m_max=55, ell=2):
|
||||
"""
|
||||
A helper function for the _expm_multiply_* functions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
norm_info : LazyOperatorNormInfo
|
||||
Information about norms of certain linear operators of interest.
|
||||
n0 : int
|
||||
Number of columns in the _expm_multiply_* B matrix.
|
||||
tol : float
|
||||
Expected to be
|
||||
:math:`2^{-24}` for single precision or
|
||||
:math:`2^{-53}` for double precision.
|
||||
m_max : int
|
||||
A value related to a bound.
|
||||
ell : int
|
||||
The number of columns used in the 1-norm approximation.
|
||||
This is usually taken to be small, maybe between 1 and 5.
|
||||
|
||||
Returns
|
||||
-------
|
||||
best_m : int
|
||||
Related to bounds for error control.
|
||||
best_s : int
|
||||
Amount of scaling.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is code fragment (3.1) in Al-Mohy and Higham (2011).
|
||||
The discussion of default values for m_max and ell
|
||||
is given between the definitions of equation (3.11)
|
||||
and the definition of equation (3.12).
|
||||
|
||||
"""
|
||||
if ell < 1:
|
||||
raise ValueError('expected ell to be a positive integer')
|
||||
best_m = None
|
||||
best_s = None
|
||||
if _condition_3_13(norm_info.onenorm(), n0, m_max, ell):
|
||||
for m, theta in _theta.items():
|
||||
s = int(np.ceil(norm_info.onenorm() / theta))
|
||||
if best_m is None or m * s < best_m * best_s:
|
||||
best_m = m
|
||||
best_s = s
|
||||
else:
|
||||
# Equation (3.11).
|
||||
for p in range(2, _compute_p_max(m_max) + 1):
|
||||
for m in range(p*(p-1)-1, m_max+1):
|
||||
if m in _theta:
|
||||
s = _compute_cost_div_m(m, p, norm_info)
|
||||
if best_m is None or m * s < best_m * best_s:
|
||||
best_m = m
|
||||
best_s = s
|
||||
best_s = max(best_s, 1)
|
||||
return best_m, best_s
|
||||
|
||||
|
||||
def _condition_3_13(A_1_norm, n0, m_max, ell):
|
||||
"""
|
||||
A helper function for the _expm_multiply_* functions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A_1_norm : float
|
||||
The precomputed 1-norm of A.
|
||||
n0 : int
|
||||
Number of columns in the _expm_multiply_* B matrix.
|
||||
m_max : int
|
||||
A value related to a bound.
|
||||
ell : int
|
||||
The number of columns used in the 1-norm approximation.
|
||||
This is usually taken to be small, maybe between 1 and 5.
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : bool
|
||||
Indicates whether or not the condition has been met.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is condition (3.13) in Al-Mohy and Higham (2011).
|
||||
|
||||
"""
|
||||
|
||||
# This is the rhs of equation (3.12).
|
||||
p_max = _compute_p_max(m_max)
|
||||
a = 2 * ell * p_max * (p_max + 3)
|
||||
|
||||
# Evaluate the condition (3.13).
|
||||
b = _theta[m_max] / float(n0 * m_max)
|
||||
return A_1_norm <= a * b
|
||||
|
||||
|
||||
def _expm_multiply_interval(A, B, start=None, stop=None, num=None,
|
||||
endpoint=None, traceA=None, balance=False,
|
||||
status_only=False):
|
||||
"""
|
||||
Compute the action of the matrix exponential at multiple time points.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : transposable linear operator
|
||||
The operator whose exponential is of interest.
|
||||
B : ndarray
|
||||
The matrix to be multiplied by the matrix exponential of A.
|
||||
start : scalar, optional
|
||||
The starting time point of the sequence.
|
||||
stop : scalar, optional
|
||||
The end time point of the sequence, unless `endpoint` is set to False.
|
||||
In that case, the sequence consists of all but the last of ``num + 1``
|
||||
evenly spaced time points, so that `stop` is excluded.
|
||||
Note that the step size changes when `endpoint` is False.
|
||||
num : int, optional
|
||||
Number of time points to use.
|
||||
traceA : scalar, optional
|
||||
Trace of `A`. If not given the trace is estimated for linear operators,
|
||||
or calculated exactly for sparse matrices. It is used to precondition
|
||||
`A`, thus an approximate trace is acceptable
|
||||
endpoint : bool, optional
|
||||
If True, `stop` is the last time point. Otherwise, it is not included.
|
||||
balance : bool
|
||||
Indicates whether or not to apply balancing.
|
||||
status_only : bool
|
||||
A flag that is set to True for some debugging and testing operations.
|
||||
|
||||
Returns
|
||||
-------
|
||||
F : ndarray
|
||||
:math:`e^{t_k A} B`
|
||||
status : int
|
||||
An integer status for testing and debugging.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is algorithm (5.2) in Al-Mohy and Higham (2011).
|
||||
|
||||
There seems to be a typo, where line 15 of the algorithm should be
|
||||
moved to line 6.5 (between lines 6 and 7).
|
||||
|
||||
"""
|
||||
if balance:
|
||||
raise NotImplementedError
|
||||
if len(A.shape) != 2 or A.shape[0] != A.shape[1]:
|
||||
raise ValueError('expected A to be like a square matrix')
|
||||
if A.shape[1] != B.shape[0]:
|
||||
raise ValueError('shapes of matrices A {} and B {} are incompatible'
|
||||
.format(A.shape, B.shape))
|
||||
ident = _ident_like(A)
|
||||
is_linear_operator = isinstance(A, scipy.sparse.linalg.LinearOperator)
|
||||
n = A.shape[0]
|
||||
if len(B.shape) == 1:
|
||||
n0 = 1
|
||||
elif len(B.shape) == 2:
|
||||
n0 = B.shape[1]
|
||||
else:
|
||||
raise ValueError('expected B to be like a matrix or a vector')
|
||||
u_d = 2**-53
|
||||
tol = u_d
|
||||
if traceA is None:
|
||||
if is_linear_operator:
|
||||
warn("Trace of LinearOperator not available, it will be estimated."
|
||||
" Provide `traceA` to ensure performance.", stacklevel=3)
|
||||
# m3=5 is bit arbitrary choice, a more accurate trace (larger m3) might
|
||||
# speed up exponential calculation, but trace estimation is also costly
|
||||
# an educated guess would need to consider the number of time points
|
||||
traceA = traceest(A, m3=5) if is_linear_operator else _trace(A)
|
||||
mu = traceA / float(n)
|
||||
|
||||
# Get the linspace samples, attempting to preserve the linspace defaults.
|
||||
linspace_kwargs = {'retstep': True}
|
||||
if num is not None:
|
||||
linspace_kwargs['num'] = num
|
||||
if endpoint is not None:
|
||||
linspace_kwargs['endpoint'] = endpoint
|
||||
samples, step = np.linspace(start, stop, **linspace_kwargs)
|
||||
|
||||
# Convert the linspace output to the notation used by the publication.
|
||||
nsamples = len(samples)
|
||||
if nsamples < 2:
|
||||
raise ValueError('at least two time points are required')
|
||||
q = nsamples - 1
|
||||
h = step
|
||||
t_0 = samples[0]
|
||||
t_q = samples[q]
|
||||
|
||||
# Define the output ndarray.
|
||||
# Use an ndim=3 shape, such that the last two indices
|
||||
# are the ones that may be involved in level 3 BLAS operations.
|
||||
X_shape = (nsamples,) + B.shape
|
||||
X = np.empty(X_shape, dtype=np.result_type(A.dtype, B.dtype, float))
|
||||
t = t_q - t_0
|
||||
A = A - mu * ident
|
||||
A_1_norm = onenormest(A) if is_linear_operator else _exact_1_norm(A)
|
||||
ell = 2
|
||||
norm_info = LazyOperatorNormInfo(t*A, A_1_norm=t*A_1_norm, ell=ell)
|
||||
if t*A_1_norm == 0:
|
||||
m_star, s = 0, 1
|
||||
else:
|
||||
m_star, s = _fragment_3_1(norm_info, n0, tol, ell=ell)
|
||||
|
||||
# Compute the expm action up to the initial time point.
|
||||
X[0] = _expm_multiply_simple_core(A, B, t_0, mu, m_star, s)
|
||||
|
||||
# Compute the expm action at the rest of the time points.
|
||||
if q <= s:
|
||||
if status_only:
|
||||
return 0
|
||||
else:
|
||||
return _expm_multiply_interval_core_0(A, X,
|
||||
h, mu, q, norm_info, tol, ell,n0)
|
||||
elif not (q % s):
|
||||
if status_only:
|
||||
return 1
|
||||
else:
|
||||
return _expm_multiply_interval_core_1(A, X,
|
||||
h, mu, m_star, s, q, tol)
|
||||
elif (q % s):
|
||||
if status_only:
|
||||
return 2
|
||||
else:
|
||||
return _expm_multiply_interval_core_2(A, X,
|
||||
h, mu, m_star, s, q, tol)
|
||||
else:
|
||||
raise Exception('internal error')
|
||||
|
||||
|
||||
def _expm_multiply_interval_core_0(A, X, h, mu, q, norm_info, tol, ell, n0):
|
||||
"""
|
||||
A helper function, for the case q <= s.
|
||||
"""
|
||||
|
||||
# Compute the new values of m_star and s which should be applied
|
||||
# over intervals of size t/q
|
||||
if norm_info.onenorm() == 0:
|
||||
m_star, s = 0, 1
|
||||
else:
|
||||
norm_info.set_scale(1./q)
|
||||
m_star, s = _fragment_3_1(norm_info, n0, tol, ell=ell)
|
||||
norm_info.set_scale(1)
|
||||
|
||||
for k in range(q):
|
||||
X[k+1] = _expm_multiply_simple_core(A, X[k], h, mu, m_star, s)
|
||||
return X, 0
|
||||
|
||||
|
||||
def _expm_multiply_interval_core_1(A, X, h, mu, m_star, s, q, tol):
|
||||
"""
|
||||
A helper function, for the case q > s and q % s == 0.
|
||||
"""
|
||||
d = q // s
|
||||
input_shape = X.shape[1:]
|
||||
K_shape = (m_star + 1, ) + input_shape
|
||||
K = np.empty(K_shape, dtype=X.dtype)
|
||||
for i in range(s):
|
||||
Z = X[i*d]
|
||||
K[0] = Z
|
||||
high_p = 0
|
||||
for k in range(1, d+1):
|
||||
F = K[0]
|
||||
c1 = _exact_inf_norm(F)
|
||||
for p in range(1, m_star+1):
|
||||
if p > high_p:
|
||||
K[p] = h * A.dot(K[p-1]) / float(p)
|
||||
coeff = float(pow(k, p))
|
||||
F = F + coeff * K[p]
|
||||
inf_norm_K_p_1 = _exact_inf_norm(K[p])
|
||||
c2 = coeff * inf_norm_K_p_1
|
||||
if c1 + c2 <= tol * _exact_inf_norm(F):
|
||||
break
|
||||
c1 = c2
|
||||
X[k + i*d] = np.exp(k*h*mu) * F
|
||||
return X, 1
|
||||
|
||||
|
||||
def _expm_multiply_interval_core_2(A, X, h, mu, m_star, s, q, tol):
|
||||
"""
|
||||
A helper function, for the case q > s and q % s > 0.
|
||||
"""
|
||||
d = q // s
|
||||
j = q // d
|
||||
r = q - d * j
|
||||
input_shape = X.shape[1:]
|
||||
K_shape = (m_star + 1, ) + input_shape
|
||||
K = np.empty(K_shape, dtype=X.dtype)
|
||||
for i in range(j + 1):
|
||||
Z = X[i*d]
|
||||
K[0] = Z
|
||||
high_p = 0
|
||||
if i < j:
|
||||
effective_d = d
|
||||
else:
|
||||
effective_d = r
|
||||
for k in range(1, effective_d+1):
|
||||
F = K[0]
|
||||
c1 = _exact_inf_norm(F)
|
||||
for p in range(1, m_star+1):
|
||||
if p == high_p + 1:
|
||||
K[p] = h * A.dot(K[p-1]) / float(p)
|
||||
high_p = p
|
||||
coeff = float(pow(k, p))
|
||||
F = F + coeff * K[p]
|
||||
inf_norm_K_p_1 = _exact_inf_norm(K[p])
|
||||
c2 = coeff * inf_norm_K_p_1
|
||||
if c1 + c2 <= tol * _exact_inf_norm(F):
|
||||
break
|
||||
c1 = c2
|
||||
X[k + i*d] = np.exp(k*h*mu) * F
|
||||
return X, 2
|
||||
@ -0,0 +1,896 @@
|
||||
"""Abstract linear algebra library.
|
||||
|
||||
This module defines a class hierarchy that implements a kind of "lazy"
|
||||
matrix representation, called the ``LinearOperator``. It can be used to do
|
||||
linear algebra with extremely large sparse or structured matrices, without
|
||||
representing those explicitly in memory. Such matrices can be added,
|
||||
multiplied, transposed, etc.
|
||||
|
||||
As a motivating example, suppose you want have a matrix where almost all of
|
||||
the elements have the value one. The standard sparse matrix representation
|
||||
skips the storage of zeros, but not ones. By contrast, a LinearOperator is
|
||||
able to represent such matrices efficiently. First, we need a compact way to
|
||||
represent an all-ones matrix::
|
||||
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse.linalg._interface import LinearOperator
|
||||
>>> class Ones(LinearOperator):
|
||||
... def __init__(self, shape):
|
||||
... super().__init__(dtype=None, shape=shape)
|
||||
... def _matvec(self, x):
|
||||
... return np.repeat(x.sum(), self.shape[0])
|
||||
|
||||
Instances of this class emulate ``np.ones(shape)``, but using a constant
|
||||
amount of storage, independent of ``shape``. The ``_matvec`` method specifies
|
||||
how this linear operator multiplies with (operates on) a vector. We can now
|
||||
add this operator to a sparse matrix that stores only offsets from one::
|
||||
|
||||
>>> from scipy.sparse.linalg._interface import aslinearoperator
|
||||
>>> from scipy.sparse import csr_matrix
|
||||
>>> offsets = csr_matrix([[1, 0, 2], [0, -1, 0], [0, 0, 3]])
|
||||
>>> A = aslinearoperator(offsets) + Ones(offsets.shape)
|
||||
>>> A.dot([1, 2, 3])
|
||||
array([13, 4, 15])
|
||||
|
||||
The result is the same as that given by its dense, explicitly-stored
|
||||
counterpart::
|
||||
|
||||
>>> (np.ones(A.shape, A.dtype) + offsets.toarray()).dot([1, 2, 3])
|
||||
array([13, 4, 15])
|
||||
|
||||
Several algorithms in the ``scipy.sparse`` library are able to operate on
|
||||
``LinearOperator`` instances.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
||||
from scipy.sparse import issparse
|
||||
from scipy.sparse._sputils import isshape, isintlike, asmatrix, is_pydata_spmatrix
|
||||
|
||||
__all__ = ['LinearOperator', 'aslinearoperator']
|
||||
|
||||
|
||||
class LinearOperator:
|
||||
"""Common interface for performing matrix vector products
|
||||
|
||||
Many iterative methods (e.g. cg, gmres) do not need to know the
|
||||
individual entries of a matrix to solve a linear system A*x=b.
|
||||
Such solvers only require the computation of matrix vector
|
||||
products, A*v where v is a dense vector. This class serves as
|
||||
an abstract interface between iterative solvers and matrix-like
|
||||
objects.
|
||||
|
||||
To construct a concrete LinearOperator, either pass appropriate
|
||||
callables to the constructor of this class, or subclass it.
|
||||
|
||||
A subclass must implement either one of the methods ``_matvec``
|
||||
and ``_matmat``, and the attributes/properties ``shape`` (pair of
|
||||
integers) and ``dtype`` (may be None). It may call the ``__init__``
|
||||
on this class to have these attributes validated. Implementing
|
||||
``_matvec`` automatically implements ``_matmat`` (using a naive
|
||||
algorithm) and vice-versa.
|
||||
|
||||
Optionally, a subclass may implement ``_rmatvec`` or ``_adjoint``
|
||||
to implement the Hermitian adjoint (conjugate transpose). As with
|
||||
``_matvec`` and ``_matmat``, implementing either ``_rmatvec`` or
|
||||
``_adjoint`` implements the other automatically. Implementing
|
||||
``_adjoint`` is preferable; ``_rmatvec`` is mostly there for
|
||||
backwards compatibility.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
shape : tuple
|
||||
Matrix dimensions (M, N).
|
||||
matvec : callable f(v)
|
||||
Returns returns A * v.
|
||||
rmatvec : callable f(v)
|
||||
Returns A^H * v, where A^H is the conjugate transpose of A.
|
||||
matmat : callable f(V)
|
||||
Returns A * V, where V is a dense matrix with dimensions (N, K).
|
||||
dtype : dtype
|
||||
Data type of the matrix.
|
||||
rmatmat : callable f(V)
|
||||
Returns A^H * V, where V is a dense matrix with dimensions (M, K).
|
||||
|
||||
Attributes
|
||||
----------
|
||||
args : tuple
|
||||
For linear operators describing products etc. of other linear
|
||||
operators, the operands of the binary operation.
|
||||
ndim : int
|
||||
Number of dimensions (this is always 2)
|
||||
|
||||
See Also
|
||||
--------
|
||||
aslinearoperator : Construct LinearOperators
|
||||
|
||||
Notes
|
||||
-----
|
||||
The user-defined matvec() function must properly handle the case
|
||||
where v has shape (N,) as well as the (N,1) case. The shape of
|
||||
the return type is handled internally by LinearOperator.
|
||||
|
||||
LinearOperator instances can also be multiplied, added with each
|
||||
other and exponentiated, all lazily: the result of these operations
|
||||
is always a new, composite LinearOperator, that defers linear
|
||||
operations to the original operators and combines the results.
|
||||
|
||||
More details regarding how to subclass a LinearOperator and several
|
||||
examples of concrete LinearOperator instances can be found in the
|
||||
external project `PyLops <https://pylops.readthedocs.io>`_.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse.linalg import LinearOperator
|
||||
>>> def mv(v):
|
||||
... return np.array([2*v[0], 3*v[1]])
|
||||
...
|
||||
>>> A = LinearOperator((2,2), matvec=mv)
|
||||
>>> A
|
||||
<2x2 _CustomLinearOperator with dtype=float64>
|
||||
>>> A.matvec(np.ones(2))
|
||||
array([ 2., 3.])
|
||||
>>> A * np.ones(2)
|
||||
array([ 2., 3.])
|
||||
|
||||
"""
|
||||
|
||||
ndim = 2
|
||||
# Necessary for right matmul with numpy arrays.
|
||||
__array_ufunc__ = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls is LinearOperator:
|
||||
# Operate as _CustomLinearOperator factory.
|
||||
return super().__new__(_CustomLinearOperator)
|
||||
else:
|
||||
obj = super().__new__(cls)
|
||||
|
||||
if (type(obj)._matvec == LinearOperator._matvec
|
||||
and type(obj)._matmat == LinearOperator._matmat):
|
||||
warnings.warn("LinearOperator subclass should implement"
|
||||
" at least one of _matvec and _matmat.",
|
||||
category=RuntimeWarning, stacklevel=2)
|
||||
|
||||
return obj
|
||||
|
||||
def __init__(self, dtype, shape):
|
||||
"""Initialize this LinearOperator.
|
||||
|
||||
To be called by subclasses. ``dtype`` may be None; ``shape`` should
|
||||
be convertible to a length-2 tuple.
|
||||
"""
|
||||
if dtype is not None:
|
||||
dtype = np.dtype(dtype)
|
||||
|
||||
shape = tuple(shape)
|
||||
if not isshape(shape):
|
||||
raise ValueError(f"invalid shape {shape!r} (must be 2-d)")
|
||||
|
||||
self.dtype = dtype
|
||||
self.shape = shape
|
||||
|
||||
def _init_dtype(self):
|
||||
"""Called from subclasses at the end of the __init__ routine.
|
||||
"""
|
||||
if self.dtype is None:
|
||||
v = np.zeros(self.shape[-1])
|
||||
self.dtype = np.asarray(self.matvec(v)).dtype
|
||||
|
||||
def _matmat(self, X):
|
||||
"""Default matrix-matrix multiplication handler.
|
||||
|
||||
Falls back on the user-defined _matvec method, so defining that will
|
||||
define matrix multiplication (though in a very suboptimal way).
|
||||
"""
|
||||
|
||||
return np.hstack([self.matvec(col.reshape(-1,1)) for col in X.T])
|
||||
|
||||
def _matvec(self, x):
|
||||
"""Default matrix-vector multiplication handler.
|
||||
|
||||
If self is a linear operator of shape (M, N), then this method will
|
||||
be called on a shape (N,) or (N, 1) ndarray, and should return a
|
||||
shape (M,) or (M, 1) ndarray.
|
||||
|
||||
This default implementation falls back on _matmat, so defining that
|
||||
will define matrix-vector multiplication as well.
|
||||
"""
|
||||
return self.matmat(x.reshape(-1, 1))
|
||||
|
||||
def matvec(self, x):
|
||||
"""Matrix-vector multiplication.
|
||||
|
||||
Performs the operation y=A*x where A is an MxN linear
|
||||
operator and x is a column vector or 1-d array.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : {matrix, ndarray}
|
||||
An array with shape (N,) or (N,1).
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : {matrix, ndarray}
|
||||
A matrix or ndarray with shape (M,) or (M,1) depending
|
||||
on the type and shape of the x argument.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This matvec wraps the user-specified matvec routine or overridden
|
||||
_matvec method to ensure that y has the correct shape and type.
|
||||
|
||||
"""
|
||||
|
||||
x = np.asanyarray(x)
|
||||
|
||||
M,N = self.shape
|
||||
|
||||
if x.shape != (N,) and x.shape != (N,1):
|
||||
raise ValueError('dimension mismatch')
|
||||
|
||||
y = self._matvec(x)
|
||||
|
||||
if isinstance(x, np.matrix):
|
||||
y = asmatrix(y)
|
||||
else:
|
||||
y = np.asarray(y)
|
||||
|
||||
if x.ndim == 1:
|
||||
y = y.reshape(M)
|
||||
elif x.ndim == 2:
|
||||
y = y.reshape(M,1)
|
||||
else:
|
||||
raise ValueError('invalid shape returned by user-defined matvec()')
|
||||
|
||||
return y
|
||||
|
||||
def rmatvec(self, x):
|
||||
"""Adjoint matrix-vector multiplication.
|
||||
|
||||
Performs the operation y = A^H * x where A is an MxN linear
|
||||
operator and x is a column vector or 1-d array.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : {matrix, ndarray}
|
||||
An array with shape (M,) or (M,1).
|
||||
|
||||
Returns
|
||||
-------
|
||||
y : {matrix, ndarray}
|
||||
A matrix or ndarray with shape (N,) or (N,1) depending
|
||||
on the type and shape of the x argument.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This rmatvec wraps the user-specified rmatvec routine or overridden
|
||||
_rmatvec method to ensure that y has the correct shape and type.
|
||||
|
||||
"""
|
||||
|
||||
x = np.asanyarray(x)
|
||||
|
||||
M,N = self.shape
|
||||
|
||||
if x.shape != (M,) and x.shape != (M,1):
|
||||
raise ValueError('dimension mismatch')
|
||||
|
||||
y = self._rmatvec(x)
|
||||
|
||||
if isinstance(x, np.matrix):
|
||||
y = asmatrix(y)
|
||||
else:
|
||||
y = np.asarray(y)
|
||||
|
||||
if x.ndim == 1:
|
||||
y = y.reshape(N)
|
||||
elif x.ndim == 2:
|
||||
y = y.reshape(N,1)
|
||||
else:
|
||||
raise ValueError('invalid shape returned by user-defined rmatvec()')
|
||||
|
||||
return y
|
||||
|
||||
def _rmatvec(self, x):
|
||||
"""Default implementation of _rmatvec; defers to adjoint."""
|
||||
if type(self)._adjoint == LinearOperator._adjoint:
|
||||
# _adjoint not overridden, prevent infinite recursion
|
||||
raise NotImplementedError
|
||||
else:
|
||||
return self.H.matvec(x)
|
||||
|
||||
def matmat(self, X):
|
||||
"""Matrix-matrix multiplication.
|
||||
|
||||
Performs the operation y=A*X where A is an MxN linear
|
||||
operator and X dense N*K matrix or ndarray.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : {matrix, ndarray}
|
||||
An array with shape (N,K).
|
||||
|
||||
Returns
|
||||
-------
|
||||
Y : {matrix, ndarray}
|
||||
A matrix or ndarray with shape (M,K) depending on
|
||||
the type of the X argument.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This matmat wraps any user-specified matmat routine or overridden
|
||||
_matmat method to ensure that y has the correct type.
|
||||
|
||||
"""
|
||||
if not (issparse(X) or is_pydata_spmatrix(X)):
|
||||
X = np.asanyarray(X)
|
||||
|
||||
if X.ndim != 2:
|
||||
raise ValueError(f'expected 2-d ndarray or matrix, not {X.ndim}-d')
|
||||
|
||||
if X.shape[0] != self.shape[1]:
|
||||
raise ValueError(f'dimension mismatch: {self.shape}, {X.shape}')
|
||||
|
||||
try:
|
||||
Y = self._matmat(X)
|
||||
except Exception as e:
|
||||
if issparse(X) or is_pydata_spmatrix(X):
|
||||
raise TypeError(
|
||||
"Unable to multiply a LinearOperator with a sparse matrix."
|
||||
" Wrap the matrix in aslinearoperator first."
|
||||
) from e
|
||||
raise
|
||||
|
||||
if isinstance(Y, np.matrix):
|
||||
Y = asmatrix(Y)
|
||||
|
||||
return Y
|
||||
|
||||
def rmatmat(self, X):
|
||||
"""Adjoint matrix-matrix multiplication.
|
||||
|
||||
Performs the operation y = A^H * x where A is an MxN linear
|
||||
operator and x is a column vector or 1-d array, or 2-d array.
|
||||
The default implementation defers to the adjoint.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : {matrix, ndarray}
|
||||
A matrix or 2D array.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Y : {matrix, ndarray}
|
||||
A matrix or 2D array depending on the type of the input.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This rmatmat wraps the user-specified rmatmat routine.
|
||||
|
||||
"""
|
||||
if not (issparse(X) or is_pydata_spmatrix(X)):
|
||||
X = np.asanyarray(X)
|
||||
|
||||
if X.ndim != 2:
|
||||
raise ValueError('expected 2-d ndarray or matrix, not %d-d'
|
||||
% X.ndim)
|
||||
|
||||
if X.shape[0] != self.shape[0]:
|
||||
raise ValueError(f'dimension mismatch: {self.shape}, {X.shape}')
|
||||
|
||||
try:
|
||||
Y = self._rmatmat(X)
|
||||
except Exception as e:
|
||||
if issparse(X) or is_pydata_spmatrix(X):
|
||||
raise TypeError(
|
||||
"Unable to multiply a LinearOperator with a sparse matrix."
|
||||
" Wrap the matrix in aslinearoperator() first."
|
||||
) from e
|
||||
raise
|
||||
|
||||
if isinstance(Y, np.matrix):
|
||||
Y = asmatrix(Y)
|
||||
return Y
|
||||
|
||||
def _rmatmat(self, X):
|
||||
"""Default implementation of _rmatmat defers to rmatvec or adjoint."""
|
||||
if type(self)._adjoint == LinearOperator._adjoint:
|
||||
return np.hstack([self.rmatvec(col.reshape(-1, 1)) for col in X.T])
|
||||
else:
|
||||
return self.H.matmat(X)
|
||||
|
||||
def __call__(self, x):
|
||||
return self*x
|
||||
|
||||
def __mul__(self, x):
|
||||
return self.dot(x)
|
||||
|
||||
def __truediv__(self, other):
|
||||
if not np.isscalar(other):
|
||||
raise ValueError("Can only divide a linear operator by a scalar.")
|
||||
|
||||
return _ScaledLinearOperator(self, 1.0/other)
|
||||
|
||||
def dot(self, x):
|
||||
"""Matrix-matrix or matrix-vector multiplication.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : array_like
|
||||
1-d or 2-d array, representing a vector or matrix.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Ax : array
|
||||
1-d or 2-d array (depending on the shape of x) that represents
|
||||
the result of applying this linear operator on x.
|
||||
|
||||
"""
|
||||
if isinstance(x, LinearOperator):
|
||||
return _ProductLinearOperator(self, x)
|
||||
elif np.isscalar(x):
|
||||
return _ScaledLinearOperator(self, x)
|
||||
else:
|
||||
if not issparse(x) and not is_pydata_spmatrix(x):
|
||||
# Sparse matrices shouldn't be converted to numpy arrays.
|
||||
x = np.asarray(x)
|
||||
|
||||
if x.ndim == 1 or x.ndim == 2 and x.shape[1] == 1:
|
||||
return self.matvec(x)
|
||||
elif x.ndim == 2:
|
||||
return self.matmat(x)
|
||||
else:
|
||||
raise ValueError('expected 1-d or 2-d array or matrix, got %r'
|
||||
% x)
|
||||
|
||||
def __matmul__(self, other):
|
||||
if np.isscalar(other):
|
||||
raise ValueError("Scalar operands are not allowed, "
|
||||
"use '*' instead")
|
||||
return self.__mul__(other)
|
||||
|
||||
def __rmatmul__(self, other):
|
||||
if np.isscalar(other):
|
||||
raise ValueError("Scalar operands are not allowed, "
|
||||
"use '*' instead")
|
||||
return self.__rmul__(other)
|
||||
|
||||
def __rmul__(self, x):
|
||||
if np.isscalar(x):
|
||||
return _ScaledLinearOperator(self, x)
|
||||
else:
|
||||
return self._rdot(x)
|
||||
|
||||
def _rdot(self, x):
|
||||
"""Matrix-matrix or matrix-vector multiplication from the right.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : array_like
|
||||
1-d or 2-d array, representing a vector or matrix.
|
||||
|
||||
Returns
|
||||
-------
|
||||
xA : array
|
||||
1-d or 2-d array (depending on the shape of x) that represents
|
||||
the result of applying this linear operator on x from the right.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is copied from dot to implement right multiplication.
|
||||
"""
|
||||
if isinstance(x, LinearOperator):
|
||||
return _ProductLinearOperator(x, self)
|
||||
elif np.isscalar(x):
|
||||
return _ScaledLinearOperator(self, x)
|
||||
else:
|
||||
if not issparse(x) and not is_pydata_spmatrix(x):
|
||||
# Sparse matrices shouldn't be converted to numpy arrays.
|
||||
x = np.asarray(x)
|
||||
|
||||
# We use transpose instead of rmatvec/rmatmat to avoid
|
||||
# unnecessary complex conjugation if possible.
|
||||
if x.ndim == 1 or x.ndim == 2 and x.shape[0] == 1:
|
||||
return self.T.matvec(x.T).T
|
||||
elif x.ndim == 2:
|
||||
return self.T.matmat(x.T).T
|
||||
else:
|
||||
raise ValueError('expected 1-d or 2-d array or matrix, got %r'
|
||||
% x)
|
||||
|
||||
def __pow__(self, p):
|
||||
if np.isscalar(p):
|
||||
return _PowerLinearOperator(self, p)
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __add__(self, x):
|
||||
if isinstance(x, LinearOperator):
|
||||
return _SumLinearOperator(self, x)
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __neg__(self):
|
||||
return _ScaledLinearOperator(self, -1)
|
||||
|
||||
def __sub__(self, x):
|
||||
return self.__add__(-x)
|
||||
|
||||
def __repr__(self):
|
||||
M,N = self.shape
|
||||
if self.dtype is None:
|
||||
dt = 'unspecified dtype'
|
||||
else:
|
||||
dt = 'dtype=' + str(self.dtype)
|
||||
|
||||
return '<%dx%d %s with %s>' % (M, N, self.__class__.__name__, dt)
|
||||
|
||||
def adjoint(self):
|
||||
"""Hermitian adjoint.
|
||||
|
||||
Returns the Hermitian adjoint of self, aka the Hermitian
|
||||
conjugate or Hermitian transpose. For a complex matrix, the
|
||||
Hermitian adjoint is equal to the conjugate transpose.
|
||||
|
||||
Can be abbreviated self.H instead of self.adjoint().
|
||||
|
||||
Returns
|
||||
-------
|
||||
A_H : LinearOperator
|
||||
Hermitian adjoint of self.
|
||||
"""
|
||||
return self._adjoint()
|
||||
|
||||
H = property(adjoint)
|
||||
|
||||
def transpose(self):
|
||||
"""Transpose this linear operator.
|
||||
|
||||
Returns a LinearOperator that represents the transpose of this one.
|
||||
Can be abbreviated self.T instead of self.transpose().
|
||||
"""
|
||||
return self._transpose()
|
||||
|
||||
T = property(transpose)
|
||||
|
||||
def _adjoint(self):
|
||||
"""Default implementation of _adjoint; defers to rmatvec."""
|
||||
return _AdjointLinearOperator(self)
|
||||
|
||||
def _transpose(self):
|
||||
""" Default implementation of _transpose; defers to rmatvec + conj"""
|
||||
return _TransposedLinearOperator(self)
|
||||
|
||||
|
||||
class _CustomLinearOperator(LinearOperator):
|
||||
"""Linear operator defined in terms of user-specified operations."""
|
||||
|
||||
def __init__(self, shape, matvec, rmatvec=None, matmat=None,
|
||||
dtype=None, rmatmat=None):
|
||||
super().__init__(dtype, shape)
|
||||
|
||||
self.args = ()
|
||||
|
||||
self.__matvec_impl = matvec
|
||||
self.__rmatvec_impl = rmatvec
|
||||
self.__rmatmat_impl = rmatmat
|
||||
self.__matmat_impl = matmat
|
||||
|
||||
self._init_dtype()
|
||||
|
||||
def _matmat(self, X):
|
||||
if self.__matmat_impl is not None:
|
||||
return self.__matmat_impl(X)
|
||||
else:
|
||||
return super()._matmat(X)
|
||||
|
||||
def _matvec(self, x):
|
||||
return self.__matvec_impl(x)
|
||||
|
||||
def _rmatvec(self, x):
|
||||
func = self.__rmatvec_impl
|
||||
if func is None:
|
||||
raise NotImplementedError("rmatvec is not defined")
|
||||
return self.__rmatvec_impl(x)
|
||||
|
||||
def _rmatmat(self, X):
|
||||
if self.__rmatmat_impl is not None:
|
||||
return self.__rmatmat_impl(X)
|
||||
else:
|
||||
return super()._rmatmat(X)
|
||||
|
||||
def _adjoint(self):
|
||||
return _CustomLinearOperator(shape=(self.shape[1], self.shape[0]),
|
||||
matvec=self.__rmatvec_impl,
|
||||
rmatvec=self.__matvec_impl,
|
||||
matmat=self.__rmatmat_impl,
|
||||
rmatmat=self.__matmat_impl,
|
||||
dtype=self.dtype)
|
||||
|
||||
|
||||
class _AdjointLinearOperator(LinearOperator):
|
||||
"""Adjoint of arbitrary Linear Operator"""
|
||||
|
||||
def __init__(self, A):
|
||||
shape = (A.shape[1], A.shape[0])
|
||||
super().__init__(dtype=A.dtype, shape=shape)
|
||||
self.A = A
|
||||
self.args = (A,)
|
||||
|
||||
def _matvec(self, x):
|
||||
return self.A._rmatvec(x)
|
||||
|
||||
def _rmatvec(self, x):
|
||||
return self.A._matvec(x)
|
||||
|
||||
def _matmat(self, x):
|
||||
return self.A._rmatmat(x)
|
||||
|
||||
def _rmatmat(self, x):
|
||||
return self.A._matmat(x)
|
||||
|
||||
class _TransposedLinearOperator(LinearOperator):
|
||||
"""Transposition of arbitrary Linear Operator"""
|
||||
|
||||
def __init__(self, A):
|
||||
shape = (A.shape[1], A.shape[0])
|
||||
super().__init__(dtype=A.dtype, shape=shape)
|
||||
self.A = A
|
||||
self.args = (A,)
|
||||
|
||||
def _matvec(self, x):
|
||||
# NB. np.conj works also on sparse matrices
|
||||
return np.conj(self.A._rmatvec(np.conj(x)))
|
||||
|
||||
def _rmatvec(self, x):
|
||||
return np.conj(self.A._matvec(np.conj(x)))
|
||||
|
||||
def _matmat(self, x):
|
||||
# NB. np.conj works also on sparse matrices
|
||||
return np.conj(self.A._rmatmat(np.conj(x)))
|
||||
|
||||
def _rmatmat(self, x):
|
||||
return np.conj(self.A._matmat(np.conj(x)))
|
||||
|
||||
def _get_dtype(operators, dtypes=None):
|
||||
if dtypes is None:
|
||||
dtypes = []
|
||||
for obj in operators:
|
||||
if obj is not None and hasattr(obj, 'dtype'):
|
||||
dtypes.append(obj.dtype)
|
||||
return np.result_type(*dtypes)
|
||||
|
||||
|
||||
class _SumLinearOperator(LinearOperator):
|
||||
def __init__(self, A, B):
|
||||
if not isinstance(A, LinearOperator) or \
|
||||
not isinstance(B, LinearOperator):
|
||||
raise ValueError('both operands have to be a LinearOperator')
|
||||
if A.shape != B.shape:
|
||||
raise ValueError(f'cannot add {A} and {B}: shape mismatch')
|
||||
self.args = (A, B)
|
||||
super().__init__(_get_dtype([A, B]), A.shape)
|
||||
|
||||
def _matvec(self, x):
|
||||
return self.args[0].matvec(x) + self.args[1].matvec(x)
|
||||
|
||||
def _rmatvec(self, x):
|
||||
return self.args[0].rmatvec(x) + self.args[1].rmatvec(x)
|
||||
|
||||
def _rmatmat(self, x):
|
||||
return self.args[0].rmatmat(x) + self.args[1].rmatmat(x)
|
||||
|
||||
def _matmat(self, x):
|
||||
return self.args[0].matmat(x) + self.args[1].matmat(x)
|
||||
|
||||
def _adjoint(self):
|
||||
A, B = self.args
|
||||
return A.H + B.H
|
||||
|
||||
|
||||
class _ProductLinearOperator(LinearOperator):
|
||||
def __init__(self, A, B):
|
||||
if not isinstance(A, LinearOperator) or \
|
||||
not isinstance(B, LinearOperator):
|
||||
raise ValueError('both operands have to be a LinearOperator')
|
||||
if A.shape[1] != B.shape[0]:
|
||||
raise ValueError(f'cannot multiply {A} and {B}: shape mismatch')
|
||||
super().__init__(_get_dtype([A, B]),
|
||||
(A.shape[0], B.shape[1]))
|
||||
self.args = (A, B)
|
||||
|
||||
def _matvec(self, x):
|
||||
return self.args[0].matvec(self.args[1].matvec(x))
|
||||
|
||||
def _rmatvec(self, x):
|
||||
return self.args[1].rmatvec(self.args[0].rmatvec(x))
|
||||
|
||||
def _rmatmat(self, x):
|
||||
return self.args[1].rmatmat(self.args[0].rmatmat(x))
|
||||
|
||||
def _matmat(self, x):
|
||||
return self.args[0].matmat(self.args[1].matmat(x))
|
||||
|
||||
def _adjoint(self):
|
||||
A, B = self.args
|
||||
return B.H * A.H
|
||||
|
||||
|
||||
class _ScaledLinearOperator(LinearOperator):
|
||||
def __init__(self, A, alpha):
|
||||
if not isinstance(A, LinearOperator):
|
||||
raise ValueError('LinearOperator expected as A')
|
||||
if not np.isscalar(alpha):
|
||||
raise ValueError('scalar expected as alpha')
|
||||
if isinstance(A, _ScaledLinearOperator):
|
||||
A, alpha_original = A.args
|
||||
# Avoid in-place multiplication so that we don't accidentally mutate
|
||||
# the original prefactor.
|
||||
alpha = alpha * alpha_original
|
||||
|
||||
dtype = _get_dtype([A], [type(alpha)])
|
||||
super().__init__(dtype, A.shape)
|
||||
self.args = (A, alpha)
|
||||
|
||||
def _matvec(self, x):
|
||||
return self.args[1] * self.args[0].matvec(x)
|
||||
|
||||
def _rmatvec(self, x):
|
||||
return np.conj(self.args[1]) * self.args[0].rmatvec(x)
|
||||
|
||||
def _rmatmat(self, x):
|
||||
return np.conj(self.args[1]) * self.args[0].rmatmat(x)
|
||||
|
||||
def _matmat(self, x):
|
||||
return self.args[1] * self.args[0].matmat(x)
|
||||
|
||||
def _adjoint(self):
|
||||
A, alpha = self.args
|
||||
return A.H * np.conj(alpha)
|
||||
|
||||
|
||||
class _PowerLinearOperator(LinearOperator):
|
||||
def __init__(self, A, p):
|
||||
if not isinstance(A, LinearOperator):
|
||||
raise ValueError('LinearOperator expected as A')
|
||||
if A.shape[0] != A.shape[1]:
|
||||
raise ValueError('square LinearOperator expected, got %r' % A)
|
||||
if not isintlike(p) or p < 0:
|
||||
raise ValueError('non-negative integer expected as p')
|
||||
|
||||
super().__init__(_get_dtype([A]), A.shape)
|
||||
self.args = (A, p)
|
||||
|
||||
def _power(self, fun, x):
|
||||
res = np.array(x, copy=True)
|
||||
for i in range(self.args[1]):
|
||||
res = fun(res)
|
||||
return res
|
||||
|
||||
def _matvec(self, x):
|
||||
return self._power(self.args[0].matvec, x)
|
||||
|
||||
def _rmatvec(self, x):
|
||||
return self._power(self.args[0].rmatvec, x)
|
||||
|
||||
def _rmatmat(self, x):
|
||||
return self._power(self.args[0].rmatmat, x)
|
||||
|
||||
def _matmat(self, x):
|
||||
return self._power(self.args[0].matmat, x)
|
||||
|
||||
def _adjoint(self):
|
||||
A, p = self.args
|
||||
return A.H ** p
|
||||
|
||||
|
||||
class MatrixLinearOperator(LinearOperator):
|
||||
def __init__(self, A):
|
||||
super().__init__(A.dtype, A.shape)
|
||||
self.A = A
|
||||
self.__adj = None
|
||||
self.args = (A,)
|
||||
|
||||
def _matmat(self, X):
|
||||
return self.A.dot(X)
|
||||
|
||||
def _adjoint(self):
|
||||
if self.__adj is None:
|
||||
self.__adj = _AdjointMatrixOperator(self)
|
||||
return self.__adj
|
||||
|
||||
class _AdjointMatrixOperator(MatrixLinearOperator):
|
||||
def __init__(self, adjoint):
|
||||
self.A = adjoint.A.T.conj()
|
||||
self.__adjoint = adjoint
|
||||
self.args = (adjoint,)
|
||||
self.shape = adjoint.shape[1], adjoint.shape[0]
|
||||
|
||||
@property
|
||||
def dtype(self):
|
||||
return self.__adjoint.dtype
|
||||
|
||||
def _adjoint(self):
|
||||
return self.__adjoint
|
||||
|
||||
|
||||
class IdentityOperator(LinearOperator):
|
||||
def __init__(self, shape, dtype=None):
|
||||
super().__init__(dtype, shape)
|
||||
|
||||
def _matvec(self, x):
|
||||
return x
|
||||
|
||||
def _rmatvec(self, x):
|
||||
return x
|
||||
|
||||
def _rmatmat(self, x):
|
||||
return x
|
||||
|
||||
def _matmat(self, x):
|
||||
return x
|
||||
|
||||
def _adjoint(self):
|
||||
return self
|
||||
|
||||
|
||||
def aslinearoperator(A):
|
||||
"""Return A as a LinearOperator.
|
||||
|
||||
'A' may be any of the following types:
|
||||
- ndarray
|
||||
- matrix
|
||||
- sparse matrix (e.g. csr_matrix, lil_matrix, etc.)
|
||||
- LinearOperator
|
||||
- An object with .shape and .matvec attributes
|
||||
|
||||
See the LinearOperator documentation for additional information.
|
||||
|
||||
Notes
|
||||
-----
|
||||
If 'A' has no .dtype attribute, the data type is determined by calling
|
||||
:func:`LinearOperator.matvec()` - set the .dtype attribute to prevent this
|
||||
call upon the linear operator creation.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse.linalg import aslinearoperator
|
||||
>>> M = np.array([[1,2,3],[4,5,6]], dtype=np.int32)
|
||||
>>> aslinearoperator(M)
|
||||
<2x3 MatrixLinearOperator with dtype=int32>
|
||||
"""
|
||||
if isinstance(A, LinearOperator):
|
||||
return A
|
||||
|
||||
elif isinstance(A, np.ndarray) or isinstance(A, np.matrix):
|
||||
if A.ndim > 2:
|
||||
raise ValueError('array must have ndim <= 2')
|
||||
A = np.atleast_2d(np.asarray(A))
|
||||
return MatrixLinearOperator(A)
|
||||
|
||||
elif issparse(A) or is_pydata_spmatrix(A):
|
||||
return MatrixLinearOperator(A)
|
||||
|
||||
else:
|
||||
if hasattr(A, 'shape') and hasattr(A, 'matvec'):
|
||||
rmatvec = None
|
||||
rmatmat = None
|
||||
dtype = None
|
||||
|
||||
if hasattr(A, 'rmatvec'):
|
||||
rmatvec = A.rmatvec
|
||||
if hasattr(A, 'rmatmat'):
|
||||
rmatmat = A.rmatmat
|
||||
if hasattr(A, 'dtype'):
|
||||
dtype = A.dtype
|
||||
return LinearOperator(A.shape, A.matvec, rmatvec=rmatvec,
|
||||
rmatmat=rmatmat, dtype=dtype)
|
||||
|
||||
else:
|
||||
raise TypeError('type not understood')
|
||||
@ -0,0 +1,20 @@
|
||||
"Iterative Solvers for Sparse Linear Systems"
|
||||
|
||||
#from info import __doc__
|
||||
from .iterative import *
|
||||
from .minres import minres
|
||||
from .lgmres import lgmres
|
||||
from .lsqr import lsqr
|
||||
from .lsmr import lsmr
|
||||
from ._gcrotmk import gcrotmk
|
||||
from .tfqmr import tfqmr
|
||||
|
||||
__all__ = [
|
||||
'bicg', 'bicgstab', 'cg', 'cgs', 'gcrotmk', 'gmres',
|
||||
'lgmres', 'lsmr', 'lsqr',
|
||||
'minres', 'qmr', 'tfqmr'
|
||||
]
|
||||
|
||||
from scipy._lib._testutils import PytestTester
|
||||
test = PytestTester(__name__)
|
||||
del PytestTester
|
||||
@ -0,0 +1,501 @@
|
||||
# Copyright (C) 2015, Pauli Virtanen <pav@iki.fi>
|
||||
# Distributed under the same license as SciPy.
|
||||
|
||||
import numpy as np
|
||||
from numpy.linalg import LinAlgError
|
||||
from scipy.linalg import (get_blas_funcs, qr, solve, svd, qr_insert, lstsq)
|
||||
from .iterative import _get_atol_rtol
|
||||
from scipy.sparse.linalg._isolve.utils import make_system
|
||||
|
||||
|
||||
__all__ = ['gcrotmk']
|
||||
|
||||
|
||||
def _fgmres(matvec, v0, m, atol, lpsolve=None, rpsolve=None, cs=(), outer_v=(),
|
||||
prepend_outer_v=False):
|
||||
"""
|
||||
FGMRES Arnoldi process, with optional projection or augmentation
|
||||
|
||||
Parameters
|
||||
----------
|
||||
matvec : callable
|
||||
Operation A*x
|
||||
v0 : ndarray
|
||||
Initial vector, normalized to nrm2(v0) == 1
|
||||
m : int
|
||||
Number of GMRES rounds
|
||||
atol : float
|
||||
Absolute tolerance for early exit
|
||||
lpsolve : callable
|
||||
Left preconditioner L
|
||||
rpsolve : callable
|
||||
Right preconditioner R
|
||||
cs : list of (ndarray, ndarray)
|
||||
Columns of matrices C and U in GCROT
|
||||
outer_v : list of ndarrays
|
||||
Augmentation vectors in LGMRES
|
||||
prepend_outer_v : bool, optional
|
||||
Whether augmentation vectors come before or after
|
||||
Krylov iterates
|
||||
|
||||
Raises
|
||||
------
|
||||
LinAlgError
|
||||
If nans encountered
|
||||
|
||||
Returns
|
||||
-------
|
||||
Q, R : ndarray
|
||||
QR decomposition of the upper Hessenberg H=QR
|
||||
B : ndarray
|
||||
Projections corresponding to matrix C
|
||||
vs : list of ndarray
|
||||
Columns of matrix V
|
||||
zs : list of ndarray
|
||||
Columns of matrix Z
|
||||
y : ndarray
|
||||
Solution to ||H y - e_1||_2 = min!
|
||||
res : float
|
||||
The final (preconditioned) residual norm
|
||||
|
||||
"""
|
||||
|
||||
if lpsolve is None:
|
||||
def lpsolve(x):
|
||||
return x
|
||||
if rpsolve is None:
|
||||
def rpsolve(x):
|
||||
return x
|
||||
|
||||
axpy, dot, scal, nrm2 = get_blas_funcs(['axpy', 'dot', 'scal', 'nrm2'], (v0,))
|
||||
|
||||
vs = [v0]
|
||||
zs = []
|
||||
y = None
|
||||
res = np.nan
|
||||
|
||||
m = m + len(outer_v)
|
||||
|
||||
# Orthogonal projection coefficients
|
||||
B = np.zeros((len(cs), m), dtype=v0.dtype)
|
||||
|
||||
# H is stored in QR factorized form
|
||||
Q = np.ones((1, 1), dtype=v0.dtype)
|
||||
R = np.zeros((1, 0), dtype=v0.dtype)
|
||||
|
||||
eps = np.finfo(v0.dtype).eps
|
||||
|
||||
breakdown = False
|
||||
|
||||
# FGMRES Arnoldi process
|
||||
for j in range(m):
|
||||
# L A Z = C B + V H
|
||||
|
||||
if prepend_outer_v and j < len(outer_v):
|
||||
z, w = outer_v[j]
|
||||
elif prepend_outer_v and j == len(outer_v):
|
||||
z = rpsolve(v0)
|
||||
w = None
|
||||
elif not prepend_outer_v and j >= m - len(outer_v):
|
||||
z, w = outer_v[j - (m - len(outer_v))]
|
||||
else:
|
||||
z = rpsolve(vs[-1])
|
||||
w = None
|
||||
|
||||
if w is None:
|
||||
w = lpsolve(matvec(z))
|
||||
else:
|
||||
# w is clobbered below
|
||||
w = w.copy()
|
||||
|
||||
w_norm = nrm2(w)
|
||||
|
||||
# GCROT projection: L A -> (1 - C C^H) L A
|
||||
# i.e. orthogonalize against C
|
||||
for i, c in enumerate(cs):
|
||||
alpha = dot(c, w)
|
||||
B[i,j] = alpha
|
||||
w = axpy(c, w, c.shape[0], -alpha) # w -= alpha*c
|
||||
|
||||
# Orthogonalize against V
|
||||
hcur = np.zeros(j+2, dtype=Q.dtype)
|
||||
for i, v in enumerate(vs):
|
||||
alpha = dot(v, w)
|
||||
hcur[i] = alpha
|
||||
w = axpy(v, w, v.shape[0], -alpha) # w -= alpha*v
|
||||
hcur[i+1] = nrm2(w)
|
||||
|
||||
with np.errstate(over='ignore', divide='ignore'):
|
||||
# Careful with denormals
|
||||
alpha = 1/hcur[-1]
|
||||
|
||||
if np.isfinite(alpha):
|
||||
w = scal(alpha, w)
|
||||
|
||||
if not (hcur[-1] > eps * w_norm):
|
||||
# w essentially in the span of previous vectors,
|
||||
# or we have nans. Bail out after updating the QR
|
||||
# solution.
|
||||
breakdown = True
|
||||
|
||||
vs.append(w)
|
||||
zs.append(z)
|
||||
|
||||
# Arnoldi LSQ problem
|
||||
|
||||
# Add new column to H=Q@R, padding other columns with zeros
|
||||
Q2 = np.zeros((j+2, j+2), dtype=Q.dtype, order='F')
|
||||
Q2[:j+1,:j+1] = Q
|
||||
Q2[j+1,j+1] = 1
|
||||
|
||||
R2 = np.zeros((j+2, j), dtype=R.dtype, order='F')
|
||||
R2[:j+1,:] = R
|
||||
|
||||
Q, R = qr_insert(Q2, R2, hcur, j, which='col',
|
||||
overwrite_qru=True, check_finite=False)
|
||||
|
||||
# Transformed least squares problem
|
||||
# || Q R y - inner_res_0 * e_1 ||_2 = min!
|
||||
# Since R = [R'; 0], solution is y = inner_res_0 (R')^{-1} (Q^H)[:j,0]
|
||||
|
||||
# Residual is immediately known
|
||||
res = abs(Q[0,-1])
|
||||
|
||||
# Check for termination
|
||||
if res < atol or breakdown:
|
||||
break
|
||||
|
||||
if not np.isfinite(R[j,j]):
|
||||
# nans encountered, bail out
|
||||
raise LinAlgError()
|
||||
|
||||
# -- Get the LSQ problem solution
|
||||
|
||||
# The problem is triangular, but the condition number may be
|
||||
# bad (or in case of breakdown the last diagonal entry may be
|
||||
# zero), so use lstsq instead of trtrs.
|
||||
y, _, _, _, = lstsq(R[:j+1,:j+1], Q[0,:j+1].conj())
|
||||
|
||||
B = B[:,:j+1]
|
||||
|
||||
return Q, R, B, vs, zs, y, res
|
||||
|
||||
|
||||
def gcrotmk(A, b, x0=None, *, rtol=1e-5, atol=0., maxiter=1000, M=None, callback=None,
|
||||
m=20, k=None, CU=None, discard_C=False, truncate='oldest'):
|
||||
"""
|
||||
Solve a matrix equation using flexible GCROT(m,k) algorithm.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : {sparse matrix, ndarray, LinearOperator}
|
||||
The real or complex N-by-N matrix of the linear system.
|
||||
Alternatively, ``A`` can be a linear operator which can
|
||||
produce ``Ax`` using, e.g.,
|
||||
``scipy.sparse.linalg.LinearOperator``.
|
||||
b : ndarray
|
||||
Right hand side of the linear system. Has shape (N,) or (N,1).
|
||||
x0 : ndarray
|
||||
Starting guess for the solution.
|
||||
rtol, atol : float, optional
|
||||
Parameters for the convergence test. For convergence,
|
||||
``norm(b - A @ x) <= max(rtol*norm(b), atol)`` should be satisfied.
|
||||
The default is ``rtol=1e-5``, the default for ``atol`` is ``0.0``.
|
||||
maxiter : int, optional
|
||||
Maximum number of iterations. Iteration will stop after maxiter
|
||||
steps even if the specified tolerance has not been achieved.
|
||||
M : {sparse matrix, ndarray, LinearOperator}, optional
|
||||
Preconditioner for A. The preconditioner should approximate the
|
||||
inverse of A. gcrotmk is a 'flexible' algorithm and the preconditioner
|
||||
can vary from iteration to iteration. Effective preconditioning
|
||||
dramatically improves the rate of convergence, which implies that
|
||||
fewer iterations are needed to reach a given error tolerance.
|
||||
callback : function, optional
|
||||
User-supplied function to call after each iteration. It is called
|
||||
as callback(xk), where xk is the current solution vector.
|
||||
m : int, optional
|
||||
Number of inner FGMRES iterations per each outer iteration.
|
||||
Default: 20
|
||||
k : int, optional
|
||||
Number of vectors to carry between inner FGMRES iterations.
|
||||
According to [2]_, good values are around m.
|
||||
Default: m
|
||||
CU : list of tuples, optional
|
||||
List of tuples ``(c, u)`` which contain the columns of the matrices
|
||||
C and U in the GCROT(m,k) algorithm. For details, see [2]_.
|
||||
The list given and vectors contained in it are modified in-place.
|
||||
If not given, start from empty matrices. The ``c`` elements in the
|
||||
tuples can be ``None``, in which case the vectors are recomputed
|
||||
via ``c = A u`` on start and orthogonalized as described in [3]_.
|
||||
discard_C : bool, optional
|
||||
Discard the C-vectors at the end. Useful if recycling Krylov subspaces
|
||||
for different linear systems.
|
||||
truncate : {'oldest', 'smallest'}, optional
|
||||
Truncation scheme to use. Drop: oldest vectors, or vectors with
|
||||
smallest singular values using the scheme discussed in [1,2].
|
||||
See [2]_ for detailed comparison.
|
||||
Default: 'oldest'
|
||||
|
||||
Returns
|
||||
-------
|
||||
x : ndarray
|
||||
The solution found.
|
||||
info : int
|
||||
Provides convergence information:
|
||||
|
||||
* 0 : successful exit
|
||||
* >0 : convergence to tolerance not achieved, number of iterations
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> from scipy.sparse.linalg import gcrotmk
|
||||
>>> R = np.random.randn(5, 5)
|
||||
>>> A = csc_matrix(R)
|
||||
>>> b = np.random.randn(5)
|
||||
>>> x, exit_code = gcrotmk(A, b, atol=1e-5)
|
||||
>>> print(exit_code)
|
||||
0
|
||||
>>> np.allclose(A.dot(x), b)
|
||||
True
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] E. de Sturler, ''Truncation strategies for optimal Krylov subspace
|
||||
methods'', SIAM J. Numer. Anal. 36, 864 (1999).
|
||||
.. [2] J.E. Hicken and D.W. Zingg, ''A simplified and flexible variant
|
||||
of GCROT for solving nonsymmetric linear systems'',
|
||||
SIAM J. Sci. Comput. 32, 172 (2010).
|
||||
.. [3] M.L. Parks, E. de Sturler, G. Mackey, D.D. Johnson, S. Maiti,
|
||||
''Recycling Krylov subspaces for sequences of linear systems'',
|
||||
SIAM J. Sci. Comput. 28, 1651 (2006).
|
||||
|
||||
"""
|
||||
A,M,x,b,postprocess = make_system(A,M,x0,b)
|
||||
|
||||
if not np.isfinite(b).all():
|
||||
raise ValueError("RHS must contain only finite numbers")
|
||||
|
||||
if truncate not in ('oldest', 'smallest'):
|
||||
raise ValueError(f"Invalid value for 'truncate': {truncate!r}")
|
||||
|
||||
matvec = A.matvec
|
||||
psolve = M.matvec
|
||||
|
||||
if CU is None:
|
||||
CU = []
|
||||
|
||||
if k is None:
|
||||
k = m
|
||||
|
||||
axpy, dot, scal = None, None, None
|
||||
|
||||
if x0 is None:
|
||||
r = b.copy()
|
||||
else:
|
||||
r = b - matvec(x)
|
||||
|
||||
axpy, dot, scal, nrm2 = get_blas_funcs(['axpy', 'dot', 'scal', 'nrm2'], (x, r))
|
||||
|
||||
b_norm = nrm2(b)
|
||||
|
||||
# we call this to get the right atol/rtol and raise errors as necessary
|
||||
atol, rtol = _get_atol_rtol('gcrotmk', b_norm, atol, rtol)
|
||||
|
||||
if b_norm == 0:
|
||||
x = b
|
||||
return (postprocess(x), 0)
|
||||
|
||||
if discard_C:
|
||||
CU[:] = [(None, u) for c, u in CU]
|
||||
|
||||
# Reorthogonalize old vectors
|
||||
if CU:
|
||||
# Sort already existing vectors to the front
|
||||
CU.sort(key=lambda cu: cu[0] is not None)
|
||||
|
||||
# Fill-in missing ones
|
||||
C = np.empty((A.shape[0], len(CU)), dtype=r.dtype, order='F')
|
||||
us = []
|
||||
j = 0
|
||||
while CU:
|
||||
# More memory-efficient: throw away old vectors as we go
|
||||
c, u = CU.pop(0)
|
||||
if c is None:
|
||||
c = matvec(u)
|
||||
C[:,j] = c
|
||||
j += 1
|
||||
us.append(u)
|
||||
|
||||
# Orthogonalize
|
||||
Q, R, P = qr(C, overwrite_a=True, mode='economic', pivoting=True)
|
||||
del C
|
||||
|
||||
# C := Q
|
||||
cs = list(Q.T)
|
||||
|
||||
# U := U P R^-1, back-substitution
|
||||
new_us = []
|
||||
for j in range(len(cs)):
|
||||
u = us[P[j]]
|
||||
for i in range(j):
|
||||
u = axpy(us[P[i]], u, u.shape[0], -R[i,j])
|
||||
if abs(R[j,j]) < 1e-12 * abs(R[0,0]):
|
||||
# discard rest of the vectors
|
||||
break
|
||||
u = scal(1.0/R[j,j], u)
|
||||
new_us.append(u)
|
||||
|
||||
# Form the new CU lists
|
||||
CU[:] = list(zip(cs, new_us))[::-1]
|
||||
|
||||
if CU:
|
||||
axpy, dot = get_blas_funcs(['axpy', 'dot'], (r,))
|
||||
|
||||
# Solve first the projection operation with respect to the CU
|
||||
# vectors. This corresponds to modifying the initial guess to
|
||||
# be
|
||||
#
|
||||
# x' = x + U y
|
||||
# y = argmin_y || b - A (x + U y) ||^2
|
||||
#
|
||||
# The solution is y = C^H (b - A x)
|
||||
for c, u in CU:
|
||||
yc = dot(c, r)
|
||||
x = axpy(u, x, x.shape[0], yc)
|
||||
r = axpy(c, r, r.shape[0], -yc)
|
||||
|
||||
# GCROT main iteration
|
||||
for j_outer in range(maxiter):
|
||||
# -- callback
|
||||
if callback is not None:
|
||||
callback(x)
|
||||
|
||||
beta = nrm2(r)
|
||||
|
||||
# -- check stopping condition
|
||||
beta_tol = max(atol, rtol * b_norm)
|
||||
|
||||
if beta <= beta_tol and (j_outer > 0 or CU):
|
||||
# recompute residual to avoid rounding error
|
||||
r = b - matvec(x)
|
||||
beta = nrm2(r)
|
||||
|
||||
if beta <= beta_tol:
|
||||
j_outer = -1
|
||||
break
|
||||
|
||||
ml = m + max(k - len(CU), 0)
|
||||
|
||||
cs = [c for c, u in CU]
|
||||
|
||||
try:
|
||||
Q, R, B, vs, zs, y, pres = _fgmres(matvec,
|
||||
r/beta,
|
||||
ml,
|
||||
rpsolve=psolve,
|
||||
atol=max(atol, rtol*b_norm)/beta,
|
||||
cs=cs)
|
||||
y *= beta
|
||||
except LinAlgError:
|
||||
# Floating point over/underflow, non-finite result from
|
||||
# matmul etc. -- report failure.
|
||||
break
|
||||
|
||||
#
|
||||
# At this point,
|
||||
#
|
||||
# [A U, A Z] = [C, V] G; G = [ I B ]
|
||||
# [ 0 H ]
|
||||
#
|
||||
# where [C, V] has orthonormal columns, and r = beta v_0. Moreover,
|
||||
#
|
||||
# || b - A (x + Z y + U q) ||_2 = || r - C B y - V H y - C q ||_2 = min!
|
||||
#
|
||||
# from which y = argmin_y || beta e_1 - H y ||_2, and q = -B y
|
||||
#
|
||||
|
||||
#
|
||||
# GCROT(m,k) update
|
||||
#
|
||||
|
||||
# Define new outer vectors
|
||||
|
||||
# ux := (Z - U B) y
|
||||
ux = zs[0]*y[0]
|
||||
for z, yc in zip(zs[1:], y[1:]):
|
||||
ux = axpy(z, ux, ux.shape[0], yc) # ux += z*yc
|
||||
by = B.dot(y)
|
||||
for cu, byc in zip(CU, by):
|
||||
c, u = cu
|
||||
ux = axpy(u, ux, ux.shape[0], -byc) # ux -= u*byc
|
||||
|
||||
# cx := V H y
|
||||
hy = Q.dot(R.dot(y))
|
||||
cx = vs[0] * hy[0]
|
||||
for v, hyc in zip(vs[1:], hy[1:]):
|
||||
cx = axpy(v, cx, cx.shape[0], hyc) # cx += v*hyc
|
||||
|
||||
# Normalize cx, maintaining cx = A ux
|
||||
# This new cx is orthogonal to the previous C, by construction
|
||||
try:
|
||||
alpha = 1/nrm2(cx)
|
||||
if not np.isfinite(alpha):
|
||||
raise FloatingPointError()
|
||||
except (FloatingPointError, ZeroDivisionError):
|
||||
# Cannot update, so skip it
|
||||
continue
|
||||
|
||||
cx = scal(alpha, cx)
|
||||
ux = scal(alpha, ux)
|
||||
|
||||
# Update residual and solution
|
||||
gamma = dot(cx, r)
|
||||
r = axpy(cx, r, r.shape[0], -gamma) # r -= gamma*cx
|
||||
x = axpy(ux, x, x.shape[0], gamma) # x += gamma*ux
|
||||
|
||||
# Truncate CU
|
||||
if truncate == 'oldest':
|
||||
while len(CU) >= k and CU:
|
||||
del CU[0]
|
||||
elif truncate == 'smallest':
|
||||
if len(CU) >= k and CU:
|
||||
# cf. [1,2]
|
||||
D = solve(R[:-1,:].T, B.T).T
|
||||
W, sigma, V = svd(D)
|
||||
|
||||
# C := C W[:,:k-1], U := U W[:,:k-1]
|
||||
new_CU = []
|
||||
for j, w in enumerate(W[:,:k-1].T):
|
||||
c, u = CU[0]
|
||||
c = c * w[0]
|
||||
u = u * w[0]
|
||||
for cup, wp in zip(CU[1:], w[1:]):
|
||||
cp, up = cup
|
||||
c = axpy(cp, c, c.shape[0], wp)
|
||||
u = axpy(up, u, u.shape[0], wp)
|
||||
|
||||
# Reorthogonalize at the same time; not necessary
|
||||
# in exact arithmetic, but floating point error
|
||||
# tends to accumulate here
|
||||
for cp, up in new_CU:
|
||||
alpha = dot(cp, c)
|
||||
c = axpy(cp, c, c.shape[0], -alpha)
|
||||
u = axpy(up, u, u.shape[0], -alpha)
|
||||
alpha = nrm2(c)
|
||||
c = scal(1.0/alpha, c)
|
||||
u = scal(1.0/alpha, u)
|
||||
|
||||
new_CU.append((c, u))
|
||||
CU[:] = new_CU
|
||||
|
||||
# Add new vector to CU
|
||||
CU.append((cx, ux))
|
||||
|
||||
# Include the solution vector to the span
|
||||
CU.append((None, x.copy()))
|
||||
if discard_C:
|
||||
CU[:] = [(None, uz) for cz, uz in CU]
|
||||
|
||||
return postprocess(x), j_outer + 1
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,230 @@
|
||||
# Copyright (C) 2009, Pauli Virtanen <pav@iki.fi>
|
||||
# Distributed under the same license as SciPy.
|
||||
|
||||
import numpy as np
|
||||
from numpy.linalg import LinAlgError
|
||||
from scipy.linalg import get_blas_funcs
|
||||
from .iterative import _get_atol_rtol
|
||||
from .utils import make_system
|
||||
|
||||
from ._gcrotmk import _fgmres
|
||||
|
||||
__all__ = ['lgmres']
|
||||
|
||||
|
||||
def lgmres(A, b, x0=None, *, rtol=1e-5, atol=0., maxiter=1000, M=None, callback=None,
|
||||
inner_m=30, outer_k=3, outer_v=None, store_outer_Av=True,
|
||||
prepend_outer_v=False):
|
||||
"""
|
||||
Solve a matrix equation using the LGMRES algorithm.
|
||||
|
||||
The LGMRES algorithm [1]_ [2]_ is designed to avoid some problems
|
||||
in the convergence in restarted GMRES, and often converges in fewer
|
||||
iterations.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : {sparse matrix, ndarray, LinearOperator}
|
||||
The real or complex N-by-N matrix of the linear system.
|
||||
Alternatively, ``A`` can be a linear operator which can
|
||||
produce ``Ax`` using, e.g.,
|
||||
``scipy.sparse.linalg.LinearOperator``.
|
||||
b : ndarray
|
||||
Right hand side of the linear system. Has shape (N,) or (N,1).
|
||||
x0 : ndarray
|
||||
Starting guess for the solution.
|
||||
rtol, atol : float, optional
|
||||
Parameters for the convergence test. For convergence,
|
||||
``norm(b - A @ x) <= max(rtol*norm(b), atol)`` should be satisfied.
|
||||
The default is ``rtol=1e-5``, the default for ``atol`` is ``0.0``.
|
||||
maxiter : int, optional
|
||||
Maximum number of iterations. Iteration will stop after maxiter
|
||||
steps even if the specified tolerance has not been achieved.
|
||||
M : {sparse matrix, ndarray, LinearOperator}, optional
|
||||
Preconditioner for A. The preconditioner should approximate the
|
||||
inverse of A. Effective preconditioning dramatically improves the
|
||||
rate of convergence, which implies that fewer iterations are needed
|
||||
to reach a given error tolerance.
|
||||
callback : function, optional
|
||||
User-supplied function to call after each iteration. It is called
|
||||
as callback(xk), where xk is the current solution vector.
|
||||
inner_m : int, optional
|
||||
Number of inner GMRES iterations per each outer iteration.
|
||||
outer_k : int, optional
|
||||
Number of vectors to carry between inner GMRES iterations.
|
||||
According to [1]_, good values are in the range of 1...3.
|
||||
However, note that if you want to use the additional vectors to
|
||||
accelerate solving multiple similar problems, larger values may
|
||||
be beneficial.
|
||||
outer_v : list of tuples, optional
|
||||
List containing tuples ``(v, Av)`` of vectors and corresponding
|
||||
matrix-vector products, used to augment the Krylov subspace, and
|
||||
carried between inner GMRES iterations. The element ``Av`` can
|
||||
be `None` if the matrix-vector product should be re-evaluated.
|
||||
This parameter is modified in-place by `lgmres`, and can be used
|
||||
to pass "guess" vectors in and out of the algorithm when solving
|
||||
similar problems.
|
||||
store_outer_Av : bool, optional
|
||||
Whether LGMRES should store also A@v in addition to vectors `v`
|
||||
in the `outer_v` list. Default is True.
|
||||
prepend_outer_v : bool, optional
|
||||
Whether to put outer_v augmentation vectors before Krylov iterates.
|
||||
In standard LGMRES, prepend_outer_v=False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
x : ndarray
|
||||
The converged solution.
|
||||
info : int
|
||||
Provides convergence information:
|
||||
|
||||
- 0 : successful exit
|
||||
- >0 : convergence to tolerance not achieved, number of iterations
|
||||
- <0 : illegal input or breakdown
|
||||
|
||||
Notes
|
||||
-----
|
||||
The LGMRES algorithm [1]_ [2]_ is designed to avoid the
|
||||
slowing of convergence in restarted GMRES, due to alternating
|
||||
residual vectors. Typically, it often outperforms GMRES(m) of
|
||||
comparable memory requirements by some measure, or at least is not
|
||||
much worse.
|
||||
|
||||
Another advantage in this algorithm is that you can supply it with
|
||||
'guess' vectors in the `outer_v` argument that augment the Krylov
|
||||
subspace. If the solution lies close to the span of these vectors,
|
||||
the algorithm converges faster. This can be useful if several very
|
||||
similar matrices need to be inverted one after another, such as in
|
||||
Newton-Krylov iteration where the Jacobian matrix often changes
|
||||
little in the nonlinear steps.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] A.H. Baker and E.R. Jessup and T. Manteuffel, "A Technique for
|
||||
Accelerating the Convergence of Restarted GMRES", SIAM J. Matrix
|
||||
Anal. Appl. 26, 962 (2005).
|
||||
.. [2] A.H. Baker, "On Improving the Performance of the Linear Solver
|
||||
restarted GMRES", PhD thesis, University of Colorado (2003).
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> from scipy.sparse.linalg import lgmres
|
||||
>>> A = csc_matrix([[3, 2, 0], [1, -1, 0], [0, 5, 1]], dtype=float)
|
||||
>>> b = np.array([2, 4, -1], dtype=float)
|
||||
>>> x, exitCode = lgmres(A, b, atol=1e-5)
|
||||
>>> print(exitCode) # 0 indicates successful convergence
|
||||
0
|
||||
>>> np.allclose(A.dot(x), b)
|
||||
True
|
||||
"""
|
||||
A,M,x,b,postprocess = make_system(A,M,x0,b)
|
||||
|
||||
if not np.isfinite(b).all():
|
||||
raise ValueError("RHS must contain only finite numbers")
|
||||
|
||||
matvec = A.matvec
|
||||
psolve = M.matvec
|
||||
|
||||
if outer_v is None:
|
||||
outer_v = []
|
||||
|
||||
axpy, dot, scal = None, None, None
|
||||
nrm2 = get_blas_funcs('nrm2', [b])
|
||||
|
||||
b_norm = nrm2(b)
|
||||
|
||||
# we call this to get the right atol/rtol and raise errors as necessary
|
||||
atol, rtol = _get_atol_rtol('lgmres', b_norm, atol, rtol)
|
||||
|
||||
if b_norm == 0:
|
||||
x = b
|
||||
return (postprocess(x), 0)
|
||||
|
||||
ptol_max_factor = 1.0
|
||||
|
||||
for k_outer in range(maxiter):
|
||||
r_outer = matvec(x) - b
|
||||
|
||||
# -- callback
|
||||
if callback is not None:
|
||||
callback(x)
|
||||
|
||||
# -- determine input type routines
|
||||
if axpy is None:
|
||||
if np.iscomplexobj(r_outer) and not np.iscomplexobj(x):
|
||||
x = x.astype(r_outer.dtype)
|
||||
axpy, dot, scal, nrm2 = get_blas_funcs(['axpy', 'dot', 'scal', 'nrm2'],
|
||||
(x, r_outer))
|
||||
|
||||
# -- check stopping condition
|
||||
r_norm = nrm2(r_outer)
|
||||
if r_norm <= max(atol, rtol * b_norm):
|
||||
break
|
||||
|
||||
# -- inner LGMRES iteration
|
||||
v0 = -psolve(r_outer)
|
||||
inner_res_0 = nrm2(v0)
|
||||
|
||||
if inner_res_0 == 0:
|
||||
rnorm = nrm2(r_outer)
|
||||
raise RuntimeError("Preconditioner returned a zero vector; "
|
||||
"|v| ~ %.1g, |M v| = 0" % rnorm)
|
||||
|
||||
v0 = scal(1.0/inner_res_0, v0)
|
||||
|
||||
ptol = min(ptol_max_factor, max(atol, rtol*b_norm)/r_norm)
|
||||
|
||||
try:
|
||||
Q, R, B, vs, zs, y, pres = _fgmres(matvec,
|
||||
v0,
|
||||
inner_m,
|
||||
lpsolve=psolve,
|
||||
atol=ptol,
|
||||
outer_v=outer_v,
|
||||
prepend_outer_v=prepend_outer_v)
|
||||
y *= inner_res_0
|
||||
if not np.isfinite(y).all():
|
||||
# Overflow etc. in computation. There's no way to
|
||||
# recover from this, so we have to bail out.
|
||||
raise LinAlgError()
|
||||
except LinAlgError:
|
||||
# Floating point over/underflow, non-finite result from
|
||||
# matmul etc. -- report failure.
|
||||
return postprocess(x), k_outer + 1
|
||||
|
||||
# Inner loop tolerance control
|
||||
if pres > ptol:
|
||||
ptol_max_factor = min(1.0, 1.5 * ptol_max_factor)
|
||||
else:
|
||||
ptol_max_factor = max(1e-16, 0.25 * ptol_max_factor)
|
||||
|
||||
# -- GMRES terminated: eval solution
|
||||
dx = zs[0]*y[0]
|
||||
for w, yc in zip(zs[1:], y[1:]):
|
||||
dx = axpy(w, dx, dx.shape[0], yc) # dx += w*yc
|
||||
|
||||
# -- Store LGMRES augmentation vectors
|
||||
nx = nrm2(dx)
|
||||
if nx > 0:
|
||||
if store_outer_Av:
|
||||
q = Q.dot(R.dot(y))
|
||||
ax = vs[0]*q[0]
|
||||
for v, qc in zip(vs[1:], q[1:]):
|
||||
ax = axpy(v, ax, ax.shape[0], qc)
|
||||
outer_v.append((dx/nx, ax/nx))
|
||||
else:
|
||||
outer_v.append((dx/nx, None))
|
||||
|
||||
# -- Retain only a finite number of augmentation vectors
|
||||
while len(outer_v) > outer_k:
|
||||
del outer_v[0]
|
||||
|
||||
# -- Apply step
|
||||
x += dx
|
||||
else:
|
||||
# didn't converge ...
|
||||
return postprocess(x), maxiter
|
||||
|
||||
return postprocess(x), 0
|
||||
@ -0,0 +1,486 @@
|
||||
"""
|
||||
Copyright (C) 2010 David Fong and Michael Saunders
|
||||
|
||||
LSMR uses an iterative method.
|
||||
|
||||
07 Jun 2010: Documentation updated
|
||||
03 Jun 2010: First release version in Python
|
||||
|
||||
David Chin-lung Fong clfong@stanford.edu
|
||||
Institute for Computational and Mathematical Engineering
|
||||
Stanford University
|
||||
|
||||
Michael Saunders saunders@stanford.edu
|
||||
Systems Optimization Laboratory
|
||||
Dept of MS&E, Stanford University.
|
||||
|
||||
"""
|
||||
|
||||
__all__ = ['lsmr']
|
||||
|
||||
from numpy import zeros, inf, atleast_1d, result_type
|
||||
from numpy.linalg import norm
|
||||
from math import sqrt
|
||||
from scipy.sparse.linalg._interface import aslinearoperator
|
||||
|
||||
from scipy.sparse.linalg._isolve.lsqr import _sym_ortho
|
||||
|
||||
|
||||
def lsmr(A, b, damp=0.0, atol=1e-6, btol=1e-6, conlim=1e8,
|
||||
maxiter=None, show=False, x0=None):
|
||||
"""Iterative solver for least-squares problems.
|
||||
|
||||
lsmr solves the system of linear equations ``Ax = b``. If the system
|
||||
is inconsistent, it solves the least-squares problem ``min ||b - Ax||_2``.
|
||||
``A`` is a rectangular matrix of dimension m-by-n, where all cases are
|
||||
allowed: m = n, m > n, or m < n. ``b`` is a vector of length m.
|
||||
The matrix A may be dense or sparse (usually sparse).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : {sparse matrix, ndarray, LinearOperator}
|
||||
Matrix A in the linear system.
|
||||
Alternatively, ``A`` can be a linear operator which can
|
||||
produce ``Ax`` and ``A^H x`` using, e.g.,
|
||||
``scipy.sparse.linalg.LinearOperator``.
|
||||
b : array_like, shape (m,)
|
||||
Vector ``b`` in the linear system.
|
||||
damp : float
|
||||
Damping factor for regularized least-squares. `lsmr` solves
|
||||
the regularized least-squares problem::
|
||||
|
||||
min ||(b) - ( A )x||
|
||||
||(0) (damp*I) ||_2
|
||||
|
||||
where damp is a scalar. If damp is None or 0, the system
|
||||
is solved without regularization. Default is 0.
|
||||
atol, btol : float, optional
|
||||
Stopping tolerances. `lsmr` continues iterations until a
|
||||
certain backward error estimate is smaller than some quantity
|
||||
depending on atol and btol. Let ``r = b - Ax`` be the
|
||||
residual vector for the current approximate solution ``x``.
|
||||
If ``Ax = b`` seems to be consistent, `lsmr` terminates
|
||||
when ``norm(r) <= atol * norm(A) * norm(x) + btol * norm(b)``.
|
||||
Otherwise, `lsmr` terminates when ``norm(A^H r) <=
|
||||
atol * norm(A) * norm(r)``. If both tolerances are 1.0e-6 (default),
|
||||
the final ``norm(r)`` should be accurate to about 6
|
||||
digits. (The final ``x`` will usually have fewer correct digits,
|
||||
depending on ``cond(A)`` and the size of LAMBDA.) If `atol`
|
||||
or `btol` is None, a default value of 1.0e-6 will be used.
|
||||
Ideally, they should be estimates of the relative error in the
|
||||
entries of ``A`` and ``b`` respectively. For example, if the entries
|
||||
of ``A`` have 7 correct digits, set ``atol = 1e-7``. This prevents
|
||||
the algorithm from doing unnecessary work beyond the
|
||||
uncertainty of the input data.
|
||||
conlim : float, optional
|
||||
`lsmr` terminates if an estimate of ``cond(A)`` exceeds
|
||||
`conlim`. For compatible systems ``Ax = b``, conlim could be
|
||||
as large as 1.0e+12 (say). For least-squares problems,
|
||||
`conlim` should be less than 1.0e+8. If `conlim` is None, the
|
||||
default value is 1e+8. Maximum precision can be obtained by
|
||||
setting ``atol = btol = conlim = 0``, but the number of
|
||||
iterations may then be excessive. Default is 1e8.
|
||||
maxiter : int, optional
|
||||
`lsmr` terminates if the number of iterations reaches
|
||||
`maxiter`. The default is ``maxiter = min(m, n)``. For
|
||||
ill-conditioned systems, a larger value of `maxiter` may be
|
||||
needed. Default is False.
|
||||
show : bool, optional
|
||||
Print iterations logs if ``show=True``. Default is False.
|
||||
x0 : array_like, shape (n,), optional
|
||||
Initial guess of ``x``, if None zeros are used. Default is None.
|
||||
|
||||
.. versionadded:: 1.0.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
x : ndarray of float
|
||||
Least-square solution returned.
|
||||
istop : int
|
||||
istop gives the reason for stopping::
|
||||
|
||||
istop = 0 means x=0 is a solution. If x0 was given, then x=x0 is a
|
||||
solution.
|
||||
= 1 means x is an approximate solution to A@x = B,
|
||||
according to atol and btol.
|
||||
= 2 means x approximately solves the least-squares problem
|
||||
according to atol.
|
||||
= 3 means COND(A) seems to be greater than CONLIM.
|
||||
= 4 is the same as 1 with atol = btol = eps (machine
|
||||
precision)
|
||||
= 5 is the same as 2 with atol = eps.
|
||||
= 6 is the same as 3 with CONLIM = 1/eps.
|
||||
= 7 means ITN reached maxiter before the other stopping
|
||||
conditions were satisfied.
|
||||
|
||||
itn : int
|
||||
Number of iterations used.
|
||||
normr : float
|
||||
``norm(b-Ax)``
|
||||
normar : float
|
||||
``norm(A^H (b - Ax))``
|
||||
norma : float
|
||||
``norm(A)``
|
||||
conda : float
|
||||
Condition number of A.
|
||||
normx : float
|
||||
``norm(x)``
|
||||
|
||||
Notes
|
||||
-----
|
||||
|
||||
.. versionadded:: 0.11.0
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] D. C.-L. Fong and M. A. Saunders,
|
||||
"LSMR: An iterative algorithm for sparse least-squares problems",
|
||||
SIAM J. Sci. Comput., vol. 33, pp. 2950-2971, 2011.
|
||||
:arxiv:`1006.0758`
|
||||
.. [2] LSMR Software, https://web.stanford.edu/group/SOL/software/lsmr/
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> from scipy.sparse.linalg import lsmr
|
||||
>>> A = csc_matrix([[1., 0.], [1., 1.], [0., 1.]], dtype=float)
|
||||
|
||||
The first example has the trivial solution ``[0, 0]``
|
||||
|
||||
>>> b = np.array([0., 0., 0.], dtype=float)
|
||||
>>> x, istop, itn, normr = lsmr(A, b)[:4]
|
||||
>>> istop
|
||||
0
|
||||
>>> x
|
||||
array([0., 0.])
|
||||
|
||||
The stopping code `istop=0` returned indicates that a vector of zeros was
|
||||
found as a solution. The returned solution `x` indeed contains
|
||||
``[0., 0.]``. The next example has a non-trivial solution:
|
||||
|
||||
>>> b = np.array([1., 0., -1.], dtype=float)
|
||||
>>> x, istop, itn, normr = lsmr(A, b)[:4]
|
||||
>>> istop
|
||||
1
|
||||
>>> x
|
||||
array([ 1., -1.])
|
||||
>>> itn
|
||||
1
|
||||
>>> normr
|
||||
4.440892098500627e-16
|
||||
|
||||
As indicated by `istop=1`, `lsmr` found a solution obeying the tolerance
|
||||
limits. The given solution ``[1., -1.]`` obviously solves the equation. The
|
||||
remaining return values include information about the number of iterations
|
||||
(`itn=1`) and the remaining difference of left and right side of the solved
|
||||
equation.
|
||||
The final example demonstrates the behavior in the case where there is no
|
||||
solution for the equation:
|
||||
|
||||
>>> b = np.array([1., 0.01, -1.], dtype=float)
|
||||
>>> x, istop, itn, normr = lsmr(A, b)[:4]
|
||||
>>> istop
|
||||
2
|
||||
>>> x
|
||||
array([ 1.00333333, -0.99666667])
|
||||
>>> A.dot(x)-b
|
||||
array([ 0.00333333, -0.00333333, 0.00333333])
|
||||
>>> normr
|
||||
0.005773502691896255
|
||||
|
||||
`istop` indicates that the system is inconsistent and thus `x` is rather an
|
||||
approximate solution to the corresponding least-squares problem. `normr`
|
||||
contains the minimal distance that was found.
|
||||
"""
|
||||
|
||||
A = aslinearoperator(A)
|
||||
b = atleast_1d(b)
|
||||
if b.ndim > 1:
|
||||
b = b.squeeze()
|
||||
|
||||
msg = ('The exact solution is x = 0, or x = x0, if x0 was given ',
|
||||
'Ax - b is small enough, given atol, btol ',
|
||||
'The least-squares solution is good enough, given atol ',
|
||||
'The estimate of cond(Abar) has exceeded conlim ',
|
||||
'Ax - b is small enough for this machine ',
|
||||
'The least-squares solution is good enough for this machine',
|
||||
'Cond(Abar) seems to be too large for this machine ',
|
||||
'The iteration limit has been reached ')
|
||||
|
||||
hdg1 = ' itn x(1) norm r norm Ar'
|
||||
hdg2 = ' compatible LS norm A cond A'
|
||||
pfreq = 20 # print frequency (for repeating the heading)
|
||||
pcount = 0 # print counter
|
||||
|
||||
m, n = A.shape
|
||||
|
||||
# stores the num of singular values
|
||||
minDim = min([m, n])
|
||||
|
||||
if maxiter is None:
|
||||
maxiter = minDim
|
||||
|
||||
if x0 is None:
|
||||
dtype = result_type(A, b, float)
|
||||
else:
|
||||
dtype = result_type(A, b, x0, float)
|
||||
|
||||
if show:
|
||||
print(' ')
|
||||
print('LSMR Least-squares solution of Ax = b\n')
|
||||
print(f'The matrix A has {m} rows and {n} columns')
|
||||
print('damp = %20.14e\n' % (damp))
|
||||
print(f'atol = {atol:8.2e} conlim = {conlim:8.2e}\n')
|
||||
print(f'btol = {btol:8.2e} maxiter = {maxiter:8g}\n')
|
||||
|
||||
u = b
|
||||
normb = norm(b)
|
||||
if x0 is None:
|
||||
x = zeros(n, dtype)
|
||||
beta = normb.copy()
|
||||
else:
|
||||
x = atleast_1d(x0.copy())
|
||||
u = u - A.matvec(x)
|
||||
beta = norm(u)
|
||||
|
||||
if beta > 0:
|
||||
u = (1 / beta) * u
|
||||
v = A.rmatvec(u)
|
||||
alpha = norm(v)
|
||||
else:
|
||||
v = zeros(n, dtype)
|
||||
alpha = 0
|
||||
|
||||
if alpha > 0:
|
||||
v = (1 / alpha) * v
|
||||
|
||||
# Initialize variables for 1st iteration.
|
||||
|
||||
itn = 0
|
||||
zetabar = alpha * beta
|
||||
alphabar = alpha
|
||||
rho = 1
|
||||
rhobar = 1
|
||||
cbar = 1
|
||||
sbar = 0
|
||||
|
||||
h = v.copy()
|
||||
hbar = zeros(n, dtype)
|
||||
|
||||
# Initialize variables for estimation of ||r||.
|
||||
|
||||
betadd = beta
|
||||
betad = 0
|
||||
rhodold = 1
|
||||
tautildeold = 0
|
||||
thetatilde = 0
|
||||
zeta = 0
|
||||
d = 0
|
||||
|
||||
# Initialize variables for estimation of ||A|| and cond(A)
|
||||
|
||||
normA2 = alpha * alpha
|
||||
maxrbar = 0
|
||||
minrbar = 1e+100
|
||||
normA = sqrt(normA2)
|
||||
condA = 1
|
||||
normx = 0
|
||||
|
||||
# Items for use in stopping rules, normb set earlier
|
||||
istop = 0
|
||||
ctol = 0
|
||||
if conlim > 0:
|
||||
ctol = 1 / conlim
|
||||
normr = beta
|
||||
|
||||
# Reverse the order here from the original matlab code because
|
||||
# there was an error on return when arnorm==0
|
||||
normar = alpha * beta
|
||||
if normar == 0:
|
||||
if show:
|
||||
print(msg[0])
|
||||
return x, istop, itn, normr, normar, normA, condA, normx
|
||||
|
||||
if normb == 0:
|
||||
x[()] = 0
|
||||
return x, istop, itn, normr, normar, normA, condA, normx
|
||||
|
||||
if show:
|
||||
print(' ')
|
||||
print(hdg1, hdg2)
|
||||
test1 = 1
|
||||
test2 = alpha / beta
|
||||
str1 = f'{itn:6g} {x[0]:12.5e}'
|
||||
str2 = f' {normr:10.3e} {normar:10.3e}'
|
||||
str3 = f' {test1:8.1e} {test2:8.1e}'
|
||||
print(''.join([str1, str2, str3]))
|
||||
|
||||
# Main iteration loop.
|
||||
while itn < maxiter:
|
||||
itn = itn + 1
|
||||
|
||||
# Perform the next step of the bidiagonalization to obtain the
|
||||
# next beta, u, alpha, v. These satisfy the relations
|
||||
# beta*u = A@v - alpha*u,
|
||||
# alpha*v = A'@u - beta*v.
|
||||
|
||||
u *= -alpha
|
||||
u += A.matvec(v)
|
||||
beta = norm(u)
|
||||
|
||||
if beta > 0:
|
||||
u *= (1 / beta)
|
||||
v *= -beta
|
||||
v += A.rmatvec(u)
|
||||
alpha = norm(v)
|
||||
if alpha > 0:
|
||||
v *= (1 / alpha)
|
||||
|
||||
# At this point, beta = beta_{k+1}, alpha = alpha_{k+1}.
|
||||
|
||||
# Construct rotation Qhat_{k,2k+1}.
|
||||
|
||||
chat, shat, alphahat = _sym_ortho(alphabar, damp)
|
||||
|
||||
# Use a plane rotation (Q_i) to turn B_i to R_i
|
||||
|
||||
rhoold = rho
|
||||
c, s, rho = _sym_ortho(alphahat, beta)
|
||||
thetanew = s*alpha
|
||||
alphabar = c*alpha
|
||||
|
||||
# Use a plane rotation (Qbar_i) to turn R_i^T to R_i^bar
|
||||
|
||||
rhobarold = rhobar
|
||||
zetaold = zeta
|
||||
thetabar = sbar * rho
|
||||
rhotemp = cbar * rho
|
||||
cbar, sbar, rhobar = _sym_ortho(cbar * rho, thetanew)
|
||||
zeta = cbar * zetabar
|
||||
zetabar = - sbar * zetabar
|
||||
|
||||
# Update h, h_hat, x.
|
||||
|
||||
hbar *= - (thetabar * rho / (rhoold * rhobarold))
|
||||
hbar += h
|
||||
x += (zeta / (rho * rhobar)) * hbar
|
||||
h *= - (thetanew / rho)
|
||||
h += v
|
||||
|
||||
# Estimate of ||r||.
|
||||
|
||||
# Apply rotation Qhat_{k,2k+1}.
|
||||
betaacute = chat * betadd
|
||||
betacheck = -shat * betadd
|
||||
|
||||
# Apply rotation Q_{k,k+1}.
|
||||
betahat = c * betaacute
|
||||
betadd = -s * betaacute
|
||||
|
||||
# Apply rotation Qtilde_{k-1}.
|
||||
# betad = betad_{k-1} here.
|
||||
|
||||
thetatildeold = thetatilde
|
||||
ctildeold, stildeold, rhotildeold = _sym_ortho(rhodold, thetabar)
|
||||
thetatilde = stildeold * rhobar
|
||||
rhodold = ctildeold * rhobar
|
||||
betad = - stildeold * betad + ctildeold * betahat
|
||||
|
||||
# betad = betad_k here.
|
||||
# rhodold = rhod_k here.
|
||||
|
||||
tautildeold = (zetaold - thetatildeold * tautildeold) / rhotildeold
|
||||
taud = (zeta - thetatilde * tautildeold) / rhodold
|
||||
d = d + betacheck * betacheck
|
||||
normr = sqrt(d + (betad - taud)**2 + betadd * betadd)
|
||||
|
||||
# Estimate ||A||.
|
||||
normA2 = normA2 + beta * beta
|
||||
normA = sqrt(normA2)
|
||||
normA2 = normA2 + alpha * alpha
|
||||
|
||||
# Estimate cond(A).
|
||||
maxrbar = max(maxrbar, rhobarold)
|
||||
if itn > 1:
|
||||
minrbar = min(minrbar, rhobarold)
|
||||
condA = max(maxrbar, rhotemp) / min(minrbar, rhotemp)
|
||||
|
||||
# Test for convergence.
|
||||
|
||||
# Compute norms for convergence testing.
|
||||
normar = abs(zetabar)
|
||||
normx = norm(x)
|
||||
|
||||
# Now use these norms to estimate certain other quantities,
|
||||
# some of which will be small near a solution.
|
||||
|
||||
test1 = normr / normb
|
||||
if (normA * normr) != 0:
|
||||
test2 = normar / (normA * normr)
|
||||
else:
|
||||
test2 = inf
|
||||
test3 = 1 / condA
|
||||
t1 = test1 / (1 + normA * normx / normb)
|
||||
rtol = btol + atol * normA * normx / normb
|
||||
|
||||
# The following tests guard against extremely small values of
|
||||
# atol, btol or ctol. (The user may have set any or all of
|
||||
# the parameters atol, btol, conlim to 0.)
|
||||
# The effect is equivalent to the normAl tests using
|
||||
# atol = eps, btol = eps, conlim = 1/eps.
|
||||
|
||||
if itn >= maxiter:
|
||||
istop = 7
|
||||
if 1 + test3 <= 1:
|
||||
istop = 6
|
||||
if 1 + test2 <= 1:
|
||||
istop = 5
|
||||
if 1 + t1 <= 1:
|
||||
istop = 4
|
||||
|
||||
# Allow for tolerances set by the user.
|
||||
|
||||
if test3 <= ctol:
|
||||
istop = 3
|
||||
if test2 <= atol:
|
||||
istop = 2
|
||||
if test1 <= rtol:
|
||||
istop = 1
|
||||
|
||||
# See if it is time to print something.
|
||||
|
||||
if show:
|
||||
if (n <= 40) or (itn <= 10) or (itn >= maxiter - 10) or \
|
||||
(itn % 10 == 0) or (test3 <= 1.1 * ctol) or \
|
||||
(test2 <= 1.1 * atol) or (test1 <= 1.1 * rtol) or \
|
||||
(istop != 0):
|
||||
|
||||
if pcount >= pfreq:
|
||||
pcount = 0
|
||||
print(' ')
|
||||
print(hdg1, hdg2)
|
||||
pcount = pcount + 1
|
||||
str1 = f'{itn:6g} {x[0]:12.5e}'
|
||||
str2 = f' {normr:10.3e} {normar:10.3e}'
|
||||
str3 = f' {test1:8.1e} {test2:8.1e}'
|
||||
str4 = f' {normA:8.1e} {condA:8.1e}'
|
||||
print(''.join([str1, str2, str3, str4]))
|
||||
|
||||
if istop > 0:
|
||||
break
|
||||
|
||||
# Print the stopping condition.
|
||||
|
||||
if show:
|
||||
print(' ')
|
||||
print('LSMR finished')
|
||||
print(msg[istop])
|
||||
print(f'istop ={istop:8g} normr ={normr:8.1e}')
|
||||
print(f' normA ={normA:8.1e} normAr ={normar:8.1e}')
|
||||
print(f'itn ={itn:8g} condA ={condA:8.1e}')
|
||||
print(' normx =%8.1e' % (normx))
|
||||
print(str1, str2)
|
||||
print(str3, str4)
|
||||
|
||||
return x, istop, itn, normr, normar, normA, condA, normx
|
||||
@ -0,0 +1,587 @@
|
||||
"""Sparse Equations and Least Squares.
|
||||
|
||||
The original Fortran code was written by C. C. Paige and M. A. Saunders as
|
||||
described in
|
||||
|
||||
C. C. Paige and M. A. Saunders, LSQR: An algorithm for sparse linear
|
||||
equations and sparse least squares, TOMS 8(1), 43--71 (1982).
|
||||
|
||||
C. C. Paige and M. A. Saunders, Algorithm 583; LSQR: Sparse linear
|
||||
equations and least-squares problems, TOMS 8(2), 195--209 (1982).
|
||||
|
||||
It is licensed under the following BSD license:
|
||||
|
||||
Copyright (c) 2006, Systems Optimization Laboratory
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* Neither the name of Stanford University nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The Fortran code was translated to Python for use in CVXOPT by Jeffery
|
||||
Kline with contributions by Mridul Aanjaneya and Bob Myhill.
|
||||
|
||||
Adapted for SciPy by Stefan van der Walt.
|
||||
|
||||
"""
|
||||
|
||||
__all__ = ['lsqr']
|
||||
|
||||
import numpy as np
|
||||
from math import sqrt
|
||||
from scipy.sparse.linalg._interface import aslinearoperator
|
||||
|
||||
eps = np.finfo(np.float64).eps
|
||||
|
||||
|
||||
def _sym_ortho(a, b):
|
||||
"""
|
||||
Stable implementation of Givens rotation.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The routine 'SymOrtho' was added for numerical stability. This is
|
||||
recommended by S.-C. Choi in [1]_. It removes the unpleasant potential of
|
||||
``1/eps`` in some important places (see, for example text following
|
||||
"Compute the next plane rotation Qk" in minres.py).
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] S.-C. Choi, "Iterative Methods for Singular Linear Equations
|
||||
and Least-Squares Problems", Dissertation,
|
||||
http://www.stanford.edu/group/SOL/dissertations/sou-cheng-choi-thesis.pdf
|
||||
|
||||
"""
|
||||
if b == 0:
|
||||
return np.sign(a), 0, abs(a)
|
||||
elif a == 0:
|
||||
return 0, np.sign(b), abs(b)
|
||||
elif abs(b) > abs(a):
|
||||
tau = a / b
|
||||
s = np.sign(b) / sqrt(1 + tau * tau)
|
||||
c = s * tau
|
||||
r = b / s
|
||||
else:
|
||||
tau = b / a
|
||||
c = np.sign(a) / sqrt(1+tau*tau)
|
||||
s = c * tau
|
||||
r = a / c
|
||||
return c, s, r
|
||||
|
||||
|
||||
def lsqr(A, b, damp=0.0, atol=1e-6, btol=1e-6, conlim=1e8,
|
||||
iter_lim=None, show=False, calc_var=False, x0=None):
|
||||
"""Find the least-squares solution to a large, sparse, linear system
|
||||
of equations.
|
||||
|
||||
The function solves ``Ax = b`` or ``min ||Ax - b||^2`` or
|
||||
``min ||Ax - b||^2 + d^2 ||x - x0||^2``.
|
||||
|
||||
The matrix A may be square or rectangular (over-determined or
|
||||
under-determined), and may have any rank.
|
||||
|
||||
::
|
||||
|
||||
1. Unsymmetric equations -- solve Ax = b
|
||||
|
||||
2. Linear least squares -- solve Ax = b
|
||||
in the least-squares sense
|
||||
|
||||
3. Damped least squares -- solve ( A )*x = ( b )
|
||||
( damp*I ) ( damp*x0 )
|
||||
in the least-squares sense
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : {sparse matrix, ndarray, LinearOperator}
|
||||
Representation of an m-by-n matrix.
|
||||
Alternatively, ``A`` can be a linear operator which can
|
||||
produce ``Ax`` and ``A^T x`` using, e.g.,
|
||||
``scipy.sparse.linalg.LinearOperator``.
|
||||
b : array_like, shape (m,)
|
||||
Right-hand side vector ``b``.
|
||||
damp : float
|
||||
Damping coefficient. Default is 0.
|
||||
atol, btol : float, optional
|
||||
Stopping tolerances. `lsqr` continues iterations until a
|
||||
certain backward error estimate is smaller than some quantity
|
||||
depending on atol and btol. Let ``r = b - Ax`` be the
|
||||
residual vector for the current approximate solution ``x``.
|
||||
If ``Ax = b`` seems to be consistent, `lsqr` terminates
|
||||
when ``norm(r) <= atol * norm(A) * norm(x) + btol * norm(b)``.
|
||||
Otherwise, `lsqr` terminates when ``norm(A^H r) <=
|
||||
atol * norm(A) * norm(r)``. If both tolerances are 1.0e-6 (default),
|
||||
the final ``norm(r)`` should be accurate to about 6
|
||||
digits. (The final ``x`` will usually have fewer correct digits,
|
||||
depending on ``cond(A)`` and the size of LAMBDA.) If `atol`
|
||||
or `btol` is None, a default value of 1.0e-6 will be used.
|
||||
Ideally, they should be estimates of the relative error in the
|
||||
entries of ``A`` and ``b`` respectively. For example, if the entries
|
||||
of ``A`` have 7 correct digits, set ``atol = 1e-7``. This prevents
|
||||
the algorithm from doing unnecessary work beyond the
|
||||
uncertainty of the input data.
|
||||
conlim : float, optional
|
||||
Another stopping tolerance. lsqr terminates if an estimate of
|
||||
``cond(A)`` exceeds `conlim`. For compatible systems ``Ax =
|
||||
b``, `conlim` could be as large as 1.0e+12 (say). For
|
||||
least-squares problems, conlim should be less than 1.0e+8.
|
||||
Maximum precision can be obtained by setting ``atol = btol =
|
||||
conlim = zero``, but the number of iterations may then be
|
||||
excessive. Default is 1e8.
|
||||
iter_lim : int, optional
|
||||
Explicit limitation on number of iterations (for safety).
|
||||
show : bool, optional
|
||||
Display an iteration log. Default is False.
|
||||
calc_var : bool, optional
|
||||
Whether to estimate diagonals of ``(A'A + damp^2*I)^{-1}``.
|
||||
x0 : array_like, shape (n,), optional
|
||||
Initial guess of x, if None zeros are used. Default is None.
|
||||
|
||||
.. versionadded:: 1.0.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
x : ndarray of float
|
||||
The final solution.
|
||||
istop : int
|
||||
Gives the reason for termination.
|
||||
1 means x is an approximate solution to Ax = b.
|
||||
2 means x approximately solves the least-squares problem.
|
||||
itn : int
|
||||
Iteration number upon termination.
|
||||
r1norm : float
|
||||
``norm(r)``, where ``r = b - Ax``.
|
||||
r2norm : float
|
||||
``sqrt( norm(r)^2 + damp^2 * norm(x - x0)^2 )``. Equal to `r1norm`
|
||||
if ``damp == 0``.
|
||||
anorm : float
|
||||
Estimate of Frobenius norm of ``Abar = [[A]; [damp*I]]``.
|
||||
acond : float
|
||||
Estimate of ``cond(Abar)``.
|
||||
arnorm : float
|
||||
Estimate of ``norm(A'@r - damp^2*(x - x0))``.
|
||||
xnorm : float
|
||||
``norm(x)``
|
||||
var : ndarray of float
|
||||
If ``calc_var`` is True, estimates all diagonals of
|
||||
``(A'A)^{-1}`` (if ``damp == 0``) or more generally ``(A'A +
|
||||
damp^2*I)^{-1}``. This is well defined if A has full column
|
||||
rank or ``damp > 0``. (Not sure what var means if ``rank(A)
|
||||
< n`` and ``damp = 0.``)
|
||||
|
||||
Notes
|
||||
-----
|
||||
LSQR uses an iterative method to approximate the solution. The
|
||||
number of iterations required to reach a certain accuracy depends
|
||||
strongly on the scaling of the problem. Poor scaling of the rows
|
||||
or columns of A should therefore be avoided where possible.
|
||||
|
||||
For example, in problem 1 the solution is unaltered by
|
||||
row-scaling. If a row of A is very small or large compared to
|
||||
the other rows of A, the corresponding row of ( A b ) should be
|
||||
scaled up or down.
|
||||
|
||||
In problems 1 and 2, the solution x is easily recovered
|
||||
following column-scaling. Unless better information is known,
|
||||
the nonzero columns of A should be scaled so that they all have
|
||||
the same Euclidean norm (e.g., 1.0).
|
||||
|
||||
In problem 3, there is no freedom to re-scale if damp is
|
||||
nonzero. However, the value of damp should be assigned only
|
||||
after attention has been paid to the scaling of A.
|
||||
|
||||
The parameter damp is intended to help regularize
|
||||
ill-conditioned systems, by preventing the true solution from
|
||||
being very large. Another aid to regularization is provided by
|
||||
the parameter acond, which may be used to terminate iterations
|
||||
before the computed solution becomes very large.
|
||||
|
||||
If some initial estimate ``x0`` is known and if ``damp == 0``,
|
||||
one could proceed as follows:
|
||||
|
||||
1. Compute a residual vector ``r0 = b - A@x0``.
|
||||
2. Use LSQR to solve the system ``A@dx = r0``.
|
||||
3. Add the correction dx to obtain a final solution ``x = x0 + dx``.
|
||||
|
||||
This requires that ``x0`` be available before and after the call
|
||||
to LSQR. To judge the benefits, suppose LSQR takes k1 iterations
|
||||
to solve A@x = b and k2 iterations to solve A@dx = r0.
|
||||
If x0 is "good", norm(r0) will be smaller than norm(b).
|
||||
If the same stopping tolerances atol and btol are used for each
|
||||
system, k1 and k2 will be similar, but the final solution x0 + dx
|
||||
should be more accurate. The only way to reduce the total work
|
||||
is to use a larger stopping tolerance for the second system.
|
||||
If some value btol is suitable for A@x = b, the larger value
|
||||
btol*norm(b)/norm(r0) should be suitable for A@dx = r0.
|
||||
|
||||
Preconditioning is another way to reduce the number of iterations.
|
||||
If it is possible to solve a related system ``M@x = b``
|
||||
efficiently, where M approximates A in some helpful way (e.g. M -
|
||||
A has low rank or its elements are small relative to those of A),
|
||||
LSQR may converge more rapidly on the system ``A@M(inverse)@z =
|
||||
b``, after which x can be recovered by solving M@x = z.
|
||||
|
||||
If A is symmetric, LSQR should not be used!
|
||||
|
||||
Alternatives are the symmetric conjugate-gradient method (cg)
|
||||
and/or SYMMLQ. SYMMLQ is an implementation of symmetric cg that
|
||||
applies to any symmetric A and will converge more rapidly than
|
||||
LSQR. If A is positive definite, there are other implementations
|
||||
of symmetric cg that require slightly less work per iteration than
|
||||
SYMMLQ (but will take the same number of iterations).
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] C. C. Paige and M. A. Saunders (1982a).
|
||||
"LSQR: An algorithm for sparse linear equations and
|
||||
sparse least squares", ACM TOMS 8(1), 43-71.
|
||||
.. [2] C. C. Paige and M. A. Saunders (1982b).
|
||||
"Algorithm 583. LSQR: Sparse linear equations and least
|
||||
squares problems", ACM TOMS 8(2), 195-209.
|
||||
.. [3] M. A. Saunders (1995). "Solution of sparse rectangular
|
||||
systems using LSQR and CRAIG", BIT 35, 588-604.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> from scipy.sparse.linalg import lsqr
|
||||
>>> A = csc_matrix([[1., 0.], [1., 1.], [0., 1.]], dtype=float)
|
||||
|
||||
The first example has the trivial solution ``[0, 0]``
|
||||
|
||||
>>> b = np.array([0., 0., 0.], dtype=float)
|
||||
>>> x, istop, itn, normr = lsqr(A, b)[:4]
|
||||
>>> istop
|
||||
0
|
||||
>>> x
|
||||
array([ 0., 0.])
|
||||
|
||||
The stopping code `istop=0` returned indicates that a vector of zeros was
|
||||
found as a solution. The returned solution `x` indeed contains
|
||||
``[0., 0.]``. The next example has a non-trivial solution:
|
||||
|
||||
>>> b = np.array([1., 0., -1.], dtype=float)
|
||||
>>> x, istop, itn, r1norm = lsqr(A, b)[:4]
|
||||
>>> istop
|
||||
1
|
||||
>>> x
|
||||
array([ 1., -1.])
|
||||
>>> itn
|
||||
1
|
||||
>>> r1norm
|
||||
4.440892098500627e-16
|
||||
|
||||
As indicated by `istop=1`, `lsqr` found a solution obeying the tolerance
|
||||
limits. The given solution ``[1., -1.]`` obviously solves the equation. The
|
||||
remaining return values include information about the number of iterations
|
||||
(`itn=1`) and the remaining difference of left and right side of the solved
|
||||
equation.
|
||||
The final example demonstrates the behavior in the case where there is no
|
||||
solution for the equation:
|
||||
|
||||
>>> b = np.array([1., 0.01, -1.], dtype=float)
|
||||
>>> x, istop, itn, r1norm = lsqr(A, b)[:4]
|
||||
>>> istop
|
||||
2
|
||||
>>> x
|
||||
array([ 1.00333333, -0.99666667])
|
||||
>>> A.dot(x)-b
|
||||
array([ 0.00333333, -0.00333333, 0.00333333])
|
||||
>>> r1norm
|
||||
0.005773502691896255
|
||||
|
||||
`istop` indicates that the system is inconsistent and thus `x` is rather an
|
||||
approximate solution to the corresponding least-squares problem. `r1norm`
|
||||
contains the norm of the minimal residual that was found.
|
||||
"""
|
||||
A = aslinearoperator(A)
|
||||
b = np.atleast_1d(b)
|
||||
if b.ndim > 1:
|
||||
b = b.squeeze()
|
||||
|
||||
m, n = A.shape
|
||||
if iter_lim is None:
|
||||
iter_lim = 2 * n
|
||||
var = np.zeros(n)
|
||||
|
||||
msg = ('The exact solution is x = 0 ',
|
||||
'Ax - b is small enough, given atol, btol ',
|
||||
'The least-squares solution is good enough, given atol ',
|
||||
'The estimate of cond(Abar) has exceeded conlim ',
|
||||
'Ax - b is small enough for this machine ',
|
||||
'The least-squares solution is good enough for this machine',
|
||||
'Cond(Abar) seems to be too large for this machine ',
|
||||
'The iteration limit has been reached ')
|
||||
|
||||
if show:
|
||||
print(' ')
|
||||
print('LSQR Least-squares solution of Ax = b')
|
||||
str1 = f'The matrix A has {m} rows and {n} columns'
|
||||
str2 = f'damp = {damp:20.14e} calc_var = {calc_var:8g}'
|
||||
str3 = f'atol = {atol:8.2e} conlim = {conlim:8.2e}'
|
||||
str4 = f'btol = {btol:8.2e} iter_lim = {iter_lim:8g}'
|
||||
print(str1)
|
||||
print(str2)
|
||||
print(str3)
|
||||
print(str4)
|
||||
|
||||
itn = 0
|
||||
istop = 0
|
||||
ctol = 0
|
||||
if conlim > 0:
|
||||
ctol = 1/conlim
|
||||
anorm = 0
|
||||
acond = 0
|
||||
dampsq = damp**2
|
||||
ddnorm = 0
|
||||
res2 = 0
|
||||
xnorm = 0
|
||||
xxnorm = 0
|
||||
z = 0
|
||||
cs2 = -1
|
||||
sn2 = 0
|
||||
|
||||
# Set up the first vectors u and v for the bidiagonalization.
|
||||
# These satisfy beta*u = b - A@x, alfa*v = A'@u.
|
||||
u = b
|
||||
bnorm = np.linalg.norm(b)
|
||||
|
||||
if x0 is None:
|
||||
x = np.zeros(n)
|
||||
beta = bnorm.copy()
|
||||
else:
|
||||
x = np.asarray(x0)
|
||||
u = u - A.matvec(x)
|
||||
beta = np.linalg.norm(u)
|
||||
|
||||
if beta > 0:
|
||||
u = (1/beta) * u
|
||||
v = A.rmatvec(u)
|
||||
alfa = np.linalg.norm(v)
|
||||
else:
|
||||
v = x.copy()
|
||||
alfa = 0
|
||||
|
||||
if alfa > 0:
|
||||
v = (1/alfa) * v
|
||||
w = v.copy()
|
||||
|
||||
rhobar = alfa
|
||||
phibar = beta
|
||||
rnorm = beta
|
||||
r1norm = rnorm
|
||||
r2norm = rnorm
|
||||
|
||||
# Reverse the order here from the original matlab code because
|
||||
# there was an error on return when arnorm==0
|
||||
arnorm = alfa * beta
|
||||
if arnorm == 0:
|
||||
if show:
|
||||
print(msg[0])
|
||||
return x, istop, itn, r1norm, r2norm, anorm, acond, arnorm, xnorm, var
|
||||
|
||||
head1 = ' Itn x[0] r1norm r2norm '
|
||||
head2 = ' Compatible LS Norm A Cond A'
|
||||
|
||||
if show:
|
||||
print(' ')
|
||||
print(head1, head2)
|
||||
test1 = 1
|
||||
test2 = alfa / beta
|
||||
str1 = f'{itn:6g} {x[0]:12.5e}'
|
||||
str2 = f' {r1norm:10.3e} {r2norm:10.3e}'
|
||||
str3 = f' {test1:8.1e} {test2:8.1e}'
|
||||
print(str1, str2, str3)
|
||||
|
||||
# Main iteration loop.
|
||||
while itn < iter_lim:
|
||||
itn = itn + 1
|
||||
# Perform the next step of the bidiagonalization to obtain the
|
||||
# next beta, u, alfa, v. These satisfy the relations
|
||||
# beta*u = a@v - alfa*u,
|
||||
# alfa*v = A'@u - beta*v.
|
||||
u = A.matvec(v) - alfa * u
|
||||
beta = np.linalg.norm(u)
|
||||
|
||||
if beta > 0:
|
||||
u = (1/beta) * u
|
||||
anorm = sqrt(anorm**2 + alfa**2 + beta**2 + dampsq)
|
||||
v = A.rmatvec(u) - beta * v
|
||||
alfa = np.linalg.norm(v)
|
||||
if alfa > 0:
|
||||
v = (1 / alfa) * v
|
||||
|
||||
# Use a plane rotation to eliminate the damping parameter.
|
||||
# This alters the diagonal (rhobar) of the lower-bidiagonal matrix.
|
||||
if damp > 0:
|
||||
rhobar1 = sqrt(rhobar**2 + dampsq)
|
||||
cs1 = rhobar / rhobar1
|
||||
sn1 = damp / rhobar1
|
||||
psi = sn1 * phibar
|
||||
phibar = cs1 * phibar
|
||||
else:
|
||||
# cs1 = 1 and sn1 = 0
|
||||
rhobar1 = rhobar
|
||||
psi = 0.
|
||||
|
||||
# Use a plane rotation to eliminate the subdiagonal element (beta)
|
||||
# of the lower-bidiagonal matrix, giving an upper-bidiagonal matrix.
|
||||
cs, sn, rho = _sym_ortho(rhobar1, beta)
|
||||
|
||||
theta = sn * alfa
|
||||
rhobar = -cs * alfa
|
||||
phi = cs * phibar
|
||||
phibar = sn * phibar
|
||||
tau = sn * phi
|
||||
|
||||
# Update x and w.
|
||||
t1 = phi / rho
|
||||
t2 = -theta / rho
|
||||
dk = (1 / rho) * w
|
||||
|
||||
x = x + t1 * w
|
||||
w = v + t2 * w
|
||||
ddnorm = ddnorm + np.linalg.norm(dk)**2
|
||||
|
||||
if calc_var:
|
||||
var = var + dk**2
|
||||
|
||||
# Use a plane rotation on the right to eliminate the
|
||||
# super-diagonal element (theta) of the upper-bidiagonal matrix.
|
||||
# Then use the result to estimate norm(x).
|
||||
delta = sn2 * rho
|
||||
gambar = -cs2 * rho
|
||||
rhs = phi - delta * z
|
||||
zbar = rhs / gambar
|
||||
xnorm = sqrt(xxnorm + zbar**2)
|
||||
gamma = sqrt(gambar**2 + theta**2)
|
||||
cs2 = gambar / gamma
|
||||
sn2 = theta / gamma
|
||||
z = rhs / gamma
|
||||
xxnorm = xxnorm + z**2
|
||||
|
||||
# Test for convergence.
|
||||
# First, estimate the condition of the matrix Abar,
|
||||
# and the norms of rbar and Abar'rbar.
|
||||
acond = anorm * sqrt(ddnorm)
|
||||
res1 = phibar**2
|
||||
res2 = res2 + psi**2
|
||||
rnorm = sqrt(res1 + res2)
|
||||
arnorm = alfa * abs(tau)
|
||||
|
||||
# Distinguish between
|
||||
# r1norm = ||b - Ax|| and
|
||||
# r2norm = rnorm in current code
|
||||
# = sqrt(r1norm^2 + damp^2*||x - x0||^2).
|
||||
# Estimate r1norm from
|
||||
# r1norm = sqrt(r2norm^2 - damp^2*||x - x0||^2).
|
||||
# Although there is cancellation, it might be accurate enough.
|
||||
if damp > 0:
|
||||
r1sq = rnorm**2 - dampsq * xxnorm
|
||||
r1norm = sqrt(abs(r1sq))
|
||||
if r1sq < 0:
|
||||
r1norm = -r1norm
|
||||
else:
|
||||
r1norm = rnorm
|
||||
r2norm = rnorm
|
||||
|
||||
# Now use these norms to estimate certain other quantities,
|
||||
# some of which will be small near a solution.
|
||||
test1 = rnorm / bnorm
|
||||
test2 = arnorm / (anorm * rnorm + eps)
|
||||
test3 = 1 / (acond + eps)
|
||||
t1 = test1 / (1 + anorm * xnorm / bnorm)
|
||||
rtol = btol + atol * anorm * xnorm / bnorm
|
||||
|
||||
# The following tests guard against extremely small values of
|
||||
# atol, btol or ctol. (The user may have set any or all of
|
||||
# the parameters atol, btol, conlim to 0.)
|
||||
# The effect is equivalent to the normal tests using
|
||||
# atol = eps, btol = eps, conlim = 1/eps.
|
||||
if itn >= iter_lim:
|
||||
istop = 7
|
||||
if 1 + test3 <= 1:
|
||||
istop = 6
|
||||
if 1 + test2 <= 1:
|
||||
istop = 5
|
||||
if 1 + t1 <= 1:
|
||||
istop = 4
|
||||
|
||||
# Allow for tolerances set by the user.
|
||||
if test3 <= ctol:
|
||||
istop = 3
|
||||
if test2 <= atol:
|
||||
istop = 2
|
||||
if test1 <= rtol:
|
||||
istop = 1
|
||||
|
||||
if show:
|
||||
# See if it is time to print something.
|
||||
prnt = False
|
||||
if n <= 40:
|
||||
prnt = True
|
||||
if itn <= 10:
|
||||
prnt = True
|
||||
if itn >= iter_lim-10:
|
||||
prnt = True
|
||||
# if itn%10 == 0: prnt = True
|
||||
if test3 <= 2*ctol:
|
||||
prnt = True
|
||||
if test2 <= 10*atol:
|
||||
prnt = True
|
||||
if test1 <= 10*rtol:
|
||||
prnt = True
|
||||
if istop != 0:
|
||||
prnt = True
|
||||
|
||||
if prnt:
|
||||
str1 = f'{itn:6g} {x[0]:12.5e}'
|
||||
str2 = f' {r1norm:10.3e} {r2norm:10.3e}'
|
||||
str3 = f' {test1:8.1e} {test2:8.1e}'
|
||||
str4 = f' {anorm:8.1e} {acond:8.1e}'
|
||||
print(str1, str2, str3, str4)
|
||||
|
||||
if istop != 0:
|
||||
break
|
||||
|
||||
# End of iteration loop.
|
||||
# Print the stopping condition.
|
||||
if show:
|
||||
print(' ')
|
||||
print('LSQR finished')
|
||||
print(msg[istop])
|
||||
print(' ')
|
||||
str1 = f'istop ={istop:8g} r1norm ={r1norm:8.1e}'
|
||||
str2 = f'anorm ={anorm:8.1e} arnorm ={arnorm:8.1e}'
|
||||
str3 = f'itn ={itn:8g} r2norm ={r2norm:8.1e}'
|
||||
str4 = f'acond ={acond:8.1e} xnorm ={xnorm:8.1e}'
|
||||
print(str1 + ' ' + str2)
|
||||
print(str3 + ' ' + str4)
|
||||
print(' ')
|
||||
|
||||
return x, istop, itn, r1norm, r2norm, anorm, acond, arnorm, xnorm, var
|
||||
@ -0,0 +1,372 @@
|
||||
from numpy import inner, zeros, inf, finfo
|
||||
from numpy.linalg import norm
|
||||
from math import sqrt
|
||||
|
||||
from .utils import make_system
|
||||
|
||||
__all__ = ['minres']
|
||||
|
||||
|
||||
def minres(A, b, x0=None, *, rtol=1e-5, shift=0.0, maxiter=None,
|
||||
M=None, callback=None, show=False, check=False):
|
||||
"""
|
||||
Use MINimum RESidual iteration to solve Ax=b
|
||||
|
||||
MINRES minimizes norm(Ax - b) for a real symmetric matrix A. Unlike
|
||||
the Conjugate Gradient method, A can be indefinite or singular.
|
||||
|
||||
If shift != 0 then the method solves (A - shift*I)x = b
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : {sparse matrix, ndarray, LinearOperator}
|
||||
The real symmetric N-by-N matrix of the linear system
|
||||
Alternatively, ``A`` can be a linear operator which can
|
||||
produce ``Ax`` using, e.g.,
|
||||
``scipy.sparse.linalg.LinearOperator``.
|
||||
b : ndarray
|
||||
Right hand side of the linear system. Has shape (N,) or (N,1).
|
||||
|
||||
Returns
|
||||
-------
|
||||
x : ndarray
|
||||
The converged solution.
|
||||
info : integer
|
||||
Provides convergence information:
|
||||
0 : successful exit
|
||||
>0 : convergence to tolerance not achieved, number of iterations
|
||||
<0 : illegal input or breakdown
|
||||
|
||||
Other Parameters
|
||||
----------------
|
||||
x0 : ndarray
|
||||
Starting guess for the solution.
|
||||
shift : float
|
||||
Value to apply to the system ``(A - shift * I)x = b``. Default is 0.
|
||||
rtol : float
|
||||
Tolerance to achieve. The algorithm terminates when the relative
|
||||
residual is below ``rtol``.
|
||||
maxiter : integer
|
||||
Maximum number of iterations. Iteration will stop after maxiter
|
||||
steps even if the specified tolerance has not been achieved.
|
||||
M : {sparse matrix, ndarray, LinearOperator}
|
||||
Preconditioner for A. The preconditioner should approximate the
|
||||
inverse of A. Effective preconditioning dramatically improves the
|
||||
rate of convergence, which implies that fewer iterations are needed
|
||||
to reach a given error tolerance.
|
||||
callback : function
|
||||
User-supplied function to call after each iteration. It is called
|
||||
as callback(xk), where xk is the current solution vector.
|
||||
show : bool
|
||||
If ``True``, print out a summary and metrics related to the solution
|
||||
during iterations. Default is ``False``.
|
||||
check : bool
|
||||
If ``True``, run additional input validation to check that `A` and
|
||||
`M` (if specified) are symmetric. Default is ``False``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> from scipy.sparse.linalg import minres
|
||||
>>> A = csc_matrix([[3, 2, 0], [1, -1, 0], [0, 5, 1]], dtype=float)
|
||||
>>> A = A + A.T
|
||||
>>> b = np.array([2, 4, -1], dtype=float)
|
||||
>>> x, exitCode = minres(A, b)
|
||||
>>> print(exitCode) # 0 indicates successful convergence
|
||||
0
|
||||
>>> np.allclose(A.dot(x), b)
|
||||
True
|
||||
|
||||
References
|
||||
----------
|
||||
Solution of sparse indefinite systems of linear equations,
|
||||
C. C. Paige and M. A. Saunders (1975),
|
||||
SIAM J. Numer. Anal. 12(4), pp. 617-629.
|
||||
https://web.stanford.edu/group/SOL/software/minres/
|
||||
|
||||
This file is a translation of the following MATLAB implementation:
|
||||
https://web.stanford.edu/group/SOL/software/minres/minres-matlab.zip
|
||||
|
||||
"""
|
||||
A, M, x, b, postprocess = make_system(A, M, x0, b)
|
||||
|
||||
matvec = A.matvec
|
||||
psolve = M.matvec
|
||||
|
||||
first = 'Enter minres. '
|
||||
last = 'Exit minres. '
|
||||
|
||||
n = A.shape[0]
|
||||
|
||||
if maxiter is None:
|
||||
maxiter = 5 * n
|
||||
|
||||
msg = [' beta2 = 0. If M = I, b and x are eigenvectors ', # -1
|
||||
' beta1 = 0. The exact solution is x0 ', # 0
|
||||
' A solution to Ax = b was found, given rtol ', # 1
|
||||
' A least-squares solution was found, given rtol ', # 2
|
||||
' Reasonable accuracy achieved, given eps ', # 3
|
||||
' x has converged to an eigenvector ', # 4
|
||||
' acond has exceeded 0.1/eps ', # 5
|
||||
' The iteration limit was reached ', # 6
|
||||
' A does not define a symmetric matrix ', # 7
|
||||
' M does not define a symmetric matrix ', # 8
|
||||
' M does not define a pos-def preconditioner '] # 9
|
||||
|
||||
if show:
|
||||
print(first + 'Solution of symmetric Ax = b')
|
||||
print(first + f'n = {n:3g} shift = {shift:23.14e}')
|
||||
print(first + f'itnlim = {maxiter:3g} rtol = {rtol:11.2e}')
|
||||
print()
|
||||
|
||||
istop = 0
|
||||
itn = 0
|
||||
Anorm = 0
|
||||
Acond = 0
|
||||
rnorm = 0
|
||||
ynorm = 0
|
||||
|
||||
xtype = x.dtype
|
||||
|
||||
eps = finfo(xtype).eps
|
||||
|
||||
# Set up y and v for the first Lanczos vector v1.
|
||||
# y = beta1 P' v1, where P = C**(-1).
|
||||
# v is really P' v1.
|
||||
|
||||
if x0 is None:
|
||||
r1 = b.copy()
|
||||
else:
|
||||
r1 = b - A@x
|
||||
y = psolve(r1)
|
||||
|
||||
beta1 = inner(r1, y)
|
||||
|
||||
if beta1 < 0:
|
||||
raise ValueError('indefinite preconditioner')
|
||||
elif beta1 == 0:
|
||||
return (postprocess(x), 0)
|
||||
|
||||
bnorm = norm(b)
|
||||
if bnorm == 0:
|
||||
x = b
|
||||
return (postprocess(x), 0)
|
||||
|
||||
beta1 = sqrt(beta1)
|
||||
|
||||
if check:
|
||||
# are these too strict?
|
||||
|
||||
# see if A is symmetric
|
||||
w = matvec(y)
|
||||
r2 = matvec(w)
|
||||
s = inner(w,w)
|
||||
t = inner(y,r2)
|
||||
z = abs(s - t)
|
||||
epsa = (s + eps) * eps**(1.0/3.0)
|
||||
if z > epsa:
|
||||
raise ValueError('non-symmetric matrix')
|
||||
|
||||
# see if M is symmetric
|
||||
r2 = psolve(y)
|
||||
s = inner(y,y)
|
||||
t = inner(r1,r2)
|
||||
z = abs(s - t)
|
||||
epsa = (s + eps) * eps**(1.0/3.0)
|
||||
if z > epsa:
|
||||
raise ValueError('non-symmetric preconditioner')
|
||||
|
||||
# Initialize other quantities
|
||||
oldb = 0
|
||||
beta = beta1
|
||||
dbar = 0
|
||||
epsln = 0
|
||||
qrnorm = beta1
|
||||
phibar = beta1
|
||||
rhs1 = beta1
|
||||
rhs2 = 0
|
||||
tnorm2 = 0
|
||||
gmax = 0
|
||||
gmin = finfo(xtype).max
|
||||
cs = -1
|
||||
sn = 0
|
||||
w = zeros(n, dtype=xtype)
|
||||
w2 = zeros(n, dtype=xtype)
|
||||
r2 = r1
|
||||
|
||||
if show:
|
||||
print()
|
||||
print()
|
||||
print(' Itn x(1) Compatible LS norm(A) cond(A) gbar/|A|')
|
||||
|
||||
while itn < maxiter:
|
||||
itn += 1
|
||||
|
||||
s = 1.0/beta
|
||||
v = s*y
|
||||
|
||||
y = matvec(v)
|
||||
y = y - shift * v
|
||||
|
||||
if itn >= 2:
|
||||
y = y - (beta/oldb)*r1
|
||||
|
||||
alfa = inner(v,y)
|
||||
y = y - (alfa/beta)*r2
|
||||
r1 = r2
|
||||
r2 = y
|
||||
y = psolve(r2)
|
||||
oldb = beta
|
||||
beta = inner(r2,y)
|
||||
if beta < 0:
|
||||
raise ValueError('non-symmetric matrix')
|
||||
beta = sqrt(beta)
|
||||
tnorm2 += alfa**2 + oldb**2 + beta**2
|
||||
|
||||
if itn == 1:
|
||||
if beta/beta1 <= 10*eps:
|
||||
istop = -1 # Terminate later
|
||||
|
||||
# Apply previous rotation Qk-1 to get
|
||||
# [deltak epslnk+1] = [cs sn][dbark 0 ]
|
||||
# [gbar k dbar k+1] [sn -cs][alfak betak+1].
|
||||
|
||||
oldeps = epsln
|
||||
delta = cs * dbar + sn * alfa # delta1 = 0 deltak
|
||||
gbar = sn * dbar - cs * alfa # gbar 1 = alfa1 gbar k
|
||||
epsln = sn * beta # epsln2 = 0 epslnk+1
|
||||
dbar = - cs * beta # dbar 2 = beta2 dbar k+1
|
||||
root = norm([gbar, dbar])
|
||||
Arnorm = phibar * root
|
||||
|
||||
# Compute the next plane rotation Qk
|
||||
|
||||
gamma = norm([gbar, beta]) # gammak
|
||||
gamma = max(gamma, eps)
|
||||
cs = gbar / gamma # ck
|
||||
sn = beta / gamma # sk
|
||||
phi = cs * phibar # phik
|
||||
phibar = sn * phibar # phibark+1
|
||||
|
||||
# Update x.
|
||||
|
||||
denom = 1.0/gamma
|
||||
w1 = w2
|
||||
w2 = w
|
||||
w = (v - oldeps*w1 - delta*w2) * denom
|
||||
x = x + phi*w
|
||||
|
||||
# Go round again.
|
||||
|
||||
gmax = max(gmax, gamma)
|
||||
gmin = min(gmin, gamma)
|
||||
z = rhs1 / gamma
|
||||
rhs1 = rhs2 - delta*z
|
||||
rhs2 = - epsln*z
|
||||
|
||||
# Estimate various norms and test for convergence.
|
||||
|
||||
Anorm = sqrt(tnorm2)
|
||||
ynorm = norm(x)
|
||||
epsa = Anorm * eps
|
||||
epsx = Anorm * ynorm * eps
|
||||
epsr = Anorm * ynorm * rtol
|
||||
diag = gbar
|
||||
|
||||
if diag == 0:
|
||||
diag = epsa
|
||||
|
||||
qrnorm = phibar
|
||||
rnorm = qrnorm
|
||||
if ynorm == 0 or Anorm == 0:
|
||||
test1 = inf
|
||||
else:
|
||||
test1 = rnorm / (Anorm*ynorm) # ||r|| / (||A|| ||x||)
|
||||
if Anorm == 0:
|
||||
test2 = inf
|
||||
else:
|
||||
test2 = root / Anorm # ||Ar|| / (||A|| ||r||)
|
||||
|
||||
# Estimate cond(A).
|
||||
# In this version we look at the diagonals of R in the
|
||||
# factorization of the lower Hessenberg matrix, Q @ H = R,
|
||||
# where H is the tridiagonal matrix from Lanczos with one
|
||||
# extra row, beta(k+1) e_k^T.
|
||||
|
||||
Acond = gmax/gmin
|
||||
|
||||
# See if any of the stopping criteria are satisfied.
|
||||
# In rare cases, istop is already -1 from above (Abar = const*I).
|
||||
|
||||
if istop == 0:
|
||||
t1 = 1 + test1 # These tests work if rtol < eps
|
||||
t2 = 1 + test2
|
||||
if t2 <= 1:
|
||||
istop = 2
|
||||
if t1 <= 1:
|
||||
istop = 1
|
||||
|
||||
if itn >= maxiter:
|
||||
istop = 6
|
||||
if Acond >= 0.1/eps:
|
||||
istop = 4
|
||||
if epsx >= beta1:
|
||||
istop = 3
|
||||
# if rnorm <= epsx : istop = 2
|
||||
# if rnorm <= epsr : istop = 1
|
||||
if test2 <= rtol:
|
||||
istop = 2
|
||||
if test1 <= rtol:
|
||||
istop = 1
|
||||
|
||||
# See if it is time to print something.
|
||||
|
||||
prnt = False
|
||||
if n <= 40:
|
||||
prnt = True
|
||||
if itn <= 10:
|
||||
prnt = True
|
||||
if itn >= maxiter-10:
|
||||
prnt = True
|
||||
if itn % 10 == 0:
|
||||
prnt = True
|
||||
if qrnorm <= 10*epsx:
|
||||
prnt = True
|
||||
if qrnorm <= 10*epsr:
|
||||
prnt = True
|
||||
if Acond <= 1e-2/eps:
|
||||
prnt = True
|
||||
if istop != 0:
|
||||
prnt = True
|
||||
|
||||
if show and prnt:
|
||||
str1 = f'{itn:6g} {x[0]:12.5e} {test1:10.3e}'
|
||||
str2 = f' {test2:10.3e}'
|
||||
str3 = f' {Anorm:8.1e} {Acond:8.1e} {gbar/Anorm:8.1e}'
|
||||
|
||||
print(str1 + str2 + str3)
|
||||
|
||||
if itn % 10 == 0:
|
||||
print()
|
||||
|
||||
if callback is not None:
|
||||
callback(x)
|
||||
|
||||
if istop != 0:
|
||||
break # TODO check this
|
||||
|
||||
if show:
|
||||
print()
|
||||
print(last + f' istop = {istop:3g} itn ={itn:5g}')
|
||||
print(last + f' Anorm = {Anorm:12.4e} Acond = {Acond:12.4e}')
|
||||
print(last + f' rnorm = {rnorm:12.4e} ynorm = {ynorm:12.4e}')
|
||||
print(last + f' Arnorm = {Arnorm:12.4e}')
|
||||
print(last + msg[istop+1])
|
||||
|
||||
if istop == 6:
|
||||
info = maxiter
|
||||
else:
|
||||
info = 0
|
||||
|
||||
return (postprocess(x),info)
|
||||
@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python
|
||||
"""Tests for the linalg._isolve.gcrotmk module
|
||||
"""
|
||||
|
||||
from numpy.testing import (assert_, assert_allclose, assert_equal,
|
||||
suppress_warnings)
|
||||
|
||||
import numpy as np
|
||||
from numpy import zeros, array, allclose
|
||||
from scipy.linalg import norm
|
||||
from scipy.sparse import csr_matrix, eye, rand
|
||||
|
||||
from scipy.sparse.linalg._interface import LinearOperator
|
||||
from scipy.sparse.linalg import splu
|
||||
from scipy.sparse.linalg._isolve import gcrotmk, gmres
|
||||
|
||||
|
||||
Am = csr_matrix(array([[-2,1,0,0,0,9],
|
||||
[1,-2,1,0,5,0],
|
||||
[0,1,-2,1,0,0],
|
||||
[0,0,1,-2,1,0],
|
||||
[0,3,0,1,-2,1],
|
||||
[1,0,0,0,1,-2]]))
|
||||
b = array([1,2,3,4,5,6])
|
||||
count = [0]
|
||||
|
||||
|
||||
def matvec(v):
|
||||
count[0] += 1
|
||||
return Am@v
|
||||
|
||||
|
||||
A = LinearOperator(matvec=matvec, shape=Am.shape, dtype=Am.dtype)
|
||||
|
||||
|
||||
def do_solve(**kw):
|
||||
count[0] = 0
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(DeprecationWarning, ".*called without specifying.*")
|
||||
x0, flag = gcrotmk(A, b, x0=zeros(A.shape[0]), rtol=1e-14, **kw)
|
||||
count_0 = count[0]
|
||||
assert_(allclose(A@x0, b, rtol=1e-12, atol=1e-12), norm(A@x0-b))
|
||||
return x0, count_0
|
||||
|
||||
|
||||
class TestGCROTMK:
|
||||
def test_preconditioner(self):
|
||||
# Check that preconditioning works
|
||||
pc = splu(Am.tocsc())
|
||||
M = LinearOperator(matvec=pc.solve, shape=A.shape, dtype=A.dtype)
|
||||
|
||||
x0, count_0 = do_solve()
|
||||
x1, count_1 = do_solve(M=M)
|
||||
|
||||
assert_equal(count_1, 3)
|
||||
assert_(count_1 < count_0/2)
|
||||
assert_(allclose(x1, x0, rtol=1e-14))
|
||||
|
||||
def test_arnoldi(self):
|
||||
np.random.seed(1)
|
||||
|
||||
A = eye(2000) + rand(2000, 2000, density=5e-4)
|
||||
b = np.random.rand(2000)
|
||||
|
||||
# The inner arnoldi should be equivalent to gmres
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(DeprecationWarning, ".*called without specifying.*")
|
||||
x0, flag0 = gcrotmk(A, b, x0=zeros(A.shape[0]), m=15, k=0, maxiter=1)
|
||||
x1, flag1 = gmres(A, b, x0=zeros(A.shape[0]), restart=15, maxiter=1)
|
||||
|
||||
assert_equal(flag0, 1)
|
||||
assert_equal(flag1, 1)
|
||||
assert np.linalg.norm(A.dot(x0) - b) > 1e-3
|
||||
|
||||
assert_allclose(x0, x1)
|
||||
|
||||
def test_cornercase(self):
|
||||
np.random.seed(1234)
|
||||
|
||||
# Rounding error may prevent convergence with tol=0 --- ensure
|
||||
# that the return values in this case are correct, and no
|
||||
# exceptions are raised
|
||||
|
||||
for n in [3, 5, 10, 100]:
|
||||
A = 2*eye(n)
|
||||
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(DeprecationWarning, ".*called without specifying.*")
|
||||
b = np.ones(n)
|
||||
x, info = gcrotmk(A, b, maxiter=10)
|
||||
assert_equal(info, 0)
|
||||
assert_allclose(A.dot(x) - b, 0, atol=1e-14)
|
||||
|
||||
x, info = gcrotmk(A, b, rtol=0, maxiter=10)
|
||||
if info == 0:
|
||||
assert_allclose(A.dot(x) - b, 0, atol=1e-14)
|
||||
|
||||
b = np.random.rand(n)
|
||||
x, info = gcrotmk(A, b, maxiter=10)
|
||||
assert_equal(info, 0)
|
||||
assert_allclose(A.dot(x) - b, 0, atol=1e-14)
|
||||
|
||||
x, info = gcrotmk(A, b, rtol=0, maxiter=10)
|
||||
if info == 0:
|
||||
assert_allclose(A.dot(x) - b, 0, atol=1e-14)
|
||||
|
||||
def test_nans(self):
|
||||
A = eye(3, format='lil')
|
||||
A[1,1] = np.nan
|
||||
b = np.ones(3)
|
||||
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(DeprecationWarning, ".*called without specifying.*")
|
||||
x, info = gcrotmk(A, b, rtol=0, maxiter=10)
|
||||
assert_equal(info, 1)
|
||||
|
||||
def test_truncate(self):
|
||||
np.random.seed(1234)
|
||||
A = np.random.rand(30, 30) + np.eye(30)
|
||||
b = np.random.rand(30)
|
||||
|
||||
for truncate in ['oldest', 'smallest']:
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(DeprecationWarning, ".*called without specifying.*")
|
||||
x, info = gcrotmk(A, b, m=10, k=10, truncate=truncate,
|
||||
rtol=1e-4, maxiter=200)
|
||||
assert_equal(info, 0)
|
||||
assert_allclose(A.dot(x) - b, 0, atol=1e-3)
|
||||
|
||||
def test_CU(self):
|
||||
for discard_C in (True, False):
|
||||
# Check that C,U behave as expected
|
||||
CU = []
|
||||
x0, count_0 = do_solve(CU=CU, discard_C=discard_C)
|
||||
assert_(len(CU) > 0)
|
||||
assert_(len(CU) <= 6)
|
||||
|
||||
if discard_C:
|
||||
for c, u in CU:
|
||||
assert_(c is None)
|
||||
|
||||
# should converge immediately
|
||||
x1, count_1 = do_solve(CU=CU, discard_C=discard_C)
|
||||
if discard_C:
|
||||
assert_equal(count_1, 2 + len(CU))
|
||||
else:
|
||||
assert_equal(count_1, 3)
|
||||
assert_(count_1 <= count_0/2)
|
||||
assert_allclose(x1, x0, atol=1e-14)
|
||||
|
||||
def test_denormals(self):
|
||||
# Check that no warnings are emitted if the matrix contains
|
||||
# numbers for which 1/x has no float representation, and that
|
||||
# the solver behaves properly.
|
||||
A = np.array([[1, 2], [3, 4]], dtype=float)
|
||||
A *= 100 * np.nextafter(0, 1)
|
||||
|
||||
b = np.array([1, 1])
|
||||
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(DeprecationWarning, ".*called without specifying.*")
|
||||
xp, info = gcrotmk(A, b)
|
||||
|
||||
if info == 0:
|
||||
assert_allclose(A.dot(xp), b)
|
||||
@ -0,0 +1,809 @@
|
||||
""" Test functions for the sparse.linalg._isolve module
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import platform
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
import numpy as np
|
||||
from numpy.testing import assert_array_equal, assert_allclose
|
||||
from numpy import zeros, arange, array, ones, eye, iscomplexobj
|
||||
from numpy.linalg import norm
|
||||
|
||||
from scipy.sparse import spdiags, csr_matrix, kronsum
|
||||
|
||||
from scipy.sparse.linalg import LinearOperator, aslinearoperator
|
||||
from scipy.sparse.linalg._isolve import (bicg, bicgstab, cg, cgs,
|
||||
gcrotmk, gmres, lgmres,
|
||||
minres, qmr, tfqmr)
|
||||
|
||||
# TODO check that method preserve shape and type
|
||||
# TODO test both preconditioner methods
|
||||
|
||||
|
||||
# list of all solvers under test
|
||||
_SOLVERS = [bicg, bicgstab, cg, cgs, gcrotmk, gmres, lgmres,
|
||||
minres, qmr, tfqmr]
|
||||
|
||||
CB_TYPE_FILTER = ".*called without specifying `callback_type`.*"
|
||||
|
||||
|
||||
# create parametrized fixture for easy reuse in tests
|
||||
@pytest.fixture(params=_SOLVERS, scope="session")
|
||||
def solver(request):
|
||||
"""
|
||||
Fixture for all solvers in scipy.sparse.linalg._isolve
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
class Case:
|
||||
def __init__(self, name, A, b=None, skip=None, nonconvergence=None):
|
||||
self.name = name
|
||||
self.A = A
|
||||
if b is None:
|
||||
self.b = arange(A.shape[0], dtype=float)
|
||||
else:
|
||||
self.b = b
|
||||
if skip is None:
|
||||
self.skip = []
|
||||
else:
|
||||
self.skip = skip
|
||||
if nonconvergence is None:
|
||||
self.nonconvergence = []
|
||||
else:
|
||||
self.nonconvergence = nonconvergence
|
||||
|
||||
|
||||
class SingleTest:
|
||||
def __init__(self, A, b, solver, casename, convergence=True):
|
||||
self.A = A
|
||||
self.b = b
|
||||
self.solver = solver
|
||||
self.name = casename + '-' + solver.__name__
|
||||
self.convergence = convergence
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.name}>"
|
||||
|
||||
|
||||
class IterativeParams:
|
||||
def __init__(self):
|
||||
sym_solvers = [minres, cg]
|
||||
posdef_solvers = [cg]
|
||||
real_solvers = [minres]
|
||||
|
||||
# list of Cases
|
||||
self.cases = []
|
||||
|
||||
# Symmetric and Positive Definite
|
||||
N = 40
|
||||
data = ones((3, N))
|
||||
data[0, :] = 2
|
||||
data[1, :] = -1
|
||||
data[2, :] = -1
|
||||
Poisson1D = spdiags(data, [0, -1, 1], N, N, format='csr')
|
||||
self.cases.append(Case("poisson1d", Poisson1D))
|
||||
# note: minres fails for single precision
|
||||
self.cases.append(Case("poisson1d-F", Poisson1D.astype('f'),
|
||||
skip=[minres]))
|
||||
|
||||
# Symmetric and Negative Definite
|
||||
self.cases.append(Case("neg-poisson1d", -Poisson1D,
|
||||
skip=posdef_solvers))
|
||||
# note: minres fails for single precision
|
||||
self.cases.append(Case("neg-poisson1d-F", (-Poisson1D).astype('f'),
|
||||
skip=posdef_solvers + [minres]))
|
||||
|
||||
# 2-dimensional Poisson equations
|
||||
Poisson2D = kronsum(Poisson1D, Poisson1D)
|
||||
# note: minres fails for 2-d poisson problem,
|
||||
# it will be fixed in the future PR
|
||||
self.cases.append(Case("poisson2d", Poisson2D, skip=[minres]))
|
||||
# note: minres fails for single precision
|
||||
self.cases.append(Case("poisson2d-F", Poisson2D.astype('f'),
|
||||
skip=[minres]))
|
||||
|
||||
# Symmetric and Indefinite
|
||||
data = array([[6, -5, 2, 7, -1, 10, 4, -3, -8, 9]], dtype='d')
|
||||
RandDiag = spdiags(data, [0], 10, 10, format='csr')
|
||||
self.cases.append(Case("rand-diag", RandDiag, skip=posdef_solvers))
|
||||
self.cases.append(Case("rand-diag-F", RandDiag.astype('f'),
|
||||
skip=posdef_solvers))
|
||||
|
||||
# Random real-valued
|
||||
np.random.seed(1234)
|
||||
data = np.random.rand(4, 4)
|
||||
self.cases.append(Case("rand", data,
|
||||
skip=posdef_solvers + sym_solvers))
|
||||
self.cases.append(Case("rand-F", data.astype('f'),
|
||||
skip=posdef_solvers + sym_solvers))
|
||||
|
||||
# Random symmetric real-valued
|
||||
np.random.seed(1234)
|
||||
data = np.random.rand(4, 4)
|
||||
data = data + data.T
|
||||
self.cases.append(Case("rand-sym", data, skip=posdef_solvers))
|
||||
self.cases.append(Case("rand-sym-F", data.astype('f'),
|
||||
skip=posdef_solvers))
|
||||
|
||||
# Random pos-def symmetric real
|
||||
np.random.seed(1234)
|
||||
data = np.random.rand(9, 9)
|
||||
data = np.dot(data.conj(), data.T)
|
||||
self.cases.append(Case("rand-sym-pd", data))
|
||||
# note: minres fails for single precision
|
||||
self.cases.append(Case("rand-sym-pd-F", data.astype('f'),
|
||||
skip=[minres]))
|
||||
|
||||
# Random complex-valued
|
||||
np.random.seed(1234)
|
||||
data = np.random.rand(4, 4) + 1j * np.random.rand(4, 4)
|
||||
skip_cmplx = posdef_solvers + sym_solvers + real_solvers
|
||||
self.cases.append(Case("rand-cmplx", data, skip=skip_cmplx))
|
||||
self.cases.append(Case("rand-cmplx-F", data.astype('F'),
|
||||
skip=skip_cmplx))
|
||||
|
||||
# Random hermitian complex-valued
|
||||
np.random.seed(1234)
|
||||
data = np.random.rand(4, 4) + 1j * np.random.rand(4, 4)
|
||||
data = data + data.T.conj()
|
||||
self.cases.append(Case("rand-cmplx-herm", data,
|
||||
skip=posdef_solvers + real_solvers))
|
||||
self.cases.append(Case("rand-cmplx-herm-F", data.astype('F'),
|
||||
skip=posdef_solvers + real_solvers))
|
||||
|
||||
# Random pos-def hermitian complex-valued
|
||||
np.random.seed(1234)
|
||||
data = np.random.rand(9, 9) + 1j * np.random.rand(9, 9)
|
||||
data = np.dot(data.conj(), data.T)
|
||||
self.cases.append(Case("rand-cmplx-sym-pd", data, skip=real_solvers))
|
||||
self.cases.append(Case("rand-cmplx-sym-pd-F", data.astype('F'),
|
||||
skip=real_solvers))
|
||||
|
||||
# Non-symmetric and Positive Definite
|
||||
#
|
||||
# cgs, qmr, bicg and tfqmr fail to converge on this one
|
||||
# -- algorithmic limitation apparently
|
||||
data = ones((2, 10))
|
||||
data[0, :] = 2
|
||||
data[1, :] = -1
|
||||
A = spdiags(data, [0, -1], 10, 10, format='csr')
|
||||
self.cases.append(Case("nonsymposdef", A,
|
||||
skip=sym_solvers + [cgs, qmr, bicg, tfqmr]))
|
||||
self.cases.append(Case("nonsymposdef-F", A.astype('F'),
|
||||
skip=sym_solvers + [cgs, qmr, bicg, tfqmr]))
|
||||
|
||||
# Symmetric, non-pd, hitting cgs/bicg/bicgstab/qmr/tfqmr breakdown
|
||||
A = np.array([[0, 0, 0, 0, 0, 1, -1, -0, -0, -0, -0],
|
||||
[0, 0, 0, 0, 0, 2, -0, -1, -0, -0, -0],
|
||||
[0, 0, 0, 0, 0, 2, -0, -0, -1, -0, -0],
|
||||
[0, 0, 0, 0, 0, 2, -0, -0, -0, -1, -0],
|
||||
[0, 0, 0, 0, 0, 1, -0, -0, -0, -0, -1],
|
||||
[1, 2, 2, 2, 1, 0, -0, -0, -0, -0, -0],
|
||||
[-1, 0, 0, 0, 0, 0, -1, -0, -0, -0, -0],
|
||||
[0, -1, 0, 0, 0, 0, -0, -1, -0, -0, -0],
|
||||
[0, 0, -1, 0, 0, 0, -0, -0, -1, -0, -0],
|
||||
[0, 0, 0, -1, 0, 0, -0, -0, -0, -1, -0],
|
||||
[0, 0, 0, 0, -1, 0, -0, -0, -0, -0, -1]], dtype=float)
|
||||
b = np.array([0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], dtype=float)
|
||||
assert (A == A.T).all()
|
||||
self.cases.append(Case("sym-nonpd", A, b,
|
||||
skip=posdef_solvers,
|
||||
nonconvergence=[cgs, bicg, bicgstab, qmr, tfqmr]
|
||||
)
|
||||
)
|
||||
|
||||
def generate_tests(self):
|
||||
# generate test cases with skips applied
|
||||
tests = []
|
||||
for case in self.cases:
|
||||
for solver in _SOLVERS:
|
||||
if (solver in case.skip):
|
||||
continue
|
||||
if solver in case.nonconvergence:
|
||||
tests += [SingleTest(case.A, case.b, solver, case.name,
|
||||
convergence=False)]
|
||||
else:
|
||||
tests += [SingleTest(case.A, case.b, solver, case.name)]
|
||||
return tests
|
||||
|
||||
|
||||
cases = IterativeParams().generate_tests()
|
||||
|
||||
|
||||
@pytest.fixture(params=cases, ids=[x.name for x in cases], scope="module")
|
||||
def case(request):
|
||||
"""
|
||||
Fixture for all cases in IterativeParams
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
def test_maxiter(case):
|
||||
if not case.convergence:
|
||||
pytest.skip("Solver - Breakdown case, see gh-8829")
|
||||
A = case.A
|
||||
rtol = 1e-12
|
||||
|
||||
b = case.b
|
||||
x0 = 0 * b
|
||||
|
||||
residuals = []
|
||||
|
||||
def callback(x):
|
||||
residuals.append(norm(b - case.A * x))
|
||||
|
||||
if case.solver == gmres:
|
||||
with pytest.warns(DeprecationWarning, match=CB_TYPE_FILTER):
|
||||
x, info = case.solver(A, b, x0=x0, rtol=rtol, maxiter=1, callback=callback)
|
||||
else:
|
||||
x, info = case.solver(A, b, x0=x0, rtol=rtol, maxiter=1, callback=callback)
|
||||
|
||||
assert len(residuals) == 1
|
||||
assert info == 1
|
||||
|
||||
|
||||
def test_convergence(case):
|
||||
A = case.A
|
||||
|
||||
if A.dtype.char in "dD":
|
||||
rtol = 1e-8
|
||||
else:
|
||||
rtol = 1e-2
|
||||
|
||||
b = case.b
|
||||
x0 = 0 * b
|
||||
|
||||
x, info = case.solver(A, b, x0=x0, rtol=rtol)
|
||||
|
||||
assert_array_equal(x0, 0 * b) # ensure that x0 is not overwritten
|
||||
if case.convergence:
|
||||
assert info == 0
|
||||
assert norm(A @ x - b) <= norm(b) * rtol
|
||||
else:
|
||||
assert info != 0
|
||||
assert norm(A @ x - b) <= norm(b)
|
||||
|
||||
|
||||
def test_precond_dummy(case):
|
||||
if not case.convergence:
|
||||
pytest.skip("Solver - Breakdown case, see gh-8829")
|
||||
|
||||
rtol = 1e-8
|
||||
|
||||
def identity(b, which=None):
|
||||
"""trivial preconditioner"""
|
||||
return b
|
||||
|
||||
A = case.A
|
||||
|
||||
M, N = A.shape
|
||||
# Ensure the diagonal elements of A are non-zero before calculating
|
||||
# 1.0/A.diagonal()
|
||||
diagOfA = A.diagonal()
|
||||
if np.count_nonzero(diagOfA) == len(diagOfA):
|
||||
spdiags([1.0 / diagOfA], [0], M, N)
|
||||
|
||||
b = case.b
|
||||
x0 = 0 * b
|
||||
|
||||
precond = LinearOperator(A.shape, identity, rmatvec=identity)
|
||||
|
||||
if case.solver is qmr:
|
||||
x, info = case.solver(A, b, M1=precond, M2=precond, x0=x0, rtol=rtol)
|
||||
else:
|
||||
x, info = case.solver(A, b, M=precond, x0=x0, rtol=rtol)
|
||||
assert info == 0
|
||||
assert norm(A @ x - b) <= norm(b) * rtol
|
||||
|
||||
A = aslinearoperator(A)
|
||||
A.psolve = identity
|
||||
A.rpsolve = identity
|
||||
|
||||
x, info = case.solver(A, b, x0=x0, rtol=rtol)
|
||||
assert info == 0
|
||||
assert norm(A @ x - b) <= norm(b) * rtol
|
||||
|
||||
|
||||
# Specific test for poisson1d and poisson2d cases
|
||||
@pytest.mark.fail_slow(5)
|
||||
@pytest.mark.parametrize('case', [x for x in IterativeParams().cases
|
||||
if x.name in ('poisson1d', 'poisson2d')],
|
||||
ids=['poisson1d', 'poisson2d'])
|
||||
def test_precond_inverse(case):
|
||||
for solver in _SOLVERS:
|
||||
if solver in case.skip or solver is qmr:
|
||||
continue
|
||||
|
||||
rtol = 1e-8
|
||||
|
||||
def inverse(b, which=None):
|
||||
"""inverse preconditioner"""
|
||||
A = case.A
|
||||
if not isinstance(A, np.ndarray):
|
||||
A = A.toarray()
|
||||
return np.linalg.solve(A, b)
|
||||
|
||||
def rinverse(b, which=None):
|
||||
"""inverse preconditioner"""
|
||||
A = case.A
|
||||
if not isinstance(A, np.ndarray):
|
||||
A = A.toarray()
|
||||
return np.linalg.solve(A.T, b)
|
||||
|
||||
matvec_count = [0]
|
||||
|
||||
def matvec(b):
|
||||
matvec_count[0] += 1
|
||||
return case.A @ b
|
||||
|
||||
def rmatvec(b):
|
||||
matvec_count[0] += 1
|
||||
return case.A.T @ b
|
||||
|
||||
b = case.b
|
||||
x0 = 0 * b
|
||||
|
||||
A = LinearOperator(case.A.shape, matvec, rmatvec=rmatvec)
|
||||
precond = LinearOperator(case.A.shape, inverse, rmatvec=rinverse)
|
||||
|
||||
# Solve with preconditioner
|
||||
matvec_count = [0]
|
||||
x, info = solver(A, b, M=precond, x0=x0, rtol=rtol)
|
||||
|
||||
assert info == 0
|
||||
assert norm(case.A @ x - b) <= norm(b) * rtol
|
||||
|
||||
# Solution should be nearly instant
|
||||
assert matvec_count[0] <= 3
|
||||
|
||||
|
||||
def test_atol(solver):
|
||||
# TODO: minres / tfqmr. It didn't historically use absolute tolerances, so
|
||||
# fixing it is less urgent.
|
||||
if solver in (minres, tfqmr):
|
||||
pytest.skip("TODO: Add atol to minres/tfqmr")
|
||||
|
||||
# Historically this is tested as below, all pass but for some reason
|
||||
# gcrotmk is over-sensitive to difference between random.seed/rng.random
|
||||
# Hence tol lower bound is changed from -10 to -9
|
||||
# np.random.seed(1234)
|
||||
# A = np.random.rand(10, 10)
|
||||
# A = A @ A.T + 10 * np.eye(10)
|
||||
# b = 1e3*np.random.rand(10)
|
||||
|
||||
rng = np.random.default_rng(168441431005389)
|
||||
A = rng.uniform(size=[10, 10])
|
||||
A = A @ A.T + 10*np.eye(10)
|
||||
b = 1e3 * rng.uniform(size=10)
|
||||
|
||||
b_norm = np.linalg.norm(b)
|
||||
|
||||
tols = np.r_[0, np.logspace(-9, 2, 7), np.inf]
|
||||
|
||||
# Check effect of badly scaled preconditioners
|
||||
M0 = rng.standard_normal(size=(10, 10))
|
||||
M0 = M0 @ M0.T
|
||||
Ms = [None, 1e-6 * M0, 1e6 * M0]
|
||||
|
||||
for M, rtol, atol in itertools.product(Ms, tols, tols):
|
||||
if rtol == 0 and atol == 0:
|
||||
continue
|
||||
|
||||
if solver is qmr:
|
||||
if M is not None:
|
||||
M = aslinearoperator(M)
|
||||
M2 = aslinearoperator(np.eye(10))
|
||||
else:
|
||||
M2 = None
|
||||
x, info = solver(A, b, M1=M, M2=M2, rtol=rtol, atol=atol)
|
||||
else:
|
||||
x, info = solver(A, b, M=M, rtol=rtol, atol=atol)
|
||||
|
||||
assert info == 0
|
||||
residual = A @ x - b
|
||||
err = np.linalg.norm(residual)
|
||||
atol2 = rtol * b_norm
|
||||
# Added 1.00025 fudge factor because of `err` exceeding `atol` just
|
||||
# very slightly on s390x (see gh-17839)
|
||||
assert err <= 1.00025 * max(atol, atol2)
|
||||
|
||||
|
||||
def test_zero_rhs(solver):
|
||||
rng = np.random.default_rng(1684414984100503)
|
||||
A = rng.random(size=[10, 10])
|
||||
A = A @ A.T + 10 * np.eye(10)
|
||||
|
||||
b = np.zeros(10)
|
||||
tols = np.r_[np.logspace(-10, 2, 7)]
|
||||
|
||||
for tol in tols:
|
||||
x, info = solver(A, b, rtol=tol)
|
||||
assert info == 0
|
||||
assert_allclose(x, 0., atol=1e-15)
|
||||
|
||||
x, info = solver(A, b, rtol=tol, x0=ones(10))
|
||||
assert info == 0
|
||||
assert_allclose(x, 0., atol=tol)
|
||||
|
||||
if solver is not minres:
|
||||
x, info = solver(A, b, rtol=tol, atol=0, x0=ones(10))
|
||||
if info == 0:
|
||||
assert_allclose(x, 0)
|
||||
|
||||
x, info = solver(A, b, rtol=tol, atol=tol)
|
||||
assert info == 0
|
||||
assert_allclose(x, 0, atol=1e-300)
|
||||
|
||||
x, info = solver(A, b, rtol=tol, atol=0)
|
||||
assert info == 0
|
||||
assert_allclose(x, 0, atol=1e-300)
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason="see gh-18697")
|
||||
def test_maxiter_worsening(solver):
|
||||
if solver not in (gmres, lgmres, qmr):
|
||||
# these were skipped from the very beginning, see gh-9201; gh-14160
|
||||
pytest.skip("Solver breakdown case")
|
||||
# Check error does not grow (boundlessly) with increasing maxiter.
|
||||
# This can occur due to the solvers hitting close to breakdown,
|
||||
# which they should detect and halt as necessary.
|
||||
# cf. gh-9100
|
||||
if (solver is gmres and platform.machine() == 'aarch64'
|
||||
and sys.version_info[1] == 9):
|
||||
pytest.xfail(reason="gh-13019")
|
||||
if (solver is lgmres and
|
||||
platform.machine() not in ['x86_64' 'x86', 'aarch64', 'arm64']):
|
||||
# see gh-17839
|
||||
pytest.xfail(reason="fails on at least ppc64le, ppc64 and riscv64")
|
||||
|
||||
# Singular matrix, rhs numerically not in range
|
||||
A = np.array([[-0.1112795288033378, 0, 0, 0.16127952880333685],
|
||||
[0, -0.13627952880333782 + 6.283185307179586j, 0, 0],
|
||||
[0, 0, -0.13627952880333782 - 6.283185307179586j, 0],
|
||||
[0.1112795288033368, 0j, 0j, -0.16127952880333785]])
|
||||
v = np.ones(4)
|
||||
best_error = np.inf
|
||||
|
||||
# Unable to match the Fortran code tolerance levels with this example
|
||||
# Original tolerance values
|
||||
|
||||
# slack_tol = 7 if platform.machine() == 'aarch64' else 5
|
||||
slack_tol = 9
|
||||
|
||||
for maxiter in range(1, 20):
|
||||
x, info = solver(A, v, maxiter=maxiter, rtol=1e-8, atol=0)
|
||||
|
||||
if info == 0:
|
||||
assert norm(A @ x - v) <= 1e-8 * norm(v)
|
||||
|
||||
error = np.linalg.norm(A @ x - v)
|
||||
best_error = min(best_error, error)
|
||||
|
||||
# Check with slack
|
||||
assert error <= slack_tol * best_error
|
||||
|
||||
|
||||
def test_x0_working(solver):
|
||||
# Easy problem
|
||||
rng = np.random.default_rng(1685363802304750)
|
||||
n = 10
|
||||
A = rng.random(size=[n, n])
|
||||
A = A @ A.T
|
||||
b = rng.random(n)
|
||||
x0 = rng.random(n)
|
||||
|
||||
if solver is minres:
|
||||
kw = dict(rtol=1e-6)
|
||||
else:
|
||||
kw = dict(atol=0, rtol=1e-6)
|
||||
|
||||
x, info = solver(A, b, **kw)
|
||||
assert info == 0
|
||||
assert norm(A @ x - b) <= 1e-6 * norm(b)
|
||||
|
||||
x, info = solver(A, b, x0=x0, **kw)
|
||||
assert info == 0
|
||||
assert norm(A @ x - b) <= 3e-6*norm(b)
|
||||
|
||||
|
||||
def test_x0_equals_Mb(case):
|
||||
if (case.solver is bicgstab) and (case.name == 'nonsymposdef-bicgstab'):
|
||||
pytest.skip("Solver fails due to numerical noise "
|
||||
"on some architectures (see gh-15533).")
|
||||
if case.solver is tfqmr:
|
||||
pytest.skip("Solver does not support x0='Mb'")
|
||||
|
||||
A = case.A
|
||||
b = case.b
|
||||
x0 = 'Mb'
|
||||
rtol = 1e-8
|
||||
x, info = case.solver(A, b, x0=x0, rtol=rtol)
|
||||
|
||||
assert_array_equal(x0, 'Mb') # ensure that x0 is not overwritten
|
||||
assert info == 0
|
||||
assert norm(A @ x - b) <= rtol * norm(b)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('solver', _SOLVERS)
|
||||
def test_x0_solves_problem_exactly(solver):
|
||||
# See gh-19948
|
||||
mat = np.eye(2)
|
||||
rhs = np.array([-1., -1.])
|
||||
|
||||
sol, info = solver(mat, rhs, x0=rhs)
|
||||
assert_allclose(sol, rhs)
|
||||
assert info == 0
|
||||
|
||||
|
||||
# Specific tfqmr test
|
||||
@pytest.mark.parametrize('case', IterativeParams().cases)
|
||||
def test_show(case, capsys):
|
||||
def cb(x):
|
||||
pass
|
||||
|
||||
x, info = tfqmr(case.A, case.b, callback=cb, show=True)
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
if case.name == "sym-nonpd":
|
||||
# no logs for some reason
|
||||
exp = ""
|
||||
elif case.name in ("nonsymposdef", "nonsymposdef-F"):
|
||||
# Asymmetric and Positive Definite
|
||||
exp = "TFQMR: Linear solve not converged due to reach MAXIT iterations"
|
||||
else: # all other cases
|
||||
exp = "TFQMR: Linear solve converged due to reach TOL iterations"
|
||||
|
||||
assert out.startswith(exp)
|
||||
assert err == ""
|
||||
|
||||
|
||||
def test_positional_error(solver):
|
||||
# from test_x0_working
|
||||
rng = np.random.default_rng(1685363802304750)
|
||||
n = 10
|
||||
A = rng.random(size=[n, n])
|
||||
A = A @ A.T
|
||||
b = rng.random(n)
|
||||
x0 = rng.random(n)
|
||||
with pytest.raises(TypeError):
|
||||
solver(A, b, x0, 1e-5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("atol", ["legacy", None, -1])
|
||||
def test_invalid_atol(solver, atol):
|
||||
if solver == minres:
|
||||
pytest.skip("minres has no `atol` argument")
|
||||
# from test_x0_working
|
||||
rng = np.random.default_rng(1685363802304750)
|
||||
n = 10
|
||||
A = rng.random(size=[n, n])
|
||||
A = A @ A.T
|
||||
b = rng.random(n)
|
||||
x0 = rng.random(n)
|
||||
with pytest.raises(ValueError):
|
||||
solver(A, b, x0, atol=atol)
|
||||
|
||||
|
||||
class TestQMR:
|
||||
@pytest.mark.filterwarnings('ignore::scipy.sparse.SparseEfficiencyWarning')
|
||||
def test_leftright_precond(self):
|
||||
"""Check that QMR works with left and right preconditioners"""
|
||||
|
||||
from scipy.sparse.linalg._dsolve import splu
|
||||
from scipy.sparse.linalg._interface import LinearOperator
|
||||
|
||||
n = 100
|
||||
|
||||
dat = ones(n)
|
||||
A = spdiags([-2 * dat, 4 * dat, -dat], [-1, 0, 1], n, n)
|
||||
b = arange(n, dtype='d')
|
||||
|
||||
L = spdiags([-dat / 2, dat], [-1, 0], n, n)
|
||||
U = spdiags([4 * dat, -dat], [0, 1], n, n)
|
||||
L_solver = splu(L)
|
||||
U_solver = splu(U)
|
||||
|
||||
def L_solve(b):
|
||||
return L_solver.solve(b)
|
||||
|
||||
def U_solve(b):
|
||||
return U_solver.solve(b)
|
||||
|
||||
def LT_solve(b):
|
||||
return L_solver.solve(b, 'T')
|
||||
|
||||
def UT_solve(b):
|
||||
return U_solver.solve(b, 'T')
|
||||
|
||||
M1 = LinearOperator((n, n), matvec=L_solve, rmatvec=LT_solve)
|
||||
M2 = LinearOperator((n, n), matvec=U_solve, rmatvec=UT_solve)
|
||||
|
||||
rtol = 1e-8
|
||||
x, info = qmr(A, b, rtol=rtol, maxiter=15, M1=M1, M2=M2)
|
||||
|
||||
assert info == 0
|
||||
assert norm(A @ x - b) <= rtol * norm(b)
|
||||
|
||||
|
||||
class TestGMRES:
|
||||
def test_basic(self):
|
||||
A = np.vander(np.arange(10) + 1)[:, ::-1]
|
||||
b = np.zeros(10)
|
||||
b[0] = 1
|
||||
|
||||
x_gm, err = gmres(A, b, restart=5, maxiter=1)
|
||||
|
||||
assert_allclose(x_gm[0], 0.359, rtol=1e-2)
|
||||
|
||||
@pytest.mark.filterwarnings(f"ignore:{CB_TYPE_FILTER}:DeprecationWarning")
|
||||
def test_callback(self):
|
||||
|
||||
def store_residual(r, rvec):
|
||||
rvec[rvec.nonzero()[0].max() + 1] = r
|
||||
|
||||
# Define, A,b
|
||||
A = csr_matrix(array([[-2, 1, 0, 0, 0, 0],
|
||||
[1, -2, 1, 0, 0, 0],
|
||||
[0, 1, -2, 1, 0, 0],
|
||||
[0, 0, 1, -2, 1, 0],
|
||||
[0, 0, 0, 1, -2, 1],
|
||||
[0, 0, 0, 0, 1, -2]]))
|
||||
b = ones((A.shape[0],))
|
||||
maxiter = 1
|
||||
rvec = zeros(maxiter + 1)
|
||||
rvec[0] = 1.0
|
||||
|
||||
def callback(r):
|
||||
return store_residual(r, rvec)
|
||||
|
||||
x, flag = gmres(A, b, x0=zeros(A.shape[0]), rtol=1e-16,
|
||||
maxiter=maxiter, callback=callback)
|
||||
|
||||
# Expected output from SciPy 1.0.0
|
||||
assert_allclose(rvec, array([1.0, 0.81649658092772603]), rtol=1e-10)
|
||||
|
||||
# Test preconditioned callback
|
||||
M = 1e-3 * np.eye(A.shape[0])
|
||||
rvec = zeros(maxiter + 1)
|
||||
rvec[0] = 1.0
|
||||
x, flag = gmres(A, b, M=M, rtol=1e-16, maxiter=maxiter,
|
||||
callback=callback)
|
||||
|
||||
# Expected output from SciPy 1.0.0
|
||||
# (callback has preconditioned residual!)
|
||||
assert_allclose(rvec, array([1.0, 1e-3 * 0.81649658092772603]),
|
||||
rtol=1e-10)
|
||||
|
||||
def test_abi(self):
|
||||
# Check we don't segfault on gmres with complex argument
|
||||
A = eye(2)
|
||||
b = ones(2)
|
||||
r_x, r_info = gmres(A, b)
|
||||
r_x = r_x.astype(complex)
|
||||
x, info = gmres(A.astype(complex), b.astype(complex))
|
||||
|
||||
assert iscomplexobj(x)
|
||||
assert_allclose(r_x, x)
|
||||
assert r_info == info
|
||||
|
||||
@pytest.mark.fail_slow(5)
|
||||
def test_atol_legacy(self):
|
||||
|
||||
A = eye(2)
|
||||
b = ones(2)
|
||||
x, info = gmres(A, b, rtol=1e-5)
|
||||
assert np.linalg.norm(A @ x - b) <= 1e-5 * np.linalg.norm(b)
|
||||
assert_allclose(x, b, atol=0, rtol=1e-8)
|
||||
|
||||
rndm = np.random.RandomState(12345)
|
||||
A = rndm.rand(30, 30)
|
||||
b = 1e-6 * ones(30)
|
||||
x, info = gmres(A, b, rtol=1e-7, restart=20)
|
||||
assert np.linalg.norm(A @ x - b) > 1e-7
|
||||
|
||||
A = eye(2)
|
||||
b = 1e-10 * ones(2)
|
||||
x, info = gmres(A, b, rtol=1e-8, atol=0)
|
||||
assert np.linalg.norm(A @ x - b) <= 1e-8 * np.linalg.norm(b)
|
||||
|
||||
def test_defective_precond_breakdown(self):
|
||||
# Breakdown due to defective preconditioner
|
||||
M = np.eye(3)
|
||||
M[2, 2] = 0
|
||||
|
||||
b = np.array([0, 1, 1])
|
||||
x = np.array([1, 0, 0])
|
||||
A = np.diag([2, 3, 4])
|
||||
|
||||
x, info = gmres(A, b, x0=x, M=M, rtol=1e-15, atol=0)
|
||||
|
||||
# Should not return nans, nor terminate with false success
|
||||
assert not np.isnan(x).any()
|
||||
if info == 0:
|
||||
assert np.linalg.norm(A @ x - b) <= 1e-15 * np.linalg.norm(b)
|
||||
|
||||
# The solution should be OK outside null space of M
|
||||
assert_allclose(M @ (A @ x), M @ b)
|
||||
|
||||
def test_defective_matrix_breakdown(self):
|
||||
# Breakdown due to defective matrix
|
||||
A = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]])
|
||||
b = np.array([1, 0, 1])
|
||||
rtol = 1e-8
|
||||
x, info = gmres(A, b, rtol=rtol, atol=0)
|
||||
|
||||
# Should not return nans, nor terminate with false success
|
||||
assert not np.isnan(x).any()
|
||||
if info == 0:
|
||||
assert np.linalg.norm(A @ x - b) <= rtol * np.linalg.norm(b)
|
||||
|
||||
# The solution should be OK outside null space of A
|
||||
assert_allclose(A @ (A @ x), A @ b)
|
||||
|
||||
@pytest.mark.filterwarnings(f"ignore:{CB_TYPE_FILTER}:DeprecationWarning")
|
||||
def test_callback_type(self):
|
||||
# The legacy callback type changes meaning of 'maxiter'
|
||||
np.random.seed(1)
|
||||
A = np.random.rand(20, 20)
|
||||
b = np.random.rand(20)
|
||||
|
||||
cb_count = [0]
|
||||
|
||||
def pr_norm_cb(r):
|
||||
cb_count[0] += 1
|
||||
assert isinstance(r, float)
|
||||
|
||||
def x_cb(x):
|
||||
cb_count[0] += 1
|
||||
assert isinstance(x, np.ndarray)
|
||||
|
||||
# 2 iterations is not enough to solve the problem
|
||||
cb_count = [0]
|
||||
x, info = gmres(A, b, rtol=1e-6, atol=0, callback=pr_norm_cb,
|
||||
maxiter=2, restart=50)
|
||||
assert info == 2
|
||||
assert cb_count[0] == 2
|
||||
|
||||
# With `callback_type` specified, no warning should be raised
|
||||
cb_count = [0]
|
||||
x, info = gmres(A, b, rtol=1e-6, atol=0, callback=pr_norm_cb,
|
||||
maxiter=2, restart=50, callback_type='legacy')
|
||||
assert info == 2
|
||||
assert cb_count[0] == 2
|
||||
|
||||
# 2 restart cycles is enough to solve the problem
|
||||
cb_count = [0]
|
||||
x, info = gmres(A, b, rtol=1e-6, atol=0, callback=pr_norm_cb,
|
||||
maxiter=2, restart=50, callback_type='pr_norm')
|
||||
assert info == 0
|
||||
assert cb_count[0] > 2
|
||||
|
||||
# 2 restart cycles is enough to solve the problem
|
||||
cb_count = [0]
|
||||
x, info = gmres(A, b, rtol=1e-6, atol=0, callback=x_cb, maxiter=2,
|
||||
restart=50, callback_type='x')
|
||||
assert info == 0
|
||||
assert cb_count[0] == 1
|
||||
|
||||
def test_callback_x_monotonic(self):
|
||||
# Check that callback_type='x' gives monotonic norm decrease
|
||||
np.random.seed(1)
|
||||
A = np.random.rand(20, 20) + np.eye(20)
|
||||
b = np.random.rand(20)
|
||||
|
||||
prev_r = [np.inf]
|
||||
count = [0]
|
||||
|
||||
def x_cb(x):
|
||||
r = np.linalg.norm(A @ x - b)
|
||||
assert r <= prev_r[0]
|
||||
prev_r[0] = r
|
||||
count[0] += 1
|
||||
|
||||
x, info = gmres(A, b, rtol=1e-6, atol=0, callback=x_cb, maxiter=20,
|
||||
restart=10, callback_type='x')
|
||||
assert info == 20
|
||||
assert count[0] == 20
|
||||
@ -0,0 +1,211 @@
|
||||
"""Tests for the linalg._isolve.lgmres module
|
||||
"""
|
||||
|
||||
from numpy.testing import (assert_, assert_allclose, assert_equal,
|
||||
suppress_warnings)
|
||||
|
||||
import pytest
|
||||
from platform import python_implementation
|
||||
|
||||
import numpy as np
|
||||
from numpy import zeros, array, allclose
|
||||
from scipy.linalg import norm
|
||||
from scipy.sparse import csr_matrix, eye, rand
|
||||
|
||||
from scipy.sparse.linalg._interface import LinearOperator
|
||||
from scipy.sparse.linalg import splu
|
||||
from scipy.sparse.linalg._isolve import lgmres, gmres
|
||||
|
||||
|
||||
Am = csr_matrix(array([[-2, 1, 0, 0, 0, 9],
|
||||
[1, -2, 1, 0, 5, 0],
|
||||
[0, 1, -2, 1, 0, 0],
|
||||
[0, 0, 1, -2, 1, 0],
|
||||
[0, 3, 0, 1, -2, 1],
|
||||
[1, 0, 0, 0, 1, -2]]))
|
||||
b = array([1, 2, 3, 4, 5, 6])
|
||||
count = [0]
|
||||
|
||||
|
||||
def matvec(v):
|
||||
count[0] += 1
|
||||
return Am@v
|
||||
|
||||
|
||||
A = LinearOperator(matvec=matvec, shape=Am.shape, dtype=Am.dtype)
|
||||
|
||||
|
||||
def do_solve(**kw):
|
||||
count[0] = 0
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(DeprecationWarning, ".*called without specifying.*")
|
||||
x0, flag = lgmres(A, b, x0=zeros(A.shape[0]),
|
||||
inner_m=6, rtol=1e-14, **kw)
|
||||
count_0 = count[0]
|
||||
assert_(allclose(A@x0, b, rtol=1e-12, atol=1e-12), norm(A@x0-b))
|
||||
return x0, count_0
|
||||
|
||||
|
||||
class TestLGMRES:
|
||||
def test_preconditioner(self):
|
||||
# Check that preconditioning works
|
||||
pc = splu(Am.tocsc())
|
||||
M = LinearOperator(matvec=pc.solve, shape=A.shape, dtype=A.dtype)
|
||||
|
||||
x0, count_0 = do_solve()
|
||||
x1, count_1 = do_solve(M=M)
|
||||
|
||||
assert_(count_1 == 3)
|
||||
assert_(count_1 < count_0/2)
|
||||
assert_(allclose(x1, x0, rtol=1e-14))
|
||||
|
||||
def test_outer_v(self):
|
||||
# Check that the augmentation vectors behave as expected
|
||||
|
||||
outer_v = []
|
||||
x0, count_0 = do_solve(outer_k=6, outer_v=outer_v)
|
||||
assert_(len(outer_v) > 0)
|
||||
assert_(len(outer_v) <= 6)
|
||||
|
||||
x1, count_1 = do_solve(outer_k=6, outer_v=outer_v,
|
||||
prepend_outer_v=True)
|
||||
assert_(count_1 == 2, count_1)
|
||||
assert_(count_1 < count_0/2)
|
||||
assert_(allclose(x1, x0, rtol=1e-14))
|
||||
|
||||
# ---
|
||||
|
||||
outer_v = []
|
||||
x0, count_0 = do_solve(outer_k=6, outer_v=outer_v,
|
||||
store_outer_Av=False)
|
||||
assert_(array([v[1] is None for v in outer_v]).all())
|
||||
assert_(len(outer_v) > 0)
|
||||
assert_(len(outer_v) <= 6)
|
||||
|
||||
x1, count_1 = do_solve(outer_k=6, outer_v=outer_v,
|
||||
prepend_outer_v=True)
|
||||
assert_(count_1 == 3, count_1)
|
||||
assert_(count_1 < count_0/2)
|
||||
assert_(allclose(x1, x0, rtol=1e-14))
|
||||
|
||||
@pytest.mark.skipif(python_implementation() == 'PyPy',
|
||||
reason="Fails on PyPy CI runs. See #9507")
|
||||
def test_arnoldi(self):
|
||||
np.random.seed(1234)
|
||||
|
||||
A = eye(2000) + rand(2000, 2000, density=5e-4)
|
||||
b = np.random.rand(2000)
|
||||
|
||||
# The inner arnoldi should be equivalent to gmres
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(DeprecationWarning, ".*called without specifying.*")
|
||||
x0, flag0 = lgmres(A, b, x0=zeros(A.shape[0]),
|
||||
inner_m=15, maxiter=1)
|
||||
x1, flag1 = gmres(A, b, x0=zeros(A.shape[0]),
|
||||
restart=15, maxiter=1)
|
||||
|
||||
assert_equal(flag0, 1)
|
||||
assert_equal(flag1, 1)
|
||||
norm = np.linalg.norm(A.dot(x0) - b)
|
||||
assert_(norm > 1e-4)
|
||||
assert_allclose(x0, x1)
|
||||
|
||||
def test_cornercase(self):
|
||||
np.random.seed(1234)
|
||||
|
||||
# Rounding error may prevent convergence with tol=0 --- ensure
|
||||
# that the return values in this case are correct, and no
|
||||
# exceptions are raised
|
||||
|
||||
for n in [3, 5, 10, 100]:
|
||||
A = 2*eye(n)
|
||||
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(DeprecationWarning, ".*called without specifying.*")
|
||||
|
||||
b = np.ones(n)
|
||||
x, info = lgmres(A, b, maxiter=10)
|
||||
assert_equal(info, 0)
|
||||
assert_allclose(A.dot(x) - b, 0, atol=1e-14)
|
||||
|
||||
x, info = lgmres(A, b, rtol=0, maxiter=10)
|
||||
if info == 0:
|
||||
assert_allclose(A.dot(x) - b, 0, atol=1e-14)
|
||||
|
||||
b = np.random.rand(n)
|
||||
x, info = lgmres(A, b, maxiter=10)
|
||||
assert_equal(info, 0)
|
||||
assert_allclose(A.dot(x) - b, 0, atol=1e-14)
|
||||
|
||||
x, info = lgmres(A, b, rtol=0, maxiter=10)
|
||||
if info == 0:
|
||||
assert_allclose(A.dot(x) - b, 0, atol=1e-14)
|
||||
|
||||
def test_nans(self):
|
||||
A = eye(3, format='lil')
|
||||
A[1, 1] = np.nan
|
||||
b = np.ones(3)
|
||||
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(DeprecationWarning, ".*called without specifying.*")
|
||||
x, info = lgmres(A, b, rtol=0, maxiter=10)
|
||||
assert_equal(info, 1)
|
||||
|
||||
def test_breakdown_with_outer_v(self):
|
||||
A = np.array([[1, 2], [3, 4]], dtype=float)
|
||||
b = np.array([1, 2])
|
||||
|
||||
x = np.linalg.solve(A, b)
|
||||
v0 = np.array([1, 0])
|
||||
|
||||
# The inner iteration should converge to the correct solution,
|
||||
# since it's in the outer vector list
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(DeprecationWarning, ".*called without specifying.*")
|
||||
xp, info = lgmres(A, b, outer_v=[(v0, None), (x, None)], maxiter=1)
|
||||
|
||||
assert_allclose(xp, x, atol=1e-12)
|
||||
|
||||
def test_breakdown_underdetermined(self):
|
||||
# Should find LSQ solution in the Krylov span in one inner
|
||||
# iteration, despite solver breakdown from nilpotent A.
|
||||
A = np.array([[0, 1, 1, 1],
|
||||
[0, 0, 1, 1],
|
||||
[0, 0, 0, 1],
|
||||
[0, 0, 0, 0]], dtype=float)
|
||||
|
||||
bs = [
|
||||
np.array([1, 1, 1, 1]),
|
||||
np.array([1, 1, 1, 0]),
|
||||
np.array([1, 1, 0, 0]),
|
||||
np.array([1, 0, 0, 0]),
|
||||
]
|
||||
|
||||
for b in bs:
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(DeprecationWarning, ".*called without specifying.*")
|
||||
xp, info = lgmres(A, b, maxiter=1)
|
||||
resp = np.linalg.norm(A.dot(xp) - b)
|
||||
|
||||
K = np.c_[b, A.dot(b), A.dot(A.dot(b)), A.dot(A.dot(A.dot(b)))]
|
||||
y, _, _, _ = np.linalg.lstsq(A.dot(K), b, rcond=-1)
|
||||
x = K.dot(y)
|
||||
res = np.linalg.norm(A.dot(x) - b)
|
||||
|
||||
assert_allclose(resp, res, err_msg=repr(b))
|
||||
|
||||
def test_denormals(self):
|
||||
# Check that no warnings are emitted if the matrix contains
|
||||
# numbers for which 1/x has no float representation, and that
|
||||
# the solver behaves properly.
|
||||
A = np.array([[1, 2], [3, 4]], dtype=float)
|
||||
A *= 100 * np.nextafter(0, 1)
|
||||
|
||||
b = np.array([1, 1])
|
||||
|
||||
with suppress_warnings() as sup:
|
||||
sup.filter(DeprecationWarning, ".*called without specifying.*")
|
||||
xp, info = lgmres(A, b)
|
||||
|
||||
if info == 0:
|
||||
assert_allclose(A.dot(xp), b)
|
||||
@ -0,0 +1,185 @@
|
||||
"""
|
||||
Copyright (C) 2010 David Fong and Michael Saunders
|
||||
Distributed under the same license as SciPy
|
||||
|
||||
Testing Code for LSMR.
|
||||
|
||||
03 Jun 2010: First version release with lsmr.py
|
||||
|
||||
David Chin-lung Fong clfong@stanford.edu
|
||||
Institute for Computational and Mathematical Engineering
|
||||
Stanford University
|
||||
|
||||
Michael Saunders saunders@stanford.edu
|
||||
Systems Optimization Laboratory
|
||||
Dept of MS&E, Stanford University.
|
||||
|
||||
"""
|
||||
|
||||
from numpy import array, arange, eye, zeros, ones, transpose, hstack
|
||||
from numpy.linalg import norm
|
||||
from numpy.testing import assert_allclose
|
||||
import pytest
|
||||
from scipy.sparse import coo_matrix
|
||||
from scipy.sparse.linalg._interface import aslinearoperator
|
||||
from scipy.sparse.linalg import lsmr
|
||||
from .test_lsqr import G, b
|
||||
|
||||
|
||||
class TestLSMR:
|
||||
def setup_method(self):
|
||||
self.n = 10
|
||||
self.m = 10
|
||||
|
||||
def assertCompatibleSystem(self, A, xtrue):
|
||||
Afun = aslinearoperator(A)
|
||||
b = Afun.matvec(xtrue)
|
||||
x = lsmr(A, b)[0]
|
||||
assert norm(x - xtrue) == pytest.approx(0, abs=1e-5)
|
||||
|
||||
def testIdentityACase1(self):
|
||||
A = eye(self.n)
|
||||
xtrue = zeros((self.n, 1))
|
||||
self.assertCompatibleSystem(A, xtrue)
|
||||
|
||||
def testIdentityACase2(self):
|
||||
A = eye(self.n)
|
||||
xtrue = ones((self.n,1))
|
||||
self.assertCompatibleSystem(A, xtrue)
|
||||
|
||||
def testIdentityACase3(self):
|
||||
A = eye(self.n)
|
||||
xtrue = transpose(arange(self.n,0,-1))
|
||||
self.assertCompatibleSystem(A, xtrue)
|
||||
|
||||
def testBidiagonalA(self):
|
||||
A = lowerBidiagonalMatrix(20,self.n)
|
||||
xtrue = transpose(arange(self.n,0,-1))
|
||||
self.assertCompatibleSystem(A,xtrue)
|
||||
|
||||
def testScalarB(self):
|
||||
A = array([[1.0, 2.0]])
|
||||
b = 3.0
|
||||
x = lsmr(A, b)[0]
|
||||
assert norm(A.dot(x) - b) == pytest.approx(0)
|
||||
|
||||
def testComplexX(self):
|
||||
A = eye(self.n)
|
||||
xtrue = transpose(arange(self.n, 0, -1) * (1 + 1j))
|
||||
self.assertCompatibleSystem(A, xtrue)
|
||||
|
||||
def testComplexX0(self):
|
||||
A = 4 * eye(self.n) + ones((self.n, self.n))
|
||||
xtrue = transpose(arange(self.n, 0, -1))
|
||||
b = aslinearoperator(A).matvec(xtrue)
|
||||
x0 = zeros(self.n, dtype=complex)
|
||||
x = lsmr(A, b, x0=x0)[0]
|
||||
assert norm(x - xtrue) == pytest.approx(0, abs=1e-5)
|
||||
|
||||
def testComplexA(self):
|
||||
A = 4 * eye(self.n) + 1j * ones((self.n, self.n))
|
||||
xtrue = transpose(arange(self.n, 0, -1).astype(complex))
|
||||
self.assertCompatibleSystem(A, xtrue)
|
||||
|
||||
def testComplexB(self):
|
||||
A = 4 * eye(self.n) + ones((self.n, self.n))
|
||||
xtrue = transpose(arange(self.n, 0, -1) * (1 + 1j))
|
||||
b = aslinearoperator(A).matvec(xtrue)
|
||||
x = lsmr(A, b)[0]
|
||||
assert norm(x - xtrue) == pytest.approx(0, abs=1e-5)
|
||||
|
||||
def testColumnB(self):
|
||||
A = eye(self.n)
|
||||
b = ones((self.n, 1))
|
||||
x = lsmr(A, b)[0]
|
||||
assert norm(A.dot(x) - b.ravel()) == pytest.approx(0)
|
||||
|
||||
def testInitialization(self):
|
||||
# Test that the default setting is not modified
|
||||
x_ref, _, itn_ref, normr_ref, *_ = lsmr(G, b)
|
||||
assert_allclose(norm(b - G@x_ref), normr_ref, atol=1e-6)
|
||||
|
||||
# Test passing zeros yields similar result
|
||||
x0 = zeros(b.shape)
|
||||
x = lsmr(G, b, x0=x0)[0]
|
||||
assert_allclose(x, x_ref)
|
||||
|
||||
# Test warm-start with single iteration
|
||||
x0 = lsmr(G, b, maxiter=1)[0]
|
||||
|
||||
x, _, itn, normr, *_ = lsmr(G, b, x0=x0)
|
||||
assert_allclose(norm(b - G@x), normr, atol=1e-6)
|
||||
|
||||
# NOTE(gh-12139): This doesn't always converge to the same value as
|
||||
# ref because error estimates will be slightly different when calculated
|
||||
# from zeros vs x0 as a result only compare norm and itn (not x).
|
||||
|
||||
# x generally converges 1 iteration faster because it started at x0.
|
||||
# itn == itn_ref means that lsmr(x0) took an extra iteration see above.
|
||||
# -1 is technically possible but is rare (1 in 100000) so it's more
|
||||
# likely to be an error elsewhere.
|
||||
assert itn - itn_ref in (0, 1)
|
||||
|
||||
# If an extra iteration is performed normr may be 0, while normr_ref
|
||||
# may be much larger.
|
||||
assert normr < normr_ref * (1 + 1e-6)
|
||||
|
||||
|
||||
class TestLSMRReturns:
|
||||
def setup_method(self):
|
||||
self.n = 10
|
||||
self.A = lowerBidiagonalMatrix(20, self.n)
|
||||
self.xtrue = transpose(arange(self.n, 0, -1))
|
||||
self.Afun = aslinearoperator(self.A)
|
||||
self.b = self.Afun.matvec(self.xtrue)
|
||||
self.x0 = ones(self.n)
|
||||
self.x00 = self.x0.copy()
|
||||
self.returnValues = lsmr(self.A, self.b)
|
||||
self.returnValuesX0 = lsmr(self.A, self.b, x0=self.x0)
|
||||
|
||||
def test_unchanged_x0(self):
|
||||
x, istop, itn, normr, normar, normA, condA, normx = self.returnValuesX0
|
||||
assert_allclose(self.x00, self.x0)
|
||||
|
||||
def testNormr(self):
|
||||
x, istop, itn, normr, normar, normA, condA, normx = self.returnValues
|
||||
assert norm(self.b - self.Afun.matvec(x)) == pytest.approx(normr)
|
||||
|
||||
def testNormar(self):
|
||||
x, istop, itn, normr, normar, normA, condA, normx = self.returnValues
|
||||
assert (norm(self.Afun.rmatvec(self.b - self.Afun.matvec(x)))
|
||||
== pytest.approx(normar))
|
||||
|
||||
def testNormx(self):
|
||||
x, istop, itn, normr, normar, normA, condA, normx = self.returnValues
|
||||
assert norm(x) == pytest.approx(normx)
|
||||
|
||||
|
||||
def lowerBidiagonalMatrix(m, n):
|
||||
# This is a simple example for testing LSMR.
|
||||
# It uses the leading m*n submatrix from
|
||||
# A = [ 1
|
||||
# 1 2
|
||||
# 2 3
|
||||
# 3 4
|
||||
# ...
|
||||
# n ]
|
||||
# suitably padded by zeros.
|
||||
#
|
||||
# 04 Jun 2010: First version for distribution with lsmr.py
|
||||
if m <= n:
|
||||
row = hstack((arange(m, dtype=int),
|
||||
arange(1, m, dtype=int)))
|
||||
col = hstack((arange(m, dtype=int),
|
||||
arange(m-1, dtype=int)))
|
||||
data = hstack((arange(1, m+1, dtype=float),
|
||||
arange(1,m, dtype=float)))
|
||||
return coo_matrix((data, (row, col)), shape=(m,n))
|
||||
else:
|
||||
row = hstack((arange(n, dtype=int),
|
||||
arange(1, n+1, dtype=int)))
|
||||
col = hstack((arange(n, dtype=int),
|
||||
arange(n, dtype=int)))
|
||||
data = hstack((arange(1, n+1, dtype=float),
|
||||
arange(1,n+1, dtype=float)))
|
||||
return coo_matrix((data,(row, col)), shape=(m,n))
|
||||
@ -0,0 +1,120 @@
|
||||
import numpy as np
|
||||
from numpy.testing import assert_allclose, assert_array_equal, assert_equal
|
||||
import pytest
|
||||
import scipy.sparse
|
||||
import scipy.sparse.linalg
|
||||
from scipy.sparse.linalg import lsqr
|
||||
|
||||
# Set up a test problem
|
||||
n = 35
|
||||
G = np.eye(n)
|
||||
normal = np.random.normal
|
||||
norm = np.linalg.norm
|
||||
|
||||
for jj in range(5):
|
||||
gg = normal(size=n)
|
||||
hh = gg * gg.T
|
||||
G += (hh + hh.T) * 0.5
|
||||
G += normal(size=n) * normal(size=n)
|
||||
|
||||
b = normal(size=n)
|
||||
|
||||
# tolerance for atol/btol keywords of lsqr()
|
||||
tol = 2e-10
|
||||
# tolerances for testing the results of the lsqr() call with assert_allclose
|
||||
# These tolerances are a bit fragile - see discussion in gh-15301.
|
||||
atol_test = 4e-10
|
||||
rtol_test = 2e-8
|
||||
show = False
|
||||
maxit = None
|
||||
|
||||
|
||||
def test_lsqr_basic():
|
||||
b_copy = b.copy()
|
||||
xo, *_ = lsqr(G, b, show=show, atol=tol, btol=tol, iter_lim=maxit)
|
||||
assert_array_equal(b_copy, b)
|
||||
|
||||
svx = np.linalg.solve(G, b)
|
||||
assert_allclose(xo, svx, atol=atol_test, rtol=rtol_test)
|
||||
|
||||
# Now the same but with damp > 0.
|
||||
# This is equivalent to solving the extended system:
|
||||
# ( G ) @ x = ( b )
|
||||
# ( damp*I ) ( 0 )
|
||||
damp = 1.5
|
||||
xo, *_ = lsqr(
|
||||
G, b, damp=damp, show=show, atol=tol, btol=tol, iter_lim=maxit)
|
||||
|
||||
Gext = np.r_[G, damp * np.eye(G.shape[1])]
|
||||
bext = np.r_[b, np.zeros(G.shape[1])]
|
||||
svx, *_ = np.linalg.lstsq(Gext, bext, rcond=None)
|
||||
assert_allclose(xo, svx, atol=atol_test, rtol=rtol_test)
|
||||
|
||||
|
||||
def test_gh_2466():
|
||||
row = np.array([0, 0])
|
||||
col = np.array([0, 1])
|
||||
val = np.array([1, -1])
|
||||
A = scipy.sparse.coo_matrix((val, (row, col)), shape=(1, 2))
|
||||
b = np.asarray([4])
|
||||
lsqr(A, b)
|
||||
|
||||
|
||||
def test_well_conditioned_problems():
|
||||
# Test that sparse the lsqr solver returns the right solution
|
||||
# on various problems with different random seeds.
|
||||
# This is a non-regression test for a potential ZeroDivisionError
|
||||
# raised when computing the `test2` & `test3` convergence conditions.
|
||||
n = 10
|
||||
A_sparse = scipy.sparse.eye(n, n)
|
||||
A_dense = A_sparse.toarray()
|
||||
|
||||
with np.errstate(invalid='raise'):
|
||||
for seed in range(30):
|
||||
rng = np.random.RandomState(seed + 10)
|
||||
beta = rng.rand(n)
|
||||
beta[beta == 0] = 0.00001 # ensure that all the betas are not null
|
||||
b = A_sparse @ beta[:, np.newaxis]
|
||||
output = lsqr(A_sparse, b, show=show)
|
||||
|
||||
# Check that the termination condition corresponds to an approximate
|
||||
# solution to Ax = b
|
||||
assert_equal(output[1], 1)
|
||||
solution = output[0]
|
||||
|
||||
# Check that we recover the ground truth solution
|
||||
assert_allclose(solution, beta)
|
||||
|
||||
# Sanity check: compare to the dense array solver
|
||||
reference_solution = np.linalg.solve(A_dense, b).ravel()
|
||||
assert_allclose(solution, reference_solution)
|
||||
|
||||
|
||||
def test_b_shapes():
|
||||
# Test b being a scalar.
|
||||
A = np.array([[1.0, 2.0]])
|
||||
b = 3.0
|
||||
x = lsqr(A, b)[0]
|
||||
assert norm(A.dot(x) - b) == pytest.approx(0)
|
||||
|
||||
# Test b being a column vector.
|
||||
A = np.eye(10)
|
||||
b = np.ones((10, 1))
|
||||
x = lsqr(A, b)[0]
|
||||
assert norm(A.dot(x) - b.ravel()) == pytest.approx(0)
|
||||
|
||||
|
||||
def test_initialization():
|
||||
# Test the default setting is the same as zeros
|
||||
b_copy = b.copy()
|
||||
x_ref = lsqr(G, b, show=show, atol=tol, btol=tol, iter_lim=maxit)
|
||||
x0 = np.zeros(x_ref[0].shape)
|
||||
x = lsqr(G, b, show=show, atol=tol, btol=tol, iter_lim=maxit, x0=x0)
|
||||
assert_array_equal(b_copy, b)
|
||||
assert_allclose(x_ref[0], x[0])
|
||||
|
||||
# Test warm-start with single iteration
|
||||
x0 = lsqr(G, b, show=show, atol=tol, btol=tol, iter_lim=1)[0]
|
||||
x = lsqr(G, b, show=show, atol=tol, btol=tol, iter_lim=maxit, x0=x0)
|
||||
assert_allclose(x_ref[0], x[0])
|
||||
assert_array_equal(b_copy, b)
|
||||
@ -0,0 +1,97 @@
|
||||
import numpy as np
|
||||
from numpy.linalg import norm
|
||||
from numpy.testing import assert_equal, assert_allclose, assert_
|
||||
from scipy.sparse.linalg._isolve import minres
|
||||
|
||||
from pytest import raises as assert_raises
|
||||
|
||||
|
||||
def get_sample_problem():
|
||||
# A random 10 x 10 symmetric matrix
|
||||
np.random.seed(1234)
|
||||
matrix = np.random.rand(10, 10)
|
||||
matrix = matrix + matrix.T
|
||||
# A random vector of length 10
|
||||
vector = np.random.rand(10)
|
||||
return matrix, vector
|
||||
|
||||
|
||||
def test_singular():
|
||||
A, b = get_sample_problem()
|
||||
A[0, ] = 0
|
||||
b[0] = 0
|
||||
xp, info = minres(A, b)
|
||||
assert_equal(info, 0)
|
||||
assert norm(A @ xp - b) <= 1e-5 * norm(b)
|
||||
|
||||
|
||||
def test_x0_is_used_by():
|
||||
A, b = get_sample_problem()
|
||||
# Random x0 to feed minres
|
||||
np.random.seed(12345)
|
||||
x0 = np.random.rand(10)
|
||||
trace = []
|
||||
|
||||
def trace_iterates(xk):
|
||||
trace.append(xk)
|
||||
minres(A, b, x0=x0, callback=trace_iterates)
|
||||
trace_with_x0 = trace
|
||||
|
||||
trace = []
|
||||
minres(A, b, callback=trace_iterates)
|
||||
assert_(not np.array_equal(trace_with_x0[0], trace[0]))
|
||||
|
||||
|
||||
def test_shift():
|
||||
A, b = get_sample_problem()
|
||||
shift = 0.5
|
||||
shifted_A = A - shift * np.eye(10)
|
||||
x1, info1 = minres(A, b, shift=shift)
|
||||
x2, info2 = minres(shifted_A, b)
|
||||
assert_equal(info1, 0)
|
||||
assert_allclose(x1, x2, rtol=1e-5)
|
||||
|
||||
|
||||
def test_asymmetric_fail():
|
||||
"""Asymmetric matrix should raise `ValueError` when check=True"""
|
||||
A, b = get_sample_problem()
|
||||
A[1, 2] = 1
|
||||
A[2, 1] = 2
|
||||
with assert_raises(ValueError):
|
||||
xp, info = minres(A, b, check=True)
|
||||
|
||||
|
||||
def test_minres_non_default_x0():
|
||||
np.random.seed(1234)
|
||||
rtol = 1e-6
|
||||
a = np.random.randn(5, 5)
|
||||
a = np.dot(a, a.T)
|
||||
b = np.random.randn(5)
|
||||
c = np.random.randn(5)
|
||||
x = minres(a, b, x0=c, rtol=rtol)[0]
|
||||
assert norm(a @ x - b) <= rtol * norm(b)
|
||||
|
||||
|
||||
def test_minres_precond_non_default_x0():
|
||||
np.random.seed(12345)
|
||||
rtol = 1e-6
|
||||
a = np.random.randn(5, 5)
|
||||
a = np.dot(a, a.T)
|
||||
b = np.random.randn(5)
|
||||
c = np.random.randn(5)
|
||||
m = np.random.randn(5, 5)
|
||||
m = np.dot(m, m.T)
|
||||
x = minres(a, b, M=m, x0=c, rtol=rtol)[0]
|
||||
assert norm(a @ x - b) <= rtol * norm(b)
|
||||
|
||||
|
||||
def test_minres_precond_exact_x0():
|
||||
np.random.seed(1234)
|
||||
rtol = 1e-6
|
||||
a = np.eye(10)
|
||||
b = np.ones(10)
|
||||
c = np.ones(10)
|
||||
m = np.random.randn(10, 10)
|
||||
m = np.dot(m, m.T)
|
||||
x = minres(a, b, M=m, x0=c, rtol=rtol)[0]
|
||||
assert norm(a @ x - b) <= rtol * norm(b)
|
||||
@ -0,0 +1,9 @@
|
||||
import numpy as np
|
||||
from pytest import raises as assert_raises
|
||||
|
||||
import scipy.sparse.linalg._isolve.utils as utils
|
||||
|
||||
|
||||
def test_make_system_bad_shape():
|
||||
assert_raises(ValueError,
|
||||
utils.make_system, np.zeros((5,3)), None, np.zeros(4), np.zeros(4))
|
||||
@ -0,0 +1,179 @@
|
||||
import numpy as np
|
||||
from .iterative import _get_atol_rtol
|
||||
from .utils import make_system
|
||||
|
||||
|
||||
__all__ = ['tfqmr']
|
||||
|
||||
|
||||
def tfqmr(A, b, x0=None, *, rtol=1e-5, atol=0., maxiter=None, M=None,
|
||||
callback=None, show=False):
|
||||
"""
|
||||
Use Transpose-Free Quasi-Minimal Residual iteration to solve ``Ax = b``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : {sparse matrix, ndarray, LinearOperator}
|
||||
The real or complex N-by-N matrix of the linear system.
|
||||
Alternatively, `A` can be a linear operator which can
|
||||
produce ``Ax`` using, e.g.,
|
||||
`scipy.sparse.linalg.LinearOperator`.
|
||||
b : {ndarray}
|
||||
Right hand side of the linear system. Has shape (N,) or (N,1).
|
||||
x0 : {ndarray}
|
||||
Starting guess for the solution.
|
||||
rtol, atol : float, optional
|
||||
Parameters for the convergence test. For convergence,
|
||||
``norm(b - A @ x) <= max(rtol*norm(b), atol)`` should be satisfied.
|
||||
The default is ``rtol=1e-5``, the default for ``atol`` is ``0.0``.
|
||||
maxiter : int, optional
|
||||
Maximum number of iterations. Iteration will stop after maxiter
|
||||
steps even if the specified tolerance has not been achieved.
|
||||
Default is ``min(10000, ndofs * 10)``, where ``ndofs = A.shape[0]``.
|
||||
M : {sparse matrix, ndarray, LinearOperator}
|
||||
Inverse of the preconditioner of A. M should approximate the
|
||||
inverse of A and be easy to solve for (see Notes). Effective
|
||||
preconditioning dramatically improves the rate of convergence,
|
||||
which implies that fewer iterations are needed to reach a given
|
||||
error tolerance. By default, no preconditioner is used.
|
||||
callback : function, optional
|
||||
User-supplied function to call after each iteration. It is called
|
||||
as `callback(xk)`, where `xk` is the current solution vector.
|
||||
show : bool, optional
|
||||
Specify ``show = True`` to show the convergence, ``show = False`` is
|
||||
to close the output of the convergence.
|
||||
Default is `False`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
x : ndarray
|
||||
The converged solution.
|
||||
info : int
|
||||
Provides convergence information:
|
||||
|
||||
- 0 : successful exit
|
||||
- >0 : convergence to tolerance not achieved, number of iterations
|
||||
- <0 : illegal input or breakdown
|
||||
|
||||
Notes
|
||||
-----
|
||||
The Transpose-Free QMR algorithm is derived from the CGS algorithm.
|
||||
However, unlike CGS, the convergence curves for the TFQMR method is
|
||||
smoothed by computing a quasi minimization of the residual norm. The
|
||||
implementation supports left preconditioner, and the "residual norm"
|
||||
to compute in convergence criterion is actually an upper bound on the
|
||||
actual residual norm ``||b - Axk||``.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] R. W. Freund, A Transpose-Free Quasi-Minimal Residual Algorithm for
|
||||
Non-Hermitian Linear Systems, SIAM J. Sci. Comput., 14(2), 470-482,
|
||||
1993.
|
||||
.. [2] Y. Saad, Iterative Methods for Sparse Linear Systems, 2nd edition,
|
||||
SIAM, Philadelphia, 2003.
|
||||
.. [3] C. T. Kelley, Iterative Methods for Linear and Nonlinear Equations,
|
||||
number 16 in Frontiers in Applied Mathematics, SIAM, Philadelphia,
|
||||
1995.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> from scipy.sparse.linalg import tfqmr
|
||||
>>> A = csc_matrix([[3, 2, 0], [1, -1, 0], [0, 5, 1]], dtype=float)
|
||||
>>> b = np.array([2, 4, -1], dtype=float)
|
||||
>>> x, exitCode = tfqmr(A, b, atol=0.0)
|
||||
>>> print(exitCode) # 0 indicates successful convergence
|
||||
0
|
||||
>>> np.allclose(A.dot(x), b)
|
||||
True
|
||||
"""
|
||||
|
||||
# Check data type
|
||||
dtype = A.dtype
|
||||
if np.issubdtype(dtype, np.int64):
|
||||
dtype = float
|
||||
A = A.astype(dtype)
|
||||
if np.issubdtype(b.dtype, np.int64):
|
||||
b = b.astype(dtype)
|
||||
|
||||
A, M, x, b, postprocess = make_system(A, M, x0, b)
|
||||
|
||||
# Check if the R.H.S is a zero vector
|
||||
if np.linalg.norm(b) == 0.:
|
||||
x = b.copy()
|
||||
return (postprocess(x), 0)
|
||||
|
||||
ndofs = A.shape[0]
|
||||
if maxiter is None:
|
||||
maxiter = min(10000, ndofs * 10)
|
||||
|
||||
if x0 is None:
|
||||
r = b.copy()
|
||||
else:
|
||||
r = b - A.matvec(x)
|
||||
u = r
|
||||
w = r.copy()
|
||||
# Take rstar as b - Ax0, that is rstar := r = b - Ax0 mathematically
|
||||
rstar = r
|
||||
v = M.matvec(A.matvec(r))
|
||||
uhat = v
|
||||
d = theta = eta = 0.
|
||||
# at this point we know rstar == r, so rho is always real
|
||||
rho = np.inner(rstar.conjugate(), r).real
|
||||
rhoLast = rho
|
||||
r0norm = np.sqrt(rho)
|
||||
tau = r0norm
|
||||
if r0norm == 0:
|
||||
return (postprocess(x), 0)
|
||||
|
||||
# we call this to get the right atol and raise errors as necessary
|
||||
atol, _ = _get_atol_rtol('tfqmr', r0norm, atol, rtol)
|
||||
|
||||
for iter in range(maxiter):
|
||||
even = iter % 2 == 0
|
||||
if (even):
|
||||
vtrstar = np.inner(rstar.conjugate(), v)
|
||||
# Check breakdown
|
||||
if vtrstar == 0.:
|
||||
return (postprocess(x), -1)
|
||||
alpha = rho / vtrstar
|
||||
uNext = u - alpha * v # [1]-(5.6)
|
||||
w -= alpha * uhat # [1]-(5.8)
|
||||
d = u + (theta**2 / alpha) * eta * d # [1]-(5.5)
|
||||
# [1]-(5.2)
|
||||
theta = np.linalg.norm(w) / tau
|
||||
c = np.sqrt(1. / (1 + theta**2))
|
||||
tau *= theta * c
|
||||
# Calculate step and direction [1]-(5.4)
|
||||
eta = (c**2) * alpha
|
||||
z = M.matvec(d)
|
||||
x += eta * z
|
||||
|
||||
if callback is not None:
|
||||
callback(x)
|
||||
|
||||
# Convergence criterion
|
||||
if tau * np.sqrt(iter+1) < atol:
|
||||
if (show):
|
||||
print("TFQMR: Linear solve converged due to reach TOL "
|
||||
f"iterations {iter+1}")
|
||||
return (postprocess(x), 0)
|
||||
|
||||
if (not even):
|
||||
# [1]-(5.7)
|
||||
rho = np.inner(rstar.conjugate(), w)
|
||||
beta = rho / rhoLast
|
||||
u = w + beta * u
|
||||
v = beta * uhat + (beta**2) * v
|
||||
uhat = M.matvec(A.matvec(u))
|
||||
v += uhat
|
||||
else:
|
||||
uhat = M.matvec(A.matvec(uNext))
|
||||
u = uNext
|
||||
rhoLast = rho
|
||||
|
||||
if (show):
|
||||
print("TFQMR: Linear solve not converged due to reach MAXIT "
|
||||
f"iterations {iter+1}")
|
||||
return (postprocess(x), maxiter)
|
||||
@ -0,0 +1,127 @@
|
||||
__docformat__ = "restructuredtext en"
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
from numpy import asanyarray, asarray, array, zeros
|
||||
|
||||
from scipy.sparse.linalg._interface import aslinearoperator, LinearOperator, \
|
||||
IdentityOperator
|
||||
|
||||
_coerce_rules = {('f','f'):'f', ('f','d'):'d', ('f','F'):'F',
|
||||
('f','D'):'D', ('d','f'):'d', ('d','d'):'d',
|
||||
('d','F'):'D', ('d','D'):'D', ('F','f'):'F',
|
||||
('F','d'):'D', ('F','F'):'F', ('F','D'):'D',
|
||||
('D','f'):'D', ('D','d'):'D', ('D','F'):'D',
|
||||
('D','D'):'D'}
|
||||
|
||||
|
||||
def coerce(x,y):
|
||||
if x not in 'fdFD':
|
||||
x = 'd'
|
||||
if y not in 'fdFD':
|
||||
y = 'd'
|
||||
return _coerce_rules[x,y]
|
||||
|
||||
|
||||
def id(x):
|
||||
return x
|
||||
|
||||
|
||||
def make_system(A, M, x0, b):
|
||||
"""Make a linear system Ax=b
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : LinearOperator
|
||||
sparse or dense matrix (or any valid input to aslinearoperator)
|
||||
M : {LinearOperator, Nones}
|
||||
preconditioner
|
||||
sparse or dense matrix (or any valid input to aslinearoperator)
|
||||
x0 : {array_like, str, None}
|
||||
initial guess to iterative method.
|
||||
``x0 = 'Mb'`` means using the nonzero initial guess ``M @ b``.
|
||||
Default is `None`, which means using the zero initial guess.
|
||||
b : array_like
|
||||
right hand side
|
||||
|
||||
Returns
|
||||
-------
|
||||
(A, M, x, b, postprocess)
|
||||
A : LinearOperator
|
||||
matrix of the linear system
|
||||
M : LinearOperator
|
||||
preconditioner
|
||||
x : rank 1 ndarray
|
||||
initial guess
|
||||
b : rank 1 ndarray
|
||||
right hand side
|
||||
postprocess : function
|
||||
converts the solution vector to the appropriate
|
||||
type and dimensions (e.g. (N,1) matrix)
|
||||
|
||||
"""
|
||||
A_ = A
|
||||
A = aslinearoperator(A)
|
||||
|
||||
if A.shape[0] != A.shape[1]:
|
||||
raise ValueError(f'expected square matrix, but got shape={(A.shape,)}')
|
||||
|
||||
N = A.shape[0]
|
||||
|
||||
b = asanyarray(b)
|
||||
|
||||
if not (b.shape == (N,1) or b.shape == (N,)):
|
||||
raise ValueError(f'shapes of A {A.shape} and b {b.shape} are '
|
||||
'incompatible')
|
||||
|
||||
if b.dtype.char not in 'fdFD':
|
||||
b = b.astype('d') # upcast non-FP types to double
|
||||
|
||||
def postprocess(x):
|
||||
return x
|
||||
|
||||
if hasattr(A,'dtype'):
|
||||
xtype = A.dtype.char
|
||||
else:
|
||||
xtype = A.matvec(b).dtype.char
|
||||
xtype = coerce(xtype, b.dtype.char)
|
||||
|
||||
b = asarray(b,dtype=xtype) # make b the same type as x
|
||||
b = b.ravel()
|
||||
|
||||
# process preconditioner
|
||||
if M is None:
|
||||
if hasattr(A_,'psolve'):
|
||||
psolve = A_.psolve
|
||||
else:
|
||||
psolve = id
|
||||
if hasattr(A_,'rpsolve'):
|
||||
rpsolve = A_.rpsolve
|
||||
else:
|
||||
rpsolve = id
|
||||
if psolve is id and rpsolve is id:
|
||||
M = IdentityOperator(shape=A.shape, dtype=A.dtype)
|
||||
else:
|
||||
M = LinearOperator(A.shape, matvec=psolve, rmatvec=rpsolve,
|
||||
dtype=A.dtype)
|
||||
else:
|
||||
M = aslinearoperator(M)
|
||||
if A.shape != M.shape:
|
||||
raise ValueError('matrix and preconditioner have different shapes')
|
||||
|
||||
# set initial guess
|
||||
if x0 is None:
|
||||
x = zeros(N, dtype=xtype)
|
||||
elif isinstance(x0, str):
|
||||
if x0 == 'Mb': # use nonzero initial guess ``M @ b``
|
||||
bCopy = b.copy()
|
||||
x = M.matvec(bCopy)
|
||||
else:
|
||||
x = array(x0, dtype=xtype)
|
||||
if not (x.shape == (N, 1) or x.shape == (N,)):
|
||||
raise ValueError(f'shapes of A {A.shape} and '
|
||||
f'x0 {x.shape} are incompatible')
|
||||
x = x.ravel()
|
||||
|
||||
return A, M, x, b, postprocess
|
||||
@ -0,0 +1,940 @@
|
||||
"""
|
||||
Sparse matrix functions
|
||||
"""
|
||||
|
||||
#
|
||||
# Authors: Travis Oliphant, March 2002
|
||||
# Anthony Scopatz, August 2012 (Sparse Updates)
|
||||
# Jake Vanderplas, August 2012 (Sparse Updates)
|
||||
#
|
||||
|
||||
__all__ = ['expm', 'inv', 'matrix_power']
|
||||
|
||||
import numpy as np
|
||||
from scipy.linalg._basic import solve, solve_triangular
|
||||
|
||||
from scipy.sparse._base import issparse
|
||||
from scipy.sparse.linalg import spsolve
|
||||
from scipy.sparse._sputils import is_pydata_spmatrix, isintlike
|
||||
|
||||
import scipy.sparse
|
||||
import scipy.sparse.linalg
|
||||
from scipy.sparse.linalg._interface import LinearOperator
|
||||
from scipy.sparse._construct import eye
|
||||
|
||||
from ._expm_multiply import _ident_like, _exact_1_norm as _onenorm
|
||||
|
||||
|
||||
UPPER_TRIANGULAR = 'upper_triangular'
|
||||
|
||||
|
||||
def inv(A):
|
||||
"""
|
||||
Compute the inverse of a sparse matrix
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : (M, M) sparse matrix
|
||||
square matrix to be inverted
|
||||
|
||||
Returns
|
||||
-------
|
||||
Ainv : (M, M) sparse matrix
|
||||
inverse of `A`
|
||||
|
||||
Notes
|
||||
-----
|
||||
This computes the sparse inverse of `A`. If the inverse of `A` is expected
|
||||
to be non-sparse, it will likely be faster to convert `A` to dense and use
|
||||
`scipy.linalg.inv`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> from scipy.sparse.linalg import inv
|
||||
>>> A = csc_matrix([[1., 0.], [1., 2.]])
|
||||
>>> Ainv = inv(A)
|
||||
>>> Ainv
|
||||
<Compressed Sparse Column sparse matrix of dtype 'float64'
|
||||
with 3 stored elements and shape (2, 2)>
|
||||
>>> A.dot(Ainv)
|
||||
<Compressed Sparse Column sparse matrix of dtype 'float64'
|
||||
with 2 stored elements and shape (2, 2)>
|
||||
>>> A.dot(Ainv).toarray()
|
||||
array([[ 1., 0.],
|
||||
[ 0., 1.]])
|
||||
|
||||
.. versionadded:: 0.12.0
|
||||
|
||||
"""
|
||||
# Check input
|
||||
if not (scipy.sparse.issparse(A) or is_pydata_spmatrix(A)):
|
||||
raise TypeError('Input must be a sparse matrix')
|
||||
|
||||
# Use sparse direct solver to solve "AX = I" accurately
|
||||
I = _ident_like(A)
|
||||
Ainv = spsolve(A, I)
|
||||
return Ainv
|
||||
|
||||
|
||||
def _onenorm_matrix_power_nnm(A, p):
|
||||
"""
|
||||
Compute the 1-norm of a non-negative integer power of a non-negative matrix.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : a square ndarray or matrix or sparse matrix
|
||||
Input matrix with non-negative entries.
|
||||
p : non-negative integer
|
||||
The power to which the matrix is to be raised.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : float
|
||||
The 1-norm of the matrix power p of A.
|
||||
|
||||
"""
|
||||
# Check input
|
||||
if int(p) != p or p < 0:
|
||||
raise ValueError('expected non-negative integer p')
|
||||
p = int(p)
|
||||
if len(A.shape) != 2 or A.shape[0] != A.shape[1]:
|
||||
raise ValueError('expected A to be like a square matrix')
|
||||
|
||||
# Explicitly make a column vector so that this works when A is a
|
||||
# numpy matrix (in addition to ndarray and sparse matrix).
|
||||
v = np.ones((A.shape[0], 1), dtype=float)
|
||||
M = A.T
|
||||
for i in range(p):
|
||||
v = M.dot(v)
|
||||
return np.max(v)
|
||||
|
||||
|
||||
def _is_upper_triangular(A):
|
||||
# This function could possibly be of wider interest.
|
||||
if issparse(A):
|
||||
lower_part = scipy.sparse.tril(A, -1)
|
||||
# Check structural upper triangularity,
|
||||
# then coincidental upper triangularity if needed.
|
||||
return lower_part.nnz == 0 or lower_part.count_nonzero() == 0
|
||||
elif is_pydata_spmatrix(A):
|
||||
import sparse
|
||||
lower_part = sparse.tril(A, -1)
|
||||
return lower_part.nnz == 0
|
||||
else:
|
||||
return not np.tril(A, -1).any()
|
||||
|
||||
|
||||
def _smart_matrix_product(A, B, alpha=None, structure=None):
|
||||
"""
|
||||
A matrix product that knows about sparse and structured matrices.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : 2d ndarray
|
||||
First matrix.
|
||||
B : 2d ndarray
|
||||
Second matrix.
|
||||
alpha : float
|
||||
The matrix product will be scaled by this constant.
|
||||
structure : str, optional
|
||||
A string describing the structure of both matrices `A` and `B`.
|
||||
Only `upper_triangular` is currently supported.
|
||||
|
||||
Returns
|
||||
-------
|
||||
M : 2d ndarray
|
||||
Matrix product of A and B.
|
||||
|
||||
"""
|
||||
if len(A.shape) != 2:
|
||||
raise ValueError('expected A to be a rectangular matrix')
|
||||
if len(B.shape) != 2:
|
||||
raise ValueError('expected B to be a rectangular matrix')
|
||||
f = None
|
||||
if structure == UPPER_TRIANGULAR:
|
||||
if (not issparse(A) and not issparse(B)
|
||||
and not is_pydata_spmatrix(A) and not is_pydata_spmatrix(B)):
|
||||
f, = scipy.linalg.get_blas_funcs(('trmm',), (A, B))
|
||||
if f is not None:
|
||||
if alpha is None:
|
||||
alpha = 1.
|
||||
out = f(alpha, A, B)
|
||||
else:
|
||||
if alpha is None:
|
||||
out = A.dot(B)
|
||||
else:
|
||||
out = alpha * A.dot(B)
|
||||
return out
|
||||
|
||||
|
||||
class MatrixPowerOperator(LinearOperator):
|
||||
|
||||
def __init__(self, A, p, structure=None):
|
||||
if A.ndim != 2 or A.shape[0] != A.shape[1]:
|
||||
raise ValueError('expected A to be like a square matrix')
|
||||
if p < 0:
|
||||
raise ValueError('expected p to be a non-negative integer')
|
||||
self._A = A
|
||||
self._p = p
|
||||
self._structure = structure
|
||||
self.dtype = A.dtype
|
||||
self.ndim = A.ndim
|
||||
self.shape = A.shape
|
||||
|
||||
def _matvec(self, x):
|
||||
for i in range(self._p):
|
||||
x = self._A.dot(x)
|
||||
return x
|
||||
|
||||
def _rmatvec(self, x):
|
||||
A_T = self._A.T
|
||||
x = x.ravel()
|
||||
for i in range(self._p):
|
||||
x = A_T.dot(x)
|
||||
return x
|
||||
|
||||
def _matmat(self, X):
|
||||
for i in range(self._p):
|
||||
X = _smart_matrix_product(self._A, X, structure=self._structure)
|
||||
return X
|
||||
|
||||
@property
|
||||
def T(self):
|
||||
return MatrixPowerOperator(self._A.T, self._p)
|
||||
|
||||
|
||||
class ProductOperator(LinearOperator):
|
||||
"""
|
||||
For now, this is limited to products of multiple square matrices.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._structure = kwargs.get('structure', None)
|
||||
for A in args:
|
||||
if len(A.shape) != 2 or A.shape[0] != A.shape[1]:
|
||||
raise ValueError(
|
||||
'For now, the ProductOperator implementation is '
|
||||
'limited to the product of multiple square matrices.')
|
||||
if args:
|
||||
n = args[0].shape[0]
|
||||
for A in args:
|
||||
for d in A.shape:
|
||||
if d != n:
|
||||
raise ValueError(
|
||||
'The square matrices of the ProductOperator '
|
||||
'must all have the same shape.')
|
||||
self.shape = (n, n)
|
||||
self.ndim = len(self.shape)
|
||||
self.dtype = np.result_type(*[x.dtype for x in args])
|
||||
self._operator_sequence = args
|
||||
|
||||
def _matvec(self, x):
|
||||
for A in reversed(self._operator_sequence):
|
||||
x = A.dot(x)
|
||||
return x
|
||||
|
||||
def _rmatvec(self, x):
|
||||
x = x.ravel()
|
||||
for A in self._operator_sequence:
|
||||
x = A.T.dot(x)
|
||||
return x
|
||||
|
||||
def _matmat(self, X):
|
||||
for A in reversed(self._operator_sequence):
|
||||
X = _smart_matrix_product(A, X, structure=self._structure)
|
||||
return X
|
||||
|
||||
@property
|
||||
def T(self):
|
||||
T_args = [A.T for A in reversed(self._operator_sequence)]
|
||||
return ProductOperator(*T_args)
|
||||
|
||||
|
||||
def _onenormest_matrix_power(A, p,
|
||||
t=2, itmax=5, compute_v=False, compute_w=False, structure=None):
|
||||
"""
|
||||
Efficiently estimate the 1-norm of A^p.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : ndarray
|
||||
Matrix whose 1-norm of a power is to be computed.
|
||||
p : int
|
||||
Non-negative integer power.
|
||||
t : int, optional
|
||||
A positive parameter controlling the tradeoff between
|
||||
accuracy versus time and memory usage.
|
||||
Larger values take longer and use more memory
|
||||
but give more accurate output.
|
||||
itmax : int, optional
|
||||
Use at most this many iterations.
|
||||
compute_v : bool, optional
|
||||
Request a norm-maximizing linear operator input vector if True.
|
||||
compute_w : bool, optional
|
||||
Request a norm-maximizing linear operator output vector if True.
|
||||
|
||||
Returns
|
||||
-------
|
||||
est : float
|
||||
An underestimate of the 1-norm of the sparse matrix.
|
||||
v : ndarray, optional
|
||||
The vector such that ||Av||_1 == est*||v||_1.
|
||||
It can be thought of as an input to the linear operator
|
||||
that gives an output with particularly large norm.
|
||||
w : ndarray, optional
|
||||
The vector Av which has relatively large 1-norm.
|
||||
It can be thought of as an output of the linear operator
|
||||
that is relatively large in norm compared to the input.
|
||||
|
||||
"""
|
||||
return scipy.sparse.linalg.onenormest(
|
||||
MatrixPowerOperator(A, p, structure=structure))
|
||||
|
||||
|
||||
def _onenormest_product(operator_seq,
|
||||
t=2, itmax=5, compute_v=False, compute_w=False, structure=None):
|
||||
"""
|
||||
Efficiently estimate the 1-norm of the matrix product of the args.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
operator_seq : linear operator sequence
|
||||
Matrices whose 1-norm of product is to be computed.
|
||||
t : int, optional
|
||||
A positive parameter controlling the tradeoff between
|
||||
accuracy versus time and memory usage.
|
||||
Larger values take longer and use more memory
|
||||
but give more accurate output.
|
||||
itmax : int, optional
|
||||
Use at most this many iterations.
|
||||
compute_v : bool, optional
|
||||
Request a norm-maximizing linear operator input vector if True.
|
||||
compute_w : bool, optional
|
||||
Request a norm-maximizing linear operator output vector if True.
|
||||
structure : str, optional
|
||||
A string describing the structure of all operators.
|
||||
Only `upper_triangular` is currently supported.
|
||||
|
||||
Returns
|
||||
-------
|
||||
est : float
|
||||
An underestimate of the 1-norm of the sparse matrix.
|
||||
v : ndarray, optional
|
||||
The vector such that ||Av||_1 == est*||v||_1.
|
||||
It can be thought of as an input to the linear operator
|
||||
that gives an output with particularly large norm.
|
||||
w : ndarray, optional
|
||||
The vector Av which has relatively large 1-norm.
|
||||
It can be thought of as an output of the linear operator
|
||||
that is relatively large in norm compared to the input.
|
||||
|
||||
"""
|
||||
return scipy.sparse.linalg.onenormest(
|
||||
ProductOperator(*operator_seq, structure=structure))
|
||||
|
||||
|
||||
class _ExpmPadeHelper:
|
||||
"""
|
||||
Help lazily evaluate a matrix exponential.
|
||||
|
||||
The idea is to not do more work than we need for high expm precision,
|
||||
so we lazily compute matrix powers and store or precompute
|
||||
other properties of the matrix.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, A, structure=None, use_exact_onenorm=False):
|
||||
"""
|
||||
Initialize the object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : a dense or sparse square numpy matrix or ndarray
|
||||
The matrix to be exponentiated.
|
||||
structure : str, optional
|
||||
A string describing the structure of matrix `A`.
|
||||
Only `upper_triangular` is currently supported.
|
||||
use_exact_onenorm : bool, optional
|
||||
If True then only the exact one-norm of matrix powers and products
|
||||
will be used. Otherwise, the one-norm of powers and products
|
||||
may initially be estimated.
|
||||
"""
|
||||
self.A = A
|
||||
self._A2 = None
|
||||
self._A4 = None
|
||||
self._A6 = None
|
||||
self._A8 = None
|
||||
self._A10 = None
|
||||
self._d4_exact = None
|
||||
self._d6_exact = None
|
||||
self._d8_exact = None
|
||||
self._d10_exact = None
|
||||
self._d4_approx = None
|
||||
self._d6_approx = None
|
||||
self._d8_approx = None
|
||||
self._d10_approx = None
|
||||
self.ident = _ident_like(A)
|
||||
self.structure = structure
|
||||
self.use_exact_onenorm = use_exact_onenorm
|
||||
|
||||
@property
|
||||
def A2(self):
|
||||
if self._A2 is None:
|
||||
self._A2 = _smart_matrix_product(
|
||||
self.A, self.A, structure=self.structure)
|
||||
return self._A2
|
||||
|
||||
@property
|
||||
def A4(self):
|
||||
if self._A4 is None:
|
||||
self._A4 = _smart_matrix_product(
|
||||
self.A2, self.A2, structure=self.structure)
|
||||
return self._A4
|
||||
|
||||
@property
|
||||
def A6(self):
|
||||
if self._A6 is None:
|
||||
self._A6 = _smart_matrix_product(
|
||||
self.A4, self.A2, structure=self.structure)
|
||||
return self._A6
|
||||
|
||||
@property
|
||||
def A8(self):
|
||||
if self._A8 is None:
|
||||
self._A8 = _smart_matrix_product(
|
||||
self.A6, self.A2, structure=self.structure)
|
||||
return self._A8
|
||||
|
||||
@property
|
||||
def A10(self):
|
||||
if self._A10 is None:
|
||||
self._A10 = _smart_matrix_product(
|
||||
self.A4, self.A6, structure=self.structure)
|
||||
return self._A10
|
||||
|
||||
@property
|
||||
def d4_tight(self):
|
||||
if self._d4_exact is None:
|
||||
self._d4_exact = _onenorm(self.A4)**(1/4.)
|
||||
return self._d4_exact
|
||||
|
||||
@property
|
||||
def d6_tight(self):
|
||||
if self._d6_exact is None:
|
||||
self._d6_exact = _onenorm(self.A6)**(1/6.)
|
||||
return self._d6_exact
|
||||
|
||||
@property
|
||||
def d8_tight(self):
|
||||
if self._d8_exact is None:
|
||||
self._d8_exact = _onenorm(self.A8)**(1/8.)
|
||||
return self._d8_exact
|
||||
|
||||
@property
|
||||
def d10_tight(self):
|
||||
if self._d10_exact is None:
|
||||
self._d10_exact = _onenorm(self.A10)**(1/10.)
|
||||
return self._d10_exact
|
||||
|
||||
@property
|
||||
def d4_loose(self):
|
||||
if self.use_exact_onenorm:
|
||||
return self.d4_tight
|
||||
if self._d4_exact is not None:
|
||||
return self._d4_exact
|
||||
else:
|
||||
if self._d4_approx is None:
|
||||
self._d4_approx = _onenormest_matrix_power(self.A2, 2,
|
||||
structure=self.structure)**(1/4.)
|
||||
return self._d4_approx
|
||||
|
||||
@property
|
||||
def d6_loose(self):
|
||||
if self.use_exact_onenorm:
|
||||
return self.d6_tight
|
||||
if self._d6_exact is not None:
|
||||
return self._d6_exact
|
||||
else:
|
||||
if self._d6_approx is None:
|
||||
self._d6_approx = _onenormest_matrix_power(self.A2, 3,
|
||||
structure=self.structure)**(1/6.)
|
||||
return self._d6_approx
|
||||
|
||||
@property
|
||||
def d8_loose(self):
|
||||
if self.use_exact_onenorm:
|
||||
return self.d8_tight
|
||||
if self._d8_exact is not None:
|
||||
return self._d8_exact
|
||||
else:
|
||||
if self._d8_approx is None:
|
||||
self._d8_approx = _onenormest_matrix_power(self.A4, 2,
|
||||
structure=self.structure)**(1/8.)
|
||||
return self._d8_approx
|
||||
|
||||
@property
|
||||
def d10_loose(self):
|
||||
if self.use_exact_onenorm:
|
||||
return self.d10_tight
|
||||
if self._d10_exact is not None:
|
||||
return self._d10_exact
|
||||
else:
|
||||
if self._d10_approx is None:
|
||||
self._d10_approx = _onenormest_product((self.A4, self.A6),
|
||||
structure=self.structure)**(1/10.)
|
||||
return self._d10_approx
|
||||
|
||||
def pade3(self):
|
||||
b = (120., 60., 12., 1.)
|
||||
U = _smart_matrix_product(self.A,
|
||||
b[3]*self.A2 + b[1]*self.ident,
|
||||
structure=self.structure)
|
||||
V = b[2]*self.A2 + b[0]*self.ident
|
||||
return U, V
|
||||
|
||||
def pade5(self):
|
||||
b = (30240., 15120., 3360., 420., 30., 1.)
|
||||
U = _smart_matrix_product(self.A,
|
||||
b[5]*self.A4 + b[3]*self.A2 + b[1]*self.ident,
|
||||
structure=self.structure)
|
||||
V = b[4]*self.A4 + b[2]*self.A2 + b[0]*self.ident
|
||||
return U, V
|
||||
|
||||
def pade7(self):
|
||||
b = (17297280., 8648640., 1995840., 277200., 25200., 1512., 56., 1.)
|
||||
U = _smart_matrix_product(self.A,
|
||||
b[7]*self.A6 + b[5]*self.A4 + b[3]*self.A2 + b[1]*self.ident,
|
||||
structure=self.structure)
|
||||
V = b[6]*self.A6 + b[4]*self.A4 + b[2]*self.A2 + b[0]*self.ident
|
||||
return U, V
|
||||
|
||||
def pade9(self):
|
||||
b = (17643225600., 8821612800., 2075673600., 302702400., 30270240.,
|
||||
2162160., 110880., 3960., 90., 1.)
|
||||
U = _smart_matrix_product(self.A,
|
||||
(b[9]*self.A8 + b[7]*self.A6 + b[5]*self.A4 +
|
||||
b[3]*self.A2 + b[1]*self.ident),
|
||||
structure=self.structure)
|
||||
V = (b[8]*self.A8 + b[6]*self.A6 + b[4]*self.A4 +
|
||||
b[2]*self.A2 + b[0]*self.ident)
|
||||
return U, V
|
||||
|
||||
def pade13_scaled(self, s):
|
||||
b = (64764752532480000., 32382376266240000., 7771770303897600.,
|
||||
1187353796428800., 129060195264000., 10559470521600.,
|
||||
670442572800., 33522128640., 1323241920., 40840800., 960960.,
|
||||
16380., 182., 1.)
|
||||
B = self.A * 2**-s
|
||||
B2 = self.A2 * 2**(-2*s)
|
||||
B4 = self.A4 * 2**(-4*s)
|
||||
B6 = self.A6 * 2**(-6*s)
|
||||
U2 = _smart_matrix_product(B6,
|
||||
b[13]*B6 + b[11]*B4 + b[9]*B2,
|
||||
structure=self.structure)
|
||||
U = _smart_matrix_product(B,
|
||||
(U2 + b[7]*B6 + b[5]*B4 +
|
||||
b[3]*B2 + b[1]*self.ident),
|
||||
structure=self.structure)
|
||||
V2 = _smart_matrix_product(B6,
|
||||
b[12]*B6 + b[10]*B4 + b[8]*B2,
|
||||
structure=self.structure)
|
||||
V = V2 + b[6]*B6 + b[4]*B4 + b[2]*B2 + b[0]*self.ident
|
||||
return U, V
|
||||
|
||||
|
||||
def expm(A):
|
||||
"""
|
||||
Compute the matrix exponential using Pade approximation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : (M,M) array_like or sparse matrix
|
||||
2D Array or Matrix (sparse or dense) to be exponentiated
|
||||
|
||||
Returns
|
||||
-------
|
||||
expA : (M,M) ndarray
|
||||
Matrix exponential of `A`
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is algorithm (6.1) which is a simplification of algorithm (5.1).
|
||||
|
||||
.. versionadded:: 0.12.0
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Awad H. Al-Mohy and Nicholas J. Higham (2009)
|
||||
"A New Scaling and Squaring Algorithm for the Matrix Exponential."
|
||||
SIAM Journal on Matrix Analysis and Applications.
|
||||
31 (3). pp. 970-989. ISSN 1095-7162
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> from scipy.sparse.linalg import expm
|
||||
>>> A = csc_matrix([[1, 0, 0], [0, 2, 0], [0, 0, 3]])
|
||||
>>> A.toarray()
|
||||
array([[1, 0, 0],
|
||||
[0, 2, 0],
|
||||
[0, 0, 3]], dtype=int64)
|
||||
>>> Aexp = expm(A)
|
||||
>>> Aexp
|
||||
<Compressed Sparse Column sparse matrix of dtype 'float64'
|
||||
with 3 stored elements and shape (3, 3)>
|
||||
>>> Aexp.toarray()
|
||||
array([[ 2.71828183, 0. , 0. ],
|
||||
[ 0. , 7.3890561 , 0. ],
|
||||
[ 0. , 0. , 20.08553692]])
|
||||
"""
|
||||
return _expm(A, use_exact_onenorm='auto')
|
||||
|
||||
|
||||
def _expm(A, use_exact_onenorm):
|
||||
# Core of expm, separated to allow testing exact and approximate
|
||||
# algorithms.
|
||||
|
||||
# Avoid indiscriminate asarray() to allow sparse or other strange arrays.
|
||||
if isinstance(A, (list, tuple, np.matrix)):
|
||||
A = np.asarray(A)
|
||||
if len(A.shape) != 2 or A.shape[0] != A.shape[1]:
|
||||
raise ValueError('expected a square matrix')
|
||||
|
||||
# gracefully handle size-0 input,
|
||||
# carefully handling sparse scenario
|
||||
if A.shape == (0, 0):
|
||||
out = np.zeros([0, 0], dtype=A.dtype)
|
||||
if issparse(A) or is_pydata_spmatrix(A):
|
||||
return A.__class__(out)
|
||||
return out
|
||||
|
||||
# Trivial case
|
||||
if A.shape == (1, 1):
|
||||
out = [[np.exp(A[0, 0])]]
|
||||
|
||||
# Avoid indiscriminate casting to ndarray to
|
||||
# allow for sparse or other strange arrays
|
||||
if issparse(A) or is_pydata_spmatrix(A):
|
||||
return A.__class__(out)
|
||||
|
||||
return np.array(out)
|
||||
|
||||
# Ensure input is of float type, to avoid integer overflows etc.
|
||||
if ((isinstance(A, np.ndarray) or issparse(A) or is_pydata_spmatrix(A))
|
||||
and not np.issubdtype(A.dtype, np.inexact)):
|
||||
A = A.astype(float)
|
||||
|
||||
# Detect upper triangularity.
|
||||
structure = UPPER_TRIANGULAR if _is_upper_triangular(A) else None
|
||||
|
||||
if use_exact_onenorm == "auto":
|
||||
# Hardcode a matrix order threshold for exact vs. estimated one-norms.
|
||||
use_exact_onenorm = A.shape[0] < 200
|
||||
|
||||
# Track functions of A to help compute the matrix exponential.
|
||||
h = _ExpmPadeHelper(
|
||||
A, structure=structure, use_exact_onenorm=use_exact_onenorm)
|
||||
|
||||
# Try Pade order 3.
|
||||
eta_1 = max(h.d4_loose, h.d6_loose)
|
||||
if eta_1 < 1.495585217958292e-002 and _ell(h.A, 3) == 0:
|
||||
U, V = h.pade3()
|
||||
return _solve_P_Q(U, V, structure=structure)
|
||||
|
||||
# Try Pade order 5.
|
||||
eta_2 = max(h.d4_tight, h.d6_loose)
|
||||
if eta_2 < 2.539398330063230e-001 and _ell(h.A, 5) == 0:
|
||||
U, V = h.pade5()
|
||||
return _solve_P_Q(U, V, structure=structure)
|
||||
|
||||
# Try Pade orders 7 and 9.
|
||||
eta_3 = max(h.d6_tight, h.d8_loose)
|
||||
if eta_3 < 9.504178996162932e-001 and _ell(h.A, 7) == 0:
|
||||
U, V = h.pade7()
|
||||
return _solve_P_Q(U, V, structure=structure)
|
||||
if eta_3 < 2.097847961257068e+000 and _ell(h.A, 9) == 0:
|
||||
U, V = h.pade9()
|
||||
return _solve_P_Q(U, V, structure=structure)
|
||||
|
||||
# Use Pade order 13.
|
||||
eta_4 = max(h.d8_loose, h.d10_loose)
|
||||
eta_5 = min(eta_3, eta_4)
|
||||
theta_13 = 4.25
|
||||
|
||||
# Choose smallest s>=0 such that 2**(-s) eta_5 <= theta_13
|
||||
if eta_5 == 0:
|
||||
# Nilpotent special case
|
||||
s = 0
|
||||
else:
|
||||
s = max(int(np.ceil(np.log2(eta_5 / theta_13))), 0)
|
||||
s = s + _ell(2**-s * h.A, 13)
|
||||
U, V = h.pade13_scaled(s)
|
||||
X = _solve_P_Q(U, V, structure=structure)
|
||||
if structure == UPPER_TRIANGULAR:
|
||||
# Invoke Code Fragment 2.1.
|
||||
X = _fragment_2_1(X, h.A, s)
|
||||
else:
|
||||
# X = r_13(A)^(2^s) by repeated squaring.
|
||||
for i in range(s):
|
||||
X = X.dot(X)
|
||||
return X
|
||||
|
||||
|
||||
def _solve_P_Q(U, V, structure=None):
|
||||
"""
|
||||
A helper function for expm_2009.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
U : ndarray
|
||||
Pade numerator.
|
||||
V : ndarray
|
||||
Pade denominator.
|
||||
structure : str, optional
|
||||
A string describing the structure of both matrices `U` and `V`.
|
||||
Only `upper_triangular` is currently supported.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The `structure` argument is inspired by similar args
|
||||
for theano and cvxopt functions.
|
||||
|
||||
"""
|
||||
P = U + V
|
||||
Q = -U + V
|
||||
if issparse(U) or is_pydata_spmatrix(U):
|
||||
return spsolve(Q, P)
|
||||
elif structure is None:
|
||||
return solve(Q, P)
|
||||
elif structure == UPPER_TRIANGULAR:
|
||||
return solve_triangular(Q, P)
|
||||
else:
|
||||
raise ValueError('unsupported matrix structure: ' + str(structure))
|
||||
|
||||
|
||||
def _exp_sinch(a, x):
|
||||
"""
|
||||
Stably evaluate exp(a)*sinh(x)/x
|
||||
|
||||
Notes
|
||||
-----
|
||||
The strategy of falling back to a sixth order Taylor expansion
|
||||
was suggested by the Spallation Neutron Source docs
|
||||
which was found on the internet by google search.
|
||||
http://www.ornl.gov/~t6p/resources/xal/javadoc/gov/sns/tools/math/ElementaryFunction.html
|
||||
The details of the cutoff point and the Horner-like evaluation
|
||||
was picked without reference to anything in particular.
|
||||
|
||||
Note that sinch is not currently implemented in scipy.special,
|
||||
whereas the "engineer's" definition of sinc is implemented.
|
||||
The implementation of sinc involves a scaling factor of pi
|
||||
that distinguishes it from the "mathematician's" version of sinc.
|
||||
|
||||
"""
|
||||
|
||||
# If x is small then use sixth order Taylor expansion.
|
||||
# How small is small? I am using the point where the relative error
|
||||
# of the approximation is less than 1e-14.
|
||||
# If x is large then directly evaluate sinh(x) / x.
|
||||
if abs(x) < 0.0135:
|
||||
x2 = x*x
|
||||
return np.exp(a) * (1 + (x2/6.)*(1 + (x2/20.)*(1 + (x2/42.))))
|
||||
else:
|
||||
return (np.exp(a + x) - np.exp(a - x)) / (2*x)
|
||||
|
||||
|
||||
def _eq_10_42(lam_1, lam_2, t_12):
|
||||
"""
|
||||
Equation (10.42) of Functions of Matrices: Theory and Computation.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a helper function for _fragment_2_1 of expm_2009.
|
||||
Equation (10.42) is on page 251 in the section on Schur algorithms.
|
||||
In particular, section 10.4.3 explains the Schur-Parlett algorithm.
|
||||
expm([[lam_1, t_12], [0, lam_1])
|
||||
=
|
||||
[[exp(lam_1), t_12*exp((lam_1 + lam_2)/2)*sinch((lam_1 - lam_2)/2)],
|
||||
[0, exp(lam_2)]
|
||||
"""
|
||||
|
||||
# The plain formula t_12 * (exp(lam_2) - exp(lam_2)) / (lam_2 - lam_1)
|
||||
# apparently suffers from cancellation, according to Higham's textbook.
|
||||
# A nice implementation of sinch, defined as sinh(x)/x,
|
||||
# will apparently work around the cancellation.
|
||||
a = 0.5 * (lam_1 + lam_2)
|
||||
b = 0.5 * (lam_1 - lam_2)
|
||||
return t_12 * _exp_sinch(a, b)
|
||||
|
||||
|
||||
def _fragment_2_1(X, T, s):
|
||||
"""
|
||||
A helper function for expm_2009.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The argument X is modified in-place, but this modification is not the same
|
||||
as the returned value of the function.
|
||||
This function also takes pains to do things in ways that are compatible
|
||||
with sparse matrices, for example by avoiding fancy indexing
|
||||
and by using methods of the matrices whenever possible instead of
|
||||
using functions of the numpy or scipy libraries themselves.
|
||||
|
||||
"""
|
||||
# Form X = r_m(2^-s T)
|
||||
# Replace diag(X) by exp(2^-s diag(T)).
|
||||
n = X.shape[0]
|
||||
diag_T = np.ravel(T.diagonal().copy())
|
||||
|
||||
# Replace diag(X) by exp(2^-s diag(T)).
|
||||
scale = 2 ** -s
|
||||
exp_diag = np.exp(scale * diag_T)
|
||||
for k in range(n):
|
||||
X[k, k] = exp_diag[k]
|
||||
|
||||
for i in range(s-1, -1, -1):
|
||||
X = X.dot(X)
|
||||
|
||||
# Replace diag(X) by exp(2^-i diag(T)).
|
||||
scale = 2 ** -i
|
||||
exp_diag = np.exp(scale * diag_T)
|
||||
for k in range(n):
|
||||
X[k, k] = exp_diag[k]
|
||||
|
||||
# Replace (first) superdiagonal of X by explicit formula
|
||||
# for superdiagonal of exp(2^-i T) from Eq (10.42) of
|
||||
# the author's 2008 textbook
|
||||
# Functions of Matrices: Theory and Computation.
|
||||
for k in range(n-1):
|
||||
lam_1 = scale * diag_T[k]
|
||||
lam_2 = scale * diag_T[k+1]
|
||||
t_12 = scale * T[k, k+1]
|
||||
value = _eq_10_42(lam_1, lam_2, t_12)
|
||||
X[k, k+1] = value
|
||||
|
||||
# Return the updated X matrix.
|
||||
return X
|
||||
|
||||
|
||||
def _ell(A, m):
|
||||
"""
|
||||
A helper function for expm_2009.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : linear operator
|
||||
A linear operator whose norm of power we care about.
|
||||
m : int
|
||||
The power of the linear operator
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : int
|
||||
A value related to a bound.
|
||||
|
||||
"""
|
||||
if len(A.shape) != 2 or A.shape[0] != A.shape[1]:
|
||||
raise ValueError('expected A to be like a square matrix')
|
||||
|
||||
# The c_i are explained in (2.2) and (2.6) of the 2005 expm paper.
|
||||
# They are coefficients of terms of a generating function series expansion.
|
||||
c_i = {3: 100800.,
|
||||
5: 10059033600.,
|
||||
7: 4487938430976000.,
|
||||
9: 5914384781877411840000.,
|
||||
13: 113250775606021113483283660800000000.
|
||||
}
|
||||
abs_c_recip = c_i[m]
|
||||
|
||||
# This is explained after Eq. (1.2) of the 2009 expm paper.
|
||||
# It is the "unit roundoff" of IEEE double precision arithmetic.
|
||||
u = 2**-53
|
||||
|
||||
# Compute the one-norm of matrix power p of abs(A).
|
||||
A_abs_onenorm = _onenorm_matrix_power_nnm(abs(A), 2*m + 1)
|
||||
|
||||
# Treat zero norm as a special case.
|
||||
if not A_abs_onenorm:
|
||||
return 0
|
||||
|
||||
alpha = A_abs_onenorm / (_onenorm(A) * abs_c_recip)
|
||||
log2_alpha_div_u = np.log2(alpha/u)
|
||||
value = int(np.ceil(log2_alpha_div_u / (2 * m)))
|
||||
return max(value, 0)
|
||||
|
||||
def matrix_power(A, power):
|
||||
"""
|
||||
Raise a square matrix to the integer power, `power`.
|
||||
|
||||
For non-negative integers, ``A**power`` is computed using repeated
|
||||
matrix multiplications. Negative integers are not supported.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : (M, M) square sparse array or matrix
|
||||
sparse array that will be raised to power `power`
|
||||
power : int
|
||||
Exponent used to raise sparse array `A`
|
||||
|
||||
Returns
|
||||
-------
|
||||
A**power : (M, M) sparse array or matrix
|
||||
The output matrix will be the same shape as A, and will preserve
|
||||
the class of A, but the format of the output may be changed.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This uses a recursive implementation of the matrix power. For computing
|
||||
the matrix power using a reasonably large `power`, this may be less efficient
|
||||
than computing the product directly, using A @ A @ ... @ A.
|
||||
This is contingent upon the number of nonzero entries in the matrix.
|
||||
|
||||
.. versionadded:: 1.12.0
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy import sparse
|
||||
>>> A = sparse.csc_array([[0,1,0],[1,0,1],[0,1,0]])
|
||||
>>> A.todense()
|
||||
array([[0, 1, 0],
|
||||
[1, 0, 1],
|
||||
[0, 1, 0]])
|
||||
>>> (A @ A).todense()
|
||||
array([[1, 0, 1],
|
||||
[0, 2, 0],
|
||||
[1, 0, 1]])
|
||||
>>> A2 = sparse.linalg.matrix_power(A, 2)
|
||||
>>> A2.todense()
|
||||
array([[1, 0, 1],
|
||||
[0, 2, 0],
|
||||
[1, 0, 1]])
|
||||
>>> A4 = sparse.linalg.matrix_power(A, 4)
|
||||
>>> A4.todense()
|
||||
array([[2, 0, 2],
|
||||
[0, 4, 0],
|
||||
[2, 0, 2]])
|
||||
|
||||
"""
|
||||
M, N = A.shape
|
||||
if M != N:
|
||||
raise TypeError('sparse matrix is not square')
|
||||
|
||||
if isintlike(power):
|
||||
power = int(power)
|
||||
if power < 0:
|
||||
raise ValueError('exponent must be >= 0')
|
||||
|
||||
if power == 0:
|
||||
return eye(M, dtype=A.dtype)
|
||||
|
||||
if power == 1:
|
||||
return A.copy()
|
||||
|
||||
tmp = matrix_power(A, power // 2)
|
||||
if power % 2:
|
||||
return A @ tmp @ tmp
|
||||
else:
|
||||
return tmp @ tmp
|
||||
else:
|
||||
raise ValueError("exponent must be an integer")
|
||||
193
venv/lib/python3.12/site-packages/scipy/sparse/linalg/_norm.py
Normal file
193
venv/lib/python3.12/site-packages/scipy/sparse/linalg/_norm.py
Normal file
@ -0,0 +1,193 @@
|
||||
"""Sparse matrix norms.
|
||||
|
||||
"""
|
||||
import numpy as np
|
||||
from scipy.sparse import issparse
|
||||
from scipy.sparse.linalg import svds
|
||||
import scipy.sparse as sp
|
||||
|
||||
from numpy import sqrt, abs
|
||||
|
||||
__all__ = ['norm']
|
||||
|
||||
|
||||
def _sparse_frobenius_norm(x):
|
||||
data = sp._sputils._todata(x)
|
||||
return np.linalg.norm(data)
|
||||
|
||||
|
||||
def norm(x, ord=None, axis=None):
|
||||
"""
|
||||
Norm of a sparse matrix
|
||||
|
||||
This function is able to return one of seven different matrix norms,
|
||||
depending on the value of the ``ord`` parameter.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x : a sparse matrix
|
||||
Input sparse matrix.
|
||||
ord : {non-zero int, inf, -inf, 'fro'}, optional
|
||||
Order of the norm (see table under ``Notes``). inf means numpy's
|
||||
`inf` object.
|
||||
axis : {int, 2-tuple of ints, None}, optional
|
||||
If `axis` is an integer, it specifies the axis of `x` along which to
|
||||
compute the vector norms. If `axis` is a 2-tuple, it specifies the
|
||||
axes that hold 2-D matrices, and the matrix norms of these matrices
|
||||
are computed. If `axis` is None then either a vector norm (when `x`
|
||||
is 1-D) or a matrix norm (when `x` is 2-D) is returned.
|
||||
|
||||
Returns
|
||||
-------
|
||||
n : float or ndarray
|
||||
|
||||
Notes
|
||||
-----
|
||||
Some of the ord are not implemented because some associated functions like,
|
||||
_multi_svd_norm, are not yet available for sparse matrix.
|
||||
|
||||
This docstring is modified based on numpy.linalg.norm.
|
||||
https://github.com/numpy/numpy/blob/main/numpy/linalg/linalg.py
|
||||
|
||||
The following norms can be calculated:
|
||||
|
||||
===== ============================
|
||||
ord norm for sparse matrices
|
||||
===== ============================
|
||||
None Frobenius norm
|
||||
'fro' Frobenius norm
|
||||
inf max(sum(abs(x), axis=1))
|
||||
-inf min(sum(abs(x), axis=1))
|
||||
0 abs(x).sum(axis=axis)
|
||||
1 max(sum(abs(x), axis=0))
|
||||
-1 min(sum(abs(x), axis=0))
|
||||
2 Spectral norm (the largest singular value)
|
||||
-2 Not implemented
|
||||
other Not implemented
|
||||
===== ============================
|
||||
|
||||
The Frobenius norm is given by [1]_:
|
||||
|
||||
:math:`||A||_F = [\\sum_{i,j} abs(a_{i,j})^2]^{1/2}`
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] G. H. Golub and C. F. Van Loan, *Matrix Computations*,
|
||||
Baltimore, MD, Johns Hopkins University Press, 1985, pg. 15
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from scipy.sparse import *
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse.linalg import norm
|
||||
>>> a = np.arange(9) - 4
|
||||
>>> a
|
||||
array([-4, -3, -2, -1, 0, 1, 2, 3, 4])
|
||||
>>> b = a.reshape((3, 3))
|
||||
>>> b
|
||||
array([[-4, -3, -2],
|
||||
[-1, 0, 1],
|
||||
[ 2, 3, 4]])
|
||||
|
||||
>>> b = csr_matrix(b)
|
||||
>>> norm(b)
|
||||
7.745966692414834
|
||||
>>> norm(b, 'fro')
|
||||
7.745966692414834
|
||||
>>> norm(b, np.inf)
|
||||
9
|
||||
>>> norm(b, -np.inf)
|
||||
2
|
||||
>>> norm(b, 1)
|
||||
7
|
||||
>>> norm(b, -1)
|
||||
6
|
||||
|
||||
The matrix 2-norm or the spectral norm is the largest singular
|
||||
value, computed approximately and with limitations.
|
||||
|
||||
>>> b = diags([-1, 1], [0, 1], shape=(9, 10))
|
||||
>>> norm(b, 2)
|
||||
1.9753...
|
||||
"""
|
||||
if not issparse(x):
|
||||
raise TypeError("input is not sparse. use numpy.linalg.norm")
|
||||
|
||||
# Check the default case first and handle it immediately.
|
||||
if axis is None and ord in (None, 'fro', 'f'):
|
||||
return _sparse_frobenius_norm(x)
|
||||
|
||||
# Some norms require functions that are not implemented for all types.
|
||||
x = x.tocsr()
|
||||
|
||||
if axis is None:
|
||||
axis = (0, 1)
|
||||
elif not isinstance(axis, tuple):
|
||||
msg = "'axis' must be None, an integer or a tuple of integers"
|
||||
try:
|
||||
int_axis = int(axis)
|
||||
except TypeError as e:
|
||||
raise TypeError(msg) from e
|
||||
if axis != int_axis:
|
||||
raise TypeError(msg)
|
||||
axis = (int_axis,)
|
||||
|
||||
nd = 2
|
||||
if len(axis) == 2:
|
||||
row_axis, col_axis = axis
|
||||
if not (-nd <= row_axis < nd and -nd <= col_axis < nd):
|
||||
message = f'Invalid axis {axis!r} for an array with shape {x.shape!r}'
|
||||
raise ValueError(message)
|
||||
if row_axis % nd == col_axis % nd:
|
||||
raise ValueError('Duplicate axes given.')
|
||||
if ord == 2:
|
||||
# Only solver="lobpcg" supports all numpy dtypes
|
||||
_, s, _ = svds(x, k=1, solver="lobpcg")
|
||||
return s[0]
|
||||
elif ord == -2:
|
||||
raise NotImplementedError
|
||||
#return _multi_svd_norm(x, row_axis, col_axis, amin)
|
||||
elif ord == 1:
|
||||
return abs(x).sum(axis=row_axis).max(axis=col_axis)[0,0]
|
||||
elif ord == np.inf:
|
||||
return abs(x).sum(axis=col_axis).max(axis=row_axis)[0,0]
|
||||
elif ord == -1:
|
||||
return abs(x).sum(axis=row_axis).min(axis=col_axis)[0,0]
|
||||
elif ord == -np.inf:
|
||||
return abs(x).sum(axis=col_axis).min(axis=row_axis)[0,0]
|
||||
elif ord in (None, 'f', 'fro'):
|
||||
# The axis order does not matter for this norm.
|
||||
return _sparse_frobenius_norm(x)
|
||||
else:
|
||||
raise ValueError("Invalid norm order for matrices.")
|
||||
elif len(axis) == 1:
|
||||
a, = axis
|
||||
if not (-nd <= a < nd):
|
||||
message = f'Invalid axis {axis!r} for an array with shape {x.shape!r}'
|
||||
raise ValueError(message)
|
||||
if ord == np.inf:
|
||||
M = abs(x).max(axis=a)
|
||||
elif ord == -np.inf:
|
||||
M = abs(x).min(axis=a)
|
||||
elif ord == 0:
|
||||
# Zero norm
|
||||
M = (x != 0).sum(axis=a)
|
||||
elif ord == 1:
|
||||
# special case for speedup
|
||||
M = abs(x).sum(axis=a)
|
||||
elif ord in (2, None):
|
||||
M = sqrt(abs(x).power(2).sum(axis=a))
|
||||
else:
|
||||
try:
|
||||
ord + 1
|
||||
except TypeError as e:
|
||||
raise ValueError('Invalid norm order for vectors.') from e
|
||||
M = np.power(abs(x).power(ord).sum(axis=a), 1 / ord)
|
||||
if hasattr(M, 'toarray'):
|
||||
return M.toarray().ravel()
|
||||
elif hasattr(M, 'A'):
|
||||
return M.A.ravel()
|
||||
else:
|
||||
return M.ravel()
|
||||
else:
|
||||
raise ValueError("Improper number of dimensions to norm.")
|
||||
@ -0,0 +1,467 @@
|
||||
"""Sparse block 1-norm estimator.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from scipy.sparse.linalg import aslinearoperator
|
||||
|
||||
|
||||
__all__ = ['onenormest']
|
||||
|
||||
|
||||
def onenormest(A, t=2, itmax=5, compute_v=False, compute_w=False):
|
||||
"""
|
||||
Compute a lower bound of the 1-norm of a sparse matrix.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : ndarray or other linear operator
|
||||
A linear operator that can be transposed and that can
|
||||
produce matrix products.
|
||||
t : int, optional
|
||||
A positive parameter controlling the tradeoff between
|
||||
accuracy versus time and memory usage.
|
||||
Larger values take longer and use more memory
|
||||
but give more accurate output.
|
||||
itmax : int, optional
|
||||
Use at most this many iterations.
|
||||
compute_v : bool, optional
|
||||
Request a norm-maximizing linear operator input vector if True.
|
||||
compute_w : bool, optional
|
||||
Request a norm-maximizing linear operator output vector if True.
|
||||
|
||||
Returns
|
||||
-------
|
||||
est : float
|
||||
An underestimate of the 1-norm of the sparse matrix.
|
||||
v : ndarray, optional
|
||||
The vector such that ||Av||_1 == est*||v||_1.
|
||||
It can be thought of as an input to the linear operator
|
||||
that gives an output with particularly large norm.
|
||||
w : ndarray, optional
|
||||
The vector Av which has relatively large 1-norm.
|
||||
It can be thought of as an output of the linear operator
|
||||
that is relatively large in norm compared to the input.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is algorithm 2.4 of [1].
|
||||
|
||||
In [2] it is described as follows.
|
||||
"This algorithm typically requires the evaluation of
|
||||
about 4t matrix-vector products and almost invariably
|
||||
produces a norm estimate (which is, in fact, a lower
|
||||
bound on the norm) correct to within a factor 3."
|
||||
|
||||
.. versionadded:: 0.13.0
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Nicholas J. Higham and Francoise Tisseur (2000),
|
||||
"A Block Algorithm for Matrix 1-Norm Estimation,
|
||||
with an Application to 1-Norm Pseudospectra."
|
||||
SIAM J. Matrix Anal. Appl. Vol. 21, No. 4, pp. 1185-1201.
|
||||
|
||||
.. [2] Awad H. Al-Mohy and Nicholas J. Higham (2009),
|
||||
"A new scaling and squaring algorithm for the matrix exponential."
|
||||
SIAM J. Matrix Anal. Appl. Vol. 31, No. 3, pp. 970-989.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import numpy as np
|
||||
>>> from scipy.sparse import csc_matrix
|
||||
>>> from scipy.sparse.linalg import onenormest
|
||||
>>> A = csc_matrix([[1., 0., 0.], [5., 8., 2.], [0., -1., 0.]], dtype=float)
|
||||
>>> A.toarray()
|
||||
array([[ 1., 0., 0.],
|
||||
[ 5., 8., 2.],
|
||||
[ 0., -1., 0.]])
|
||||
>>> onenormest(A)
|
||||
9.0
|
||||
>>> np.linalg.norm(A.toarray(), ord=1)
|
||||
9.0
|
||||
"""
|
||||
|
||||
# Check the input.
|
||||
A = aslinearoperator(A)
|
||||
if A.shape[0] != A.shape[1]:
|
||||
raise ValueError('expected the operator to act like a square matrix')
|
||||
|
||||
# If the operator size is small compared to t,
|
||||
# then it is easier to compute the exact norm.
|
||||
# Otherwise estimate the norm.
|
||||
n = A.shape[1]
|
||||
if t >= n:
|
||||
A_explicit = np.asarray(aslinearoperator(A).matmat(np.identity(n)))
|
||||
if A_explicit.shape != (n, n):
|
||||
raise Exception('internal error: ',
|
||||
'unexpected shape ' + str(A_explicit.shape))
|
||||
col_abs_sums = abs(A_explicit).sum(axis=0)
|
||||
if col_abs_sums.shape != (n, ):
|
||||
raise Exception('internal error: ',
|
||||
'unexpected shape ' + str(col_abs_sums.shape))
|
||||
argmax_j = np.argmax(col_abs_sums)
|
||||
v = elementary_vector(n, argmax_j)
|
||||
w = A_explicit[:, argmax_j]
|
||||
est = col_abs_sums[argmax_j]
|
||||
else:
|
||||
est, v, w, nmults, nresamples = _onenormest_core(A, A.H, t, itmax)
|
||||
|
||||
# Report the norm estimate along with some certificates of the estimate.
|
||||
if compute_v or compute_w:
|
||||
result = (est,)
|
||||
if compute_v:
|
||||
result += (v,)
|
||||
if compute_w:
|
||||
result += (w,)
|
||||
return result
|
||||
else:
|
||||
return est
|
||||
|
||||
|
||||
def _blocked_elementwise(func):
|
||||
"""
|
||||
Decorator for an elementwise function, to apply it blockwise along
|
||||
first dimension, to avoid excessive memory usage in temporaries.
|
||||
"""
|
||||
block_size = 2**20
|
||||
|
||||
def wrapper(x):
|
||||
if x.shape[0] < block_size:
|
||||
return func(x)
|
||||
else:
|
||||
y0 = func(x[:block_size])
|
||||
y = np.zeros((x.shape[0],) + y0.shape[1:], dtype=y0.dtype)
|
||||
y[:block_size] = y0
|
||||
del y0
|
||||
for j in range(block_size, x.shape[0], block_size):
|
||||
y[j:j+block_size] = func(x[j:j+block_size])
|
||||
return y
|
||||
return wrapper
|
||||
|
||||
|
||||
@_blocked_elementwise
|
||||
def sign_round_up(X):
|
||||
"""
|
||||
This should do the right thing for both real and complex matrices.
|
||||
|
||||
From Higham and Tisseur:
|
||||
"Everything in this section remains valid for complex matrices
|
||||
provided that sign(A) is redefined as the matrix (aij / |aij|)
|
||||
(and sign(0) = 1) transposes are replaced by conjugate transposes."
|
||||
|
||||
"""
|
||||
Y = X.copy()
|
||||
Y[Y == 0] = 1
|
||||
Y /= np.abs(Y)
|
||||
return Y
|
||||
|
||||
|
||||
@_blocked_elementwise
|
||||
def _max_abs_axis1(X):
|
||||
return np.max(np.abs(X), axis=1)
|
||||
|
||||
|
||||
def _sum_abs_axis0(X):
|
||||
block_size = 2**20
|
||||
r = None
|
||||
for j in range(0, X.shape[0], block_size):
|
||||
y = np.sum(np.abs(X[j:j+block_size]), axis=0)
|
||||
if r is None:
|
||||
r = y
|
||||
else:
|
||||
r += y
|
||||
return r
|
||||
|
||||
|
||||
def elementary_vector(n, i):
|
||||
v = np.zeros(n, dtype=float)
|
||||
v[i] = 1
|
||||
return v
|
||||
|
||||
|
||||
def vectors_are_parallel(v, w):
|
||||
# Columns are considered parallel when they are equal or negative.
|
||||
# Entries are required to be in {-1, 1},
|
||||
# which guarantees that the magnitudes of the vectors are identical.
|
||||
if v.ndim != 1 or v.shape != w.shape:
|
||||
raise ValueError('expected conformant vectors with entries in {-1,1}')
|
||||
n = v.shape[0]
|
||||
return np.dot(v, w) == n
|
||||
|
||||
|
||||
def every_col_of_X_is_parallel_to_a_col_of_Y(X, Y):
|
||||
for v in X.T:
|
||||
if not any(vectors_are_parallel(v, w) for w in Y.T):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def column_needs_resampling(i, X, Y=None):
|
||||
# column i of X needs resampling if either
|
||||
# it is parallel to a previous column of X or
|
||||
# it is parallel to a column of Y
|
||||
n, t = X.shape
|
||||
v = X[:, i]
|
||||
if any(vectors_are_parallel(v, X[:, j]) for j in range(i)):
|
||||
return True
|
||||
if Y is not None:
|
||||
if any(vectors_are_parallel(v, w) for w in Y.T):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def resample_column(i, X):
|
||||
X[:, i] = np.random.randint(0, 2, size=X.shape[0])*2 - 1
|
||||
|
||||
|
||||
def less_than_or_close(a, b):
|
||||
return np.allclose(a, b) or (a < b)
|
||||
|
||||
|
||||
def _algorithm_2_2(A, AT, t):
|
||||
"""
|
||||
This is Algorithm 2.2.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : ndarray or other linear operator
|
||||
A linear operator that can produce matrix products.
|
||||
AT : ndarray or other linear operator
|
||||
The transpose of A.
|
||||
t : int, optional
|
||||
A positive parameter controlling the tradeoff between
|
||||
accuracy versus time and memory usage.
|
||||
|
||||
Returns
|
||||
-------
|
||||
g : sequence
|
||||
A non-negative decreasing vector
|
||||
such that g[j] is a lower bound for the 1-norm
|
||||
of the column of A of jth largest 1-norm.
|
||||
The first entry of this vector is therefore a lower bound
|
||||
on the 1-norm of the linear operator A.
|
||||
This sequence has length t.
|
||||
ind : sequence
|
||||
The ith entry of ind is the index of the column A whose 1-norm
|
||||
is given by g[i].
|
||||
This sequence of indices has length t, and its entries are
|
||||
chosen from range(n), possibly with repetition,
|
||||
where n is the order of the operator A.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This algorithm is mainly for testing.
|
||||
It uses the 'ind' array in a way that is similar to
|
||||
its usage in algorithm 2.4. This algorithm 2.2 may be easier to test,
|
||||
so it gives a chance of uncovering bugs related to indexing
|
||||
which could have propagated less noticeably to algorithm 2.4.
|
||||
|
||||
"""
|
||||
A_linear_operator = aslinearoperator(A)
|
||||
AT_linear_operator = aslinearoperator(AT)
|
||||
n = A_linear_operator.shape[0]
|
||||
|
||||
# Initialize the X block with columns of unit 1-norm.
|
||||
X = np.ones((n, t))
|
||||
if t > 1:
|
||||
X[:, 1:] = np.random.randint(0, 2, size=(n, t-1))*2 - 1
|
||||
X /= float(n)
|
||||
|
||||
# Iteratively improve the lower bounds.
|
||||
# Track extra things, to assert invariants for debugging.
|
||||
g_prev = None
|
||||
h_prev = None
|
||||
k = 1
|
||||
ind = range(t)
|
||||
while True:
|
||||
Y = np.asarray(A_linear_operator.matmat(X))
|
||||
g = _sum_abs_axis0(Y)
|
||||
best_j = np.argmax(g)
|
||||
g.sort()
|
||||
g = g[::-1]
|
||||
S = sign_round_up(Y)
|
||||
Z = np.asarray(AT_linear_operator.matmat(S))
|
||||
h = _max_abs_axis1(Z)
|
||||
|
||||
# If this algorithm runs for fewer than two iterations,
|
||||
# then its return values do not have the properties indicated
|
||||
# in the description of the algorithm.
|
||||
# In particular, the entries of g are not 1-norms of any
|
||||
# column of A until the second iteration.
|
||||
# Therefore we will require the algorithm to run for at least
|
||||
# two iterations, even though this requirement is not stated
|
||||
# in the description of the algorithm.
|
||||
if k >= 2:
|
||||
if less_than_or_close(max(h), np.dot(Z[:, best_j], X[:, best_j])):
|
||||
break
|
||||
ind = np.argsort(h)[::-1][:t]
|
||||
h = h[ind]
|
||||
for j in range(t):
|
||||
X[:, j] = elementary_vector(n, ind[j])
|
||||
|
||||
# Check invariant (2.2).
|
||||
if k >= 2:
|
||||
if not less_than_or_close(g_prev[0], h_prev[0]):
|
||||
raise Exception('invariant (2.2) is violated')
|
||||
if not less_than_or_close(h_prev[0], g[0]):
|
||||
raise Exception('invariant (2.2) is violated')
|
||||
|
||||
# Check invariant (2.3).
|
||||
if k >= 3:
|
||||
for j in range(t):
|
||||
if not less_than_or_close(g[j], g_prev[j]):
|
||||
raise Exception('invariant (2.3) is violated')
|
||||
|
||||
# Update for the next iteration.
|
||||
g_prev = g
|
||||
h_prev = h
|
||||
k += 1
|
||||
|
||||
# Return the lower bounds and the corresponding column indices.
|
||||
return g, ind
|
||||
|
||||
|
||||
def _onenormest_core(A, AT, t, itmax):
|
||||
"""
|
||||
Compute a lower bound of the 1-norm of a sparse matrix.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : ndarray or other linear operator
|
||||
A linear operator that can produce matrix products.
|
||||
AT : ndarray or other linear operator
|
||||
The transpose of A.
|
||||
t : int, optional
|
||||
A positive parameter controlling the tradeoff between
|
||||
accuracy versus time and memory usage.
|
||||
itmax : int, optional
|
||||
Use at most this many iterations.
|
||||
|
||||
Returns
|
||||
-------
|
||||
est : float
|
||||
An underestimate of the 1-norm of the sparse matrix.
|
||||
v : ndarray, optional
|
||||
The vector such that ||Av||_1 == est*||v||_1.
|
||||
It can be thought of as an input to the linear operator
|
||||
that gives an output with particularly large norm.
|
||||
w : ndarray, optional
|
||||
The vector Av which has relatively large 1-norm.
|
||||
It can be thought of as an output of the linear operator
|
||||
that is relatively large in norm compared to the input.
|
||||
nmults : int, optional
|
||||
The number of matrix products that were computed.
|
||||
nresamples : int, optional
|
||||
The number of times a parallel column was observed,
|
||||
necessitating a re-randomization of the column.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is algorithm 2.4.
|
||||
|
||||
"""
|
||||
# This function is a more or less direct translation
|
||||
# of Algorithm 2.4 from the Higham and Tisseur (2000) paper.
|
||||
A_linear_operator = aslinearoperator(A)
|
||||
AT_linear_operator = aslinearoperator(AT)
|
||||
if itmax < 2:
|
||||
raise ValueError('at least two iterations are required')
|
||||
if t < 1:
|
||||
raise ValueError('at least one column is required')
|
||||
n = A.shape[0]
|
||||
if t >= n:
|
||||
raise ValueError('t should be smaller than the order of A')
|
||||
# Track the number of big*small matrix multiplications
|
||||
# and the number of resamplings.
|
||||
nmults = 0
|
||||
nresamples = 0
|
||||
# "We now explain our choice of starting matrix. We take the first
|
||||
# column of X to be the vector of 1s [...] This has the advantage that
|
||||
# for a matrix with nonnegative elements the algorithm converges
|
||||
# with an exact estimate on the second iteration, and such matrices
|
||||
# arise in applications [...]"
|
||||
X = np.ones((n, t), dtype=float)
|
||||
# "The remaining columns are chosen as rand{-1,1},
|
||||
# with a check for and correction of parallel columns,
|
||||
# exactly as for S in the body of the algorithm."
|
||||
if t > 1:
|
||||
for i in range(1, t):
|
||||
# These are technically initial samples, not resamples,
|
||||
# so the resampling count is not incremented.
|
||||
resample_column(i, X)
|
||||
for i in range(t):
|
||||
while column_needs_resampling(i, X):
|
||||
resample_column(i, X)
|
||||
nresamples += 1
|
||||
# "Choose starting matrix X with columns of unit 1-norm."
|
||||
X /= float(n)
|
||||
# "indices of used unit vectors e_j"
|
||||
ind_hist = np.zeros(0, dtype=np.intp)
|
||||
est_old = 0
|
||||
S = np.zeros((n, t), dtype=float)
|
||||
k = 1
|
||||
ind = None
|
||||
while True:
|
||||
Y = np.asarray(A_linear_operator.matmat(X))
|
||||
nmults += 1
|
||||
mags = _sum_abs_axis0(Y)
|
||||
est = np.max(mags)
|
||||
best_j = np.argmax(mags)
|
||||
if est > est_old or k == 2:
|
||||
if k >= 2:
|
||||
ind_best = ind[best_j]
|
||||
w = Y[:, best_j]
|
||||
# (1)
|
||||
if k >= 2 and est <= est_old:
|
||||
est = est_old
|
||||
break
|
||||
est_old = est
|
||||
S_old = S
|
||||
if k > itmax:
|
||||
break
|
||||
S = sign_round_up(Y)
|
||||
del Y
|
||||
# (2)
|
||||
if every_col_of_X_is_parallel_to_a_col_of_Y(S, S_old):
|
||||
break
|
||||
if t > 1:
|
||||
# "Ensure that no column of S is parallel to another column of S
|
||||
# or to a column of S_old by replacing columns of S by rand{-1,1}."
|
||||
for i in range(t):
|
||||
while column_needs_resampling(i, S, S_old):
|
||||
resample_column(i, S)
|
||||
nresamples += 1
|
||||
del S_old
|
||||
# (3)
|
||||
Z = np.asarray(AT_linear_operator.matmat(S))
|
||||
nmults += 1
|
||||
h = _max_abs_axis1(Z)
|
||||
del Z
|
||||
# (4)
|
||||
if k >= 2 and max(h) == h[ind_best]:
|
||||
break
|
||||
# "Sort h so that h_first >= ... >= h_last
|
||||
# and re-order ind correspondingly."
|
||||
#
|
||||
# Later on, we will need at most t+len(ind_hist) largest
|
||||
# entries, so drop the rest
|
||||
ind = np.argsort(h)[::-1][:t+len(ind_hist)].copy()
|
||||
del h
|
||||
if t > 1:
|
||||
# (5)
|
||||
# Break if the most promising t vectors have been visited already.
|
||||
if np.isin(ind[:t], ind_hist).all():
|
||||
break
|
||||
# Put the most promising unvisited vectors at the front of the list
|
||||
# and put the visited vectors at the end of the list.
|
||||
# Preserve the order of the indices induced by the ordering of h.
|
||||
seen = np.isin(ind, ind_hist)
|
||||
ind = np.concatenate((ind[~seen], ind[seen]))
|
||||
for j in range(t):
|
||||
X[:, j] = elementary_vector(n, ind[j])
|
||||
|
||||
new_ind = ind[:t][~np.isin(ind[:t], ind_hist)]
|
||||
ind_hist = np.concatenate((ind_hist, new_ind))
|
||||
k += 1
|
||||
v = elementary_vector(n, ind_best)
|
||||
return est, v, w, nmults, nresamples
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user