codex/release-expansion-covariance-thermometry.py

release-expansion-covariance-thermometry.py
"""Synthetic release-expansion widths; no historical measurements.

Python 3.14; NumPy 2.3.5; Matplotlib 3.10.7. Run from _media.
Times are ms, positions mm, so mm/ms = m/s. No SciPy required.
"""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def model():
    t = np.linspace(0, 30, 13)
    variance = .36-.036*t+.0036*t*t
    y = variance+np.random.default_rng(27).normal(0, .012, len(t))
    full = np.linalg.lstsq(np.column_stack([np.ones_like(t), t, t*t]), y, rcond=None)[0]
    omitted = np.linalg.lstsq(np.column_stack([np.ones_like(t), t*t]), y, rcond=None)[0]
    return t, y, full, omitted


def main():
    plt.rcParams.update({"font.size": 11, "figure.facecolor": "white"})
    fig = plt.figure(figsize=(10, 5.4), layout="constrained")
    grid = fig.add_gridspec(2, 2, width_ratios=[1.25, 1], height_ratios=[.8, 2.3])
    ax = fig.add_subplot(grid[:, 0])
    note = fig.add_subplot(grid[0, 1])
    profiles = fig.add_subplot(grid[1, 1])
    fig.suptitle("Release-expansion thermometry: initial correlations matter", fontsize=15)
    t, y, full, omitted = model()
    fine = np.linspace(0, 30, 400)
    truth = .36-.036*fine+.0036*fine*fine
    ax.errorbar(t, y, yerr=.012, fmt="o", ms=4, color="#243f4e", label="Synthetic variance data")
    ax.plot(fine, truth, "--", color="#71985b", lw=2, label="Chosen true model")
    ax.plot(fine, full[0]+full[1]*fine+full[2]*fine*fine, color="#185fa5", lw=2, label="Full quadratic fit")
    ax.plot(fine, omitted[0]+omitted[1]*fine*fine, color="#b24b13", lw=2, label="Fit forced to zero covariance")
    ax.set(xlabel="Dark expansion time (ms)", ylabel=r"Position variance $\sigma_x^2$ (mm$^2$)", xlim=(-1, 31), ylim=(0, 2.7))
    ax.legend(fontsize=8.5, loc="upper left")
    ax.grid(alpha=.18)
    ax.text(.04, .44, r"$\sigma_x^2(t)=a+bt+ct^2$"+"\n"+r"$b=2\operatorname{Cov}(x_0,v_0)$", transform=ax.transAxes, fontsize=10)
    conv = 3.82e-26/1.380649e-23*1e6
    ax.text(.05, .04, f"Chosen true T = {conv*.0036:.2f} μK\nFull fit: {conv*full[2]:.2f} μK; forced fit: {conv*omitted[1]:.2f} μK", transform=ax.transAxes, fontsize=9, bbox={"facecolor": "white", "edgecolor": ".8"})
    x = np.linspace(-5, 5, 600)
    for time, color in [(0, "#243f4e"), (5, "#185fa5"), (30, "#b24b13")]:
        var = .36-.036*time+.0036*time*time
        density = np.exp(-x*x/(2*var))/np.sqrt(2*np.pi*var)
        profiles.plot(x, density, color=color, label=f"{time} ms, σ = {np.sqrt(var):.2f} mm")
    profiles.set(xlabel="Position relative to cloud centre (mm)", ylabel="Normalised column-density marginal (mm⁻¹)", xlim=(-5, 5), ylim=(0, .86))
    profiles.legend(fontsize=9, loc="upper right")
    profiles.grid(alpha=.18)
    note.axis("off")
    note.text(.03, .95, "3D Gaussian cloud → integrate along viewing axis\n→ integrate image along y → 1D x profile", va="top", fontsize=10)
    note.text(.03, .37, "Ideal short, uniform fluorescence probe; no blur.\nNegative covariance causes initial narrowing.", va="top", fontsize=9)
    fig.savefig(Path(__file__).stem+".png", dpi=130, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()