codex/vsepr-four-domain-shapes.py

vsepr-four-domain-shapes.py
"""Idealized four-domain frameworks for CH4, NH3 and H2O.

These ideal tetrahedral directions illustrate domain counting and molecular
shape, not measured bond angles or lone-pair shapes. Writes PNG to CWD.
Tested with Python 3.14, NumPy 2.3.5 and Matplotlib 3.10.7.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    plt.rcParams.update({"font.size": 12})
    directions = np.array([[1, 1, 1], [1, -1, -1],
                           [-1, 1, -1], [-1, -1, 1]], dtype=float)/np.sqrt(3)
    # Remove selected bonded neighbours while retaining their domain directions.
    cases = [
        ("Methane  CH₄", "C", "#374653", set(), "4 bond pairs + 0 lone pairs\nTetrahedral"),
        ("Ammonia  NH₃", "N", "#386fa6", {0}, "3 bond pairs + 1 lone pair\nTrigonal pyramidal"),
        ("Water  H₂O", "O", "#bd4244", {0, 3}, "2 bond pairs + 2 lone pairs\nBent"),
    ]
    fig = plt.figure(figsize=(11.8, 6.3), dpi=100, facecolor="white")
    fig.subplots_adjust(left=0.015, right=0.985, top=0.82, bottom=0.22,
                         wspace=0.02)
    for panel, (title, centre, color, lone_pairs, description) in enumerate(cases, 1):
        ax = fig.add_subplot(1, 3, panel, projection="3d")
        for index, direction in enumerate(directions):
            lp = index in lone_pairs
            ax.plot([0, direction[0]], [0, direction[1]], [0, direction[2]],
                    color="#d2933e" if lp else "#5b6d79",
                    linestyle="--" if lp else "-", linewidth=1.7 if lp else 3)
            ax.scatter(*direction, s=700 if lp else 240,
                       c="#e3ab50" if lp else "#f0f3f5",
                       alpha=0.35 if lp else 1,
                       edgecolors="#cb9845" if lp else "#8d9aa4",
                       linewidths=1)
            ax.text(*(direction*1.16), "LP" if lp else "H",
                    color="#936119" if lp else "#334451", ha="center",
                    va="center", fontsize=12)
        ax.scatter(0, 0, 0, s=390, c=color, edgecolors="white", linewidths=1)
        ax.text(0, 0, 0, centre, color="white", ha="center",
                va="center", fontsize=11, weight="bold", zorder=10)
        ax.set(xlim=(-0.86, 0.86), ylim=(-0.86, 0.86), zlim=(-0.86, 0.86))
        ax.set_box_aspect((1, 1, 1))
        ax.view_init(elev=18, azim=-32)
        ax.set_proj_type("ortho")
        ax.set_axis_off()
        ax.set_title(title, fontsize=14, pad=5)
        ax.text2D(0.5, -0.04, description, transform=ax.transAxes,
                  ha="center", va="top", fontsize=12, linespacing=1.5)
    fig.suptitle("Same four-domain framework, different molecular shapes",
                 fontsize=16, y=0.965)
    fig.text(0.5, 0.88, "Count lone pairs as domains; count only atoms for molecular geometry.",
             ha="center", fontsize=12)
    fig.text(0.5, 0.05,
             "Shaded LP regions stand for lone-pair domains, not additional atoms.\n"
             "Directions are idealized. Real ammonia and water bond angles differ from the tetrahedral 109.47°.",
             ha="center", fontsize=11, linespacing=1.5, color="#444")
    fig.savefig(Path.cwd() / "vsepr-four-domain-shapes.png",
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()