codex/wave-polarization-trajectories.py

wave-polarization-trajectories.py
"""Real electric-field trajectories at one fixed position.

Tested with Python 3.14, matplotlib 3.10.7, numpy 2.3.5.
Write the basename PNG into cwd; run from _media for the wiki.
"""

from pathlib import Path

import matplotlib

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


def field(phase, amplitude_x, amplitude_y, delta):
    return np.array([amplitude_x * np.cos(phase),
                     amplitude_y * np.cos(phase + delta)])


def main():
    phase = np.linspace(0, 2 * np.pi, 721)
    examples = [(2.0, 1.0, 0.0), (1.0, 1.0, np.pi / 2),
                (2.0, 1.0, np.pi / 2)]
    linear, circular, elliptical = [field(phase, *p) for p in examples]
    np.testing.assert_allclose(linear[1], linear[0] / 2, atol=1e-14)
    np.testing.assert_allclose(np.sum(circular**2, axis=0), 1, atol=1e-14)
    np.testing.assert_allclose(elliptical[0]**2 / 4 + elliptical[1]**2,
                               1, atol=1e-14)
    np.testing.assert_allclose(np.linalg.norm(elliptical, axis=0).min(), 1)
    np.testing.assert_allclose(np.linalg.norm(elliptical, axis=0).max(), 2)
    np.testing.assert_allclose(field(np.pi / 4, *examples[2]),
                               [np.sqrt(2), -1 / np.sqrt(2)])
    np.testing.assert_allclose(np.linalg.norm(field(np.pi / 4, *examples[2])),
                               np.sqrt(2.5))
    for params, expected in zip(examples, [[0, 0], [0, -1], [0, -1]]):
        np.testing.assert_allclose(field(np.pi / 2, *params), expected,
                                   atol=1e-14)
    for delta in [np.pi / 3, -np.pi / 3, np.pi / 2]:
        x, y = field(phase, 2, 1, delta)
        residual = x**2 / 4 + y**2 - x * y * np.cos(delta)
        np.testing.assert_allclose(residual, np.sin(delta)**2, atol=1e-14)
    cycle = np.linspace(0, 2 * np.pi, 720, endpoint=False)
    for delta in [np.pi / 2, -np.pi / 2]:
        fields = field(cycle, 2, 1, delta)
        np.testing.assert_allclose(np.mean(np.sum(fields**2, axis=0)), 2.5)
        for angle, expected in [(0, 0.8), (np.pi / 4, 0.5), (np.pi / 2, 0.2)]:
            axis = np.array([np.cos(angle), np.sin(angle)])
            np.testing.assert_allclose(np.mean((axis @ fields)**2) / 2.5,
                                       expected, atol=1e-14)

    plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                         "axes.spines.right": False})
    fig, axes = plt.subplots(1, 3, figsize=(11, 5), dpi=120,
                             facecolor="white")
    titles = ["Linear", "Circular", "Elliptical"]
    parameters = [r"$A_x=2,\ A_y=1,\ \delta=0$",
                  r"$A_x=1,\ A_y=1,\ \delta=\pi/2$",
                  r"$A_x=2,\ A_y=1,\ \delta=\pi/2$"]
    equations = [r"$E_y=E_x/2$", r"$E_x^2+E_y^2=1$",
                 r"$E_x^2/4+E_y^2=1$"]
    for ax, params, title, param, equation in zip(
            axes, examples, titles, parameters, equations):
        x, y = field(phase, *params)
        ax.axhline(0, color="#c4ccd2", lw=0.9)
        ax.axvline(0, color="#c4ccd2", lw=0.9)
        ax.plot(x, y, color="#258a95", lw=2.6)
        ax.annotate("", field(np.pi / 4, *params), (0, 0),
                    arrowprops={"arrowstyle": "->", "lw": 2,
                                "color": "#174d85"})
        for start in [0.95, 3.8]:
            ax.annotate("", field(start + 0.3, *params), field(start, *params),
                        arrowprops={"arrowstyle": "-|>", "lw": 2.3,
                                    "color": "#bd591b", "mutation_scale": 15})
        initial = field(0, *params)
        ax.scatter(*initial, s=42, color="#bd591b", edgecolors="white",
                   linewidths=0.6, zorder=5)
        ax.set(xlim=(-2.5, 2.5), ylim=(-2.5, 2.5), aspect="equal",
               xlabel=r"$E_x$ (V/m)", ylabel=r"$E_y$ (V/m)")
        ax.set_xticks([-2, -1, 0, 1, 2])
        ax.set_yticks([-2, -1, 0, 1, 2])
        ax.grid(alpha=0.12)
        ax.set_title(title + "\n" + param, fontsize=12, pad=12)
        ax.text(0.5, -0.34, equation, transform=ax.transAxes, ha="center",
                fontsize=12)

    fig.suptitle("Polarization: the tip of the real electric field", fontsize=16,
                 y=0.96)
    fig.text(0.5, 0.88, "Fixed position; equal coordinate scales; amplitudes in V/m",
             ha="center", fontsize=11)
    fig.text(0.5, 0.06,
             "Orange arrows: increasing phase φ = ωt.  Dot: φ = 0.  Blue vector: φ = π/4.",
             ha="center", fontsize=11)
    fig.subplots_adjust(left=0.07, right=0.98, bottom=0.26, top=0.71,
                         wspace=0.40)
    output = Path.cwd() / Path(__file__).with_suffix(".png").name
    fig.savefig(output, facecolor="white", transparent=False)
    plt.close(fig)
    print("Checked: line, unit circle, 2:1 ellipse, general phase equation; "
          "ellipse magnitudes 1, 2, sqrt(5/2); analyzer fractions 4/5, 1/2, 1/5. "
          + output.name)


if __name__ == "__main__":
    main()