codex/photon-gas-entropy-adiabatic-work.py

photon-gas-entropy-adiabatic-work.py
"""Original reversible expansion of an equilibrium vacuum photon gas.

Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Run from _media: the PNG is written into CWD, using this script's stem.
Fixed two photon polarizations; no entropy exchange or production.
This is an analytic model, not a measured piston expansion.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    r = np.linspace(1, 8, 2200)  # physical volume relative to its initial value
    temperature = r**(-1 / 3)
    energy_density = temperature**4
    entropy_density = temperature**3
    energy = energy_density * r  # total internal energy, not energy density
    pressure = energy_density / 3  # p*V0/U0
    work = np.trapezoid(pressure, r)
    assert np.allclose(entropy_density * r, 1, rtol=1e-13)
    assert np.isclose(work, 1 - energy[-1], rtol=1e-6)
    assert np.allclose([temperature[-1], energy_density[-1], entropy_density[-1]],
                       [0.5, 1 / 16, 1 / 8], rtol=1e-13)
    # Differential first law: d(U/U0)/dr + p*V0/U0 = 0.
    assert np.allclose(np.gradient(energy, r, edge_order=2)[2:-2],
                       -pressure[2:-2], rtol=6e-6)

    plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
                         "axes.facecolor": "white", "savefig.facecolor": "white",
                         "axes.spines.top": False, "axes.spines.right": False})
    fig, (left, right) = plt.subplots(1, 2, figsize=(10.6, 4.7), dpi=110)
    fig.subplots_adjust(left=0.085, right=0.975, bottom=0.19, top=0.78, wspace=0.30)
    fig.suptitle("Photon gas: conserved entropy, decreasing energy", fontsize=16, y=0.97)
    fig.text(0.5, 0.89, "Analytic reversible adiabatic expansion · fixed species · μγ = 0",
             ha="center", fontsize=11)
    colors = ("#2265a3", "#bb5a1b", "#268066")
    for values, color, label in zip(
            (temperature, energy_density, entropy_density), colors,
            (r"T/T$_0$ = r$^{-1/3}$", r"u/u$_0$ = r$^{-4/3}$", r"s/s$_0$ = r$^{-1}$")):
        left.plot(r, values, color=color, lw=2.5, label=label)
        left.plot(8, values[-1], "o", ms=5, color=color, clip_on=False)
    left.set(yscale="log", xlim=(1, 8), ylim=(0.055, 1.25),
             xlabel=r"Volume ratio r = V/V$_0$", ylabel="Ratio to initial value",
             title="Dilution and cooling")
    left.set_yticks([1 / 16, 1 / 8, 1 / 2, 1], ["1/16", "1/8", "1/2", "1"])
    left.set_xticks([1, 2, 4, 6, 8])
    left.minorticks_off()
    left.legend(loc="upper right", frameon=False, fontsize=9, labelspacing=0.22)
    left.grid(alpha=0.18)

    right.plot(r, pressure, color=colors[1], lw=2.5)
    right.fill_between(r, pressure, color=colors[1], alpha=0.22)
    right.set(xlim=(1, 8), ylim=(0, 0.36), xlabel=r"Volume ratio r = V/V$_0$",
              ylabel=r"Scaled pressure pV$_0$/U$_0$", title="The missing energy becomes work")
    right.set_xticks([1, 2, 4, 6, 8])
    right.text(0.97, 0.91, r"pV$_0$/U$_0$ = r$^{-4/3}$/3" + "\n\n"
               r"Shaded area: W/U$_0$ = 1/2" + "\n"
               r"Final U/U$_0$ = 1/2" + "\n"
               r"Total S/S$_0$ = 1",
               transform=right.transAxes, ha="right", va="top", fontsize=10)
    right.grid(alpha=0.18)
    fig.text(0.5, 0.045, "Original model, not measured data · entropy density decreases while total entropy stays fixed",
             ha="center", fontsize=10)
    output = Path(__file__).with_suffix('.png').name
    fig.savefig(output, facecolor="white", transparent=False)
    plt.close(fig)
    print(f"{output}: W/U0={work:.12g}; U_final/U0={energy[-1]:.12g}; S/S0=1")


if __name__ == "__main__":
    main()