codexradical-charge-and-orbital-occupancy.py
radical-charge-and-orbital-occupancy.py
"""Generate radical-charge-and-orbital-occupancy.png in the current directory.
Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
Left: hydrogen ground-state occupancies. Right: invented two-orbital models.
Run from _media with MPLBACKEND=Agg and MPLCONFIGDIR in /tmp.
"""
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
def orbital(ax, x, y, occupancy):
ax.add_patch(Rectangle((x - 0.24, y - 0.20), 0.48, 0.40,
facecolor="white", edgecolor="#45586c", lw=1.4))
for j in range(occupancy):
xx = x if occupancy == 1 else x - 0.09 + 0.18 * j
dy = 0.26 if j == 0 else -0.26
ax.annotate("", (xx, y + dy / 2), (xx, y - dy / 2),
arrowprops={"arrowstyle": "->", "lw": 1.8, "color": "#216c91"})
def inventories():
# label, proton count, spatial-orbital occupations, expected charge, unpaired
return [(r"$\mathrm{H}^{+}$", 1, (0,), 1, 0),
(r"$\mathrm{H}^{\bullet}$", 1, (1,), 0, 1),
(r"$\mathrm{H}^{-}$", 1, (2,), -1, 0),
("Neutral model", 2, (1, 1), 0, 2),
("Cation model", 2, (1, 0), 1, 1),
("Anion model", 2, (2, 1), -1, 1)]
def main():
records = inventories()
for _, protons, occ, charge, unpaired in records:
assert all(n in (0, 1, 2) for n in occ)
assert protons - sum(occ) == charge
assert sum(n == 1 for n in occ) == unpaired
fig, axes = plt.subplots(1, 2, figsize=(10.2, 4.7), dpi=120, facecolor="white")
fig.suptitle("Charge counts particles; radical character counts unpaired electrons",
fontsize=14, weight="bold", y=0.98)
for panel, ax in enumerate(axes):
ax.set(xlim=(0, 5), ylim=(-0.8, 4.2))
ax.axis("off")
ax.text(2.5, 3.9, ["Hydrogen ground states: one proton",
"Invented models: two protons"][panel], ha="center",
fontsize=12, weight="bold")
ax.text(1.85, 3.32, [r"$1s$ orbital", "Orbitals A, B"][panel],
ha="center", fontsize=11)
ax.text(3.0, 3.32, r"$q/e$", ha="center", fontsize=12)
ax.text(4.1, 3.32, "Unpaired", ha="center", fontsize=11)
for row, (label, _, occ, charge, unpaired) in enumerate(records[3 * panel:3 * panel + 3]):
y = 2.65 - row * 1.0
if unpaired:
ax.add_patch(Rectangle((0.02, y - 0.35), 4.93, 0.70,
facecolor="#eef6fa", edgecolor="none"))
ax.text(0.1, y, label, fontsize=11.2, va="center")
for j, n in enumerate(occ):
orbital(ax, 1.6 + j * 0.65 if panel else 1.85, y, n)
ax.text(3.0, y, f"{charge:+d}" if charge else "0",
fontsize=13, ha="center", va="center")
ax.text(4.1, y, str(unpaired), fontsize=13, ha="center", va="center")
ax.text(2.5, -0.28, ["Only neutral H is a radical here.",
"All three models have unpaired electrons."][panel],
ha="center", fontsize=11, color="#216c91")
fig.text(0.5, 0.035, "Boxes are spatial orbitals; arrows label opposite spins. Right-hand models are not named substances.",
ha="center", fontsize=10.5)
fig.subplots_adjust(left=0.015, right=0.995, top=0.88, bottom=0.13, wspace=0.04)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()