codexchemical-equation-atom-balance.py
chemical-equation-atom-balance.py
"""Write chemical-equation-atom-balance.png to the current working directory.
Invented atom-token schematic, not a geometry model or a reaction mechanism.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Run from _media: MPLCONFIGDIR=/tmp python3 ../chemical-equation-atom-balance.py
"""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
import numpy as np
COLORS = {"H": "#246baa", "O": "#c64b39"}
def tokens(ax, center, elements):
offsets = np.linspace(-0.40 * (len(elements) - 1),
0.40 * (len(elements) - 1), len(elements))
points = np.column_stack((center[0] + offsets,
np.full(len(elements), center[1])))
ax.plot(points[:, 0], points[:, 1], color="#555555", lw=3, zorder=1)
for (x, y), element in zip(points, elements):
ax.add_patch(Circle((x, y), 0.27, facecolor=COLORS[element],
edgecolor="white", lw=1.2, zorder=2))
ax.text(x, y, element, color="white", fontsize=15, weight="bold",
ha="center", va="center", zorder=3)
def main():
# H and O rows; H2, O2, H2O columns. Exact integer bookkeeping.
composition = np.array([[2, 0, 2], [0, 2, 1]], dtype=int)
left = composition @ np.array([2, 1, 0])
right = composition @ np.array([0, 0, 2])
np.testing.assert_array_equal(left, [4, 2])
np.testing.assert_array_equal(left, right)
plt.rcParams.update({"font.size": 13, "savefig.facecolor": "white"})
fig = plt.figure(figsize=(10, 6.2), facecolor="white")
fig.suptitle("A balanced equation keeps the atom inventory", fontsize=20, y=0.97)
fig.text(0.5, 0.855, "2 H₂ + 1 O₂ → 2 H₂O", ha="center", fontsize=23)
ax = fig.add_axes([0.035, 0.49, 0.93, 0.29])
ax.set(xlim=(0, 12), ylim=(-0.9, 1.1), aspect="equal")
ax.axis("off")
tokens(ax, (0.7, 0.2), "HH")
tokens(ax, (2.3, 0.2), "HH")
tokens(ax, (4.5, 0.2), "OO")
tokens(ax, (8.4, 0.2), "HOH")
tokens(ax, (10.6, 0.2), "HOH")
ax.text(3.25, 0.2, "+", fontsize=25, ha="center", va="center")
ax.annotate("", xy=(7, 0.2), xytext=(5.7, 0.2),
arrowprops={"arrowstyle": "->", "lw": 2.3})
ax.text(2.6, -0.7, "Reactants: three molecules", ha="center", fontsize=13)
ax.text(9.5, -0.7, "Products: two molecules", ha="center", fontsize=13)
bars = fig.add_axes([0.13, 0.16, 0.74, 0.27])
x = np.arange(2)
bars.bar(x - 0.18, left, 0.36, color="#246baa", label="Before")
bars.bar(x + 0.18, right, 0.36, color="#ed9c49", label="After")
bars.set(xticks=x, xticklabels=["H atoms", "O atoms"], ylim=(0, 5.5),
yticks=[0, 1, 2, 3, 4], ylabel="Atom count")
bars.spines[["top", "right"]].set_visible(False)
bars.legend(loc="upper right", frameon=False, ncol=2)
for offset, values in [(-0.18, left), (0.18, right)]:
for i, count in enumerate(values):
bars.text(i + offset, count + 0.08, str(count), ha="center", fontsize=13)
fig.text(0.5, 0.045, "Invented schematic: lines show connections, not real lengths or angles.",
ha="center", fontsize=12, color="#555555")
fig.savefig("chemical-equation-atom-balance.png", dpi=110,
facecolor="white", transparent=False)
plt.close(fig)
print("Exact atom counts before/after: H = 4/4, O = 2/2.")
if __name__ == "__main__":
main()