codex/past-exam-of-the-physics-a-course-of-the-university-of-cambridge/2022/paper-1-a1-wavefunctions.py

past-exam-of-the-physics-a-course-of-the-university-of-cambridge/2022/paper-1-a1-wavefunctions.py
"""Original illustrative eigenstates for 2022 Physics A (1), A1.

Run from the desired output directory; writes its PNG basename there.
Dependencies: numpy and matplotlib, declared in the repository pyproject.toml.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
The dimensionless potential is illustrative, not a digitization of the exam.
"""
from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


def main():
    count = 599
    grid = np.linspace(0, 1, count + 2)
    x = grid[1:-1]
    dx = grid[1] - grid[0]
    kinetic = 0.002
    potential = np.where(x < 0.4, 0, np.where(x < 0.6, 0.82, 0.62))
    hopping = np.full(count - 1, -kinetic / dx**2)
    matrix = (np.diag(2 * kinetic / dx**2 + potential)
              + np.diag(hopping, 1) + np.diag(hopping, -1))
    energies, eigenvectors = np.linalg.eigh(matrix)
    assert energies[0] < energies[1] < 0.62 < energies[2] < 0.82
    fig, axes = plt.subplots(3, 1, figsize=(7.2, 6.2), sharex=True)
    for n, ax in enumerate(axes):
        wave = np.concatenate(([0.0], eigenvectors[:, n], [0.0]))
        wave *= np.sign(wave[1]) / np.max(np.abs(wave))
        changes = np.flatnonzero(wave[1:-2] * wave[2:-1] < 0) + 1
        assert len(changes) == n
        roots = grid[changes] - wave[changes] * dx / (wave[changes + 1] - wave[changes])
        ax.axvspan(0.4, 0.6, color="#d9dce2", alpha=0.8)
        if n < 2:
            ax.axvspan(0.6, 1, color="#edf0f5", alpha=0.95)
        for step in (0.4, 0.6):
            ax.axvline(step, color="#8a8d92", linestyle=":", linewidth=1)
        ax.axhline(0, color="#777777", linewidth=0.65)
        ax.plot(grid, wave, color=["#176ca4", "#ba5420", "#7553a8"][n], linewidth=2)
        ax.scatter(roots, np.zeros(n), s=28, color="#333333", zorder=5)
        ax.set_ylim(-1.17, 1.38)
        ax.set_xlim(0, 1)
        ax.set_yticks([-1, 0, 1])
        ax.set_ylabel(r"$\psi_n/\max|\psi_n|$")
        ax.text(0.02, 0.87, f"n = {n}: {n} interior node" + ("" if n == 1 else "s"),
                transform=ax.transAxes, fontsize=10)
        ax.text(0.98, 0.87, f"E = {energies[n]:.3f}", ha="right", transform=ax.transAxes, fontsize=10)
        ax.spines[["top", "right"]].set_visible(False)
    axes[0].set_title("Illustrative wavefunctions with the required energy ordering", fontsize=12, pad=15)
    axes[-1].set_xlabel(r"Position $x/D$   (finite steps at 0.4 and 0.6)")
    fig.text(0.52, 0.015, "Shading: classically forbidden regions. Dots: interior nodes. Arbitrary units.",
             ha="center", fontsize=9, color="#444444")
    fig.tight_layout(rect=(0, 0.04, 1, 1), h_pad=1.0)
    fig.savefig(Path(__file__).with_suffix(".png").name, dpi=145, facecolor="white")
    print("Lowest energies:", energies[:3])
    plt.close(fig)


if __name__ == "__main__":
    main()