codex/ideal-gas-state-slices.py

ideal-gas-state-slices.py
"""Plot pressure slices of PV = n_mol R T for one mole of ideal gas.

Writes ideal-gas-state-slices.png to the current working directory.
Tested with Python 3.14.4, Matplotlib 3.10.7, and NumPy 2.3.5.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


GAS_CONSTANT = 8.31446261815324  # Exact SI value, J / (mol K).
AMOUNT_MOL = 1.0
REFERENCE_VOLUME = 0.025  # m^3.


def pressure_kpa(volume, temperature):
    return AMOUNT_MOL * GAS_CONSTANT * temperature / volume / 1000


def main():
    plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
                         "axes.facecolor": "white", "savefig.facecolor": "white"})
    fig, (left, right) = plt.subplots(1, 2, figsize=(10, 5.2), dpi=120)
    fig.subplots_adjust(left=0.085, right=0.975, bottom=0.22,
                        top=0.79, wspace=0.31)
    fig.suptitle("One mole of ideal gas: two slices of the same state relation",
                 fontsize=15, y=0.965)
    fig.text(0.5, 0.89, r"$PV=n_{\rm mol}RT$     |     "
             r"$n_{\rm mol}=1\,\mathrm{mol}$     |     "
             r"$R=8.31446261815324\,\mathrm{J/(mol\,K)}$", ha="center")
    blue, orange, green = "#226a9a", "#b35427", "#267849"

    volume = np.linspace(0.01, 0.065, 500)
    for temperature, color in zip((150, 300, 600), (green, blue, orange)):
        left.plot(volume, pressure_kpa(volume, temperature), color=color,
                  lw=2.3, label=f"T = {temperature} K")
    left.set(title="Hold temperature fixed", xlabel=r"Volume $V$ (m$^3$)",
             ylabel=r"Absolute pressure $P$ (kPa)",
             xlim=(0.01, 0.065), ylim=(0, 515))
    left.set_xticks([0.01, 0.025, 0.05, 0.065])
    left.legend(loc="upper right", frameon=False, fontsize=10)
    p_reference = pressure_kpa(REFERENCE_VOLUME, 300)
    p_expanded = pressure_kpa(2 * REFERENCE_VOLUME, 300)
    left.scatter([REFERENCE_VOLUME, 2 * REFERENCE_VOLUME],
                 [p_reference, p_expanded], color=blue, s=38, zorder=5)
    left.annotate(f"0.025 m³\n{p_reference:.3f} kPa",
                  xy=(REFERENCE_VOLUME, p_reference), xytext=(0.017, 235),
                  arrowprops={"arrowstyle": "->", "color": blue}, fontsize=10)
    left.annotate(f"0.050 m³\n{p_expanded:.3f} kPa",
                  xy=(2 * REFERENCE_VOLUME, p_expanded), xytext=(0.046, 135),
                  arrowprops={"arrowstyle": "->", "color": blue}, fontsize=10)

    temperature = np.linspace(150, 650, 500)
    right.plot(temperature, pressure_kpa(REFERENCE_VOLUME, temperature),
               color=blue, lw=2.3)
    extrapolated = np.linspace(0, 150, 100)
    right.plot(extrapolated, pressure_kpa(REFERENCE_VOLUME, extrapolated),
               color=blue, lw=1.6, ls="--", alpha=0.55)
    right.set(title=r"Hold volume fixed: $V=0.025$ m$^3$",
              xlabel=r"Absolute temperature $T$ (K)",
              ylabel=r"Absolute pressure $P$ (kPa)",
              xlim=(0, 650), ylim=(0, 235))
    right.set_xticks([0, 150, 300, 450, 600])
    right.scatter([300, 600], [p_reference, 2 * p_reference],
                  color=[blue, orange], s=38, zorder=5)
    right.annotate(f"300 K\n{p_reference:.3f} kPa", xy=(300, p_reference),
                   xytext=(80, 125), fontsize=10,
                   arrowprops={"arrowstyle": "->", "color": blue})
    right.annotate(f"600 K\n{2 * p_reference:.3f} kPa", xy=(600, 2 * p_reference),
                   xytext=(375, 200), fontsize=10,
                   arrowprops={"arrowstyle": "->", "color": orange})
    right.text(65, 20, "Formal\nextrapolation", color="#555555", fontsize=9)

    for ax in (left, right):
        ax.grid(alpha=0.18)
        ax.spines[["top", "right"]].set_visible(False)
    fig.text(0.29, 0.10, "Double V at fixed T: halve P", ha="center", color=blue)
    fig.text(0.755, 0.10, "Double T in kelvin at fixed V: double P",
             ha="center", color=blue)
    fig.text(0.5, 0.035,
             "Model state slices; extrapolation to zero temperature does not imply real-gas validity.",
             ha="center", fontsize=10, color="#555555")
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()