Source code for psydac.linalg.basic

# coding: utf-8
#
# Copyright 2018 Yaman Güçlü, Jalal Lakhlili
# Copyright 2022 Yaman Güçlü, Said Hadjout, Julian Owezarek
"""
provides the fundamental classes for linear algebra operations.

"""

from abc import ABC, abstractmethod
from types import LambdaType 
from inspect import signature

import numpy as np
from scipy.sparse import coo_matrix

from psydac.utilities.utils import is_real

__all__ = (
    'VectorSpace',
    'Vector',
    'LinearOperator',
    'ZeroOperator',
    'IdentityOperator',
    'ScaledLinearOperator',
    'SumLinearOperator',
    'ComposedLinearOperator',
    'PowerLinearOperator',
    'InverseLinearOperator',
    'LinearSolver',
    'MatrixFreeLinearOperator'
)

#===============================================================================
class VectorSpace(ABC):
    """
    Finite-dimensional vector space V with a scalar (inner) product.

    """
    @property
    @abstractmethod
    def dimension(self):
        """
        The dimension of a vector space V is the cardinality
        (i.e. the number of vectors) of a basis of V over its base field.

        """

    @property
    @abstractmethod
    def dtype(self):
        """
        The data type of the field over which the space is built.

        """

    @abstractmethod
    def zeros(self):
        """
        Get a copy of the null element of the vector space V.

        Returns
        -------
        null : Vector
            A new vector object with all components equal to zero.

        """

    @abstractmethod
    def inner(self, x, y):
        """
        Evaluate the inner vector product between two vectors of this space V.

        If the field of V is real, compute the classical scalar product.
        If the field of V is complex, compute the classical sesquilinear
        product with linearity on the second vector.

        TODO [YG 01.05.2025]: Currently, the first vector is conjugated. We
        want to reverse this behavior in order to align with the convention
        of FEniCS.

        Parameters
        ----------
        x : Vector
            The first vector in the scalar product. In the case of a complex
            field, the inner product is antilinear w.r.t. this vector (hence
            this vector is conjugated).

        y : Vector
            The second vector in the scalar product. The inner product is
            linear w.r.t. this vector.

        Returns
        -------
        float | complex
            The scalar product of the two vectors. Note that inner(x, x) is
            a non-negative real number which is zero if and only if x = 0.

        """

    @abstractmethod
    def axpy(self, a, x, y):
        """
        Increment the vector y with the a-scaled vector x, i.e. y = a * x + y,
        provided that x and y belong to the same vector space V (self).
        The scalar value a may be real or complex, depending on the field of V.

        Parameters
        ----------
        a : scalar
            The scaling coefficient needed for the operation.

        x : Vector
            The vector which is not modified by this function.

        y : Vector
            The vector modified by this function (incremented by a * x).
        """

