codexsolenoid-ampere-loop.py
solenoid-ampere-loop.py
"""Draw a consistently oriented Ampere loop through an ideal infinite winding.
Tested with Python 3.14.4, matplotlib 3.10.7 and numpy 2.3.5.
Write the same-basename opaque PNG to CWD; use the root Make media target.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, FancyArrowPatch, Rectangle
import numpy as np
def arrow(ax, start, end, colour, scale=15, width=2):
ax.add_patch(FancyArrowPatch(start, end, arrowstyle="-|>", mutation_scale=scale,
color=colour, lw=width, zorder=5))
def main():
# Physical coordinates: +x is page-up, +y is out of page, +z is page-right.
# Top wire dl=+y and source-to-axis R=-x give dl cross R=+z.
np.testing.assert_array_equal(np.cross([0, 1, 0], [-1, 0, 0]), [0, 0, 1])
np.testing.assert_array_equal(np.cross([0, -1, 0], [1, 0, 0]), [0, 0, 1])
wire_z = np.arange(-4.5, 5, 1.0)
loop_left, loop_right = -3.0, 3.0
crossings = wire_z[(wire_z > loop_left) & (wire_z < loop_right)]
density, path_length = 1.0, loop_right - loop_left
assert len(crossings) == density * path_length == 6
# Interior +z followed by outward +x: oriented surface normal z cross x=+y.
np.testing.assert_array_equal(np.cross([0, 0, 1], [1, 0, 0]), [0, 1, 0])
print("Right-hand field and loop-normal signs verified; ns=6 upper crossings, 0 lower crossings.")
plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 11})
blue, orange, grey = "#216baf", "#b55c2b", "#52616c"
fig, ax = plt.subplots(figsize=(8, 5), dpi=120, facecolor="white")
ax.set(xlim=(-5.8, 6.0), ylim=(-3.0, 3.6), aspect="equal")
ax.axis("off")
ax.add_patch(Rectangle((-5.4, -1), 10.8, 2, color="#edf3f7", zorder=0))
for row in (-1, 1):
ax.plot([-5.5, 5.5], [row, row], color=grey, lw=1, ls=":")
for z in wire_z:
ax.add_patch(Circle((z, row), 0.14, facecolor="white", edgecolor=orange, lw=1.8, zorder=3))
if row == 1:
ax.add_patch(Circle((z, row), 0.045, color=orange, zorder=4))
else:
ax.plot([z - 0.07, z + 0.07], [row - 0.07, row + 0.07], color=orange, lw=1.5, zorder=4)
ax.plot([z - 0.07, z + 0.07], [row + 0.07, row - 0.07], color=orange, lw=1.5, zorder=4)
ax.text(-5.75, row, "…", va="center", ha="center", color=grey)
ax.text(5.75, row, "…", va="center", ha="center", color=grey)
# The lower path lies inside and goes right; the exterior return goes left.
bottom, top = 0.0, 2.1
ax.plot([loop_left, loop_right, loop_right, loop_left, loop_left],
[bottom, bottom, top, top, bottom], color=orange, lw=2)
arrow(ax, (-1.7, bottom), (-0.9, bottom), orange)
arrow(ax, (loop_right, 1.3), (loop_right, 1.9), orange)
arrow(ax, (1.0, top), (0.2, top), orange)
arrow(ax, (loop_left, 0.8), (loop_left, 0.2), orange)
for x in (-0.55, 0.55):
for z in (-4.7, -1.8, 1.2, 4.1):
arrow(ax, (z, x), (z + 0.65, x), blue, scale=12, width=1.7)
ax.text(0, 3.25, "An Ampère loop crosses one side of the winding", ha="center", fontsize=14)
ax.text(0, 2.7, r"Exterior return: $\mathbf{B}=0$ in the infinite ideal model", ha="center", color=grey)
ax.text(-2.7, 1.65, r"$ns=6$ positive current crossings", color=orange, fontsize=10)
ax.text(3.3, 1.8, r"$\Gamma$", color=orange, fontsize=14)
ax.text(0, -1.48, r"Inside: $\mathbf{B}=\mu_0 n I\,\hat{\mathbf{z}}$", ha="center", color=blue)
arrow(ax, (3.9, -1.6), (5.0, -1.6), grey, scale=12, width=1.2)
ax.text(5.12, -1.6, r"$+z$", va="center", color=grey)
ax.annotate("", (loop_right, -1.98), (loop_left, -1.98),
arrowprops={"arrowstyle": "<->", "color": grey, "lw": 1.1})
ax.text(0, -2.25, r"Path length $s$: $Bs=\mu_0(ns)I$", ha="center")
ax.text(0, -2.85, "Dot: current out of page • Cross: current into page | Winding continues at both sides",
ha="center", fontsize=9, color=grey)
fig.subplots_adjust(left=0.025, right=0.975, bottom=0.015, top=0.98)
fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()