codex/stoichiometry-particle-counts.py

stoichiometry-particle-counts.py
"""Draw stoichiometry-particle-counts.png into the current working directory.

Schematic particles, not a reaction mechanism or a molecular geometry model.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
From _media: MPLBACKEND=Agg MPLCONFIGDIR=/tmp python3 ../stoichiometry-particle-counts.py
"""

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
import numpy as np


H_COLOR = "#246baa"
O_COLOR = "#c64b39"


def molecule(ax, center, species):
    center = np.asarray(center, dtype=float)
    if species == "H2":
        positions = center + np.array([[-0.21, 0], [0.21, 0]])
        elements = ["H", "H"]
        bonds = [(0, 1)]
    elif species == "O2":
        positions = center + np.array([[-0.27, 0], [0.27, 0]])
        elements = ["O", "O"]
        bonds = [(0, 1)]
    else:
        positions = center + np.array([[0, 0.13], [-0.35, -0.15], [0.35, -0.15]])
        elements = ["O", "H", "H"]
        bonds = [(0, 1), (0, 2)]
    for i, j in bonds:
        ax.plot(positions[[i, j], 0], positions[[i, j], 1], color="#555555", lw=3, zorder=1)
    for pos, element in zip(positions, elements):
        ax.add_patch(Circle(pos, 0.18 if element == "H" else 0.23,
                            facecolor=H_COLOR if element == "H" else O_COLOR,
                            edgecolor="white", lw=1.1, zorder=2))
        ax.text(*pos, element, color="white", fontsize=13, weight="bold",
                ha="center", va="center", zorder=3)


def main():
    # Columns: H2, O2, H2O. Rows: H atoms, O atoms.
    atoms = np.array([[2, 0, 2], [0, 2, 1]], dtype=int)
    initial = np.array([2, 1, 0])
    final = np.array([0, 0, 2])
    np.testing.assert_array_equal(atoms @ initial, atoms @ final)
    np.testing.assert_array_equal(atoms @ initial, [4, 2])
    avogadro_number = 602214076000000000000000
    assert 2 * avogadro_number == 1204428152000000000000000
    plt.rcParams.update({"font.size": 14, "savefig.facecolor": "white"})
    fig = plt.figure(figsize=(10, 5.6), facecolor="white")
    ax = fig.add_axes([0.035, 0.32, 0.93, 0.50])
    ax.set(xlim=(0, 10), ylim=(-0.65, 1.95), aspect="equal")
    ax.axis("off")
    fig.suptitle("One ratio, two counting scales", fontsize=23, y=0.955)
    molecule(ax, (0.7, 0.9), "H2")
    molecule(ax, (1.95, 0.9), "H2")
    molecule(ax, (4.5, 0.9), "O2")
    molecule(ax, (7.6, 0.9), "H2O")
    molecule(ax, (9.15, 0.9), "H2O")
    ax.text(3.05, 0.9, "+", fontsize=28, ha="center", va="center")
    ax.annotate("", xy=(6.8, 0.9), xytext=(5.65, 0.9),
                arrowprops={"arrowstyle": "->", "lw": 2.5, "color": "#333333"})
    for x, label in [(1.32, "2 H₂ molecules"), (4.5, "1 O₂ molecule"), (8.38, "2 H₂O molecules")]:
        ax.text(x, 0.05, label, ha="center", fontsize=16)
    ax.text(5, -0.47, "Both sides: 4 H atoms + 2 O atoms", ha="center", fontsize=16)
    fig.text(0.5, 0.31, "Multiply every count by the same Avogadro number", ha="center", fontsize=15)
    fig.text(0.5, 0.235, "2 mol H₂   +   1 mol O₂   →   2 mol H₂O", ha="center", fontsize=21, weight="bold")
    fig.text(0.5, 0.15, "1 mol contains exactly 6.02214076 × 10²³ specified entities.", ha="center", fontsize=14)
    fig.text(0.5, 0.055, "Each top drawing is one molecule. Connections and sizes are schematic.",
             ha="center", fontsize=13, color="#555555")
    fig.savefig("stoichiometry-particle-counts.png", dpi=100, facecolor="white", transparent=False)
    plt.close(fig)
    print("Verified 4 H and 2 O atoms on each side; exact count in two moles.")


if __name__ == "__main__":
    main()