codexadiabatic-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()