codex/x-ray-rocket-scan-response.py

x-ray-rocket-scan-response.py
"""Generate a functional scanning schematic; curves are invented, not flight data.

Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
Run from _media; the output is written to the current directory.
"""
from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch, Rectangle
import numpy as np


def main():
    plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                         "axes.spines.right": False})
    fig = plt.figure(figsize=(10, 6.5), facecolor="white", layout="constrained")
    grid = fig.add_gridspec(2, 2, height_ratios=[1.15, 1])
    rocket = fig.add_subplot(grid[0, 0])
    rocket.set(xlim=(0, 10), ylim=(0, 7))
    rocket.axis("off")
    rocket.set_title("Rotating counter: functional view", loc="left", weight="bold")
    rocket.add_patch(Rectangle((3.7, 0.6), 2, 4.9, color="#e8edf2", ec="#364e62"))
    rocket.plot([3.7, 4.7, 5.7], [5.5, 6.7, 5.5], color="#364e62")
    rocket.annotate("spin axis", (4.7, 6.3), (1, 6.4), arrowprops={"arrowstyle": "->"})
    rocket.plot([4.7, 4.7], [0.4, 6.6], ls="--", color="gray")
    rocket.add_patch(Rectangle((5.1, 2.2), 1.6, 1.6, fc="#f0c8a1", ec="#9d5515"))
    rocket.add_patch(Rectangle((5.35, 2.5), 1.1, 1, fc="#fff4d7", ec="#9d5515"))
    rocket.text(7, 3.0, "counter", va="center")
    rocket.annotate("surrounding veto", (5.2, 2.25), (0.1, 1.1),
                    arrowprops={"arrowstyle": "->"})
    rocket.annotate("sky photons", (6.45, 3.3), (8.8, 5.7),
                    ha="center", arrowprops={"arrowstyle": "->", "color": "#2468a2"})
    rocket.add_patch(FancyArrowPatch((3.6, 4.6), (5.9, 4.6),
                     connectionstyle="arc3,rad=-.6", arrowstyle="->", mutation_scale=13))
    rocket.text(0.1, 0.0, "Orientation + accepted pulses → angular bins", fontsize=10)

    col = fig.add_subplot(grid[0, 1])
    col.set(xlim=(0, 10), ylim=(0, 7))
    col.axis("off")
    col.set_title("Separate teaching collimator", loc="left", weight="bold")
    for y in (2, 4):
        col.plot([2.5, 7], [y, y], lw=7, color="#526b79")
    col.plot([7.3, 7.3], [2, 4], lw=6, color="#b16624")
    col.annotate("", (7.1, 3), (0.4, 3), arrowprops={"arrowstyle": "->", "lw": 2, "color": "#2468a2"})
    col.annotate("", (4.5, 4), (1.0, 6.3), arrowprops={"arrowstyle": "->", "lw": 2, "color": "#bb4545"})
    col.text(5, 5.5, "oblique ray blocked", ha="center")
    col.text(7.2, 1.2, "detector", ha="center")
    col.text(0.2, 0, "No mechanical collimator on the 1962 rocket", fontsize=10)

    ax = fig.add_subplot(grid[1, :])
    angle = np.linspace(-180, 180, 1441)
    broad = np.maximum(0, np.cos(np.deg2rad(angle)))
    narrow = np.maximum(0, 1 - abs(angle) / 20)
    ax.plot(angle, 4 + 6*broad, label="Broad model: max(0, cos φ)", lw=2.3)
    ax.plot(angle, 4 + 6*narrow, label="Collimated model: max(0, 1 − |φ|/20°)", lw=2.3)
    ax.axhline(4, color="gray", ls="--", label="Chosen background: 4 counts/s")
    ax.set(xlim=(-180, 180), ylim=(3, 11), xlabel="Scan angle relative to source (degrees)",
           ylabel="Expected counts/s", title="Invented scan: same peak source rate, different angular acceptance")
    ax.legend(loc="upper right", fontsize=9)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=120, facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()