codex/open-surface-electric-flux.py

open-surface-electric-flux.py
#!/usr/bin/env python3
"""Original oriented-plane diagram; run from _media to save the PNG there.

Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
Arrow lengths are schematic; patches share one area and the field is constant.
"""

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.mplot3d.art3d import Poly3DCollection


def main():
    plt.rcParams.update({"font.size": 11, "figure.facecolor": "white"})
    fig = plt.figure(figsize=(11.8, 4.7), facecolor="white")
    field_color, normal_color = "#0072b2", "#b34b00"
    for index, degrees in enumerate((0, 60, 120), start=1):
        ax = fig.add_subplot(1, 3, index, projection="3d")
        theta = np.deg2rad(degrees)
        normal = np.array([np.cos(theta), 0.0, np.sin(theta)])
        tangent = np.array([-np.sin(theta), 0.0, np.cos(theta)])
        side = np.array([0.0, 1.0, 0.0])
        corners = [0.68 * (u * tangent + v * side)
                   for u, v in ((-1, -1), (1, -1), (1, 1), (-1, 1))]
        ax.add_collection3d(Poly3DCollection(
            [corners], facecolors="#a9cdda", edgecolors="#447280",
            linewidths=1.2, alpha=0.48))
        # Parallel equal arrows illustrate a single constant 3D field.
        for offset in (-0.46, 0.46):
            start = offset * side - 0.84 * np.array([1.0, 0.0, 0.0])
            ax.quiver(*start, 1.65, 0, 0, color=field_color,
                      linewidth=1.6, arrow_length_ratio=0.12)
        # Both reference arrows start at the same point on the patch.
        origin = -0.12 * side
        ax.quiver(*origin, 1.08, 0, 0, color=field_color,
                  linewidth=2.5, arrow_length_ratio=0.15)
        ax.quiver(*origin, *(0.96 * normal), color=normal_color,
                  linewidth=2.7, arrow_length_ratio=0.18)
        ax.text(*(origin + 1.24 * np.array([1.0, 0.0, 0.0])),
                r"$\mathbf{E}$", color=field_color, fontsize=14)
        label = origin + 1.1 * normal + 0.32 * side + 0.12 * tangent
        ax.text(*label, r"$\mathbf{n}$", color=normal_color, fontsize=14)
        if degrees:
            angles = np.linspace(0, theta, 80)
            arc = origin[:, None] + 0.38 * np.vstack(
                (np.cos(angles), np.zeros_like(angles), np.sin(angles)))
            ax.plot(*arc, color="#333333", linewidth=1.4)
            mid = origin + 0.55 * np.array([np.cos(theta / 2), 0,
                                           np.sin(theta / 2)])
            ax.text(*mid, rf"${degrees}^\circ$", fontsize=11)
        else:
            ax.text2D(0.06, 0.80, "E and n point\nthe same way", transform=ax.transAxes,
                      fontsize=10, color="#444444")
        value = int(round(100 * 0.02 * np.dot(normal, [1, 0, 0])))
        ax.set_title(rf"$\theta={degrees}^\circ$" + "\n" +
                     rf"$\Phi_E={value}\ \mathrm{{N\,m^2/C}}$", pad=9)
        ax.text2D(0.29, 0.04, r"open patch: $A=0.02\ \mathrm{m^2}$",
                  transform=ax.transAxes, fontsize=10, ha="center")
        ax.set(xlim=(-1.25, 1.4), ylim=(-1, 1), zlim=(-1, 1.4))
        ax.set_box_aspect((2.65, 2, 2.4))
        ax.view_init(elev=40, azim=-55)
        ax.set_axis_off()
        ax.set_facecolor("white")
    fig.suptitle("Electric flux through an oriented open plane", fontsize=18, y=0.97)
    fig.text(0.5, 0.025,
             r"Constant $E=100\ \mathrm{N/C}$    |    $\Phi_E=EA\cos\theta$    |    "
             r"$\theta$ is the angle to the chosen normal (orange)",
             ha="center", fontsize=12)
    fig.subplots_adjust(left=0.015, right=0.98, bottom=0.12, top=0.76, wspace=0.08)
    fig.savefig("open-surface-electric-flux.png", dpi=120, facecolor="white",
                transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()