codexzero-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()