codex/lewis-water-ammonia.py

lewis-water-ammonia.py
"""Draw original Lewis electron counts; write the matching PNG to cwd.

Build: make _media/lewis-water-ammonia.png
Tested with Python 3.14.4, Matplotlib 3.10.7, NumPy 2.3.5.
Layouts show allocations, not molecular geometries or electron trajectories.
"""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
import numpy as np


BOND = "#216b97"
LONE = "#a44f25"


def draw_bond(axis, start, end):
    start, end = np.asarray(start, dtype=float), np.asarray(end, dtype=float)
    direction = (end - start) / np.linalg.norm(end - start)
    endpoints = np.array([start + 0.25 * direction, end - 0.25 * direction])
    axis.plot(*endpoints.T, color=BOND, lw=3.2, solid_capstyle="round")


def draw_pair(axis, position):
    x, y = position
    axis.scatter([x - 0.07, x + 0.07], [y, y], s=36, color=LONE, zorder=3)


def main():
    plt.rcParams.update({"font.size": 12, "figure.facecolor": "white",
                         "axes.facecolor": "white", "savefig.facecolor": "white"})
    fig, axes = plt.subplots(1, 2, figsize=(8.5, 5.4), dpi=120)
    fig.subplots_adjust(left=0.055, right=0.945, bottom=0.32, top=0.75, wspace=0.3)
    fig.suptitle("Same electron budget, different pair allocations", fontsize=17, y=0.96)
    examples = [
        ("Water", r"$\mathrm{H_2O}$", "O", 6,
         [(-1.05, 0), (1.05, 0)], [(0, 0.48), (0, -0.48)]),
        ("Ammonia", r"$\mathrm{NH_3}$", "N", 5,
         [(-1.05, 0), (1.05, 0), (0, -0.95)], [(0, 0.48)]),
    ]
    for axis, (name, formula, centre, valence, hydrogens, pairs) in zip(axes, examples):
        shared = len(hydrogens)
        nonbonding = len(pairs)
        total = valence + shared  # Each neutral hydrogen supplies one electron.
        assert total == 8 == 2 * shared + 2 * nonbonding
        assert 2 * (shared + nonbonding) == 8  # Central atom's local octet.
        assert valence - 2 * nonbonding - shared == 0  # Central formal charge.
        assert 1 - 0 - 2 / 2 == 0  # Each hydrogen's formal charge.
        axis.set(xlim=(-1.5, 1.5), ylim=(-1.25, 0.85), aspect="equal")
        axis.axis("off")
        axis.set_title(f"{name}: {formula}", fontsize=15, pad=29)
        axis.text(0.5, 1.05, f"{valence} + {shared} × 1 = {total} valence electrons",
                  ha="center", transform=axis.transAxes)
        axis.text(0, 0, centre, ha="center", va="center", fontsize=30)
        for position in hydrogens:
            axis.text(*position, "H", ha="center", va="center", fontsize=28)
            draw_bond(axis, (0, 0), position)
        for position in pairs:
            draw_pair(axis, position)
        pair_word = "pairs" if nonbonding > 1 else "pair"
        axis.text(0.5, -0.02, f"{shared} bond pairs + {nonbonding} lone {pair_word}\n"
                  f"{2 * shared} shared + {2 * nonbonding} nonbonding = 8 electrons",
                  transform=axis.transAxes, ha="center", va="top", linespacing=1.6)
        print(f"{formula}: total={total}; bond pairs={shared}; lone pairs={nonbonding}; "
              "central local count=8; each H local count=2; all formal charges=0")

    fig.text(0.5, 0.195, "Local counts: O and N each count 8 electrons; each H counts 2.",
             ha="center", fontsize=11)
    fig.text(0.5, 0.145, "Count each shared pair once in the molecule's budget.",
             ha="center", fontsize=11)
    fig.legend(handles=[Line2D([0], [0], color=BOND, lw=3, label="Line = 2 shared electrons"),
                        Line2D([0], [0], marker="o", color=LONE, ls="",
                               label="Dot = 1 nonbonding electron")],
               loc="lower center", bbox_to_anchor=(0.5, 0.072), ncol=2,
               frameon=False, fontsize=10)
    fig.text(0.5, 0.035, "Schematic Lewis layouts: no shape, angle, or electron path is implied.",
             ha="center", fontsize=10, color="#555555")
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name, transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()