codexvan-der-waals-molar-pressure-model.py
van-der-waals-molar-pressure-model.py
"""Original molar van der Waals calculations; Python 3.14, NumPy 2.3, Matplotlib 3.10.
Run from _media: MPLBACKEND=Agg python ../van-der-waals-molar-pressure-model.py
All constants describe a stipulated hypothetical fluid, not measured carbon dioxide.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
R, A, B = 8.314462618, 0.360, 4.00e-5
VC = 3 * B
TC = 8 * A / (27 * R * B)
PC = A / (27 * B**2)
def pressure(volume, temperature):
return R * temperature / (volume - B) - A / volume**2
def coexistence(t):
"""Find the equal-area pressure by bisection, using the analytic integral."""
spin = np.sort(np.roots([4 * t, -9, 6, -1]).real)
spin = spin[spin > 1 / 3]
q = lambda x: 8 * t / (3 * x - 1) - 3 / x**2
low, high = q(spin[0]), q(spin[1])
for _ in range(90):
middle = (low + high) / 2
roots = np.sort(np.roots([3 * middle, -(middle + 8 * t), 9, -3]).real)
vl, vg = roots[0], roots[-1]
area = (8 * t / 3 * np.log((3 * vg - 1) / (3 * vl - 1))
+ 3 * (1 / vg - 1 / vl) - middle * (vg - vl))
if area > 0:
low = middle
else:
high = middle
assert abs(area) < 1e-12
return middle, roots
def main():
plt.rcParams.update({'font.size': 10, 'axes.spines.top': False,
'axes.spines.right': False, 'figure.facecolor': 'white'})
fig, (left, right) = plt.subplots(1, 2, figsize=(10.8, 4.8), layout='constrained')
volume = np.linspace(65, 500, 1100) * 1e-6
left.plot(volume * 1e6, R * 360 / volume / 1e6, label='Ideal: RT / vₘ', color='#777777')
left.plot(volume * 1e6, R * 360 / (volume - B) / 1e6,
label='Excluded volume only', color='#b36b00')
left.plot(volume * 1e6, pressure(volume, 360) / 1e6,
label='With attraction', color='#176baa', lw=2)
left.scatter([240], [pressure(240e-6, 360) / 1e6], color='#176baa', zorder=4)
left.annotate('Worked state\n8.716 MPa', xy=(240, 8.716), xytext=(295, 15),
arrowprops={'arrowstyle': '->', 'color': '#176baa'})
left.set(title='Competing corrections at 360 K', xlim=(65, 500), ylim=(0, 42),
xlabel='Molar volume vₘ (cm³ mol⁻¹)', ylabel='Pressure (MPa)')
left.legend(frameon=False, loc='upper right')
v = np.linspace(52, 440, 1400) * 1e-6
for t, color in [(1.1, '#b36b00'), (1, '#176baa')]:
right.plot(v * 1e6, pressure(v, t * TC) / 1e6, color=color,
label=f'T/Tc = {t:g}', lw=2)
qs, roots = coexistence(.9)
vl, _, vg = roots * VC
right.plot(v * 1e6, pressure(v, .9 * TC) / 1e6, color='#999999', ls=':',
label='0.9: homogeneous continuation')
stable = (v <= vl) | (v >= vg)
right.plot(v * 1e6, np.where(stable, pressure(v, .9 * TC) / 1e6, np.nan),
color='#278249', lw=2)
right.hlines(qs * PC / 1e6, vl * 1e6, vg * 1e6, color='#278249', lw=3,
label='0.9: equilibrium coexistence')
right.scatter([120], [PC / 1e6], color='#176baa', zorder=5)
right.annotate('Critical point\n120 cm³ mol⁻¹, 8.333 MPa',
xy=(120, PC / 1e6), xytext=(165, 13.8), fontsize=9,
arrowprops={'arrowstyle': '->', 'color': '#176baa'})
right.set(title=f'Critical temperature Tc = {TC:.3f} K', xlim=(52, 440), ylim=(0, 21),
xlabel='Molar volume vₘ (cm³ mol⁻¹)', ylabel='Pressure (MPa)')
right.legend(frameon=False, loc='upper right', fontsize=8)
for ax in (left, right):
ax.grid(alpha=.18)
fig.suptitle('A hypothetical fluid: aₘ = 0.360 Pa m⁶ mol⁻²; bₘ = 40.0 cm³ mol⁻¹', fontsize=12)
fig.savefig(Path(__file__).stem + '.png', dpi=120, facecolor='white')
plt.close(fig)
if __name__ == '__main__':
main()