codex/first-three-periods.py

first-three-periods.py
"""Draw the first 18 elements at their actual 18-column table positions.

Writes first-three-periods.png in the current working directory.
Run from _media to produce the repository image. Colours distinguish columns,
not measured trends. Counts are neutral ground-state shell occupancies only.
Positions and symbols: https://iupac.org/what-we-do/periodic-table-of-elements/
Configurations: OpenStax Chemistry 2e, section 6.4.
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


# atomic number, symbol, period, group, neutral ground-state shell occupancies
ELEMENTS = (
    (1, "H", 1, 1, (1,)), (2, "He", 1, 18, (2,)),
    (3, "Li", 2, 1, (2, 1)), (4, "Be", 2, 2, (2, 2)),
    (5, "B", 2, 13, (2, 3)), (6, "C", 2, 14, (2, 4)),
    (7, "N", 2, 15, (2, 5)), (8, "O", 2, 16, (2, 6)),
    (9, "F", 2, 17, (2, 7)), (10, "Ne", 2, 18, (2, 8)),
    (11, "Na", 3, 1, (2, 8, 1)), (12, "Mg", 3, 2, (2, 8, 2)),
    (13, "Al", 3, 13, (2, 8, 3)), (14, "Si", 3, 14, (2, 8, 4)),
    (15, "P", 3, 15, (2, 8, 5)), (16, "S", 3, 16, (2, 8, 6)),
    (17, "Cl", 3, 17, (2, 8, 7)), (18, "Ar", 3, 18, (2, 8, 8)),
)


def main():
    plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
                         "axes.facecolor": "white", "savefig.facecolor": "white"})
    fig, ax = plt.subplots(figsize=(12.8, 5.8), dpi=110)
    fig.subplots_adjust(left=0.055, right=0.985, bottom=0.27, top=0.76)
    fig.suptitle("First three periods: atomic number and shell counts", fontsize=18, y=0.96)
    fig.text(0.5, 0.885, "Neutral ground states only; colours distinguish group columns, not property trends",
             ha="center", fontsize=12)
    colors = {1: "#d9edf6", 2: "#f8e4ce", 13: "#ebe0f3", 14: "#e3e7eb",
              15: "#dceaf8", 16: "#edeecf", 17: "#f7dfe4", 18: "#dceee1"}
    ax.set(xlim=(-0.85, 18.05), ylim=(-0.05, 3.08))
    ax.axis("off")
    for group in np.arange(1, 19):
        ax.text(group - 0.5, 3.20, str(group), ha="center", va="bottom", fontsize=11,
                clip_on=False)
    ax.text(8.5, 3.57, "Group (column)", ha="center", fontsize=12, clip_on=False)
    for period in (1, 2, 3):
        ax.text(-0.36, 3 - period + 0.5, str(period), ha="center", va="center", fontsize=13)
    ax.text(-0.73, 1.5, "Period (row)", rotation=90, va="center", ha="center", fontsize=12)
    for z, symbol, period, group, shell_counts in ELEMENTS:
        x, y = group - 1, 3 - period
        ax.add_patch(Rectangle((x + 0.025, y + 0.025), 0.95, 0.95,
                               facecolor=colors[group], edgecolor="#82909a", lw=1))
        ax.text(x + 0.14, y + 0.81, str(z), fontsize=10, va="center")
        ax.text(x + 0.5, y + 0.52, symbol, fontsize=17, weight="bold", ha="center", va="center")
        ax.text(x + 0.5, y + 0.17, ",".join(map(str, shell_counts)),
                fontsize=10, ha="center", va="center")
    ax.text(7, 1.51, "Columns 3–12 have no entries\nin the first three periods",
            ha="center", va="center", color="#67727a", fontsize=13)
    fig.text(0.075, 0.185, "Cell key: upper-left = atomic number Z; centre = symbol; bottom = shell counts",
             fontsize=12)
    fig.text(0.075, 0.124, "Compare Li (2,1) with Na (2,8,1): one outer electron in each.", fontsize=12)
    fig.text(0.075, 0.063, "The 2,8,8 summary stops at Ar. It is not a general shell-filling rule or an orbit model.",
             fontsize=12)
    fig.savefig(Path("first-three-periods.png"), facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()