codex/equipartition-quantum-heat-capacity.py

equipartition-quantum-heat-capacity.py
"""Original equilibrium model, not experimental data. Python 3.14 / NumPy / Matplotlib.

Run from _media; writes the PNG basename to the current working directory.
"""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def oscillator_capacity(t):
    z = 1.0 / np.asarray(t)
    return z**2 * np.exp(-z) / (-np.expm1(-z))**2


def rotor_capacity(t):
    # At max(T/theta_r)=10^4, j=800 leaves a tail below exp(-64).
    j = np.arange(801, dtype=float)
    e = j * (j + 1)
    weights = (2 * j + 1)[None, :] * np.exp(-e[None, :] / t[:, None])
    weights /= weights.sum(axis=1, keepdims=True)
    mean = weights @ e
    variance = np.sum(weights * (e[None, :] - mean[:, None])**2, axis=1)
    return variance / t**2


def main():
    plt.rcParams.update({"font.size": 10, "axes.spines.top": False,
                         "axes.spines.right": False, "figure.facecolor": "white",
                         "axes.facecolor": "white", "savefig.facecolor": "white"})
    fig, axes = plt.subplots(1, 2, figsize=(10.4, 4.2), layout="constrained")
    fig.suptitle("Equipartition emerges when quantum levels are thermally accessible", fontsize=13)
    t = np.geomspace(0.03, 30, 600)
    axes[0].semilogx(t, oscillator_capacity(t), color="#126b9c", lw=2.6)
    axes[0].axhline(1, color="#777777", ls="--", lw=1.1, label="Classical oscillator: two terms")
    axes[0].set(xlabel=r"Temperature $T/\theta_v$", ylabel=r"Oscillator heat capacity $C_{\rm osc}/k_B$",
                ylim=(-0.03, 1.14), title="One stretching oscillator")
    axes[0].annotate("Frozen thermal\nresponse; zero-point\nenergy remains",
                     xy=(0.065, 0.002), xytext=(0.045, 0.56), fontsize=8,
                     arrowprops={"arrowstyle": "->", "color": "#555555"})
    axes[0].legend(loc="upper left", fontsize=8)
    tr = np.geomspace(0.1, 1e4, 700)
    cr = rotor_capacity(tr)
    cv = oscillator_capacity(tr / 100)
    ax = axes[1]
    ax.semilogx(tr, 1.5 + cr + cv, color="#222222", lw=2.8, label="Total")
    ax.semilogx(tr, np.full_like(tr, 1.5), color="#126b9c", lw=1.8, label="Translation")
    ax.semilogx(tr, cr, color="#bc571d", lw=1.8, label="Rotation")
    ax.semilogx(tr, cv, color="#598d36", lw=1.8, label="Stretching vibration")
    for y, label in [(1.5, "3/2"), (2.5, "5/2"), (3.5, "7/2")]:
        ax.axhline(y, color="#999999", ls=":", lw=0.8, zorder=0)
        ax.text(1.45e4, y, label, va="center", fontsize=9)
    ax.set(xlabel=r"Temperature $T/\theta_r$", ylabel=r"Heat capacity per molecule, in units of $k_B$",
           ylim=(-0.08, 3.85), xlim=(0.1, 2.3e4), title=r"Ideal diatomic model: $\theta_v/\theta_r=100$")
    ax.legend(loc="upper right", bbox_to_anchor=(0.86, 0.60), fontsize=8)
    fig.supxlabel("Analytic equilibrium models • all integer rotor states allowed • no measured data", fontsize=9)
    fig.savefig(Path("equipartition-quantum-heat-capacity.png"), dpi=120, transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()