Source code for pymablock.linalg

# ruff: noqa: N803, N806
"""Linear algebra utilities."""

from collections.abc import Callable
from typing import Any
from warnings import warn

import numpy as np
import sympy
from mumps import Context as MUMPSContext
from packaging.version import parse
from scipy import __version__ as scipy_version
from scipy import sparse
from scipy.sparse import identity, spmatrix
from scipy.sparse.linalg import LinearOperator
from scipy.sparse.linalg import aslinearoperator as scipy_aslinearoperator

from pymablock.series import one, zero

# Monkey-patch LinearOperator to support right multiplication
# TODO: Remove this when we depend on scipy >= 1.11, see issue #130.
if parse(scipy_version) < parse("1.11"):
    from scipy.sparse.linalg._interface import (
        _ProductLinearOperator,
        _ScaledLinearOperator,
    )

    def __rmul__(self, x):  # noqa: N807
        if np.isscalar(x):
            return _ScaledLinearOperator(self, x)
        return self._rdot(x)

    def _rdot(self: LinearOperator, x: Any) -> Any:
        """Matrix-matrix or matrix-vector multiplication from the right.

        Parameters
        ----------
        self :
            Linear operator to multiply with.
        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)
        if np.isscalar(x):
            return _ScaledLinearOperator(self, x)
        x = np.asarray(x)

        if x.ndim == 1 or x.ndim == 2 and x.shape[0] == 1:
            return self.rmatvec(x.T.conj()).T.conj()
        if x.ndim == 2:
            return self.rmatmat(x.T.conj()).T.conj()
        raise ValueError("expected 1-d or 2-d array or matrix, got %r" % x)

    LinearOperator.__rmul__ = __rmul__
    LinearOperator._rdot = _rdot
    LinearOperator.__array_ufunc__ = None
    del __rmul__, _rdot


[docs] def direct_greens_function( h: spmatrix, E: float, atol: float = 1e-7, eps: float = 0, ) -> Callable[[np.ndarray], np.ndarray]: """Compute the Green's function of a Hamiltonian using MUMPS solver. Parameters ---------- h : Hamiltonian matrix. E : Energy at which to compute the Green's function. atol : Accepted precision of the desired result in infinity-norm. eps : Tolerance for the MUMPS solver to identify null pivots. Passed through to MUMPS CNTL(3), see MUMPS user guide. Returns ------- greens_function : `Callable[[np.ndarray], np.ndarray]` Function that solves :math:`(E - H) sol = vec`. """ mat = E * sparse.csr_array(identity(h.shape[0], dtype=h.dtype, format="csr")) - h is_complex = np.iscomplexobj(mat.data) ctx = MUMPSContext() # MUMPS does not support Hermitian matrices, so we use the symmetric only with real. ctx.set_matrix(mat, symmetric=not is_complex) # Enable null pivot detection ctx.mumps_instance.icntl[24] = 1 # Set tolerance for null pivot detection ctx.mumps_instance.cntl[3] = eps # Enable computation of the residual ctx.mumps_instance.icntl[11] = 2 ctx.factor() def greens_function(vec: np.ndarray) -> np.ndarray: """Apply the Green's function to a vector. Parameters ---------- vec : Vector to which to apply the Green's function. If the Green's function is evaluated at an eigenenergy, this vector must be orthogonal to the corresponding eigenvector(s). Returns ------- sol : Solution of :math:`(E - H) sol = vec`. """ if np.iscomplexobj(vec) and not is_complex: vec = (vec.real, vec.imag) else: vec = (vec,) residual = 0 sol = [] for v in vec: sol.append(ctx.solve(v)) residual += ( ctx.mumps_instance.rinfog[6] # Scaled residual * ctx.mumps_instance.rinfog[4] # ||A||_inf * ctx.mumps_instance.rinfog[5] # ||x||_inf ) if residual > atol: warn( f"Solution only achieved precision {residual} > {atol}." " adjust eps or atol.", RuntimeWarning, ) return sol[0] if len(sol) == 1 else sol[0] + 1j * sol[1] return greens_function
class ComplementProjector(LinearOperator): r"""Projector on the complement of the span of a set of vectors. This is used to compute $P_B = I - P_A$ where $P_A$ is the projector on the span of the vectors $A$ in the implicit method. """ def __init__(self: LinearOperator, vecs: np.ndarray) -> LinearOperator: """Projector on the complement of the span of vecs.""" self.shape = (vecs.shape[0], vecs.shape[0]) self._vecs = vecs self.dtype = vecs.dtype __array_ufunc__ = None def _matvec(self: LinearOperator, v: np.ndarray) -> np.ndarray: return v - self._vecs @ (self._vecs.conj().T @ v) _matmat = _rmatvec = _rmatmat = _matvec def _adjoint(self: LinearOperator) -> LinearOperator: return self def conjugate(self: LinearOperator) -> LinearOperator: """Conjugate operator.""" return self.__class__(vecs=self._vecs.conj()) _transpose = conjugate def aslinearoperator(A: Any) -> Any: """Construct a linear operator. Same as `scipy.sparse.linalg.aslinearoperator`, but with passthrough for `~pymablock.series.zero` and `~pymablock.series.one`. """ if A is zero or A is one: return A return scipy_aslinearoperator(A) def is_diagonal(A: Any, atol: float = 1e-12) -> bool: """Check if A is diagonal.""" if A is zero or A is np.ma.masked: return True if isinstance(A, sympy.MatrixBase): return A.is_diagonal() if isinstance(A, np.ndarray): # Create a view of the offdiagonal array elements offdiagonal = A.reshape(-1)[:-1].reshape(len(A) - 1, len(A) + 1)[:, 1:] return not np.any(np.round(offdiagonal, int(-np.log10(atol)))) if sparse.issparse(A): A = sparse.dia_array(A) return not any(A.offsets) raise NotImplementedError(f"Cannot extract diagonal from {type(A)}")