codex/stellar-uniform-disk-correlation-baseline.py

stellar-uniform-disk-correlation-baseline.py
"""Stipulated circular-disk correlation model, with no measured points.

Python 3.14; numpy 2.3.5; matplotlib 3.10.7. No SciPy.
Run from _media to write basename.png in the current directory.
J1 uses a convergent power series, independently checked against angular
quadrature of the disk transform. The example uses a rectangular count gate,
not the bandpass electronics of the historical Sirius experiment.
"""

import math
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def j1_series(x):
    term = x / 2
    terms = [term]
    for k in range(1, 200):
        term *= -(x * x / 4) / (k * (k + 1))
        terms.append(term)
        if abs(term) < 1e-18:
            return math.fsum(terms)
    raise ArithmeticError("J1 series did not converge")


def coherence(x):
    return 1.0 if x == 0 else 2 * j1_series(x) / x


def disk_quadrature(x, n=20000):
    # 4/pi integral from 0 to pi/2 cos(x sin u) cos²u du.
    # Simpson quadrature, independently integrating the projected disk.
    u = np.linspace(0, math.pi / 2, n + 1)
    y = np.cos(x * np.sin(u)) * np.cos(u) ** 2
    return (4 / math.pi) * (math.pi / 2 / n / 3) * (
        y[0] + y[-1] + 4 * y[1:-1:2].sum() + 2 * y[2:-1:2].sum())


def first_zero():
    lo, hi = 3.0, 4.0
    for _ in range(60):
        mid = (lo + hi) / 2
        if j1_series(mid) > 0:
            lo = mid
        else:
            hi = mid
    return (lo + hi) / 2


def main():
    theta = 5e-3 * math.pi / 648000  # five milliarcseconds in radians
    wavelength = 500e-9
    x0 = first_zero()
    baseline_zero = x0 * wavelength / (math.pi * theta)
    checks = [abs(coherence(x) - disk_quadrature(x)) for x in np.linspace(0, 8, 33)]
    assert max(checks) < 5e-13
    assert abs(x0 - 3.8317059702075125) < 2e-14
    assert abs(coherence(0.01) - (1 - 0.01 ** 2 / 8 + 0.01 ** 4 / 192)) < 1e-15
    tau0, gate, rho = 1.0, 50.0, 0.8  # picoseconds; stipulated, not instrument data
    beta = 2 * tau0 / gate - 2 * (tau0 / gate) ** 2 * (1 - math.exp(-gate / tau0))
    amplitude = rho * rho * beta  # one selected polarization
    baseline = np.linspace(0, 50, 801)
    gamma = np.array([coherence(math.pi * b * theta / wavelength) for b in baseline])
    plt.rcParams.update({"font.size": 11, "font.family": "DejaVu Sans"})
    fig, axes = plt.subplots(2, 1, figsize=(9.4, 7.1), sharex=True,
                             gridspec_kw={"height_ratios": [1.2, 1]}, facecolor="white")
    upper, lower = axes
    upper.plot(baseline, gamma, color="#bd6020", lw=2, label=r"Signed field coherence $\gamma(B)$")
    upper.plot(baseline, gamma ** 2, color="#176b94", lw=2.3, label=r"Squared visibility $V^2=|\gamma|^2$")
    upper.axhline(0, color="#7d858a", lw=0.8)
    upper.set(ylabel="Normalized optical quantities", ylim=(-0.21, 1.09))
    upper.legend(loc="upper right", frameon=False, fontsize=10)
    upper.text(28, 0.36, "Sign reversal survives in γ;\nintensity correlations retain |γ|².", fontsize=10)
    lower.plot(baseline, 1 + amplitude * gamma ** 2, color="#664095", lw=2.3)
    lower.axhline(1, color="#7d858a", lw=0.8)
    lower.set(xlabel="Projected baseline B (m)", ylabel=r"Gate-averaged $g^{(2)}_{\rm obs}(B,0)$",
              ylim=(0.998, 1.029), xlim=(0, 50))
    lower.text(27.7, 1.019, "One polarization; independent backgrounds\n"
               r"$\rho_1=\rho_2=0.8$; $|g_t^{(1)}(u)|^2=e^{-|u|/\tau_0}$" + "\n"
               r"$\tau_0=1$ ps; rectangular gate $T=50$ ps" + "\n"
               rf"$\beta={beta:.4f}$; excess at B = 0: {amplitude:.6f}", fontsize=10)
    for ax in axes:
        ax.axvline(baseline_zero, color="#42614c", ls="--", lw=1.2)
        ax.grid(alpha=0.18)
    upper.annotate(f"First zero: {baseline_zero:.3f} m", xy=(baseline_zero, 0), xytext=(6, -0.16),
                   arrowprops={"arrowstyle": "->", "color": "#42614c"}, color="#42614c", fontsize=10)
    fig.suptitle("Stipulated uniform stellar disk: diameter 5 mas, wavelength 500 nm", fontsize=13)
    fig.text(0.5, 0.015, "Calculated curves only — no measured points or fit to Sirius", ha="center", fontsize=10)
    fig.tight_layout(rect=(0, 0.035, 1, 0.955), h_pad=1.2)
    fig.savefig(Path(__file__).stem + ".png", dpi=120, facecolor="white", transparent=False)
    plt.close(fig)
    print({"series_quadrature_max_abs_error": float(max(checks)), "first_j1_zero": x0,
           "first_zero_m": baseline_zero, "theta_rad": theta, "beta": beta,
           "zero_baseline_g2": 1 + amplitude,
           "at_10m": {"x": math.pi * 10 * theta / wavelength,
                       "visibility_squared": coherence(math.pi * 10 * theta / wavelength) ** 2,
                       "observed_g2": 1 + amplitude * coherence(math.pi * 10 * theta / wavelength) ** 2}})


if __name__ == "__main__":
    main()