codex/van-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()