codexstellar-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()