codexisothermal-ideal-gas-signed-work-area.py
isothermal-ideal-gas-signed-work-area.py
"""Illustrate reversible isothermal work. Python 3.14, matplotlib 3.10.7, numpy 2.3.5."""
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"})
n, gas_constant, temperature = 1.0, 8.31446261815324, 300.0
volume = np.linspace(8.5, 23.0, 600)
selected_volume = np.linspace(10.0, 20.0, 600)
# A kilopascal litre is one joule.
pressure = n * gas_constant * temperature / volume
selected_pressure = n * gas_constant * temperature / selected_volume
work_kj = n * gas_constant * temperature * np.log(2.0) / 1000
fig, axes = plt.subplots(1, 2, figsize=(10.8, 4.8), dpi=110, sharey=True)
for ax, expansion, color in zip(axes, [True, False], ["#176e9e", "#b75224"]):
ax.plot(volume, pressure, color=color, lw=2.5)
ax.fill_between(selected_volume, 0, selected_pressure, color=color, alpha=0.18)
ax.vlines([10, 20], 0, n * gas_constant * temperature / np.array([10, 20]),
color=color, ls="--", lw=1)
ax.scatter([10, 20], n * gas_constant * temperature / np.array([10, 20]),
color=color, zorder=4)
ax.text(10.2, 260, "A: 10 L", fontsize=10)
ax.text(20.2, 136, "B: 20 L", fontsize=10)
start, end = (12.0, 15.0) if expansion else (17.0, 14.0)
ax.annotate("", xy=(end, n * gas_constant * temperature / end),
xytext=(start, n * gas_constant * temperature / start),
arrowprops={"arrowstyle": "-|>", "color": color,
"lw": 2.2, "mutation_scale": 17})
sign = "+" if expansion else "−"
ax.text(15, 75, f"$W_{{\\rm by}}=Q$\n{sign}{work_kj:.3f} kJ", ha="center",
va="center", color=color, fontsize=13)
ax.set(xlim=(8, 24), ylim=(0, 320), xlabel="Gas volume V (L)",
title="Expansion: A → B" if expansion else "Compression: B → A")
ax.set_xticks([10, 15, 20])
ax.grid(alpha=0.18)
axes[0].set_ylabel("Gas pressure p (kPa)")
fig.suptitle("Reversible isotherm: 1 mol of ideal gas at 300 K", fontsize=14)
fig.text(0.5, 0.035, r"$W_{\rm by}=\int_{V_i}^{V_f}p\,dV$ • "
"shading shows magnitude; traversal sets sign • ΔU = 0", ha="center")
fig.subplots_adjust(left=0.08, right=0.97, top=0.82, bottom=0.18, wspace=0.14)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white",
transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()