#===============================================================================
class Vector(ABC):
    """
    Element of a vector space V.

    """
    @property
    def shape(self):
        """ A tuple containing the dimension of the space. """
        return (self.space.dimension, )

    @property
    def dtype(self):
        """ The data type of the vector field V this vector belongs to. """
        return self.space.dtype

    def inner(self, v):
        """
        Evaluate the scalar product with the vector v of the same space.

        Parameters
        ----------
        v : Vector
            Vector belonging to the same space as self.

        """
        assert isinstance(v, Vector)
        assert self.space is v.space
        return self.space.inner(self, v)

    def mul_iadd(self, a, v):
        """
        Compute self += a * v, where v is another vector of the same space.

        Parameters
        ----------
        a : scalar
            Rescaling coefficient, which can be cast to the correct dtype.

        v : Vector
            Vector belonging to the same space as self.
        """
        self.space.axpy(a, v, self)

    #-------------------------------------
    # Deferred methods
    #-------------------------------------
    @property
    @abstractmethod
    def space(self):
        """ Vector space to which this vector belongs. """

    @abstractmethod
    def toarray(self, **kwargs):
        """ Convert to Numpy 1D array. """

    @abstractmethod
    def copy(self, out=None):
        """
        Return an identical copy of this vector.
        
        Subclasses must ensure that x.copy(out=x) returns x and not a new
        object.
        """

    @abstractmethod
    def conjugate(self, out=None):
        """
        Compute the complex conjugate vector.
        
        Please note that x.conjugate(out=x) modifies x in place and returns x.

        If the field is real (i.e. `self.dtype in (np.float32, np.float64)`) this method is equivalent to `copy`.
        If the field is complex (i.e. `self.dtype in (np.complex64, np.complex128)`) this method returns
        the complex conjugate of `self`, element-wise.

        The behavior of this function is similar to `numpy.conjugate(self, out=None)`.
        """

    @abstractmethod
    def __neg__(self):
        pass

    @abstractmethod
    def __mul__(self, a):
        pass

    @abstractmethod
    def __add__(self, v):
        pass

    @abstractmethod
    def __sub__(self, v):
        pass

    @abstractmethod
    def __imul__(self, a):
        pass

    @abstractmethod
    def __iadd__(self, v):
        pass

    @abstractmethod
    def __isub__(self, v):
        pass

    #-------------------------------------
    # Methods with default implementation
    #-------------------------------------
    def __rmul__(self, a):
        return self * a

    def __truediv__(self, a):
        return self * (1.0 / a)

    def __itruediv__(self, a):
        self *= 1.0 / a
        return self

    def conj(self, out=None):
        """Compute the complex conjugate vector.

        If the field is real (i.e. `self.dtype in (np.float32, np.float64)`) this method is equivalent to `copy`.
        If the field is complex (i.e. `self.dtype in (np.complex64, np.complex128)`) this method returns
        the complex conjugate of `self`, element-wise.

        The behavior of this function is similar to `numpy.conj(self, out=None)`.
        """
        return self.conjugate(out)

