codexhydration-shell-charge-orientation.py
hydration-shell-charge-orientation.py
"""Original qualitative hydration schematic; positions are not molecular data.
Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
Run from _media; the opaque PNG is written to the current directory.
"""
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
import numpy as np
def water_geometry(angle, positive, radius=1.65):
"""Return arbitrary 2D atom coordinates with the appropriate polar end inward."""
radial = np.array([np.cos(angle), np.sin(angle)])
tangent = np.array([-np.sin(angle), np.cos(angle)])
oxygen = radius * radial
direction = radial if positive else -radial
half_angle = np.deg2rad(52.25)
hydrogens = np.array([
oxygen + 0.40 * (np.cos(half_angle) * direction + sign * np.sin(half_angle) * tangent)
for sign in [-1, 1]
])
return oxygen, hydrogens
def draw_water(ax, oxygen, hydrogens):
for hydrogen in hydrogens:
ax.plot(*np.array([oxygen, hydrogen]).T, color="#7b8490", lw=2, zorder=2)
ax.add_patch(Circle(hydrogen, 0.10, facecolor="white", edgecolor="#677382", lw=1.2, zorder=4))
ax.add_patch(Circle(oxygen, 0.16, facecolor="#d64945", edgecolor="#a73131", lw=1, zorder=5))
def main():
plt.rcParams.update({"font.size": 12, "font.family": "DejaVu Sans"})
fig, axes = plt.subplots(1, 2, figsize=(10.8, 6.0), facecolor="white")
fig.subplots_adjust(left=0.04, right=0.96, bottom=0.22, top=0.75, wspace=0.12)
for ax, positive in zip(axes, [True, False]):
ax.set_aspect("equal")
ax.set_xlim(-2.7, 2.7)
ax.set_ylim(-2.55, 2.55)
ax.axis("off")
ax.add_patch(Circle((0, 0), 2.13, fill=False, edgecolor="#a4aebb", ls=(0, (4, 4)), lw=1.3))
ax.add_patch(Circle((0, 0), 0.55, facecolor="#e8f0fb" if positive else "#f1eafb", edgecolor="#526780", lw=1.5))
ax.text(0, 0, "$+e$" if positive else "$-e$", ha="center", va="center", fontsize=25)
# Five selected glyphs illustrate polarity; this is not a coordination count.
for angle in np.deg2rad([18, 90, 162, 234, 306]):
oxygen, hydrogens = water_geometry(angle, positive)
oxygen_r = np.linalg.norm(oxygen)
hydrogen_r = np.linalg.norm(hydrogens, axis=1)
assert np.all(hydrogen_r > oxygen_r) if positive else np.all(hydrogen_r < oxygen_r)
draw_water(ax, oxygen, hydrogens)
ax.set_title("Cation: oxygen end inward" if positive else "Anion: hydrogen end inward", fontsize=14, pad=16)
ax.text(0, -2.50, "Ion retains its positive charge" if positive else "Ion retains its negative charge", ha="center", fontsize=12)
fig.suptitle("Hydration changes the surroundings of an ion", fontsize=18, y=0.95)
fig.text(0.5, 0.86, "Selected water molecules in a schematic first shell", ha="center", color="#4d5968", fontsize=13)
fig.text(0.5, 0.15, r"Red: oxygen ($\delta^-$) White: hydrogen ($\delta^+$) Each water molecule is neutral", ha="center", fontsize=12)
fig.text(0.5, 0.08, "Positions and distances are illustrative; five glyphs do not specify a coordination number.", ha="center", fontsize=11)
fig.text(0.5, 0.035, "Real shells fluctuate and exchange molecules with the surrounding water.", ha="center", fontsize=11)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=110, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()