codex/siegbahn-crystal-spectrometer-functions.py

siegbahn-crystal-spectrometer-functions.py
"""Original functional diagram, not a historical apparatus reconstruction.

Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
Run from _media; writes the matching PNG in the current directory.
"""
from pathlib import Path

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


def main():
    fig, ax = plt.subplots(figsize=(11, 6.2), dpi=110)
    fig.patch.set_facecolor("white")
    ax.set(xlim=(0, 11), ylim=(0, 6.2))
    ax.axis("off")
    blue, red, grey = "#176b9c", "#b44724", "#626b75"
    ax.text(5.5, 5.94, "Crystal spectroscopy: turn an angle into a wavelength",
            ha="center", fontsize=16, weight="bold")
    ax.text(5.5, 5.58, "Functional diagram • arbitrary geometry • no historical dimensions",
            ha="center", fontsize=11, color=grey)
    ax.add_patch(FancyBboxPatch((1.85, 1.55), 8.75, 3.6,
                 boxstyle="round,pad=0.08", facecolor="#f6fafc", edgecolor=grey,
                 linestyle="--", linewidth=1.5))
    ax.text(2.05, 4.86, "Evacuated beam path", fontsize=12, color=blue)
    ax.add_patch(Rectangle((0.18, 2.15), 1.35, 1.05, facecolor="#f6e4da", edgecolor=red))
    ax.text(0.855, 2.68, "X-ray tube\ntarget", ha="center", va="center", fontsize=11)
    cx, cy = 5.65, 2.65
    ax.annotate("", (cx, cy), (1.53, cy), arrowprops=dict(arrowstyle="->", lw=2.4, color=blue))
    for x in (2.6, 3.75):
        for y in (2.13, 2.79):
            ax.add_patch(Rectangle((x, y), 0.12, 0.38, facecolor=grey))
    ax.text(3.22, 1.88, "Collimating slits", ha="center", fontsize=11)
    theta = np.deg2rad(25)
    v = np.array([np.cos(theta), np.sin(theta)])
    normal = np.array([-np.sin(theta), np.cos(theta)])
    center = np.array([cx, cy])
    for shift in (-0.10, 0, 0.10):
        ends = [center - 0.9*v + shift*normal, center + 0.9*v + shift*normal]
        ax.plot(*np.array(ends).T, color=red, lw=2)
    ax.text(6.2, 1.98, "Rotatable crystal\nplane spacing d", ha="center", fontsize=11, color=red)
    out = center + 3*np.array([np.cos(2*theta), np.sin(2*theta)])
    ax.annotate("", out, center, arrowprops=dict(arrowstyle="->", lw=2.4, color=blue))
    ax.plot([cx, cx+1.35], [cy, cy], "--", color=grey, lw=1)
    ax.add_patch(Arc(center, 2.0, 2.0, theta1=0, theta2=25, color=red, lw=1.7))
    ax.text(cx+1.06, cy+0.13, r"$\theta$", color=red, fontsize=14)
    ax.add_patch(Arc(center, 2.7, 2.7, theta1=0, theta2=50, color=blue, lw=1.4))
    ax.text(cx+1.35, cy+0.60, r"$2\theta$", color=blue, fontsize=14)
    ax.add_patch(Rectangle((out[0]-0.45, out[1]-0.22), 0.9, 0.40,
                          facecolor="#dae9e1", edgecolor="#367659"))
    ax.text(8.85, 4.72, "Readout position\nphotographic plate", ha="center", fontsize=11)
    ax.annotate("", out+[0.22, 0.12], (8.1, 4.54), arrowprops=dict(arrowstyle="-", color=grey))
    ax.text(8.85, 3.45, "A scanning ionization\nchamber is an alternative\nreadout, not an extra\nserial optical element.",
            ha="center", va="center", fontsize=10, color="#367659")
    ax.text(2.7, 0.92, "Angle scale + zero checks\nTemperature fixes d", ha="center", fontsize=11)
    ax.text(7.9, 0.92, "Record line positions, then apply\nBragg's law and refraction correction", ha="center", fontsize=11)
    ax.text(5.5, 0.19, r"$n\lambda=2d\sin\theta$   •   $\theta$ is measured from the planes, not their normal",
            ha="center", fontsize=12)
    fig.subplots_adjust(left=0.015, right=0.985, bottom=0.015, top=0.985)
    fig.savefig(Path(__file__).stem + ".png", facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()