This cell adds /home/docs/checkouts/readthedocs.org/user_builds/gpe/checkouts/latest/src to your path, and contains some definitions for equations and some CSS for styling the notebook. If things look a bit strange, please try the following:
- Choose "Trust Notebook" from the "File" menu.
- Re-execute this cell.
- Reload the notebook.
%pylab is deprecated, use %matplotlib inline and import the required libraries.
Populating the interactive namespace from numpy and matplotlib
DVR (from mmfutils)#
Here we discuss some of the bases provided in mmfutils.math.bases for representing
problems with rotational symmetry such a cylindrical and spherical symmetry. We discuss
a general set of Bessel-function DVR bases for representing the radial wavefunction of
“spherically” symmetric problems in \(d\) spatial dimensions (\(d=2\) for cylindrical
symmetry and \(d=3\) for spherical symmetry), as well as a simplified basis for
spherically symmetric problems where we represent the radial wavefunction on a periodic
lattice. This latter basis allows us to perform operations such as convolution.
Here is a quick demonstration, using these bases to solve for the eigenstates of the spherical harmonic oscillator:
Spherical Basis#
from mmfutils.math import bases
eps = np.finfo(float).eps
hbar = m = w = 1
a0 = np.sqrt(hbar / m / w)
R = np.sqrt(-2 * a0**2 * np.log(eps))
k_max = np.sqrt(-np.log(eps) / a0**2)
N = int(np.ceil(k_max * 2 * R / np.pi))
d = 3
basis = bases.SphericalBasis(N=N, R=R)
r = basis.xyz[0]
psi0 = np.exp(-((r / a0) ** 2) / 2)
ax = plt.subplot(111)
ax.plot(r, basis.laplacian(psi0))
ax.plot(r, (r**2 - a0**2 * d) / a0**4 * psi0, "+")
ax.set(xlabel="r", ylabel=r"$\psi_0(r)$");
[I 03:36:27 root] Patching zope.interface.document.asReStructuredText to format code
[I 03:36:27 numexpr.utils] NumExpr defaulting to 2 threads.
Background#
Here are the properties of the basis, including abscissa, basis functions, integration weights, and some quantities that appear in the DVR literature. The general idea of a DVR basis is to introduce a set of basis functions \(F_n(x) = \braket{x|F_n}\) and an associated set of abscissa \(x_n\) such that:
These bases are generalized version of the Dirac delta function \(\delta(x-x_n) = \braket{x|x_n}\) restricted to a limited set of momenta with maximum momentum \(k\):
A challenge is to choose a consistent set of abscissa so that these functions are orthogonal:
The actual basis functions differ in their normalization so that they are orthonormal:
The normalization factors \(\lambda_n\) act as a set of integration weights that are exact for all functions \(g(x) = F_{i}^*(x) F_{j}(x)\) expressed as products of basis elements:
These weights can thus be used to effectively compute quantities such as the total particle number by integrating \(n(x) = \psi^*(x)\psi(x)\).
This condition ensures that if the wavefunction \(\psi(r)\) mostly lies in the subspace spanned by the basis, it can be expanded by simply evaluating it at the abscissa:
The coefficients can thus be computed by simply evaluating the wavefunction at the abscissa:
The key utility of these bases is that one can obtain exponentially accurate representations if with a tabulated kinetic energy operator, while simply evaluating the potential at the abscissa:
In general, choosing a consistent set of basis functions and abscissa is a challenge, and most (all?) known examples are based on some form of orthogonal polynomial.
Sinc-Function Basis#
Here we demonstrate these properties with the sinc-function basis with equally spaced abscissa \(x_n\):
Spherical Symmetry#
Recall that in \(d\)-dimensions, the Laplacian is:
where \(\Delta_{S^{d-1}}\) is the Laplacian suitably restricted to the sphere \(S^{d-1}\) (i.e. the Laplace–Beltrami operator. Expressing the wavefunction in terms of “spherical” harmonics \(Y_l^m(\uvect{x})\), one has:
The wavefunction can thus be expressed in terms of the following basis:
such that the radial wavefunction \(u(r)\) satisfies:
The Bessel-function DVR basis is chosen to provide an exponentially accurate representation of the radial portion of the wavefunction, including the singular centrifugal piece. Thus, the DVR basis functions are orthogonal under the metric:
which does not include the angular or \(r^{d-1}\) factors. The advantage of this is exponential accuracy for analytic potentials, but each \(\nu_{d,l}\) has a different set of abscissa which is inconvenient. We have found that fairly good accuracy can be achieved using only \(l=0\) and \(l=1\) bases, shifting the residual portion of the \(r^{-2}\) term into the potential, but this needs to be carefully checked. (For example, with \(d=3\), the \(l=0\) basis works well for even \(l\) while the \(l=1\) basis must be used for odd \(l\).)
Cylindrical Basis#
To represent problems with cylindrical coordinates we use a periodic basis for \(x\) and a Bessel-function DVR basis for the radial coordinate. Here we describe the properties of the Bessel-function basis for the radial direction. These are expressed in terms of the Bessel functions (with \(n \equiv \alpha \equiv \nu\)):
which satisfy the following useful relationships:
import numpy as np
from scipy.integrate import quad
from mmfutils.math import bessel
alpha = 1.1
beta = 1.2
x = 1.5
J_alpha = bessel.J(alpha, 0)
J_beta = bessel.J(beta, 0)
def J(x, nu):
"""Integral definition of J_n(x)."""
def integrand1(tau):
return np.cos(nu * tau - x * np.sin(tau)) / np.pi
def integrand2(t):
return np.sin(nu * np.pi) * np.exp(-x * np.sinh(t) - nu * t) / np.pi
res1, err1 = quad(integrand1, 0, np.pi)
with np.errstate(over="ignore"):
# Ignore harmless overflow errors
res2, err2 = quad(integrand2, 0, np.inf)
return res1 - res2, np.sqrt(err1**2 + err2**2)
assert np.allclose(J(x, alpha)[0], J_alpha(x))
def i2(x):
return J_alpha(x) * J_beta(x) / x
res, err = quad(i2, 0, np.inf, epsabs=0.001)
exact = 2 / np.pi * np.sin(np.pi / 2 * (alpha - beta)) / (alpha**2 - beta**2)
assert np.allclose(exact, res, atol=err)
%pylab inline --no-import-all
plt.figure(figsize=(12, 3))
z = np.linspace(1e-12, 10 * np.pi, 100)
for nu in [0, 0.5, 1, 1.5, 2, 2.5]:
(l,) = plt.plot(z / np.pi, bessel.J(nu, d=0)(z), label=r"$\nu={}$".format(nu))
for z0 in bessel.j_root(nu, 10):
if z0 <= z.max():
plt.axvline(z0 / np.pi, c=l.get_c(), ls="-", alpha=0.5)
plt.grid(True)
plt.legend()
plt.xlabel(r"$z/\pi$")
plt.ylabel(r"$J_0(z)$")
%pylab is deprecated, use %matplotlib inline and import the required libraries.
Populating the interactive namespace from numpy and matplotlib
Text(0, 0.5, '$J_0(z)$')
There is a separate basis for each value of \(\nu\) which handles a different angular-momentum singularity. Once this is fixed, the basis functions are:
For use with wavefunctions, we must include the transformation to and from
from mmfutils.math.bases import CylindricalBasis
basis = CylindricalBasis(Nxr=(2, 10), Lxr=(1.0, 1.0))
x, r = basis.xyz
k_x, k_r = basis.k_max
Line-of-Sight Integrals#
For visualization, it is often desired to express a density in terms of the line-of-sight integrals, for example, when imaging a cylindrically symmetric atomic cloud. The formal definitions are:
The first of these is easy to implement because of the properties of the basis. Recall that if \(f(r)\) and \(g(r)\) are well represented in the basis, that the integral \(\int_0^{\infty}\d{r}\; f^*(r)g(r) = \sum_n\lambda_n f^*(r_n)g(r_n)\) is accurate. This means that if the wavefunction
is well represented, then the integral
is accurate. This is computed by integrate1 in the code.
The second implements an Abel Transform, which is more complicated. If high accuracy is needed, then we can do the following integrals manually, and use the resulting matrix to compute the transform, but this is very slow.
Instead, we simply use the form:
We use the basis to extrapolate the wavefunction to the desired abscissa, and then use a trapazoidal rule along \(z\) to compute the integral. This can be broadcast to perform reasonably efficiently.
For testing, consider a Gaussian:#
Another test:
%pylab inline --no-import-all
import numpy as np
from mmfutils.math.bases import CylindricalBasis
from mmfutils.math.bases.tests import test_bases
basis = CylindricalBasis(Nxr=(64, 32), Lxr=(25.0, 5.0))
x, r = basis.xyz
Ny = 50
ys = np.linspace(0, r.max(), Ny)[None, :]
r0 = 1.2
n = np.exp(-(x**2 + r**2) / r0**2)
n_1D = np.pi * r0**2 * np.exp(-(x**2) / r0**2)
n_2D = np.sqrt(np.pi) * r0 * np.exp(-(x**2 + ys**2) / r0**2)
print(
"{}% max error".format(100 * abs(basis.integrate2(n, y=ys) - n_2D).max() / n_2D.max())
)
n = r**2 * np.exp(-(x**2 + r**2) / r0**2)
n_1D = np.pi * r0**4 * np.exp(-(x**2) / r0**2)
n_2D = np.sqrt(np.pi) * r0 / 2 * (r0**2 + 2 * ys**2) * np.exp(-(x**2 + ys**2) / r0**2)
print(
"{}% max error".format(100 * abs(basis.integrate2(n, y=ys) - n_2D).max() / n_2D.max())
)
%pylab is deprecated, use %matplotlib inline and import the required libraries.
Populating the interactive namespace from numpy and matplotlib
---------------------------------------------------------------------------
ModuleNotFoundError Traceback (most recent call last)
Cell In[6], line 4
1 get_ipython().run_line_magic('pylab', 'inline --no-import-all')
2 import numpy as np
3 from mmfutils.math.bases import CylindricalBasis
----> 4 from mmfutils.math.bases.tests import test_bases
5
6 basis = CylindricalBasis(Nxr=(64, 32), Lxr=(25.0, 5.0))
7 x, r = basis.xyz
ModuleNotFoundError: No module named 'mmfutils.math.bases.tests'
Harmonic Oscillator#
Here we check the basis for a 2D Harmonic oscillator. The exact results are:
The ground state wave-function is:
from mmfutils.math.bases import CylindricalBasis
eps = np.finfo(float).eps
hbar = m = w = 1
a0 = np.sqrt(hbar / m / w)
R = np.sqrt(-2 * a0**2 * np.log(eps))
k_max = np.sqrt(-np.log(eps) / a0**2)
Nr = int(np.ceil(k_max * 2 * R / np.pi))
basis = CylindricalBasis(Nxr=(1, Nr), Lxr=(1.0, R))
def get_V(r):
return m * w**2 * r**2 / 2
Vs = []
Ks = []
Hs = []
Es = []
for l in range(4):
r = basis._r(Nr, l=l)
V = get_V(r)
K = basis._get_K(l=l)[0] # Without factors of sqrt(r)
H = K / 2 + np.diag(V)
assert np.allclose(H, H.T.conj())
E = np.linalg.eigvalsh(H)
Vs.append(V)
Ks.append(K)
Hs.append(H)
Es.append(E)
Es_ = sorted(sum((E.tolist() for E in Es), []))
l = 3
l0 = 1
nu0 = basis.nu(l=l0)
nu = basis.nu(l=l)
r = basis._r(Nr, l=l0)
K = basis._get_K(l=l0)[0] # Without factors of sqrt(r)
V = get_V(r) + (nu**2 - nu0**2) / r**2 * hbar**2 / 2 / m
H = K / 2 + np.diag(V)
assert np.allclose(H, H.T.conj())
E = np.linalg.eigvalsh(H)
E
array([ 4. , 6. , 8. , 10. , 12. ,
14. , 16. , 18. , 20. , 22.00000001,
24.0000003 , 26.00000489, 28.00006078, 30.00057424, 32.00407358,
34.02130778, 36.08128298, 38.23022802, 40.50974723, 42.94011599,
45.52473409, 48.26031172, 51.14238164, 54.16702739, 57.33112378,
60.63269981, 64.072669 , 67.68788588, 71.56379868, 73.68477758,
76.84257618, 83.21411642, 93.85473386])
Spherical Basis#
For spherically symmetric problems, one solution is to use a Bessel function DVR basis.
Here we check the basis for a 3D Harmonic oscillator. The exact results are:
The ground state wave-function is:
from mmfutils.math.bases import SphericalBasis
eps = np.finfo(float).eps
hbar = m = w = 1
a0 = np.sqrt(hbar / m / w)
R = np.sqrt(-2 * a0**2 * np.log(eps))
k_max = np.sqrt(-np.log(eps) / a0**2)
Nr = int(np.ceil(k_max * 2 * R / np.pi))
basis = SphericalBasis(N=Nr, R=R)
def get_V(r):
return m * w**2 * r**2 / 2
Vs = []
Ks = []
Hs = []
Es = []
for l in range(4):
r = basis.xyz[0]
V = get_V(r)
K = basis._get_K(l=l)[0] # Without factors of sqrt(r)
H = K / 2 + np.diag(V)
assert np.allclose(H, H.T.conj())
E = np.linalg.eigvalsh(H)
Vs.append(V)
Ks.append(K)
Hs.append(H)
Es.append(E)
Es_ = sorted(sum((E.tolist() for E in Es), []))
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[10], line 12
8 Es = []
9 for l in range(4):
10 r = basis.xyz[0]
11 V = get_V(r)
---> 12 K = basis._get_K(l=l)[0] # Without factors of sqrt(r)
13 H = K / 2 + np.diag(V)
14 assert np.allclose(H, H.T.conj())
15 E = np.linalg.eigvalsh(H)
AttributeError: 'SphericalBasis' object has no attribute '_get_K'
Another possibility is to use a periodic 1D basis of odd functions. This follows from the radial equations:
Hence, we can work with the radial Schrödinger equation for \(u(r)\) instead (but still use the proper functions to compute the non-linear pieces).
Fourier Transforms#
The Fourier transform simplifies by noting that \(\tilde{n}(\vect{k}) = \tilde{n}(k)\) depends only on the magnitude of \(\vect{k}\) so we can take \(\vect{k} = (0,0,k)\):
Hence, we can use the standard 1D Fourier transform of \(rn(\abs{r})\). The only subtlety is at \(k=0\) where we can use:
The inverse is similar with some factors of \(2\pi\):
As an example, we use the proton form factor with \(r_0^{-2} = k_0^2 = 0.71\)GeV\(^2\):
import numpy as np
r_0 = 1.0
k_0 = 1.0 / r_0
N = 64
L = 10.0
# L = 10.0
dx = L / N
dk = 2 * np.pi / L
######## To Do: Make work with symmetric lattice!
symmetric = True # If True, then use a symmetric grid with no point at x=0
symmetric = False
x = np.arange(N) * dx - L / 2 + (dx / 2 if symmetric else 0)
k = 2 * np.pi * np.fft.fftfreq(N, dx)
r = abs(x)
G_r = np.exp(-r / r_0) / 8 / np.pi * r_0**3
G_k = 1.0 / (1 + k**2 / k_0**2) ** 2
def sft(n, dx=dx):
"""Spherical Fourier transform"""
if symmetric:
assert np.allclose(n, n[::-1])
else:
assert np.allclose(n[1:], n[1:][::-1])
N = len(n)
L = dx * N
x = np.arange(N) * dx - L / 2.0 + (dx / 2 if symmetric else 0)
k = 2 * np.pi * np.fft.fftfreq(N, dx)
_ft = np.fft.fft(np.fft.fftshift(x * n))
# assert np.allclose(_ft.real, 0)
return np.ma.divide(-2 * np.pi * _ft.imag * dx, k).filled(
(2 * np.pi * x**2 * n * dx).sum()
)
def isft(n, dx=dx):
"""Spherical Inverse Fourier transform"""
N = len(n)
L = dx * N
dk = 2 * np.pi / L
x = np.arange(N) * dx - L / 2.0 + (dx / 2 if symmetric else 0)
k = 2 * np.pi * np.fft.fftfreq(N, dx)
_ift = np.fft.fftshift(np.fft.ifft(k * n))
# assert np.allclose(_ft.real, 0)
return np.ma.divide(_ift.imag, 2 * np.pi * dx * x).filled(
(2 * np.pi * k**2 * n / (2 * np.pi) ** 3).sum() * dk
)
Coulomb Convolution#
One problem that arises in some nuclear physics work is to compute the Coulomb potential for a nucleus:
This has two complications: 1) the singularities and the 2) long-range nature of the interaction which can give rise to images in a periodic box. Our standard resolution is to use the truncated interaction and pad the array of charges so that the convolution with the truncated interaction does not pickup charges from the neighboring cells (which are now further away because of the padding).
def pad(f):
N = len(f)
F = np.zeros(2 * N, dtype=f.dtype)
F[N // 2 : N // 2 + N] = f
F[-N // 2] = f[0]
return F
def unpad(F):
N = len(F) // 2
return F[N // 2 : N // 2 + N]
import scipy.special
sp = scipy
eps = np.finfo(float).eps
a = 0.5
D = L
n_r = np.exp(-(r**2) / 2 / a**2)
V_r = a**3 / (r + eps) * np.sqrt(np.pi / 2) * sp.special.erf((r + eps) / a / np.sqrt(2))
_K = 2 * np.pi * np.fft.fftfreq(2 * N, dx)
K_D = np.ma.divide(1.0 - np.cos(_K * D), _K**2).filled(D**2 / 2.0)
res = unpad(isft(sft(pad(n_r)) * K_D))
assert np.allclose(res, V_r)
# Self-contained convolution. Reduces some intermediate steps.
def coulomb(n_r, dx=dx):
N = len(n_r)
L = N * dx
D = L
X = np.arange(2 * N) * dx - L
K = 2 * np.pi * np.fft.fftfreq(2 * N, dx)
K_D = np.ma.divide(1.0 - np.cos(K * D), K**2).filled(D**2 / 2.0)
n = pad(n_r)
_ft = np.fft.fft(np.fft.fftshift(X * n))
tmp = np.ma.divide(-_ft.imag, K).filled((X**2 * n).sum()) * K_D
_ift = np.fft.fftshift(np.fft.ifft(K * tmp))
dk = np.pi / L
res = np.ma.divide(_ift.imag, X).filled((K**2 * tmp / (2 * np.pi)).sum() * dk * dx)
return unpad(res)
assert np.allclose(coulomb(n_r), V_r)
# Now get rid of fftshift.
def coulomb(n_r, dx=dx):
N = len(n_r)
L = N * dx
D = L
dk = np.pi / L
X = np.arange(2 * N) * dx - L
K = 2 * np.pi * np.fft.fftfreq(2 * N, dx)
K_D = np.ma.divide(1.0 - np.cos(K * D), K**2).filled(D**2 / 2.0)
n = pad(n_r)
_ft = np.fft.fft(X * n)
tmp = np.ma.divide(-_ft.imag, K).filled((X**2 * n).sum()) * K_D
_ift = np.fft.ifft(-_ft.imag * K_D)
res = np.ma.divide(_ift.imag, X).filled(-(K**2 * tmp / (2 * np.pi)).sum() * dk * dx)
# Not sure I understand the - sign needed here...
return unpad(res)
assert np.allclose(coulomb(n_r), V_r)
Discrete Sine Transform#
We can improve performance here by using the discrete sine transform (DST). For best performance, one should use the DST-II or RODFT10 version which computes:
In physical notation, we have abscissa \(x = x_n = (n+1) R/N\) and \(k = k_n = \pi (n+1/2)/R = 2\pi (n+1/2)/L\) for \(n\in\{0,1,\cdots, N-1\}\). Note that here we have \(N\) points in the radial direction with radius \(R\), hence \(\d{x} = R/N\). The correspondance with the usual periodic box is \(L = 2R\).
From these and the expressions derived earlier, we can express the full 3D Fourier transforms:
Convolution (3D)#
Convolution of spherically symmetric functions proceeds as follows:
This is the form used in SphericalBasis.convolve().
%pylab inline --no-import-all
import scipy.fftpack
import scipy as sp
def dst(f):
return scipy.fftpack.dst(f, type=3)
def idst(F):
N = len(F)
return scipy.fftpack.dst(F, type=2) / (2.0 * N)
# Now work it for only positive abscissa
N = 32
R = 5.0
dx = R / N
r = (1.0 + np.arange(N)) * dx
rr = np.linspace(0, R, 100)
k = np.pi * (0.5 + np.arange(N)) / R
a = 0.5
n_r = np.exp(-(r**2) / 2 / a**2)
V_r = a**3 / r * np.sqrt(np.pi / 2) * sp.special.erf(r / a / np.sqrt(2))
f_r = r * n_r
f_rr = rr * np.exp(-(rr**2) / 2 / a**2)
df_r = (1.0 - (r / a) ** 2) * np.exp(-(r**2) / 2 / a**2)
df_rr = (1.0 - (rr / a) ** 2) * np.exp(-(rr**2) / 2 / a**2)
ddf_r = (r**2 - 3 * a**2) / a**4 * f_r
ddf_rr = (rr**2 - 3 * a**2) / a**4 * f_rr
assert np.allclose(idst(-(k**2) * dst(f_r)), ddf_r)
F_k = dst(f_r) / (2.0 * N)
assert np.allclose(
f_rr, 2 * (F_k[None, :] * np.sin(k[None, :] * rr[:, None])).sum(axis=-1)
)
%pylab is deprecated, use %matplotlib inline and import the required libraries.
Populating the interactive namespace from numpy and matplotlib
Finally, we compute the convolution required for the Coulomb potential. These relationships are much simpler:
Again, we do this in a padded box:
We use the following test functions (in \(d\)-dimensions):
As a test, the convolution of this Gaussian with itself in 3D is:
# Now work it for only positive abscissa
L = 2 * R
D = L
rN_r = np.concatenate([r * n_r, 0 * n_r])
K = np.pi * (0.5 + np.arange(2 * N)) / (2 * R)
K_D = np.ma.divide(1.0 - np.cos(K * D), K**2).filled(D**2 / 2.0)
kN_k = dst(rN_r)
V = idst(K_D * kN_k)[:N] / r
assert np.allclose(V, V_r)
Here we check that convolution preserves the norm. We use the proton form factor.
import scipy.fftpack
def dst(f):
return scipy.fftpack.dst(f, type=3)
def idst(F):
N = len(F)
return scipy.fftpack.dst(F, type=2) / (2.0 * N)
N = 32
R = 5.0
r0 = 0.5
k0 = 10.0
dx = R / N
r = (1.0 + np.arange(N)) * dx
k = np.pi * (0.5 + np.arange(N)) / R
metric = 4 * np.pi * r**2 * dx
n = np.exp(-((r / r0) ** 2) / 2) / (r0 * np.sqrt(2 * np.pi)) ** 3
assert np.allclose(1, (n * metric).sum())
Gk = 1.0 / (1 + k**2 / k0**2) ** 2
G = idst(Gk) / r
q = idst(Gk * dst(r * n)) / r
assert np.allclose(1, (q * metric).sum())
Convolution proceedes as follows (let \(q = \sqrt{r^2 + R^2 - 2rR\cos\theta}\) so that \(q\d{q} = -rR\d(\cos\theta)\)):
Bessel Function DVR#
To implement the spherical and cylindrical DVR bases, we need some numerical code for
evaluating Bessel functions. These are provided by the module mmfutils.math.bessel,
some of which we describe here.
J_sqrt_pole#
For evaluating the basis functions, we need the function \(F(z)\):
%pylab inline --no-import-all
from importlib import reload
from mmfutils.math import bessel
reload(bessel)
nu = 0.0
n = 0
zn = bessel.j_root(nu=nu, N=n + 1)[n]
zn = zn
F = bessel.J_sqrt_pole(nu=nu, zn=zn, d=0)
dF = bessel.J_sqrt_pole(nu=nu, zn=zn, d=1)
z = np.linspace(zn - 0.1, zn + 0.1, 1000)
plt.plot(z, F(z))
zn
%pylab is deprecated, use %matplotlib inline and import the required libraries.
Populating the interactive namespace from numpy and matplotlib
np.float64(2.404825557695773)
Interpolation#
Here is an example of using the DVR basis to interpolate from one radial basis to
another. Note that the basis is designed to represent the wavefunction, so we call the
interpolation function :meth:~mmfutils.math.bases.CylindricalBasis.Psi
from mmfutils.math.bases import CylindricalBasis
b1 = CylindricalBasis(Nxr=(1, 64), Lxr=(2.0, 1.369))
b2 = CylindricalBasis(Nxr=(1, 64), Lxr=(2.0, 1.0))
x1, r1 = b1.xr
x2, r2 = b2.xr
n1 = np.maximum(0*x1 + (0.25**2 - r1**2), 0)
n1 = 0*x1 + np.exp(-(r1/0.2)**2/2)
n2 = (b1.Psi(np.sqrt(n1), (x2, r2)))**2
plt.clf()
plt.plot(r1.ravel(), n1.ravel())
plt.plot(r2.ravel(), n2.ravel())
[<matplotlib.lines.Line2D at 0x7f3f2649f4d0>]
Transformations#
We allow for some transformations of our bases.
Translations#
We start with a wavefunction \(\psi(x, t) = f(x-vt)\) that has a constant shape moving to the right at speed \(v\). To simplify the description of this solution, we might want to work with coordinates \(X_v \equiv X = x - vt\) where the wavefunction has the simpler form
In otherwise, the wavefunction is the same, but the coordinates are different.
This is what we mean by formulating the problem in a frame moving with velocity \(v\) to the right. The moving wave is simpler in such a co-moving frame:
This transformation is effected by the active translation operator:
This follows from the expansion of the exponential which generates the Taylor expansion of \(\psi_v(x-vt, t)\) in terms of \(\lambda = vt\).
To derive the equations of motion for the wavefunction \(\psi_v\) in the moving frame, we apply the Hamiltonian:
Explicitly, for translations, we have \(\I\hbar\dot{\op{T}}_{vt} = v\op{T}_{vt}\op{p}_x\), so:
Boosting shifts the dispersion by \(-v\op{p}_x\) and moves the potential. This is a partial Galilean transform. To obtain a full Galilean transformation that restores the form of \(\op{H}\) one needs to include a phase redefinition as discussed on my website.
Rotations#
\(\newcommand{\vect}[1]{\vec{#1}}\)One can work analogously with rotations specified by a vector \(\vect{\theta}\) representing a rotation by angle \(\theta = \norm{\vect{\theta}}_2\) about the axis along \(\vect{\theta}\). In cartesian coordinates, the active transformation is effected through the following rotation matrix:
To check our sign, consider rotating about the \(z\) axis so that \(\vect{\theta} = \theta \uvect{z}\). As an active rotation, the rotation matrix should take the point \(\uvect{x} = (1, 0)\) to the point \((\cos\theta, \sin\theta) \approx (1, \theta) = \uvect{x} + \theta\uvect{y}\) for small \(\theta\). Now recall that \(\uvect{x}\times\uvect{y} = \uvect{z}\) and cyclic permutations, so \(\uvect{z}\times\uvect{x} = \uvect{y}\). Hence:
\(\newcommand{\vect}[1]{\vec{#1}}\)For a rotating frame, one has \(\vect{\theta} = \vect{\omega} t\) which we shall restrict here to \(\vect{\omega} = \omega \uvect{z}\). The equivalent transformation on a wavefunction is:
Explicitly for a rotating frame, \(\I\hbar\dot{\op{R}}_{\vect{\omega} t} = \op{R}_{\vect{\omega} t}\vect{\omega}\cdot\vect{\op{L}}\) so
Test#
As a simple test, we compute the laplacian in a stationary and a rotating frame:
from importlib import reload
from mmfutils.math.bases import bases
reload(bases)
from mmfutils.plot import imcontourf
N = 32 * 2
L = 14.0
eps = np.finfo(float).eps
b = bases.PeriodicBasis(Nxyz=(N, N), Lxyz=(L, L))
x, y = b.xyz
kx, ky = b._pxyz
f = (x + 1j * y) * np.exp(-(x**2) - y**2)
nabla_f = (4 * (x**2 + y**2) - 8) * f
Lz_f = f
ax = plt.subplot(211)
plt.semilogy(x.ravel(), abs(f)[:, N // 2])
ax.set(xlabel="x", ylim=(eps, 1))
ax = plt.subplot(212)
ft = np.fft.fftshift(b.fftn(f))
plt.semilogy(np.fft.fftshift(kx), (abs(ft) / abs(ft).max())[:, N // 2])
ax.set(xlabel="y", ylim=(eps, 1))
assert np.allclose(nabla_f, b.laplacian(f))
assert np.allclose(Lz_f, b.apply_Lz_hbar(f))
m = 1.1
hbar = 2.2
wz = 3.3
kwz2 = m * wz / hbar
factor = -(hbar**2) / 2 / m
assert np.allclose(
factor * nabla_f - wz * hbar * Lz_f, b.laplacian(f, factor=factor, kwz2=kwz2)
)
np.allclose(b.laplacian(f, factor=factor, kwz2=kwz2) / b.laplacian(f), factor)
False
plt.plot(
x.ravel(),
((factor * nabla_f - b.laplacian(f, factor=factor, kwz2=-1)) / Lz_f)[:, N // 2],
)
plt.ylim(-3, 3)
/tmp/ipykernel_4271/2635635764.py:3: RuntimeWarning: divide by zero encountered in divide
((factor * nabla_f - b.laplacian(f, factor=factor, kwz2=-1)) / Lz_f)[:, N // 2],
/home/docs/checkouts/readthedocs.org/user_builds/gpe/conda/latest/lib/python3.14/site-packages/matplotlib/cbook.py:1719: ComplexWarning: Casting complex values to real discards the imaginary part
return math.isfinite(val)
/home/docs/checkouts/readthedocs.org/user_builds/gpe/conda/latest/lib/python3.14/site-packages/matplotlib/cbook.py:1355: ComplexWarning: Casting complex values to real discards the imaginary part
return np.asarray(x, float)
(-3.0, 3.0)