codex/free-expansion-entropy-reference-path.py

free-expansion-entropy-reference-path.py
"""Compare free expansion with the reversible path used to compute entropy.

Tested with Python 3.14.4, Matplotlib 3.10.7, NumPy 2.3.5.
Run from _media; writes free-expansion-entropy-reference-path.png to CWD.
Stipulated ideal-gas example, not measurements: n=1 mol, T=300 K,
V1=0.025 m^3, V2=2V1, R=8.314 J/(mol K).
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    plt.rcParams.update({"font.size": 12, "figure.facecolor": "white",
                         "axes.facecolor": "white", "savefig.facecolor": "white",
                         "text.color": "#243445"})
    n, temperature, gas_constant, v1 = 1.0, 300.0, 8.314, 0.025
    v2 = 2 * v1
    volumes = np.linspace(v1, v2, 2001)
    pressures = n * gas_constant * temperature / volumes / 1000  # kPa
    entropy = n * gas_constant * np.log(v2 / v1)
    work = temperature * entropy
    assert abs(np.trapezoid(pressures * 1000, volumes) - work) < 0.00004
    assert np.allclose(pressures[[0, -1]], [99.768, 49.884])
    fig = plt.figure(figsize=(9.4, 5.0), dpi=120)
    fig.suptitle("Same equilibrium endpoints; different heat and work",
                 fontsize=16, y=0.96)
    fig.text(0.5, 0.885, "Toy ideal gas: 1 mol at 300 K; accessible volume doubles",
             ha="center", fontsize=12)
    ax = fig.add_axes([0.09, 0.25, 0.40, 0.54])
    blue = "#216a99"
    ax.fill_between(volumes * 1000, 0, pressures, color="#d7eaf5")
    ax.plot(volumes * 1000, pressures, color=blue, lw=3)
    ax.scatter([v1 * 1000, v2 * 1000], pressures[[0, -1]], color="#243445", zorder=4)
    ax.annotate("Initial", (25, pressures[0]), xytext=(27, 106), fontsize=11)
    ax.annotate("Final", (50, pressures[-1]), xytext=(44, 58), fontsize=11)
    ax.text(37.5, 86, "Hypothetical reversible\nisothermal path", ha="center",
             color=blue, fontsize=12)
    ax.text(37.5, 27, r"$\int p\,dV=nRT\ln 2$" + f"\n= {work:.2f} J",
             ha="center", color=blue, fontsize=13)
    ax.annotate("", xy=(40, n * gas_constant * temperature / 0.04 / 1000),
                xytext=(37, n * gas_constant * temperature / 0.037 / 1000),
                arrowprops={"arrowstyle": "-|>", "color": blue, "lw": 2})
    ax.set(xlim=(23, 52), ylim=(0, 115), xlabel="Gas volume V (L)",
           ylabel="Equilibrium gas pressure p (kPa)")
    ax.spines[["top", "right"]].set_visible(False)
    ax.set_xticks([25, 30, 35, 40, 45, 50])

    ledger = fig.add_axes([0.55, 0.20, 0.42, 0.59])
    ledger.axis("off")
    ledger.text(0.5, 0.98, "Gas accounting", ha="center", fontsize=14)
    rows = [["Heat in (J)", "0", f"{work:.2f}"],
            ["Work out (J)", "0", f"{work:.2f}"],
            ["ΔU (J)", "0", "0"],
            ["ΔS (J/K)", f"{entropy:.3f}", f"{entropy:.3f}"],
            ["Produced S (J/K)", f"{entropy:.3f}", "0"]]
    table = ledger.table(cellText=rows, colLabels=["", "Actual\nrelease", "Reversible\nreference"],
                         colWidths=[0.46, 0.25, 0.29], cellLoc="center",
                         bbox=[0, 0.23, 1, 0.65])
    table.auto_set_font_size(False)
    table.set_fontsize(10.5)
    for (row, column), cell in table.get_celld().items():
        cell.set_edgecolor("#c6d0d9")
        if row == 0:
            cell.set_facecolor("#eaf1f6")
            cell.set_text_props(weight="bold")
        elif column == 0:
            cell.set_text_props(ha="left")
    ledger.text(0.5, 0.09, "Reference bath loses 5.763 J/K;\nactual isolated gas produces 5.763 J/K.",
                 ha="center", fontsize=11)
    fig.text(0.5, 0.085, "Actual release: no equilibrium p(V) curve during the rush",
             ha="center", fontsize=12, color="#b35925")
    fig.text(0.5, 0.025, "Entropy uses a reversible comparison; the drawn curve is not measured data",
             ha="center", fontsize=10, color="#596673")
    fig.savefig(Path(__file__).with_suffix(".png").name, transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()