codex/sagnac-area-law-fringe-model.py

sagnac-area-law-fringe-model.py
"""Independent segment-sum model, not experimental measurements.

Tested with Python 3.14, numpy 2.3.5, matplotlib 3.10.7. Run from _media.
Each edge contributes 2 integral (Omega cross r) dot dr / (c lambda).
For rigid rotation the midpoint edge integral is exact at leading order.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


C = 299_792_458.0
WAVELENGTH = 633e-9


def segment_fringe_shift(vertices, omega):
    vertices = np.asarray(vertices, dtype=float)
    ends = np.roll(vertices, -1, axis=0)
    mids = (vertices + ends) / 2
    dl = ends - vertices
    # omega z-hat cross r, with signed omega in radians per second.
    velocity_per_omega = np.column_stack((-mids[:, 1], mids[:, 0]))
    circulation_per_omega = np.sum(velocity_per_omega * dl)
    return 2 * np.asarray(omega) * circulation_per_omega / (C * WAVELENGTH)


def main():
    plt.rcParams.update({"font.size": 11, "font.family": "DejaVu Sans"})
    square = np.array([[-.15, -.15], [.15, -.15], [.15, .15], [-.15, .15]])
    rectangle = np.array([[-.20, -.10], [.20, -.10], [.20, .10], [-.20, .10]])
    omega = np.linspace(-50, 50, 501)
    fig = plt.figure(figsize=(10, 6.4), facecolor="white")
    grid = fig.add_gridspec(2, 2, width_ratios=[1, 2.55], hspace=.30, wspace=.25)
    fig.subplots_adjust(left=.08, right=.97, bottom=.18, top=.85)
    colors = ["#0067a4", "#ba4a10"]
    for row, (vertices, color, title) in enumerate(zip(
            [square, rectangle], colors,
            ["Square: 0.30 m × 0.30 m", "Rectangle: 0.40 m × 0.20 m"])):
        ax = fig.add_subplot(grid[row, 0])
        closed = np.vstack([vertices, vertices[0]])
        ax.fill(vertices[:, 0], vertices[:, 1], color=color, alpha=.10)
        ax.plot(closed[:, 0], closed[:, 1], color=color, lw=2)
        ax.set_aspect("equal")
        ax.set_xlim(-.26, .26)
        ax.set_ylim(-.22, .22)
        ax.axis("off")
        ax.set_title(title, fontsize=11)
        area = .09 if row == 0 else .08
        ax.text(0, 0, f"Area = {area:.2f} m²\nPerimeter = 1.20 m", ha="center", va="center", fontsize=10)
    ax = fig.add_subplot(grid[:, 1])
    for vertices, color, label in zip([square, rectangle], colors, ["Square", "Rectangle"]):
        shift = segment_fringe_shift(vertices, omega)
        ax.plot(omega, shift, color=color, lw=2.3, label=label)
        chosen = float(segment_fringe_shift(vertices, 10))
        ax.scatter([10], [chosen], color=color, zorder=4)
    ax.axhline(0, color="#777", lw=.8)
    ax.axvline(0, color="#777", lw=.8)
    ax.grid(alpha=.18)
    ax.set_xlabel(r"Signed rotation rate $\Omega$ (rad s$^{-1}$)")
    ax.set_ylabel(r"Signed fringe shift from rest $N=\Delta\phi/(2\pi)$")
    ax.set_xlim(-50, 50)
    ax.set_ylim(-.105, .105)
    ax.legend(loc="upper left")
    ax.text(.98, .08, r"$\lambda_0=633$ nm" + "\nSegment-sum calculation", transform=ax.transAxes,
            ha="right", va="bottom", fontsize=10)
    fig.suptitle("Same perimeter, different area: calculated Sagnac fringes", fontsize=16, y=.97)
    fig.text(.5, .90, "Leading-order vacuum model; all points and lines are predictions, not measured data", ha="center", fontsize=11)
    fig.text(.5, .075, r"At $\Omega=+10$ rad s$^{-1}$: square 0.01897 fringe; rectangle 0.01686 fringe.",
             ha="center", fontsize=11)
    fig.text(.5, .035, "Reversing +10 to −10 doubles the magnitude of the displacement from rest.", ha="center", fontsize=10)
    fig.savefig(Path(__file__).stem + ".png", dpi=140, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()