codex/maxwell-speed-temperature-mass.py

maxwell-speed-temperature-mass.py
"""Show Maxwell speed scaling and the mode, mean, and root mean square.

Writes an opaque white PNG with this script's basename in the working directory.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def density(w, scale):
    return np.sqrt(2 / np.pi) * w**2 / scale**3 * np.exp(-w**2 / (2 * scale**2))


def main():
    # Relative T and m give s/s0. Verify areas and moments at each scale.
    cases = [(1.0, 1.0), (4.0, 1.0), (1.0, 4.0)]
    scales = [np.sqrt(temperature / mass) for temperature, mass in cases]
    assert np.allclose(scales, [1.0, 2.0, 0.5])
    check_w = np.linspace(0.0, 30.0, 60001)
    for scale in scales:
        check_density = density(check_w, scale)
        moments = [np.trapezoid(check_w**order * check_density, check_w)
                   for order in range(3)]
        assert np.allclose(moments, [1.0, np.sqrt(8 / np.pi) * scale, 3 * scale**2],
                           atol=1e-10, rtol=0)
        measured_mode = check_w[np.argmax(check_density)]
        assert abs(measured_mode - np.sqrt(2) * scale) < 0.0003
    mode, mean, rms = np.sqrt(2), np.sqrt(8 / np.pi), np.sqrt(3)
    assert mode < mean < rms

    plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
                         "axes.facecolor": "white", "savefig.facecolor": "white",
                         "axes.spines.top": False, "axes.spines.right": False})
    fig, axes = plt.subplots(1, 2, figsize=(9.4, 5.0), dpi=130,
                             gridspec_kw={"width_ratios": [1.35, 1]})
    fig.subplots_adjust(left=0.07, right=0.97, bottom=0.27, top=0.78, wspace=0.29)
    fig.suptitle("Temperature and mass set the scale; the shape stays the same",
                 y=0.98, fontsize=14)
    fig.text(0.5, 0.905, r"Reference $s_0=\sqrt{k_B T_0/m_0}$; all plotted densities have area 1",
             ha="center", fontsize=11.5)
    left, right = axes
    w = np.linspace(0.0, 8.0, 1601)
    curves = [(0.5, "#6c4a92", r"$T_0,\ 4m_0$: half the speed scale"),
              (1.0, "#236a9e", r"$T_0,\ m_0$: reference"),
              (2.0, "#bd5b25", r"$4T_0,\ m_0$: twice the speed scale")]
    for scale, color, label in curves:
        left.plot(w, density(w, scale), color=color, lw=2.5, label=label)
        peak = np.sqrt(2) * scale
        left.plot([peak, peak], [0, density(peak, scale)], color=color,
                  lw=1.0, ls="--", alpha=0.7)
    left.set(xlim=(0, 8), ylim=(0, 1.24), xlabel=r"Speed $w=v/s_0$",
             ylabel=r"Density per unit $w$: $s_0 g(v)$", title="Common reference units")
    left.legend(loc="upper right", fontsize=9, frameon=False)
    u = np.linspace(0.0, 4.0, 801)
    right.plot(u, density(u, 1.0), color="#236a9e", lw=2.5)
    right.fill_between(u, density(u, 1.0), color="#236a9e", alpha=0.08)
    for value, color in [(mode, "#6c4a92"), (mean, "#bd5b25"), (rms, "#297658")]:
        right.plot([value, value], [0, density(value, 1.0)], ls="--", lw=1.5, color=color)
        right.scatter([value], [density(value, 1.0)], color=color, s=22, zorder=3)
    right.set(xlim=(0, 4), ylim=(0, 0.82), xlabel=r"Own scale $u=v/s$",
              ylabel=r"Density $h(u)$", title="Three characteristic speeds")
    labels = [(0.12, "#6c4a92", "Mode\n√2 ≈ 1.414"),
              (0.50, "#bd5b25", "Mean\n√(8/π) ≈ 1.596"),
              (0.87, "#297658", "RMS\n√3 ≈ 1.732")]
    for x, color, label in labels:
        right.text(x, 0.91, label, color=color, ha="center", va="top", fontsize=9,
                   transform=right.transAxes)
    for ax in axes:
        ax.grid(axis="y", alpha=0.18)
    fig.text(0.5, 0.13, r"Every characteristic speed is proportional to $\sqrt{T/m}$.",
             ha="center", fontsize=12)
    fig.text(0.5, 0.055, "Stretching the speed axis lowers the density to preserve probability.\n"
             "Mode locates the peak; mean averages speed; RMS connects to kinetic energy.",
             ha="center", fontsize=10.5)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()