codex/aperture-synthesis-two-source-sampling.py

aperture-synthesis-two-source-sampling.py
"""Original noiseless 1D visibility/dirty-image example; no historical data.

Tested with Python 3.14, NumPy 2.3.5 and Matplotlib 3.10.7.
No SciPy. Run from _media to write the matching PNG in that directory.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def model():
    n, length = 1024, .08
    theta = (np.arange(n) - n//2) * length/n
    sky = np.zeros(n)
    offsets, strengths = [-80, 120], [1., .65]
    for offset, strength in zip(offsets, strengths):
        sky[n//2 + offset] = strength
    frequencies = np.fft.fftfreq(n, d=length/n)
    visibility = np.fft.fft(np.fft.ifftshift(sky))
    analytic = sum(strength*np.exp(-2j*np.pi*frequencies*offset*length/n)
                   for offset, strength in zip(offsets, strengths))
    np.testing.assert_allclose(visibility, analytic, atol=2e-13)
    cases = {}
    for name, positive in [
        ("Sparse", np.array([2, 5, 9, 14, 20, 28, 36, 44, 52, 60, 64])),
        ("Denser", np.arange(1, 65)),
    ]:
        sampling = np.zeros(n)
        sampling[positive] = 1
        sampling[-positive] = 1
        normalization = n/sampling.sum()
        beam_complex = np.fft.ifft(sampling)*normalization
        image_complex = np.fft.ifft(sampling*visibility)*normalization
        assert np.max(np.abs(beam_complex.imag)) < 1e-14
        assert np.max(np.abs(image_complex.imag)) < 1e-14
        np.testing.assert_allclose(beam_complex[0], 1., atol=1e-14)
        direct = sum(strength*np.roll(beam_complex.real, offset)
                     for offset, strength in zip(offsets, strengths))
        np.testing.assert_allclose(image_complex.real, direct, atol=1e-13)
        assert abs(image_complex.real.mean()) < 1e-14
        cases[name] = {"k": positive, "sampling": sampling,
                       "beam": np.fft.fftshift(beam_complex.real),
                       "image": np.fft.fftshift(image_complex.real)}
    np.testing.assert_allclose(visibility[32], -1+.65j, atol=1e-13)
    np.testing.assert_allclose(visibility[64], .35, atol=1e-13)
    return theta, frequencies, visibility, cases


def main():
    theta, frequency, visibility, cases = model()
    plt.rcParams.update({"font.size": 10, "axes.spines.top": False,
                         "axes.spines.right": False})
    fig, axs = plt.subplots(3, 2, figsize=(10.8, 9.0), layout="constrained", facecolor="white")
    colors = {"Sparse": "#bb5315", "Denser": "#185c9c"}
    u = np.linspace(-800, 800, 2001)
    v = np.exp(2j*np.pi*u*.00625) + .65*np.exp(-2j*np.pi*u*.009375)
    ax = axs[0, 0]
    ax.plot(u, v.real, c="#185c9c", label="Real part")
    ax.plot(u, v.imag, c="#9a3e89", label="Imaginary part", alpha=.85)
    ax.set(title="A. Complex visibility of the chosen sky", xlabel="u (cycles per radian)", ylabel="Visibility (arbitrary units)")
    ax.legend(loc="upper right", ncol=2, fontsize=8)

    ax = axs[0, 1]
    for row, (name, data) in enumerate(cases.items()):
        positions = 12.5*np.r_[-data["k"], data["k"]]
        ax.vlines(positions, row-.23, row+.23, colors=colors[name], lw=1.1)
    ax.axvline(0, c=".5", ls=":", lw=1)
    ax.set(yticks=[0, 1], yticklabels=["Sparse: 22", "Denser: 128"],
           ylim=(-.5, 1.55), xlim=(-850, 850), xlabel="u (cycles per radian)",
           title="B. Unit-weight samples, paired ±u")
    ax.text(0, 1.34, "Zero baseline omitted", ha="center", fontsize=9)

    ax = axs[1, 0]
    for name, data in cases.items():
        ax.plot(theta, data["beam"], color=colors[name], lw=1.25, label=name)
    ax.axhline(0, c=".7", lw=.7)
    ax.set(xlim=(-.025, .025), ylim=(-.65, 1.12), title="C. Dirty beams: each has B(0) = 1",
           xlabel="Angular offset (radians)", ylabel="Unit-peak point response")
    ax.legend(fontsize=9)

    ax = axs[1, 1]
    source_theta, strengths = [-.00625, .009375], [1, .65]
    ax.vlines(source_theta, 0, strengths, color="#222222", lw=2.5)
    ax.scatter(source_theta, strengths, c="#222222", s=25)
    ax.text(-.00625, 1.05, "1.00", ha="center")
    ax.text(.009375, .70, "0.65", ha="center")
    ax.set(xlim=(-.025, .025), ylim=(-.1, 1.23), title="D. True sky: two ideal point sources",
           xlabel="θ (radians)", ylabel="Integrated source strength")

    for ax, (name, data) in zip(axs[2], cases.items()):
        ax.plot(theta, data["image"], c=colors[name], lw=1.4, label="Dirty image")
        ax.vlines(source_theta, 0, strengths, color="#222222", ls="--", lw=1, label="True sources")
        ax.axhline(0, c=".7", lw=.7)
        ax.set(xlim=(-.025, .025), ylim=(-.72, 1.25),
               title=f"{'E' if name == 'Sparse' else 'F'}. {name} sampling: same maximum |u| = 800",
               xlabel="θ (radians)", ylabel="Dirty-image response")
        ax.legend(loc="upper right", fontsize=8)
    for ax in axs.flat:
        ax.grid(alpha=.12)
    fig.suptitle("Two invented sources: sampling changes the image artefacts", fontsize=15, weight="bold")
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=110, facecolor="white")
    plt.close(fig)
    print("FFT visibilities equal analytic visibilities; shifted-beam and real-image checks passed.")


if __name__ == "__main__":
    main()