| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924 |
- """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_array
- >>> offsets = csr_array([[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 types
- 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`.
- It is highly recommended to explicitly specify the `dtype`, otherwise
- it is determined automatically at the cost of a single matvec application
- on ``int8`` zero vector using the promoted `dtype` of the output.
- Python ``int`` could be difficult to automatically cast to numpy integers
- in the definition of the `matvec` so the determination may be inaccurate.
- It is assumed that `matmat`, `rmatvec`, and `rmatmat` would result in
- the same dtype of the output given an ``int8`` input as `matvec`.
- 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=int8>
- >>> 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
- # generic type compatibility with scipy-stubs
- __class_getitem__ = classmethod(types.GenericAlias)
- 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):
- """Determine the dtype by executing `matvec` on an `int8` test vector.
- In `np.promote_types` hierarchy, the type `int8` is the smallest,
- so we call `matvec` on `int8` and use the promoted dtype of the output
- to set the default `dtype` of the `LinearOperator`.
- We assume that `matmat`, `rmatvec`, and `rmatmat` would result in
- the same dtype of the output given an `int8` input as `matvec`.
- Called from subclasses at the end of the __init__ routine.
- """
- if self.dtype is None:
- v = np.zeros(self.shape[-1], dtype=np.int8)
- try:
- matvec_v = np.asarray(self.matvec(v))
- except OverflowError:
- # Python large `int` promoted to `np.int64`or `np.int32`
- self.dtype = np.dtype(int)
- else:
- self.dtype = 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
- if (hasattr(self, "_rmatmat")
- and type(self)._rmatmat != LinearOperator._rmatmat):
- # Try to use _rmatmat as a fallback
- return self._rmatmat(x.reshape(-1, 1)).reshape(-1)
- 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(f'expected 2-d ndarray or matrix, not {X.ndim}-d')
- 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(f'expected 1-d or 2-d array or matrix, got {x!r}')
- 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(f'expected 1-d or 2-d array or matrix, got {x!r}')
- 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 f'<{M}x{N} {self.__class__.__name__} with {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)
- # Note: args[1] is alpha (a scalar), so use `*` below, not `@`
- 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(f'square LinearOperator expected, got {A!r}')
- 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.A)
- return self.__adj
- class _AdjointMatrixOperator(MatrixLinearOperator):
- def __init__(self, adjoint_array):
- self.A = adjoint_array.T.conj()
- self.args = (adjoint_array,)
- self.shape = adjoint_array.shape[1], adjoint_array.shape[0]
- @property
- def dtype(self):
- return self.args[0].dtype
- def _adjoint(self):
- return MatrixLinearOperator(self.args[0])
- 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 array (e.g. csr_array, lil_array, 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')
|