#===============================================================================
[docs] class LinearOperator(ABC): """ Abstract base class for all linear operators acting between two vector spaces V (domain) and W (codomain). """ @property def shape(self): """ A tuple containing the dimension of the codomain and domain. """ return (self.codomain.dimension, self.domain.dimension) #------------------------------------- # Deferred methods #------------------------------------- @property @abstractmethod def domain(self): """ The domain of the linear operator - an element of Vectorspace """ @property @abstractmethod def codomain(self): """ The codomain of the linear operator - an element of Vectorspace """ @property @abstractmethod def dtype(self): """ The data type of the coefficients of the linear operator, upon convertion to matrix. """
[docs] @abstractmethod def tosparse(self): """ Convert to a sparse matrix in any of the formats supported by scipy.sparse."""
[docs] @abstractmethod def toarray(self): """ Convert to Numpy 2D array. """
[docs] @abstractmethod def dot(self, v, out=None): """ Apply the LinearOperator self to the Vector v. The result is written to the Vector out, if provided. Parameters ---------- v : Vector The vector to which the linear operator (self) is applied. It must belong to the domain of self. out : Vector The vector in which the result of the operation is stored. It must belong to the codomain of self. If out is None, a new vector is created and returned. Returns ------- Vector The result of the operation. If out is None, a new vector is returned. Otherwise, the result is stored in out and out is returned. """
[docs] @abstractmethod def transpose(self, conjugate=False): """ Transpose the LinearOperator . If conjugate is True, return the Hermitian transpose. """
# TODO: check if we should add a copy method!!! #------------------------------------- # Magic methods #------------------------------------- def __neg__(self): """ Scales itself by -1 and thus returns the addititive inverse as a new object of the class ScaledLinearOperator. """ return ScaledLinearOperator(self.domain, self.codomain, -1.0, self) def __mul__(self, c): """ Scales a linear operator by a real scalar c by creating an object of the class ScaledLinearOperator, unless c = 0 or c = 1, in which case either a ZeroOperator or self is returned. """ assert np.isscalar(c) if c==0: return ZeroOperator(self.domain, self.codomain) elif c == 1: return self else: return ScaledLinearOperator(self.domain, self.codomain, c, self) def __rmul__(self, c): """ Calls __mul__ instead. """ return self * c def __matmul__(self, B): """ Matrix multiplication using the @ operator. If B is a LinearOperator, create a ComposedLinearOperator object. This is simplified to self if B is an IdentityOperator, and to a ZeroOperator if B is a ZeroOperator. If B is a Vector, the @ operator is treated as a matrix-vector multiplication and returns the result of self.dot(B). Parameters ---------- B : LinearOperator | Vector The object to be multiplied with self. If B is a LinearOperator, its codomain must be equal to the domain of self. If B is a Vector, it must belong to the domain of self. Returns ------- LinearOperator | Vector If B is a LinearOperator, return a ComposedLinearOperator object, or a simplification to self or a ZeroOperator. In all cases the resulting LinearOperator has the same domain as self and the same codomain as B. If B is a Vector, return the result of self.dot(B), which is a Vector belonging to the codomain of self. """ assert isinstance(B, (LinearOperator, Vector)) if isinstance(B, LinearOperator): assert self.domain == B.codomain if isinstance(B, ZeroOperator): return ZeroOperator(B.domain, self.codomain) elif isinstance(B, IdentityOperator): return self else: return ComposedLinearOperator(B.domain, self.codomain, self, B) else: return self.dot(B) def __add__(self, B): """ Creates an object of the class SumLinearOperator unless B is a ZeroOperator in which case self is returned. """ assert isinstance(B, LinearOperator) if isinstance(B, ZeroOperator): return self else: return SumLinearOperator(self.domain, self.codomain, self, B) def __sub__(self, B): """ Creates an object of the class SumLinearOperator unless B is a ZeroOperator in which case self is returned. """ assert isinstance(B, LinearOperator) if isinstance(B, ZeroOperator): return self else: return SumLinearOperator(self.domain, self.codomain, self, -B) def __pow__(self, n): """ Creates an object of class :ref:`PowerLinearOperator <powerlinearoperator>`. """ return PowerLinearOperator(self.domain, self.codomain, self, n) def __truediv__(self, c): """ Divide by scalar. """ return self * (1.0 / c) def __itruediv__(self, c): """ Divide by scalar, in place. """ self *= 1.0 / c return self #------------------------------------- # Methods with default implementation #------------------------------------- @property def T(self): """ Calls transpose method to return the transpose of self. """ return self.transpose() @property def H(self): """ Calls transpose method with `conjugate=True` flag to return the Hermitian transpose of self. """ return self.transpose(conjugate=True)
[docs] def idot(self, v, out): """ Implements out += self @ v with a temporary. Subclasses should provide an implementation without a temporary. """ assert isinstance(v, Vector) assert v.space == self.domain assert isinstance(out, Vector) assert out.space == self.codomain out += self.dot(v)
[docs] def dot_inner(self, v, w): """ Compute the inner product of (self @ v) with w, without a temporary. This is equivalent to self.dot(v).inner(w), but avoids the creation of a temporary vector because the result of self.dot(v) is stored in a local work array. If self is a positive-definite operator, this operation is a (weighted) inner product. Parameters ---------- v : Vector The vector to which the linear operator (self) is applied. It must belong to the domain of self. w : Vector The second vector in the inner product. It must belong to the codomain of self. Returns ------- float | complex The result of the inner product between (self @ v) and w. If the field of self is real, this is a real number. If the field of self is complex, this is a complex number. """ assert isinstance(v, Vector) assert isinstance(w, Vector) assert v.space is self.domain assert w.space is self.codomain if not hasattr(self, '_work'): self._work = self.codomain.zeros() return self.dot(v, out=self._work).inner(w)
#=============================================================================== class ZeroOperator(LinearOperator): """ Zero operator mapping any vector from its domain V to the zero vector of its codomain W. """ def __new__(cls, domain, codomain=None): assert isinstance(domain, VectorSpace) assert isinstance(codomain, VectorSpace) from psydac.linalg.block import BlockVectorSpace, BlockLinearOperator if isinstance(domain, BlockVectorSpace) or isinstance(codomain, BlockVectorSpace): if isinstance(domain, BlockVectorSpace): domain_spaces = domain.spaces else: domain_spaces = (domain,) if isinstance(codomain, BlockVectorSpace): codomain_spaces = codomain.spaces else: codomain_spaces = (codomain,) blocks = {} for i, D in enumerate(domain_spaces): for j, C in enumerate(codomain_spaces): blocks[j,i] = ZeroOperator(D,C) return BlockLinearOperator(domain, codomain, blocks) else: return super().__new__(cls) def __init__(self, domain, codomain): self._domain = domain self._codomain = codomain @property def domain(self): return self._domain @property def codomain(self): return self._codomain @property def dtype(self): return None def copy(self): return ZeroOperator(self.domain, self.codomain) def toarray(self): return np.zeros(self.shape, dtype=self.dtype) def tosparse(self): from scipy.sparse import csr_matrix return csr_matrix(self.shape, dtype=self.dtype) def transpose(self, conjugate=False): return ZeroOperator(domain=self.codomain, codomain=self.domain) def dot(self, v, out=None): assert isinstance(v, Vector) assert v.space == self.domain if out is not None: assert isinstance(out, Vector) assert out.space == self.codomain out *= 0 else: out = self.codomain.zeros() return out def __neg__(self): return self def __add__(self, B): assert isinstance(B, LinearOperator) assert self.domain == B.domain assert self.codomain == B.codomain return B def __sub__(self, B): assert isinstance(B, LinearOperator) assert self.domain == B.domain assert self.codomain == B.codomain return -B def __mul__(self, c): assert np.isscalar(c) return self def __matmul__(self, B): assert isinstance(B, (LinearOperator, Vector)) if isinstance(B, LinearOperator): assert self.domain == B.codomain return ZeroOperator(domain=B.domain, codomain=self.codomain) else: return self.dot(B) #=============================================================================== class IdentityOperator(LinearOperator): """ Identity operator acting between a vector space V and itself. Useful for example in custom linear operator classes together with the apply_essential_bc method to create projection operators. """ def __init__(self, domain, codomain=None): assert isinstance(domain, VectorSpace) if codomain: assert isinstance(codomain, VectorSpace) assert domain == codomain self._domain = domain self._codomain = domain @property def domain(self): return self._domain @property def codomain(self): return self._codomain @property def dtype(self): return None def copy(self): """ Returns a new IdentityOperator object acting between the same vector spaces.""" return IdentityOperator(self.domain, self.codomain) def toarray(self): return np.diag(np.ones(self.domain.dimension , dtype=self.dtype)) def tosparse(self): from scipy.sparse import identity return identity(self.domain.dimension, dtype=self.dtype, format="csr") def transpose(self, conjugate=False): """ Could return self, but by convention returns new object. """ return IdentityOperator(self.domain, self.codomain) def dot(self, v, out=None): assert isinstance(v, Vector) assert v.space == self.domain if out is not None: assert isinstance(out, Vector) assert out.space == self.codomain out *= 0 out += v return out else: return v.copy() def __matmul__(self, B): assert isinstance(B, (LinearOperator, Vector)) if isinstance(B, LinearOperator): assert self.domain == B.codomain return B else: return self.dot(B) #=============================================================================== class ScaledLinearOperator(LinearOperator): """ A linear operator $A$ scalar multiplied by a constant $c$. """ def __init__(self, domain, codomain, c, A): assert isinstance(domain, VectorSpace) assert isinstance(codomain, VectorSpace) assert np.isscalar(c) assert np.iscomplexobj(c) == (codomain._dtype == complex) assert isinstance(A, LinearOperator) assert domain == A.domain assert codomain == A.codomain if isinstance(A, ScaledLinearOperator): scalar = A.scalar * c operator = A.operator else: scalar = c operator = A self._operator = operator self._scalar = scalar self._domain = domain self._codomain = codomain @property def domain(self): return self._domain @property def codomain(self): return self._codomain @property def scalar(self): """ Returns the scalar value by which the operator is multiplied.""" return self._scalar @property def operator(self): """ Returns the operator that is multiplied by the scalar.""" return self._operator @property def dtype(self): return None def toarray(self): return self._scalar*self._operator.toarray() def tosparse(self): from scipy.sparse import csr_matrix return self._scalar*csr_matrix(self._operator.toarray()) def transpose(self, conjugate=False): return ScaledLinearOperator(domain=self.codomain, codomain=self.domain, c=self._scalar if not conjugate else np.conjugate(self._scalar), A=self._operator.transpose(conjugate=conjugate)) def __neg__(self): return ScaledLinearOperator(domain=self.domain, codomain=self.codomain, c=-1*self._scalar, A=self._operator) def dot(self, v, out=None): assert isinstance(v, Vector) assert v.space == self.domain if out is not None: assert isinstance(out, Vector) assert out.space == self.codomain self._operator.dot(v, out = out) out *= self._scalar return out else: out = self._operator.dot(v) out *= self._scalar return out #=============================================================================== class SumLinearOperator(LinearOperator): r""" Sum $\sum_{i=1}^n A_i$ of linear operators $A_1,\dots,A_n$ acting between the same vector spaces V (domain) and W (codomain). """ def __new__(cls, domain, codomain, *args): if len(args) == 0: return ZeroOperator(domain,codomain) elif len(args) == 1: return args[0] else: return super().__new__(cls) def __init__(self, domain, codomain, *args): assert isinstance(domain, VectorSpace) assert isinstance(codomain, VectorSpace) for a in args: assert isinstance(a, LinearOperator) assert a.domain == domain assert a.codomain == codomain addends = () for a in args: if isinstance(a, SumLinearOperator): addends = (*addends, *a.addends) else: addends = (*addends, a) addends = SumLinearOperator.simplify(addends) self._domain = domain self._codomain = codomain self._addends = addends @property def domain(self): """ The domain of the linear operator, element of class ``VectorSpace``. """ return self._domain @property def codomain(self): """ The codomain of the linear operator, element of class ``VectorSpace``. """ return self._codomain @property def addends(self): """ A tuple containing the addends of the linear operator, elements of class ``LinearOperator``. """ return self._addends @property def dtype(self): return None def toarray(self): out = np.zeros(self.shape, dtype=self.dtype) for a in self._addends: out += a.toarray() return out def tosparse(self): from scipy.sparse import csr_matrix out = csr_matrix(self.shape, dtype=self.dtype) for a in self._addends: out += a.tosparse() return out def transpose(self, conjugate=False): t_addends = () for a in self._addends: t_addends = (*t_addends, a.transpose(conjugate=conjugate)) return SumLinearOperator(self.codomain, self.domain, *t_addends) @staticmethod def simplify(addends): """ Simplifies a sum of linear operators by combining addends of the same class. """ class_list = [a.__class__ for a in addends] unique_list = [*{c: a for c, a in zip(class_list, addends)}] if len(unique_list) == 1: return addends out = () for j in unique_list: indices = [k for k, l in enumerate(class_list) if l == j] if len(indices) == 1: out = (*out, addends[indices[0]]) else: A = addends[indices[0]] + addends[indices[1]] for n in range(len(indices)-2): A += addends[indices[n+2]] if isinstance(A, SumLinearOperator): out = (*out, *A.addends) else: out = (*out, A) return out def dot(self, v, out=None): """ Evaluates SumLinearOperator object at a vector v element of domain. """ assert isinstance(v, Vector) assert v.space == self.domain if out is not None: assert isinstance(out, Vector) assert out.space == self.codomain out *= 0 for a in self._addends: a.idot(v, out) return out else: out = self.codomain.zeros() for a in self._addends: a.idot(v, out=out) return out #=============================================================================== class ComposedLinearOperator(LinearOperator): r""" Composition $A_n\circ\dots\circ A_1$ of two or more linear operators $A_1,\dots,A_n$. """ def __init__(self, domain, codomain, *args): assert isinstance(domain, VectorSpace) assert isinstance(codomain, VectorSpace) for a in args: assert isinstance(a, LinearOperator) assert args[0].codomain == codomain assert args[-1].domain == domain for i in range(len(args)-1): assert args[i].domain == args[i+1].codomain multiplicants = () tmp_vectors = [] for a in args[:-1]: if isinstance(a, ComposedLinearOperator): multiplicants = (*multiplicants, *a.multiplicants) tmp_vectors.extend(a.tmp_vectors) tmp_vectors.append(a.domain.zeros()) else: multiplicants = (*multiplicants, a) tmp_vectors.append(a.domain.zeros()) last = args[-1] if isinstance(last, ComposedLinearOperator): multiplicants = (*multiplicants, *last.multiplicants) tmp_vectors.extend(last.tmp_vectors) else: multiplicants = (*multiplicants, last) self._domain = domain self._codomain = codomain self._multiplicants = multiplicants self._tmp_vectors = tuple(tmp_vectors) @property def tmp_vectors(self): """ A tuple containing the storage vectors that are repeatedly being used upon calling the `dot` method. This avoids the creation of new vectors at each call of the `dot` method. """ return self._tmp_vectors @property def domain(self): return self._domain @property def codomain(self): return self._codomain @property def multiplicants(self): r""" A tuple $(A_1,\dots,A_n)$ containing the multiplicants of the linear operator $self = A_n\circ\dots\circ A_1$. """ return self._multiplicants @property def dtype(self): return None def toarray(self): raise NotImplementedError('toarray() is not defined for ComposedLinearOperators.') def tosparse(self): mats = [M.tosparse() for M in self._multiplicants] M = mats[0] for Mi in mats[1:]: M = M @ Mi return coo_matrix(M) def transpose(self, conjugate=False): t_multiplicants = () for a in self._multiplicants: t_multiplicants = (a.transpose(conjugate=conjugate), *t_multiplicants) new_dom = self.codomain new_cod = self.domain assert isinstance(new_dom, VectorSpace) assert isinstance(new_cod, VectorSpace) return ComposedLinearOperator(self.codomain, self.domain, *t_multiplicants) def dot(self, v, out=None): assert isinstance(v, Vector) assert v.space == self.domain if out is not None: assert isinstance(out, Vector) assert out.space == self.codomain x = v for i in range(len(self._tmp_vectors)): y = self._tmp_vectors[-1-i] A = self._multiplicants[-1-i] A.dot(x, out=y) x = y A = self._multiplicants[0] if out is not None: A.dot(x, out=out) else: out = A.dot(x) return out def exchange_assembly_data(self): for op in self._multiplicants: op.exchange_assembly_data() def set_backend(self, backend, precompiled=False): for op in self._multiplicants: op.set_backend(backend) #=============================================================================== class PowerLinearOperator(LinearOperator): r""" Power $A^n$ of a linear operator $A$ for some integer $n\geq 0$. """ def __new__(cls, domain, codomain, A, n): assert isinstance(n, int) assert n >= 0 assert isinstance(A, LinearOperator) assert A.domain == domain assert A.codomain == codomain assert domain == codomain if n == 0: return IdentityOperator(domain, codomain) elif n == 1: return A else: return super().__new__(cls) def __init__(self, domain, codomain, A, n): if isinstance(A, PowerLinearOperator): self._operator = A.operator self._factorial = A.factorial*n else: self._operator = A self._factorial = n self._domain = domain self._codomain = codomain @property def domain(self): return self._domain @property def codomain(self): return self._codomain @property def dtype(self): return None @property def operator(self): """ Returns the operator that is raised to the power. """ return self._operator @property def factorial(self): """ Returns the power to which the operator is raised. """ return self._factorial def toarray(self): raise NotImplementedError('toarray() is not defined for PowerLinearOperators.') def tosparse(self): raise NotImplementedError('tosparse() is not defined for PowerLinearOperators.') def transpose(self, conjugate=False): return PowerLinearOperator(domain=self.codomain, codomain=self.domain, A=self._operator.transpose(conjugate=conjugate), n=self._factorial) def dot(self, v, out=None): assert isinstance(v, Vector) assert v.space == self.domain if out is not None: assert isinstance(out, Vector) assert out.space == self.codomain for i in range(self._factorial): self._operator.dot(v, out=out) v = out.copy() else: out = v.copy() for i in range(self._factorial): out = self._operator.dot(out) return out #=============================================================================== class InverseLinearOperator(LinearOperator): """ Abstract base class for the (approximate) inverse $A^{-1}$ of a square matrix $A$. The result of A_inv.dot(b) is the (approximate) solution x of the linear system A x = b, where x and b belong to the same vector space V. We assume that the linear system is solved by an iterative method, which needs a first guess `x0` and an exit condition based on `tol` and `maxiter`. Concrete subclasses of this class must implement the `dot` method and take care of any internal storage which might be necessary. Parameters ---------- A : psydac.linalg.basic.LinearOperator Left-hand-side matrix A of linear system. x0 : psydac.linalg.basic.Vector First guess of solution for iterative solver (optional). tol : float Absolute tolerance for L2-norm of residual r = A*x - b. maxiter: int Maximum number of iterations. verbose : bool If True, L2-norm of residual r is printed at each iteration. """ def __init__(self, A, **kwargs): assert isinstance(A, LinearOperator) assert A.domain.dimension == A.codomain.dimension domain = A.codomain codomain = A.domain if kwargs['x0'] is None: kwargs['x0'] = codomain.zeros() self._A = A self._domain = domain self._codomain = codomain self._check_options(**kwargs) self._options = kwargs @property def domain(self): return self._domain @property def codomain(self): return self._codomain @property def dtype(self): return None @property def linop(self): """ The linear operator $A$ of which this object is the inverse $A^{-1}$. The linear operator $A$ can be modified in place, or replaced entirely through the setter. A substitution should only be made in cases where no other options are viable, as it breaks the one-to-one map between the original linear operator $A$ (passed to the constructor) and the current `InverseLinearOperator` object $A^{-1}$. Use with extreme care! """ return self._A @linop.setter def linop(self, a): """ Set the linear operator $A$ of which this object is the inverse $A^{-1}$. """ assert isinstance(a, LinearOperator) assert a.domain is self.domain assert a.codomain is self.codomain self._A = a def _check_options(self, **kwargs): """ Check whether the options passed to the solver class are valid. """ for key, value in kwargs.items(): if key == 'x0': if value is not None: assert isinstance(value, Vector), "x0 must be a Vector or None" assert value.space == self.codomain, "x0 belongs to the wrong VectorSpace" elif key == 'tol': assert is_real(value), "tol must be a real number" assert value > 0, "tol must be positive" elif key == 'maxiter': assert isinstance(value, int), "maxiter must be an int" assert value > 0, "maxiter must be positive" elif key == 'verbose': assert isinstance(value, bool), "verbose must be a bool" def toarray(self): raise NotImplementedError('toarray() is not defined for InverseLinearOperators.') def tosparse(self): raise NotImplementedError('tosparse() is not defined for InverseLinearOperators.') def get_info(self): """ Returns the previous convergence information. """ return self._info def get_options(self, key=None): """Get a copy of all the solver options, or a specific value of interest. Parameters ---------- key : str | None Name of the specific option of interest (default: None). Returns ------- dict | type(self._options['key']) | None If `key` is given, get the specific option of interest. If there is no such option, `None` is returned instead. If `key` is not given, get a copy of all the solver options in a dictionary. """ if key is None: return self._options.copy() else: return self._options.get(key) def set_options(self, **kwargs): """ Set the solver options by passing keyword arguments. """ self._check_options(**kwargs) self._options.update(kwargs) def transpose(self, conjugate=False): cls = type(self) At = self.linop.transpose(conjugate=conjugate) options = self._options return cls(At, **options) #=============================================================================== class LinearSolver(ABC): """ Solver for the square linear system Ax=b, where x and b belong to the same vector space V. """ @property def shape(self): return (self.space.dimension, self.space.dimension) #------------------------------------- # Deferred methods #------------------------------------- @property @abstractmethod def space(self): pass @abstractmethod def transpose(self): """Return the transpose of the LinearSolver.""" pass @abstractmethod def solve(self, rhs, out=None): pass @property def T(self): return self.transpose() #=============================================================================== class MatrixFreeLinearOperator(LinearOperator): """ General linear operator represented by a callable dot method. Parameters ---------- domain : VectorSpace The domain of the linear operator. codomain : VectorSpace The codomain of the linear operator. dot : Callable The method of the linear operator, assumed to map from domain to codomain. This method can take out as an optional argument but this is not mandatory. The callable can take other keyword arguments as for instance function parameters. dot_transpose: Callable The method of the transpose of the linear operator, assumed to map from codomain to domain. This method can take out as an optional argument but this is not mandatory. Examples -------- # example 1: a matrix encapsulated as a (fake) matrix-free linear operator A_SM = StencilMatrix(V, W) AT_SM = A_SM.transpose() A = MatrixFreeLinearOperator(domain=V, codomain=W, dot=lambda v: A_SM @ v, dot_transpose=lambda v: AT_SM @ v) # example 2: a truly matrix-free linear operator A = MatrixFreeLinearOperator(domain=V, codomain=V, dot=lambda v: 2*v, dot_transpose=lambda v: 2*v) """ def __init__(self, domain, codomain, dot, dot_transpose=None): assert isinstance(domain, VectorSpace) assert isinstance(codomain, VectorSpace) assert isinstance(dot, LambdaType) self._domain = domain self._codomain = codomain self._dot = dot sig = signature(dot) self._dot_takes_out_arg = ('out' in [p.name for p in sig.parameters.values() if p.kind == p.KEYWORD_ONLY]) if dot_transpose is not None: assert isinstance(dot_transpose, LambdaType) self._dot_transpose = dot_transpose sig = signature(dot_transpose) self._dot_transpose_takes_out_arg = ('out' in [p.name for p in sig.parameters.values() if p.kind == p.KEYWORD_ONLY]) else: self._dot_transpose = None self._dot_transpose_takes_out_arg = False @property def domain(self): return self._domain @property def codomain(self): return self._codomain @property def dtype(self): return None def dot(self, v, out=None, **kwargs): assert isinstance(v, Vector) assert v.space == self.domain if out is not None: assert isinstance(out, Vector) assert out.space == self.codomain else: out = self.codomain.zeros() if self._dot_takes_out_arg: self._dot(v, out=out, **kwargs) else: # provided dot product does not take an out argument: we simply copy the result into out self._dot(v).copy(out=out, **kwargs) return out def toarray(self): raise NotImplementedError('toarray() is not defined for MatrixFreeLinearOperator.') def tosparse(self): raise NotImplementedError('tosparse() is not defined for MatrixFreeLinearOperator.') def transpose(self, conjugate=False): if self._dot_transpose is None: raise NotImplementedError('no transpose dot method was given -- cannot create the transpose operator') if conjugate: if self._dot_transpose_takes_out_arg: new_dot = lambda v, out=None: self._dot_transpose(v, out=out).conjugate() else: new_dot = lambda v: self._dot_transpose(v).conjugate() else: new_dot = self._dot_transpose return MatrixFreeLinearOperator(domain=self.codomain, codomain=self.domain, dot=new_dot, dot_transpose=self._dot)