codex/zero-net-flux-nonzero-field.py

zero-net-flux-nonzero-field.py
#!/usr/bin/env python3
"""Original y=0 section of a 3D Coulomb dipole; run from _media.

Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
Lengths are normalized by R; field by kq/R^2. The source singularities
are masked for plotting. No two-dimensional Gauss integral is used.
"""

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Circle


def main():
    plt.rcParams.update({"font.size": 11, "figure.facecolor": "white"})
    fig, ax = plt.subplots(figsize=(8.4, 7.1), facecolor="white")
    grid = np.linspace(-1.5, 1.5, 501)
    x, z = np.meshgrid(grid, grid)
    ex, ez = np.zeros_like(x), np.zeros_like(z)
    mask = np.zeros_like(x, dtype=bool)
    for source_x, charge in ((-0.3, 1.0), (0.3, -1.0)):
        dx = x - source_x
        squared_distance = dx**2 + z**2
        mask |= squared_distance < 0.075**2
        # In the y=0 plane, the 3D inverse-square vector field has r^3
        # in its denominator, unlike a 2D logarithmic potential field.
        denominator = np.maximum(squared_distance, 1e-12)**1.5
        ex += charge * dx / denominator
        ez += charge * z / denominator
    ex, ez = np.ma.array(ex, mask=mask), np.ma.array(ez, mask=mask)
    ax.streamplot(grid, grid, ex, ez, color="#7499ac", density=1.15,
                  linewidth=0.85, arrowsize=1.0, zorder=1)
    ax.add_patch(Circle((0, 0), 1, fill=False, edgecolor="#222222",
                        linewidth=2.1, linestyle="--", zorder=3))
    for source_x, color, label in ((-0.3, "#c64f24", "+q"),
                                   (0.3, "#0072b2", "−q")):
        ax.add_patch(Circle((source_x, 0), 0.085, facecolor=color,
                            edgecolor="white", zorder=5))
        ax.text(source_x, 0, label, color="white", ha="center", va="center",
                fontsize=12, weight="bold", zorder=6)
        ax.text(source_x, -0.19, f"x/R={source_x:+.1f}",
                ha="center", fontsize=10, bbox={"facecolor": "white", "alpha": 0.9,
                                              "edgecolor": "none"}, zorder=7)
    ax.plot(0, 1, "o", color="#111111", markersize=5, zorder=7)
    ax.annotate("", xy=(0.55, 1), xytext=(0, 1),
                arrowprops={"arrowstyle": "-|>", "color": "#0072b2", "lw": 2.8}, zorder=8)
    ax.annotate("", xy=(0, 1.43), xytext=(0, 1),
                arrowprops={"arrowstyle": "-|>", "color": "#b34b00", "lw": 2.5}, zorder=8)
    ax.text(0.10, 1.37, r"outward $\mathbf{n}$", color="#b34b00", fontsize=12)
    ax.text(0.40, 1.08, r"$\mathbf{E}(P)\ne0$", color="#0072b2", fontsize=13,
            bbox={"facecolor": "white", "edgecolor": "none", "alpha": 0.9})
    ax.text(-0.11, 1.06, "P", fontsize=12)
    for text, xy, location in (("outward crossing", (-0.82, 0.57), (-1.47, 0.83)),
                                ("inward crossing", (0.82, 0.57), (0.75, 0.83))):
        ax.annotate(text, xy=xy, xytext=location, fontsize=10,
                    arrowprops={"arrowstyle": "->", "color": "#222222"},
                    bbox={"facecolor": "white", "edgecolor": "none", "alpha": 0.95},
                    zorder=9)
    ax.text(0, -1.21, r"$Q_{\mathrm{enclosed}}=q-q=0$" + "\n" +
            r"$\oint_{\mathrm{sphere}}\mathbf{E}\cdot\mathbf{n}\,dA=0$",
            ha="center", va="center", fontsize=14,
            bbox={"facecolor": "white", "edgecolor": "#dddddd", "pad": 6}, zorder=10)
    ax.set(xlim=(-1.55, 1.55), ylim=(-1.48, 1.53), aspect="equal",
           xlabel=r"$x/R$", ylabel=r"$z/R$")
    ax.set_xticks([-1, -0.3, 0, 0.3, 1])
    ax.set_yticks([-1, 0, 1])
    ax.spines[["right", "top"]].set_visible(False)
    ax.set_title("Zero net flux, nonzero electric field\n"
                 "3D dipole cross-section: y = 0, sphere radius R = 1 m",
                 fontsize=15, pad=15)
    fig.text(0.5, 0.033, "Dashed circle = sphere cross-section; flux is over the full sphere.\n"
             "Curves follow the 3D Coulomb field; arrow lengths at P are schematic.",
             ha="center", fontsize=10)
    fig.subplots_adjust(left=0.13, right=0.95, top=0.85, bottom=0.16)
    fig.savefig("zero-net-flux-nonzero-field.png", dpi=120, facecolor="white",
                transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()