codexpast-exam-of-the-quantum-matter-course-of-the-university-of-oxfordoriginal2025-interacting-correlations.py
past-exam-of-the-quantum-matter-course-of-the-university-of-oxford/original2025-interacting-correlations.py
"""Python 3.14, NumPy and Matplotlib; write same-basename PNG to Path.cwd()."""
from pathlib import Path
import os
os.environ.setdefault('MPLCONFIGDIR', '/tmp/oxford-2025-quantum-matter-mpl')
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
def radial_gp(coupling=100 * np.pi, dr=.0125, rmax=9.):
"""Cell-centred radial finite volumes; hbar=m=omega=1, integral |phi|^2=1."""
r = (np.arange(round(rmax / dr)) + .5) * dr
weights = 2 * np.pi * r * dr
lower = -.5 * (r - dr / 2) / (r * dr ** 2)
upper = -.5 * (r + dr / 2) / (r * dr ** 2)
diag = -lower - upper
potential = r ** 2 / 2
phi = np.sqrt(np.maximum(np.sqrt(coupling / np.pi) - potential, 1e-8) / coupling)
phi /= np.sqrt(np.dot(weights, phi ** 2))
dt = .025
for iteration in range(8000):
b = 1 + dt * (diag + potential + coupling * phi ** 2)
rhs = phi.copy()
for i in range(1, len(r)):
factor = dt * lower[i] / b[i - 1]
b[i] -= factor * dt * upper[i - 1]
rhs[i] -= factor * rhs[i - 1]
new = np.empty_like(phi)
new[-1] = rhs[-1] / b[-1]
for i in range(len(r) - 2, -1, -1):
new[i] = (rhs[i] - dt * upper[i] * new[i + 1]) / b[i]
new /= np.sqrt(np.dot(weights, new ** 2))
change = np.sqrt(np.dot(weights, (new - phi) ** 2))
phi = new
if change < 2e-12:
break
hphi = (diag + potential + coupling * phi ** 2) * phi
hphi[1:] += lower[1:] * phi[:-1]
hphi[:-1] += upper[:-1] * phi[1:]
mu = np.dot(weights, phi * hphi)
residual = np.sqrt(np.dot(weights, (hphi - mu * phi) ** 2))
assert residual < 1e-7, (iteration, change, residual)
return r, phi, {'mu': float(mu), 'residual': float(residual), 'iterations': iteration + 1,
'normalization': float(np.dot(weights, phi ** 2)), 'dr': dr}
def main():
out = Path.cwd()
coupling = 100 * np.pi
r, phi, info = radial_gp(coupling)
# Quadratic extrapolation in r at the regular origin.
phi0 = (9 * phi[0] - phi[1]) / 8
mu_tf = np.sqrt(coupling / np.pi)
radius = np.sqrt(2 * mu_tf)
s = np.concatenate(([0.], r / radius))
gp = coupling / mu_tf * phi0 * np.concatenate(([phi0], phi))
tf = np.sqrt(np.maximum(1 - s ** 2, 0))
fig, axes = plt.subplots(1, 2, figsize=(9.3, 3.7), layout='constrained')
axes[0].plot(s, tf, color='#e07a24', ls='--', lw=2, label='Thomas–Fermi')
axes[0].plot(s, gp, color='#126782', lw=2, label='Full radial GP')
axes[0].axvline(1, color='.7', ls=':', lw=1)
axes[0].set(xlim=(0, 1.4), ylim=(0, 1.1), xlabel=r'$r/R_{\rm TF}$',
ylabel=r'$G(r)/(\mu_{\rm TF}/g)$', title='Kinetic energy smooths the edge')
axes[0].legend(frameon=False, fontsize=9)
broad = gp / gp[0]
# These illustrative weights are not a Bogoliubov calculation.
depletion_weight = .12
short = depletion_weight * np.exp(-.5 * (s / .065) ** 2)
sketch = (1 - depletion_weight) * broad + short
axes[1].plot(s, broad, color='#126782', lw=2, label='GP, normalized at centre')
axes[1].plot(s, sketch, color='#a62f5b', lw=2, label='With depletion (schematic)')
axes[1].plot(s, short, color='.45', ls=':', lw=1.5, label='Short-range contribution')
axes[1].set(xlim=(0, 1.4), ylim=(0, 1.1), xlabel=r'$r/R_{\rm TF}$',
ylabel=r'$G(r)/G(0)$', title='Depletion changes mode occupations')
axes[1].legend(frameon=False, fontsize=8, loc='upper right')
for ax in axes:
ax.spines[['top', 'right']].set_visible(False)
fig.savefig(out / (Path(__file__).stem + '.png'), dpi=150, facecolor='white', transparent=False)
plt.close(fig)
print(info)
if __name__ == '__main__':
main()