"""Kernel Polynomial Method (KPM).
See arXiv:cond-mat/0504627 and arXiv:1909.09649.
"""
from collections.abc import Iterator
from typing import Callable
from warnings import warn
import numpy as np
from scipy import sparse
[docs]
def greens_function(
hamiltonian: np.ndarray | sparse.spmatrix,
energy: float,
vector: np.ndarray,
atol: float = 1e-7,
max_moments: int = int(1e6),
) -> Callable[[np.ndarray], np.ndarray]:
"""Return a solution of ``(energy - hamiltonian) @ x = vector``.
Uses the Kernel polynomial method (KPM) with the Jackson kernel.
Parameters
----------
hamiltonian :
Hamiltonian with shape ``(N, N)``. It is required that the Hamiltonian
is rescaled so that its spectrum lies in the interval ``[-1, 1]``.
energy :
Rescaled energy at which to evaluate the Green's function.
vector :
Vector with shape ``(N,)``.
atol :
Accepted precision of the desired result in 2-norm.
max_moments :
Maximum order of KPM expansion to compute.
Returns
-------
solution : `~numpy.ndarray`
Solution x of (E - H_0) * x = v.
"""
residue = np.inf
num_moments = 10
while residue > atol:
if num_moments > max_moments:
warn(
f"KPM expansion did not converge to precision "
f"{atol} after {max_moments} moments.",
RuntimeWarning,
)
break
prefactor = -2 / np.sqrt(1 - energy**2)
coef = prefactor * np.sin(np.arange(num_moments) * np.arccos(energy))
coef[0] /= 2
coef *= jackson_kernel(num_moments)
sol = sum(vec * c for c, vec in zip(coef, kpm_vectors(hamiltonian, vector)))
residue = np.linalg.norm((hamiltonian @ sol - energy * sol) + vector)
num_moments *= 4
return sol
def kpm_vectors(
hamiltonian: np.ndarray | sparse.spmatrix,
vector: np.ndarray,
) -> Iterator[np.ndarray]:
r"""Generate vectors for the Kernel Polynomial Method (KPM).
Generates vectors as :math:`T_n(H) \lvert v \rangle`.
Parameters
----------
hamiltonian :
Hamiltonian, dense or sparse array with shape ``(N, N)``.
vector :
Vector of length ``N``.
Yields
------
expanded_vectors : Iterable
Infinite sequence of Chebyshev polynomials of the Hamiltonian applied
to the vector.
"""
yield (alpha_prev := vector)
yield (alpha := hamiltonian @ alpha_prev)
while True:
alpha, alpha_prev = 2 * hamiltonian @ alpha - alpha_prev, alpha
yield alpha
[docs]
def rescale(
hamiltonian: np.ndarray | sparse.spmatrix,
eps: float = 0.01,
bounds: tuple[float, float] | None = None,
lower_bounds: tuple[float, float] | None = None,
) -> tuple[np.ndarray | sparse.spmatrix, tuple[float, float]]:
"""Rescale a Hamiltonian to the interval ``[-1 - eps/2, 1 + eps/2]``.
Adapted with modifications from kwant.kpm Copyright 2011-2016 Kwant
developers, BSD simplified license
https://gitlab.kwant-project.org/kwant/kwant/-/blob/v1.4.3/LICENSE.rst
Parameters
----------
hamiltonian :
Hamiltonian of the system.
eps :
Extra tolerance to add to the spectral bounds as a fraction of the bandwidth.
bounds :
Estimated boundaries of the spectrum. If not provided the maximum and
minimum eigenvalues are calculated.
lower_bounds :
Energy interval to definitely include within the [-1, 1] rescaled energies.
Returns
-------
hamiltonian_rescaled :
Rescaled Hamiltonian.
(a, b) :
Rescaling parameters such that ``h_rescaled = (h - b) / a``.
"""
if bounds is not None:
lmin, lmax = bounds
else:
# Relative tolerance to which to calculate eigenvalues. Because after
# rescaling we will add eps / 2 to the spectral bounds, we don't need
# to know the bounds more accurately than eps / 2.
tol = eps / 2
lmax, lmin = [
sparse.linalg.eigsh(
hamiltonian, k=1, which=which, return_eigenvectors=False, tol=tol
)[0]
for which in ("LA", "SA")
]
if lmax - lmin <= abs(lmax + lmin) * tol / 2:
raise ValueError(
"The Hamiltonian has a single eigenvalue, it is not possible to "
"obtain a spectral density."
)
if lower_bounds is not None:
lmin = min(lmin, lower_bounds[0])
lmax = max(lmax, lower_bounds[1])
a = np.abs(lmax - lmin) / (2.0 - eps)
b = (lmax + lmin) / 2.0
if sparse.issparse(hamiltonian):
identity = sparse.csr_array(sparse.identity(hamiltonian.shape[0], format="csr"))
rescaled_h = (hamiltonian - b * identity) / a
elif isinstance(hamiltonian, np.ndarray):
rescaled_h = (hamiltonian - b * np.eye(hamiltonian.shape[0])) / a
else:
raise TypeError("hamiltonian must be a numpy array or a sparse matrix")
return rescaled_h, (a, b)
def jackson_kernel(N: int) -> np.ndarray:
"""Coefficients of the Jackson kernel of length n.
Taken from Eq. (71) of `Rev. Mod. Phys., Vol. 78, No. 1 (2006)
<https://arxiv.org/abs/cond-mat/0504627>`_.
"""
m = np.arange(N) / (N + 1)
denom = (N + 1) * np.tan(np.pi / (N + 1))
return (1 - m) * np.cos(m * np.pi) + np.sin(m * np.pi) / denom