codex/atomic-number-isotopes-and-ions.py

atomic-number-isotopes-and-ions.py
"""Draw particle inventories, not an atomic geometry or orbit model.

Writes atomic-number-isotopes-and-ions.png in the current working directory.
Run from _media to produce the repository image.
Tested with Python 3.14.4, Matplotlib 3.10.7 (+dfsg1), NumPy 2.3.5.
"""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import numpy as np


# name, isotope symbol, atomic number, mass number, charge in units of e
INVENTORIES = (
    ("Carbon-12: neutral", r"$^{12}_{6}\mathrm{C}$", 6, 12, 0),
    ("Carbon-13: neutral", r"$^{13}_{6}\mathrm{C}$", 6, 13, 0),
    ("Sodium-23: neutral", r"$^{23}_{11}\mathrm{Na}$", 11, 23, 0),
    ("Sodium-23: singly positive", r"$^{23}_{11}\mathrm{Na}^{+}$", 11, 23, 1),
)


def tokens(ax, count, y, color):
    """One evenly spaced token per particle; positions have no spatial meaning."""
    xs = 1.56 + np.arange(count) * 0.215
    ax.scatter(xs, np.full(count, y), s=77, color=color,
               edgecolors="white", linewidths=0.5, zorder=3)


def main():
    plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
                         "axes.facecolor": "white", "savefig.facecolor": "white"})
    fig, axes = plt.subplots(2, 2, figsize=(9.6, 6.8), dpi=120)
    fig.subplots_adjust(left=0.035, right=0.985, bottom=0.13, top=0.84,
                        wspace=0.09, hspace=0.36)
    fig.suptitle("Same element: change neutrons or electrons", fontsize=17, y=0.975)
    fig.text(0.5, 0.916, "Top pair: different isotopes   |   Bottom pair: neutral atom and ion",
             ha="center", fontsize=12)
    blue, gray, orange = "#2476a3", "#707b84", "#bd5a24"
    for ax, (name, symbol, z, a, q) in zip(axes.flat, INVENTORIES):
        neutrons, electrons = a - z, z - q
        ax.set(xlim=(0, 4.2), ylim=(0, 3.1))
        ax.axis("off")
        ax.text(0.12, 3.02, name, fontsize=12, weight="bold", va="top")
        ax.text(3.9, 3.04, symbol, fontsize=17, ha="right", va="top")
        ax.add_patch(Rectangle((0.07, 1.27), 4.08, 1.18, facecolor="#f4f7f9",
                               edgecolor="#9ca7af", lw=1))
        ax.text(0.19, 2.24, "Nucleus inventory", fontsize=10, color="#3f4d56", va="center")
        ax.text(0.2, 1.97, f"{z} protons", va="center", color=blue)
        ax.text(0.2, 1.53, f"{neutrons} neutrons", va="center", color=gray)
        tokens(ax, z, 1.97, blue)
        tokens(ax, neutrons, 1.53, gray)
        ax.text(0.2, 0.88, f"{electrons} electrons", va="center", color=orange)
        tokens(ax, electrons, 0.88, orange)
        charge = "0" if q == 0 else f"+{q}e"
        ax.text(0.2, 0.28, f"Z = {z}     A = {a}     Q = {charge}", fontsize=11)
    fig.text(0.5, 0.070, "One dot = one counted particle. Rows are inventories, not physical positions or orbits.",
             ha="center", fontsize=11)
    fig.text(0.5, 0.026, r"$N=A-Z$     and     $n_e=Z-Q/e$", ha="center", fontsize=13)
    fig.savefig(Path("atomic-number-isotopes-and-ions.png"), facecolor="white",
                transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()