codexvan-der-waals-reduced-isotherms.py
van-der-waals-reduced-isotherms.py
"""Plot particle-volume van der Waals isotherms; run from _media.
Tested with Python 3.14, NumPy 2.3.5 and Matplotlib 3.10.7.
All curves are model calculations, never fitted experimental data.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def pressure(volume, temperature):
return 8 * temperature / (3 * volume - 1) - 3 / volume**2
def slope(volume, temperature):
return -24 * temperature / (3 * volume - 1)**2 + 6 / volume**3
def spinodals(temperature):
roots = np.roots([4 * temperature, -9, 6, -1])
physical = sorted(r.real for r in roots if abs(r.imag) < 1e-8 and r.real > 1 / 3)
if len(physical) != 2:
raise ValueError("Two physical spinodals require 0 < reduced T < 1")
return np.array(physical)
def physical_roots(p, temperature):
roots = np.roots([3 * p, -(p + 8 * temperature), 9, -3])
physical = sorted(r.real for r in roots if abs(r.imag) < 1e-8 and r.real > 1 / 3)
if len(physical) != 3:
raise ValueError("Coexistence requires three real roots above the pole")
return np.array(physical)
def area(p, temperature, roots):
liquid, _, gas = roots
return (8 * temperature / 3 * np.log((3 * gas - 1) / (3 * liquid - 1))
+ 3 * (1 / gas - 1 / liquid) - p * (gas - liquid))
def coexistence(temperature):
low_v, high_v = spinodals(temperature)
low_p = max(0.0, pressure(low_v, temperature))
high_p = pressure(high_v, temperature)
margin = (high_p - low_p) * 1e-9
low_p += margin
high_p -= margin
assert area(low_p, temperature, physical_roots(low_p, temperature)) > 0
assert area(high_p, temperature, physical_roots(high_p, temperature)) < 0
for _ in range(80):
mid = (low_p + high_p) / 2
roots = physical_roots(mid, temperature)
if area(mid, temperature, roots) > 0:
low_p = mid
else:
high_p = mid
p = (low_p + high_p) / 2
roots = physical_roots(p, temperature)
assert abs(area(p, temperature, roots)) < 2e-11
assert np.max(abs(pressure(roots, temperature) - p)) < 2e-10
assert np.all(roots > 1 / 3)
return p, roots
def main():
plt.rcParams.update({"font.size": 11, "axes.spines.top": False, "axes.spines.right": False})
fig, ax = plt.subplots(figsize=(8.2, 5.4), layout="constrained", facecolor="white")
x = np.linspace(0.38, 4.3, 3000)
for t, color in [(1.1, "#2673a5"), (1.0, "#303030")]:
ax.plot(x, pressure(x, t), color=color, lw=2, label=fr"$T/T_c={t:.1f}$")
t = 0.9
ps, roots = coexistence(t)
liquid, middle, gas = roots
left, right = spinodals(t)
ax.plot(x, pressure(x, t), color="#b47016", lw=1.5, ls="--", label=r"$T/T_c=0.9$: homogeneous continuation")
unstable = np.linspace(left, right, 250)
ax.plot(unstable, pressure(unstable, t), color="#b92434", lw=3, label=r"Unstable: $\kappa_T<0$")
for a, b, color in [(liquid, middle, "#9acbd8"), (middle, gas, "#e7bc75")]:
segment = np.linspace(a, b, 350)
ax.fill_between(segment, pressure(segment, t), ps, color=color, alpha=0.6)
ax.plot([liquid, gas], [ps, ps], color="#137a47", lw=3, label="Maxwell equilibrium plateau")
ax.scatter([liquid, gas], [ps, ps], color="#137a47", s=35, zorder=5)
ax.scatter([1], [1], color="#303030", s=40, zorder=5)
ax.annotate("Critical point (1, 1)", (1, 1), (1.5, 1.15), arrowprops={"arrowstyle": "->"})
ax.annotate(fr"$p_*/p_c={ps:.4f}$", (gas, ps), (2.75, 0.38), arrowprops={"arrowstyle": "->", "color": "#137a47"}, color="#137a47")
ax.text(1.48, 0.79, "Equal shaded areas", fontsize=10)
ax.set(xlim=(0.38, 4.3), ylim=(0.05, 1.7), xlabel=r"Particle volume $x=v/v_c$ ($v=V/N$)", ylabel=r"Reduced pressure $p/p_c$", title="Van der Waals isotherms: model loop and equilibrium")
ax.legend(loc="upper right", fontsize=9, framealpha=0.95)
ax.grid(alpha=0.18)
fig.savefig(Path(__file__).stem + ".png", dpi=130, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()