codex/adiabatic-processes-work-and-temperature.py

adiabatic-processes-work-and-temperature.py
"""Original reversible ideal-gas scaling and signed first-law budgets.

Run from _media; tested with Python 3.14.4, NumPy 2.3.5,
and Matplotlib 3.10.7. gamma=1.4 and p1*V1=1 kJ are stipulated.
"""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                         "axes.spines.right": False, "figure.facecolor": "white",
                         "axes.facecolor": "white", "savefig.facecolor": "white"})
    gamma = 1.4
    v = np.linspace(.5, 2, 601)
    p = v ** (-gamma)
    temperature = v ** (1 - gamma)
    fig, axes = plt.subplots(1, 3, figsize=(12, 5.7), dpi=100,
                             gridspec_kw={"width_ratios": [1, 1, 1.05]})
    fig.subplots_adjust(left=.07, right=.97, bottom=.25, top=.78, wspace=.39)
    fig.suptitle("Reversible adiabatic model: work changes internal energy", y=.96, fontsize=17)
    fig.text(.5, .88, r"Fixed amount of ideal gas; constant $\gamma=1.4$; $p_1V_1=1$ kJ; no heat transfer",
             ha="center", fontsize=12)
    ax, bx, cx = axes
    warm, cool = "#b85a2a", "#26709b"
    ax.plot(v, p, color="#334750", lw=2.5, label=r"$p/p_1=v^{-1.4}$")
    ax.plot(v, 1 / v, color="#8c9398", lw=1.8, ls="--", label="Isothermal comparison")
    ax.fill_between(v, p, where=v <= 1, color=warm, alpha=.18)
    ax.fill_between(v, p, where=v >= 1, color=cool, alpha=.18)
    for a, b, color in [(.92, .7, warm), (1.22, 1.58, cool)]:
        ax.annotate("", xy=(b, b ** -gamma), xytext=(a, a ** -gamma),
                    arrowprops={"arrowstyle": "->", "color": color, "lw": 2.5})
    ax.set(xlim=(.45, 2.08), ylim=(0, 2.95), xlabel=r"Volume ratio $v=V/V_1$",
           ylabel=r"Pressure ratio $p/p_1$")
    ax.set_title("Area gives work magnitude", fontsize=12)
    ax.legend(fontsize=9, frameon=False, loc="upper right")
    bx.plot(v, temperature, color="#334750", lw=2.5)
    bx.axhline(1, color="#8c9398", lw=1.8, ls="--")
    for endpoint, color, va, offset in [(.5, warm, "bottom", .028), (2, cool, "bottom", .035)]:
        value = endpoint ** (1 - gamma)
        bx.scatter([endpoint], [value], color=color, zorder=4)
        bx.annotate(f"{value:.5f}", xy=(endpoint, value), xytext=(endpoint, value + offset),
                    ha="left" if endpoint == .5 else "right", va=va, color=color, fontsize=10)
    bx.text(.82, 1.12, "Compression\nwarms", color=warm, fontsize=11)
    bx.text(1.25, .82, "Loaded expansion\ncools", color=cool, fontsize=11)
    bx.set(xlim=(.45, 2.08), ylim=(.68, 1.39), xlabel=r"Volume ratio $v=V/V_1$",
           ylabel=r"Temperature ratio $T/T_1$")
    bx.set_title(r"$T/T_1=v^{-0.4}$", fontsize=12)
    for axis in [ax, bx]:
        axis.scatter([1], [1], color="#182b33", zorder=5)
        axis.set_xticks([.5, 1, 1.5, 2])
        axis.grid(alpha=.16)
    endpoints = np.array([.5, 2.0])
    delta_u = (endpoints ** (1 - gamma) - 1) / (gamma - 1)
    y = np.array([1, 0])
    cx.barh(y + .14, delta_u, height=.24, color=warm, label=r"$\Delta U$")
    cx.barh(y - .14, -delta_u, height=.24, color=cool, label=r"$W_{\rm out}$")
    for yi, energy in zip(y, delta_u):
        for offset, value in [(.14, energy), (-.14, -energy)]:
            cx.text(value + np.sign(value) * .025, yi + offset, f"{value:+.5f}",
                    ha="left" if value > 0 else "right", va="center", fontsize=10)
    cx.axvline(0, color="#394750", lw=1)
    cx.set(xlim=(-1.18, 1.18), ylim=(-.5, 1.5), xlabel="Signed energy (kJ)",
           yticks=y, yticklabels=[r"$v=1/2$", r"$v=2$"])
    cx.set_title(r"First law: $\Delta U=-W_{\rm out}$", fontsize=12)
    cx.legend(loc="upper center", fontsize=10, frameon=False, ncol=2)
    cx.grid(axis="x", alpha=.16)
    fig.text(.5, .125, "Two separate runs from the black initial state: halve or double the volume.",
             ha="center", fontsize=11)
    fig.text(.5, .065, "Analytical illustration, not measurements. The curves require a quasistatic, frictionless reversible path.",
             ha="center", fontsize=10, color="#4f5a60")
    fig.savefig(Path(__file__).with_suffix('.png').name, transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()