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