Spectral Methods: Singular Potentials#
To obtain this order of accuracy, we must be free to choose both the weights and the points. If we want \(x_0\) and/or \(x_{N-1}\) as the endpoints of the integration interval, then we must sacrifice one or two orders:
Gauss-Radau rules or Radau quadrature fixes one endpoint and is exact up to order \(2N-2\).
Gauss-Lobatto rules or Lobatto quadrature fixes both endpoints and is exact up to order \(2N-3\).
On the surface, this sees to be a problem for constructing the DVR basis, but as pointed out in [Schneider and Nygaard, 2004], the Radau quadrature based on the Laguerre polynomials is find for solving the radial equation if we exclude the first abscissa \(x_0 = 0\) where \(u(0) = 0\). Thus, we can use an \(N+1\)-point Radau quadrature, exact for polynomials of order \(2N\), and take the \(N\) positive abscissa and weights to construct the DVR basis.
Laguerre-Radau Quadrature#
The generalized Laguerre polynomials \(L_{N}^{(\alpha)}\) are orthogonal on \([0, \infty)\) with weight \(w(x) = x^{\alpha}e^{-x}\). The Radau quadrature abscissa \(x_i\) include \(x_0=0\) and the roots of \(L_{N}^{(\alpha+1)}(x)\), with quadrature weights \(w_i\) [Gautschi, 2000]:
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[2], line 8
4
5 import numpy as np
6 from scipy.integrate import quad
7
----> 8 from phys_581.dvr import roots_genlaguerre_radau, LaguerreRadauDVR
9
10 dvr = LaguerreRadauDVR(N=10)
11 ns = range(dvr.N+1)
ModuleNotFoundError: No module named 'phys_581'
Kinetic Energy#
To compute the kinetic energy we note that the basis functions are a sum of terms of the form
Thus, we can compute the kinetic energy matrix using the quadrature forumlae:
Detailed expressions for \(N=1\) (used to check code).
Getting everything working here is quite challenging as there are lots of places for making mistakes. Ultimately we should have a comprehensive set of tests, but initially it is helpful to work with a small basis. Working through these and explicitly checking the code helped me find several subtle issues. We take \(N=1\) with two abscissa.
With \(\alpha = 0\), we have explicitly:
The abscissa are \(r_0 = 0\) and \(r_1 = 2\), the latter being the roots of \(L_{N}^{(\alpha+1)}(r)\).
For \(\alpha=1\) we have
Thus, despite the fact that the \(\alpha = 1\) basis has \(L_{n}^{(1)}\) basis polynomials, the factor of \(\sqrt{r}\) ruins the convergence.
Note that for \(m = \alpha = \hbar = 1\), the first few state for Hydrogen are
One final comment: We have expressed our basis in some sort of “natural” units where \(r\) is dimensionless. This is no good for physics. Instead, we must choose a length scale \(a\) in which to express our problem. Once we choose, this scale, the basis can be used by scaling \(r\rightarrow r/a\) to be dimensionless, and then adding the missing factors \(\mat{K}/a^2\) to the kinetic energy. To choose \(a\), note that the quadrature is exact for radial wavefunctions of the form (setting \(\alpha = 0\) here)
This might indicate that an optimal choice is \(a \propto n \propto 1/\sqrt{-E}\) for Coulomb-like potentials. We will explore how well this works below, but note that choosing a slightly incorrect \(a\) still works well because
Thus, the errors induce polynomial factors that are well represented in the basis.
Example: Coulomb-like Potentials.#
Here we use the Laguerre-Radau approach described above to solve for states in a Coulomb-like potential.
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[3], line 15
11 ns = 1+np.arange(Nstates)
12 E_exact = -1/2/ns**2
13
14 Ns = np.arange(Nstates, Nmax+1, 4)
---> 15 errs = [get_Es(N, a=a)[:Nstates]/E_exact - 1 for N in Ns]
16
17 fig, ax = plt.subplots()
18 ax.semilogy(Ns, np.abs(errs), '-+')
Cell In[3], line 2, in get_Es(N, a)
1 def get_Es(N=32, a=1.0):
----> 2 dvr = LaguerreRadauDVR(N=N, alpha=0)
3 H = dvr.T[1:, 1:] + np.diag(-a/dvr.r[1:])
4 En = np.linalg.eigvalsh(H)/a**2
5 return En
NameError: name 'LaguerreRadauDVR' is not defined
Here we see the convergence of the lowest 15 states as a function of basis size \(N\). Around \(N \approx 180\), numerical errors in the computation start to corrupt the basis, leading to zero weights (see warnings), overflows, etc. While there might be ways to mitigate this (we have included one in the code, using exact integer math for some of the coefficients), it is probably not worth pursuing this direction.
Instead, it us much more profitable to explore adjusting the length scale \(a\).
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[4], line 8
4 ns = 1+np.arange(Nstates)
5 E_exact = -1/2/ns**2
6
7 Ns = np.arange(Nstates, Nmax+1, 4)
----> 8 errs = [get_Es(N, a=a)[:Nstates]/E_exact - 1 for N in Ns]
9
10 fig, ax = plt.subplots()
11 ax.semilogy(Ns, np.abs(errs), '-+')
Cell In[3], line 2, in get_Es(N, a)
1 def get_Es(N=32, a=1.0):
----> 2 dvr = LaguerreRadauDVR(N=N, alpha=0)
3 H = dvr.T[1:, 1:] + np.diag(-a/dvr.r[1:])
4 En = np.linalg.eigvalsh(H)/a**2
5 return En
NameError: name 'LaguerreRadauDVR' is not defined
Here we will present without proof a spectral method based on the Bessel-function discrete variable representation (DVR). Here we introduce a basis \(\ket{F_{\nu,n}}\) obtained by projecting onto a space with wave-vectors less than \(\abs{k} < k_\max\):
In this basis, the following representation for the operator \(\op{K}\) is exact:
where \(z_{\nu, n}\) are the roots of the Bessel functions of the first kind.
Furthermore, the basis is quasi-local, so that the potential operator can be expressed as a diagonal matrix
This is not exact, but provides exponential accuracy for analytic potentials.
Basis functions
We will not need them for our work here, but for reference, the basis functions are:
Note
One must be careful evaluating these near the roots where the denominator vanishes. In our code, we do this with a careful application of Taylor series and L’Hopital’s rule.
Note that these are zero at all abscissa \(r_{\nu, m}\) except for \(m = n\):
This is a key property of DVR bases which are closely related to the classical orthogonal polynomials where a careful choice of both abscissa and weights leads to twice the expected accuracy when integrating. It also allows us to express a function in the basis by simply computing its value at the abscissa:
Finally, we note that from these properties, the coefficients \(w_n\) act as integration weights for functions expressed in the basis:
Demonstration#
As a quick demonstration, we find the eigenstates of the spherical harmonic oscillator, and the eigenstates of hydrogenic atoms:
The numerical value is given for hydrogen where \(\alpha = e^2/4\pi\epsilon_0 \approx 14.4\)eV Å.
from phys_581 import bessel, dvr
N = 10
d = 3
hbar = m = w = 1.0
a_ho = hbar / np.sqrt(m*w)
R = N*a_ho
k_max = N/a_ho
def V(r):
return m * (w*r)**2 / 2
def get_E(l, N=N):
n = np.arange(N)
return hbar * w * (2*n + l + d/2)
for d in [2, 3, 4]:
basis = dvr.SphericalDVRBasis(R=R, d=d, k_max=k_max)
for l in [0, 1, 2, 3, 4]:
r = basis.get_rn(l=l)
H = hbar**2 / 2/ m * basis.get_K(l) + np.diag(V(r))
assert np.allclose(np.linalg.eigvalsh(H)[:N], get_E(l=l))
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[5], line 1
----> 1 from phys_581 import bessel, dvr
2
3 N = 10
4 d = 3
ModuleNotFoundError: No module named 'phys_581'
from phys_581 import bessel, dvr
N = 5
d = 3
hbar = m = e = alpha = 1.0
a_h = hbar**2 / m / alpha
R = 10*N*a_h
k_max = 10*N/a_h
def V(r):
return -alpha / r
def get_E(l, N=N):
n = np.arange(N)
return -m * alpha**2 / 2 / hbar**2 / (1 + n + l)**2
basis = dvr.SphericalDVRBasis(R=R, d=d, k_max=k_max)
for l in [0, 1, 2, 3, 4]:
r = basis.get_rn(l=l)
H = hbar**2 / 2/ m * basis.get_K(l) + np.diag(V(r))
print(d, l)
print(np.linalg.eigvalsh(H)[:N])
print(get_E(l=l))
#assert np.allclose(np.linalg.eigvalsh(H)[:N], get_E(l=l))
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[6], line 1
----> 1 from phys_581 import bessel, dvr
2
3 N = 5
4 d = 3
ModuleNotFoundError: No module named 'phys_581'
k = 2*np.pi * np.fft.fftfreq(N, dx)
K = (hbar*k)**2/2/m
A = np.fft.ifft(K*np.fft.fft(np.eye(N), axis=1), axis=1)
V = np.diag(Vx)
C = None
E, psi = sp.linalg.eig(A+V, C)
inds = np.argsort(abs(E))
E, psi = E[inds], psi[:, inds]
E[:10].real
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[7], line 1
----> 1 k = 2*np.pi * np.fft.fftfreq(N, dx)
2 K = (hbar*k)**2/2/m
3 A = np.fft.ifft(K*np.fft.fft(np.eye(N), axis=1), axis=1)
4 V = np.diag(Vx)
NameError: name 'N' is not defined
See Also#
k = 2*np.pi * np.fft.fftfreq(N, dx)
K = (hbar*k)**2/2/m
A = np.fft.ifft(K*np.fft.fft(np.eye(N), axis=1), axis=1)
V = np.diag(Vx)
C = None
E, psi = sp.linalg.eig(A+V, C)
inds = np.argsort(abs(E))
E, psi = E[inds], psi[:, inds]
E[:10].real
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[8], line 1
----> 1 k = 2*np.pi * np.fft.fftfreq(N, dx)
2 K = (hbar*k)**2/2/m
3 A = np.fft.ifft(K*np.fft.fft(np.eye(N), axis=1), axis=1)
4 V = np.diag(Vx)
NameError: name 'N' is not defined