codexstoichiometry-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()