codex/dielectric-microscopic-displacement-and-orientation.py

dielectric-microscopic-displacement-and-orientation.py
"""Generate an illustrative microscopic dielectric diagram (Python 3.14).

Requires the repository's matplotlib 3.10.7 and numpy 2.3.5 environment.
Run from _media; all geometry is schematic, not molecular simulation data.
"""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import Circle, FancyArrowPatch
import numpy as np


NEGATIVE = "#2566a0"
POSITIVE = "#bc443c"
ARROW = "#325940"


def charge(ax, xy, sign, radius=0.075):
    color = POSITIVE if sign == "+" else NEGATIVE
    ax.add_patch(Circle(xy, radius, facecolor=color, edgecolor="white", zorder=4))
    ax.text(*xy, sign, color="white", ha="center", va="center", fontsize=11, zorder=5)


def dipole(ax, x, y, angle):
    direction = np.array([np.cos(angle), np.sin(angle)])
    midpoint = np.array([x, y])
    tail, head = midpoint - 0.23 * direction, midpoint + 0.23 * direction
    ax.add_patch(FancyArrowPatch(tail, head, arrowstyle="-|>", mutation_scale=12,
                                color=ARROW, linewidth=1.7, zorder=3))
    charge(ax, tail, "−", 0.065)
    charge(ax, head, "+", 0.065)


def main():
    plt.rcParams.update({"font.size": 12, "font.family": "DejaVu Sans"})
    fig, axes = plt.subplots(2, 2, figsize=(10, 6), dpi=110)
    fig.patch.set_facecolor("white")
    fig.suptitle("Displacement and orientation both produce polarization", fontsize=17, y=0.98)
    for ax in axes.flat:
        ax.set(xlim=(0, 4), ylim=(0, 2.35), aspect="equal")
        ax.axis("off")
        ax.set_facecolor("white")

    axes[0, 0].set_title("Charge centres coincide • zero field", fontsize=13)
    axes[0, 1].set_title("Charge centres separate • field applied", fontsize=13)
    for col, ax in enumerate(axes[0]):
        for x in (0.8, 2.0, 3.2):
            centre = x - 0.17 * col
            ax.add_patch(Circle((centre, 1.2), 0.40, facecolor="#dbeafa", edgecolor=NEGATIVE,
                                linewidth=1.5))
            ax.plot(centre, 1.2, "o", color=NEGATIVE, markersize=4, zorder=3)
            charge(ax, (x + 0.17 * col, 1.2), "+")
            if col:
                ax.add_patch(FancyArrowPatch((centre, 0.62), (x + 0.17, 0.62),
                                            arrowstyle="-|>", color=ARROW, mutation_scale=12))
        ax.text(2, 0.15, "Neutral units; positive and negative charge remain equal",
                ha="center", fontsize=10)
    axes[0, 0].text(2, 1.95, "Blue cloud: negative charge", color=NEGATIVE, ha="center", fontsize=11)
    axes[0, 1].annotate("E", xy=(3.6, 1.95), xytext=(2.8, 1.95), va="center",
                        arrowprops={"arrowstyle": "->", "color": ARROW}, color=ARROW)

    axes[1, 0].set_title("Existing dipoles • cancelling orientations", fontsize=13)
    axes[1, 1].set_title("Existing dipoles • biased orientations", fontsize=13)
    zero_angles = np.arange(8) * np.pi / 4
    biased_angles = np.deg2rad([-80, -45, -20, 10, 35, 65, 105, -60])
    for col, angles in enumerate((zero_angles, biased_angles)):
        ax = axes[1, col]
        for j, angle in enumerate(angles):
            dipole(ax, 0.55 + (j % 4) * 0.95, 1.65 - (j // 4) * 0.85, angle)
        ax.text(2, 0.14, r"$\langle\mathbf{p}\rangle=0$" if col == 0 else
                r"$\langle p_x\rangle>0$; alignment is incomplete", ha="center", fontsize=12)
    axes[1, 1].annotate("E", xy=(3.6, 2.2), xytext=(2.8, 2.2), va="center",
                        arrowprops={"arrowstyle": "->", "color": ARROW}, color=ARROW)
    fig.text(0.5, 0.02, "Schematic only • dipole arrows point from − to + • no charge flows between electrodes",
             ha="center", fontsize=11)
    fig.subplots_adjust(top=0.87, bottom=0.09, left=0.025, right=0.975, hspace=0.23, wspace=0.05)
    fig.savefig(Path(__file__).stem + ".png", facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()