codex/cloud-chamber-expansion-and-nucleation-model.py

cloud-chamber-expansion-and-nucleation-model.py
"""Original stipulated water-vapor model, not historical data.

Python 3.14, matplotlib 3.10.7, numpy 2.3.5. Run from _media.
No ion-dependent nucleation calculation or measured detector calibration.
"""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

T0 = 293.15
P0 = 2339.0
GAMMA = 1.40
LM = 45000.0
R = 8.314
SIGMA = 0.075
VL = 2.99e-29
KB = 1.380649e-23


def state(expansion):
    temperature = T0 * np.asarray(expansion) ** (1.0 - GAMMA)
    actual = P0 * np.asarray(expansion) ** (-GAMMA)
    saturation = P0 * np.exp(LM / R * (1.0 / T0 - 1.0 / temperature))
    return temperature, actual, saturation, actual / saturation


def main():
    plt.rcParams.update({"font.size": 11, "font.family": "DejaVu Sans"})
    fig, axes = plt.subplots(1, 3, figsize=(13, 4.9), layout="constrained", facecolor="white")
    fig.suptitle("Expansion promotes supersaturation; a neutral drop still faces a barrier", fontsize=15)
    expansion = np.linspace(1, 1.17, 350)
    temperature, actual, saturation, ratio = state(expansion)
    blue, orange = "#247caa", "#bb641c"

    ax = axes[0]
    ax.plot(expansion, actual / 1000, color=blue, lw=2.4, label=r"Actual $p_v$")
    ax.plot(expansion, saturation / 1000, color=orange, lw=2.4, label=r"Equilibrium $p_{\rm sat}$")
    ax.fill_between(expansion, saturation / 1000, actual / 1000, color=blue, alpha=.09)
    ax.set(title="A  Cooling beats dilution", xlabel=r"Expansion ratio $V/V_0$", ylabel="Water-vapor pressure (kPa)", ylim=(.6, 2.5))
    ax.legend(frameon=False, loc="lower left", fontsize=10)
    ax.text(.97, .96, "293.15 K → 275.31 K", transform=ax.transAxes, ha="right", va="top", fontsize=10)

    ax = axes[1]
    ax.plot(expansion, ratio, color=blue, lw=2.4)
    ax.axhline(1, color="#657581", lw=1, ls="--")
    selected = state(1.15)
    ax.scatter([1.15], [selected[3]], color=blue, s=35, zorder=3)
    ax.annotate("1.15: S = 2.3772", (1.15, selected[3]), xytext=(1.025, 2.57),
                fontsize=10, arrowprops={"arrowstyle": "->", "color": blue})
    ax.set(title="B  Supersaturated vapor", xlabel=r"Expansion ratio $V/V_0$", ylabel=r"Saturation ratio $S=p_v/p_{\rm sat}$", ylim=(.85, 2.85))
    ax.text(.05, .08, "No droplet depletion\nor latent-heat feedback", transform=ax.transAxes, fontsize=10, color="#526573")

    ax = axes[2]
    radius = np.linspace(0, 3.6e-9, 500)
    for lam, color in [(1.10, orange), (1.15, blue)]:
        t, _, _, s = state(lam)
        driving = KB * t * np.log(s)
        free_energy = 4 * np.pi * SIGMA * radius**2 - (4 * np.pi / 3) * radius**3 * driving / VL
        critical = 2 * SIGMA * VL / driving
        barrier = 4 * np.pi * SIGMA * critical**2 / 3
        ax.plot(radius * 1e9, free_energy / (KB * t), color=color, lw=2, label=fr"$V/V_0={lam:.2f}$")
        ax.scatter([critical * 1e9], [barrier / (KB * t)], s=25, color=color, zorder=3)
    ax.set(title="C  Neutral-drop comparison", xlabel="Drop radius (nm)", ylabel=r"Formation free energy $\Delta G/(k_B T)$", ylim=(-100, 370), xlim=(0, 3.6))
    ax.axhline(0, color="#657581", lw=1)
    ax.legend(frameon=False, fontsize=10)
    ax.text(.05, .08, "Dots: critical nuclei\nIon hydration omitted", transform=ax.transAxes, fontsize=10, color="#526573")

    for ax in axes:
        ax.spines[["top", "right"]].set_visible(False)
        ax.grid(alpha=.18)
    fig.supxlabel("Stipulated water constants • ideal adiabatic carrier gas • constant latent heat and surface tension • no measured traces", fontsize=10)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=120, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()