Initial commit: grid-based multi-photon reconstruction

Code, configs, charge LUTs, best.pth and evaluation records for each
training run, the knife-edge / Siemens-star / XRF notebooks with
outputs, and the overnight run reports. Epoch checkpoints, inference
results and HTML exports stay local (see .gitignore).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xie_x1andClaude Opus 5.5 committed 2026-10-20 07:36:49 +02:00
commit 0f9a3f496f
222 files changed
+53227

No files matched your search

+583
View File
@@ -0,0 +1,583 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "32f9dbc8",
"metadata": {},
"source": [
"# XRF AgMo_1_factor_10 -- polychromatic GridBased model on measured lines\n",
"\n",
"2603 MaxIV, MOENCH014 at 150 V, full 400x400 chip flood-illuminated by the\n",
"fluorescence of an Ag/Mo sample; slit factor 10, **0.67 % occupancy**, 1.0e8\n",
"photons in 100 000 frames. Mo Ka 17.44 / Kb 19.64 and Ag Ka 22.10 keV sit in\n",
"the middle of the 3-30 keV training range; Compton + elastic scatter forms a\n",
"bump at 28-30 keV. The clean case -- `XRF_CuCrCo_1_factor_10.ipynb` has the\n",
"low-energy lines.\n",
"\n",
"Model `Results/260919_poly3to30keV_Moench014_c96_00`, the default since\n",
"2026-09-19: `[32, 64, 96, 96]`, flat 3-30 keV, chosen from four sizes in\n",
"`Results/overnight_poly3to30keV/report`. Reconstruction from\n",
"`scripts/05_infer_measurement.py --config Configs/infer_measurement_poly_Moench014.yaml\n",
"--run AgMo_1_factor_10 --save-photons` (FP16, masked column common mode). The previous\n",
"5-30 keV model's reconstructions are kept as `<run>_poly5to30`.\n",
"Compared throughout:\n",
"\n",
"* **grid direct** -- the model output;\n",
"* **grid corrected** -- $E = g^{-1}(E_\\mathrm{pred})$, where $g(E)$ is the MC\n",
" position of a line of true energy $E$ (`scripts/07_energy_response.py`);\n",
"* **classical 3x3** -- 3x3 sum around a local maximum on the same calibrated\n",
" frames (first 10 000 frames), no model.\n"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "930c69d4",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:13.369303Z",
"iopub.status.busy": "2026-09-19T09:10:13.369219Z",
"iopub.status.idle": "2026-09-19T09:10:13.639588Z",
"shell.execute_reply": "2026-09-19T09:10:13.639268Z"
}
},
"outputs": [],
"source": [
"import json\n",
"import sys\n",
"from pathlib import Path\n",
"\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"REPO = Path('/home/xie_x1/MLXID/GridBased')\n",
"sys.path.insert(0, str(REPO / 'src'))\n",
"from xrf_analysis import (EnergyCorrection, classical_and_model, fit_lines,\n",
" line_centroid, load_reconstruction)\n",
"\n",
"CONFIG = REPO / 'Configs/infer_measurement_poly_Moench014.yaml'\n",
"RUN = 'AgMo_1_factor_10'\n",
"EXPERIMENT = REPO / 'Results/260919_poly3to30keV_Moench014_c96_00'\n",
"RESPONSE = EXPERIMENT / 'energy_response.npz'\n",
"GRID = Path('/mnt/sls_det_storage/moench_data/MLXID/InferenceResults/GridBased/XRF_2603MaxIV')\n",
"EDGES = np.arange(0.0, 40.025, 0.05) # 50 eV bins\n",
"CENTRES = 0.5 * (EDGES[1:] + EDGES[:-1])\n",
"\n",
"FIT_GROUPS = [ # each line fitted on its own window\n",
" dict(label='Mo Ka', alpha=['Mo_Ka'], window=(16.0, 18.9)),\n",
" dict(label='Mo Kb', alpha=['Mo_Kb'], window=(18.75, 20.7)),\n",
" dict(label='Ag Ka', alpha=['Ag_Ka'], window=(20.6, 23.6)),\n",
"] # Ag Kb sits on the rising Compton bump and is left out\n",
"SPECTRUM_ZOOM = (14.5, 27.5)\n",
"LINES = [n for g in FIT_GROUPS for n in g['alpha']]\n",
"PEAKS = LINES + [e[0] for g in FIT_GROUPS for e in g.get('extra', ())]\n",
"COLOURS = {'grid direct': '#1f6fb4', 'grid corrected': '#10695f',\n",
" 'classical 3x3': '#6b6b6b'}"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "81b6499b",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:13.641460Z",
"iopub.status.busy": "2026-09-19T09:10:13.641283Z",
"iopub.status.idle": "2026-09-19T09:10:20.295943Z",
"shell.execute_reply": "2026-09-19T09:10:20.295634Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"AgMo_1_factor_10: 100000 frames, 1054 photons/frame, occupancy 0.69 %, 1.1 % of the photons below 3 keV\n"
]
}
],
"source": [
"reconstruction = load_reconstruction(GRID, RUN)\n",
"summary = reconstruction['summary']\n",
"n_frames = summary['n_frames']\n",
"energy = np.array(reconstruction['photons'][:, 2])\n",
"correction = EnergyCorrection(RESPONSE) # core-based g\n",
"correction_mean = EnergyCorrection(RESPONSE, estimator='mean')\n",
"\n",
"reference = classical_and_model(CONFIG, RUN, n_frames=10_000) # cached\n",
"classical, grid_subset = reference['classical'], reference['grid']\n",
"n_subset = int(reference['n_frames'])\n",
"\n",
"energies = {'grid direct': energy, 'grid corrected': correction(energy),\n",
" 'classical 3x3': classical[:, 3]}\n",
"frames_of = {'grid direct': n_frames, 'grid corrected': n_frames,\n",
" 'classical 3x3': n_subset}\n",
"print(f\"{RUN}: {n_frames} frames, {summary['photons_per_frame']:.0f} photons/frame, \"\n",
" f\"occupancy {summary['occupancy_valid_region']*100:.2f} %, \"\n",
" f\"{(energy < 3).mean()*100:.1f} % of the photons below 3 keV\")"
]
},
{
"cell_type": "markdown",
"id": "f7b8cb19",
"metadata": {},
"source": [
"## 1. The MC energy response\n",
"\n",
"The energy head is a regression trained on a flat 3-30 keV spectrum. On MC\n",
"(occupancy <= 2.5 %) the **core** of the predicted energy -- what a line fit\n",
"measures -- stays within ~25 meV of the truth from 4 to 28 keV and moves only\n",
"within ~0.75 keV of the edges (+98 meV at 3.2 keV, -93 meV at 29.6 keV), where\n",
"the energy scale is also compressed. The **mean** is pulled towards the centre\n",
"by ~0.1 keV at 5-12 keV and more near the edges, because the few per cent of\n",
"photons whose energy is off by more than 1 keV (the tails) are cut by the\n",
"bounded range. That mean is the `energy_bias_keV` of `eval_summary.json`. It\n",
"is not a line shift, and correcting with it moves lines at 5-12 keV by ~0.1 keV\n",
"the wrong way. The correction used here therefore inverts the core position\n",
"$g(E)$; with the 3-30 keV model it matters only below ~4 keV."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "72f22c9b",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:20.297268Z",
"iopub.status.busy": "2026-09-19T09:10:20.297102Z",
"iopub.status.idle": "2026-09-19T09:10:20.563301Z",
"shell.execute_reply": "2026-09-19T09:10:20.563003Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Mo_Ka: g - E = -12 meV, g' = 1.00, MC core sigma 0.275 keV\n",
"Mo_Kb: g - E = -8 meV, g' = 1.00, MC core sigma 0.290 keV\n",
"Ag_Ka: g - E = -6 meV, g' = 1.00, MC core sigma 0.301 keV\n"
]
},
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABQoAAAGZCAYAAADB4AymAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd1zU9R/A8dexN6KAA5WhOAER956p5TZ/mpYjzTQrM22ZVq7S0szSzMzSHJlajjJzTxS3oIgiKogDAVHmse/7+4O4ZMmh4HH2fj4ePJTvfe7zfX8/3wM+977PUCmKoiCEEEIIIYQQQgghhPhPM9J3AEIIIYQQQgghhBBCCP2TRKEQQgghhBBCCCGEEEIShUIIIYQQQgghhBBCCEkUCiGEEEIIIYQQQgghkEShEEIIIYQQQgghhBACSRQKIYQQQgghhBBCCCGQRKEQQgghhBBCCCGEEAJJFAohhBBCCCGEEEIIIZBEoRBCCCGEEEIIIYQQAkkUiqfQypUrUalUqFQqDhw4UOBxRVGoXbs2KpWKjh07Fng8MTGRTz/9lKZNm2JnZ4e5uTlubm6MGjWKM2fOlP0FPMDNzY1evXo90XPqw/Tp01GpVHmOdezYsdD7U57cu3ePF154AWdnZ1QqFf369Suy7JIlS1i5cuUTi03oLvf3RWFfI0eO1Hd4QgghhN48Tf1qUfoK68PrKiQkhOnTpxMREVHgsZEjR+Lm5vZ4wQkhHpmJvgMQoqzY2try448/Fui0HDx4kKtXr2Jra1vgOVevXqVbt27ExMQwbtw4ZsyYgY2NDREREWzYsIEmTZoQHx+Pvb39E7qK/64lS5boO4RizZo1i82bN/PTTz9Rq1YtKlasWGTZJUuW4OjoKImncmrgwIFMnjy5wHEnJyc9RCOEEEKUL9KvFqUtJCSEGTNm0LFjxwJJwY8++oi33npLP4EJISRRKJ5egwcPZu3atXz77bfY2dlpj//444+0atWKxMTEPOWzs7Pp378/d+/eJSAgAC8vL+1jHTp0YMSIEfz999+Ympo+sWt4ErKzs8nKysLc3FzfoeTRoEEDfYdQrODgYGrVqsWLL75YqvVmZmaiUqkwMflv/opOTU3FwsLikT+hfhSVK1emZcuWT+x8hkQf90MIIUT5Iv3qoqnVaqysrPR2/qex31irVi19hyDEf5pMPRZPrSFDhgCwbt067bGEhAR+//13Ro0aVaD8li1bOH/+PFOmTMnTmXnQs88+W+KOwIULF1CpVGzcuFF77PTp06hUKho2bJinbJ8+fWjSpEmBOnbs2IGfnx+WlpbUq1ePn376qUCZO3fuMHbsWKpXr46ZmRnu7u7MmDGDrKwsbZmIiAhUKhVffPEFs2fPxt3dHXNzc/bv3w/AqVOn6NOnDxUrVsTCwoLGjRuzYcMGna5zxowZtGjRgooVK2JnZ4efnx8//vgjiqLo9Pz88k89zo19/vz5LFiwAHd3d2xsbGjVqhXHjh0r8PzHuZZ79+4xfvx4XFxcMDMzw8PDg6lTp5Kenp4nlj179nDx4sWHTsmBnCnkFy5c4ODBg9qyuZ+cHjhwAJVKxerVq5k8eTIuLi6Ym5tz5cqVIqdz5E4Dyj9VY/369bRq1Qpra2tsbGzo3r07Z8+e1emaS/L6Kc17kHstu3btYtSoUTg5OWFlZUV6ejqKovDZZ5/h6uqKhYUFTZs2Zffu3XleG8nJyVSoUIGxY8cWOH9ERATGxsbMmzdPpzZ4FLGxsYwfP54GDRpgY2ODs7MznTt35vDhwwViyf3Z+/TTT6lZs6b2mvbu3ZunbO59P3v2LAMGDMDOzg57e3teeuklYmNjC8Sgy30/deoUL7zwAm5ublhaWuLm5saQIUO4fv16nnIPux+xsbG8+uqr1KhRA3Nzc5ycnGjTpg179uwppdYUQghRXpWXfnWusLAwhg4dirOzM+bm5tSvX59vv/02T5ncPta6deuYOnUq1apVw87Ojq5duxIaGlqgzj179tClSxfs7OywsrKiTZs2Rf6NPnPmDAMHDsTBwUGb1EpPT2fy5MlUqVIFKysr2rdvz+nTp3Fzc9POKImIiMDExIQ5c+YUOP+hQ4cKvGfI72H9Rl2vQde/5z/99BONGjXCwsKCihUr0r9/fy5evFhkbLlUKhXTp08vcPzBdli5ciX/+9//AOjUqZO2f5y7TE9hU4/T0tKYMmUK7u7umJmZ4eLiwuuvv058fHyB8/Tq1Uun909CiMJJolA8tezs7Bg4cGCePwrr1q3DyMiIwYMHFyi/a9cugIeuM5dfUeuxPKhhw4ZUrVo1zx/fPXv2YGlpSUhICLdv3wYgKyuLgwcP0rVr1zzPDwoKYvLkybz99tts3boVHx8fRo8ezaFDh7Rl7ty5Q/Pmzdm5cycff/wxf//9N6NHj2bOnDmMGTOmQEzffPMN+/btY/78+fz999/Uq1eP/fv306ZNG+Lj41m6dClbt27F19eXwYMH67S2XkREBGPHjmXDhg1s2rSJAQMG8OabbzJr1qxin1sS3377Lbt372bhwoWsXbuWlJQUnnvuORISErRlHuda0tLS6NSpE6tWrWLSpEn89ddfvPTSS3zxxRcMGDAAgKpVqxIQEEDjxo3x8PAgICCAgIAA/Pz8Cq1z8+bNeHh40LhxY23ZzZs35ykzZcoUIiMjWbp0KX/++SfOzs4lapfPPvuMIUOG0KBBAzZs2MDq1atJSkqiXbt2hISEPPS5JX39lMU9GDVqFKampqxevZrffvsNU1NTpk6dytSpU+nRowdbt25l3LhxvPLKK1y+fFn7PBsbG0aNGsXatWvznB9ypnubmZkV+gYmP0VRyMrKKvBVXKL73r17AHzyySf89ddfrFixAg8PDzp27Fho4njx4sXs2LGDhQsXsmbNGoyMjHj22WcJCAgoULZ///7Url2b3377jenTp7Nlyxa6d+9OZmamtoyu9z0iIoK6deuycOFCdu7cyeeff05UVBTNmjXj7t27Bc5d2P0YNmwYW7Zs4eOPP2bXrl0sX76crl27EhcXV2z7CiGEMGzlpV8NOVNWmzVrRnBwMF9++SXbtm2jZ8+eTJgwgRkzZhQo/+GHH3L9+nWWL1/OsmXLCAsLo3fv3mRnZ2vLrFmzhm7dumFnZ8fPP//Mhg0bqFixIt27dy+QaAMYMGAAtWvXZuPGjSxduhSAl19+mYULF/Lyyy+zdetWnn/+efr3758nkeXm5kafPn1YunRpnvNDTh+hWrVq9O/fv9g2KKzfqOs16PL3fM6cOYwePZqGDRuyadMmvv76a86dO0erVq0ICwsrNr7i9OzZk88++wzI6Vfm9o979uxZaHlFUejXrx/z589n2LBh/PXXX0yaNImff/6Zzp07az/Mz6XL+ychxEMoQjxlVqxYoQDKyZMnlf379yuAEhwcrCiKojRr1kwZOXKkoiiK0rBhQ6VDhw7a5/Xo0UMBlLS0NJ3PZWxsrHTu3LnYci+99JLi4eGh/b5r167KmDFjFAcHB+Xnn39WFEVRjhw5ogDKrl27tOVcXV0VCwsL5fr169pjqampSsWKFZWxY8dqj40dO1axsbHJU05RFGX+/PkKoFy4cEFRFEUJDw9XAKVWrVpKRkZGnrL16tVTGjdurGRmZuY53qtXL6Vq1apKdnZ2sdeZKzs7W8nMzFRmzpypVKpUSdFoNA8t/8knnyj5fx116NAhz/3Jjd3b21vJysrSHj9x4oQCKOvWrSuVa1m6dKkCKBs2bMhz/PPPPy9wfzp06KA0bNjwodeWK//rLVfua7R9+/YFHiusXRTl39d4eHi4oiiKEhkZqZiYmChvvvlmnnJJSUlKlSpVlEGDBj00tpK+fkrzHuRey/Dhw/OUu3fvnmJubq4MHjw4z/GAgAAFyNOWV69eVYyMjJSvvvpKeyw1NVWpVKmS8vLLLz/02hVFUYAiv1avXl3s8x+UlZWlZGZmKl26dFH69++vPZ7bdtWqVVNSU1O1xxMTE5WKFSsqXbt21R7Lve9vv/12nrrXrl2rAMqaNWsURXm8+56VlaUkJycr1tbWytdff609XtT9UBRFsbGxUSZOnKhjSwghhHgalMd+dffu3ZXq1asrCQkJeY6/8cYbioWFhXLv3j1FUf7tYz333HN5ym3YsEEBlICAAEVRFCUlJUWpWLGi0rt37zzlsrOzlUaNGinNmzfXHsv9G/3xxx/nKXvhwgUFUN5///08x9etW6cAyogRI7THcuPavHmz9titW7cUExMTZcaMGQ+99qL6jSW5huL+nt+/f1+xtLQs0G6RkZGKubm5MnToUO2xwvqqgPLJJ58UqNfV1TVPO2zcuFEBlP379xcoO2LECMXV1VX7/Y4dOxRA+eKLL/KUW79+vQIoy5Yty3MeXd4/CSGKJiMKxVOtQ4cO1KpVi59++onz589z8uRJnUYX6SorK6vQTxnz69KlC9euXSM8PJy0tDT8/f3p0aMHnTp1Yvfu3UDOKENzc3Patm2b57m+vr7UrFlT+72FhQV16tTJM2Vw27ZtdOrUiWrVquUZDfXss88COQtNP6hPnz551oS5cuUKly5d0q6192Adzz33HFFRUYVO0XjQvn376Nq1K/b29hgbG2NqasrHH39MXFwcMTExxbaRrnr27ImxsbH2ex8fHwBtezzutezbtw9ra2sGDhyY53juVAld7vejeP755x/5uTt37iQrK4vhw4fnuV4LCws6dOhQ5JToXCV9/ZTFPch//ceOHSM9PZ1BgwblOd6yZcsCU1E8PDzo1asXS5Ys0Y4A/OWXX4iLi+ONN9546LXnGjRoECdPnizw9dxzzxX73KVLl+Ln54eFhQUmJiaYmpqyd+/eQqfnDBgwAAsLC+33tra29O7dm0OHDhUYWZB/7ctBgwZhYmKiXSqgJPc9OTmZ999/n9q1a2NiYoKJiQk2NjakpKQUGmdhr8fmzZuzcuVKZs+ezbFjx/KMbBRLine truncated
"text/plain": [
"<Figure size 1300x420 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"response = np.load(RESPONSE)\n",
"centre = response['table_centre']\n",
"figure, axes = plt.subplots(1, 2, figsize=(13, 4.2))\n",
"for curve, colour, label in ((correction, COLOURS['grid corrected'], 'core of E_pred (used)'),\n",
" (correction_mean, '#c0392b', 'mean of E_pred')):\n",
" axes[0].plot(curve.grid, curve.g_grid - curve.grid, color=colour, lw=1.5, label=label)\n",
"for name in LINES:\n",
" axes[0].axvline(line_centroid(name), color='k', lw=.5, ls=':')\n",
"axes[0].axhline(0, color='k', lw=.6); axes[0].set_ylim(-0.6, 0.3)\n",
"axes[0].set_xlabel('E_true [keV]'); axes[0].set_ylabel('g(E) - E [keV]')\n",
"axes[0].set_title('MC: where a line of true energy E appears'); axes[0].legend(fontsize=8)\n",
"axes[0].grid(alpha=.3)\n",
"axes[1].plot(centre, response['table_core_sigma'], 'k.', ms=3, label='core sigma, raw')\n",
"axes[1].plot(correction.grid, correction.mc_sigma(correction.grid) / correction.slope(correction.grid),\n",
" color=COLOURS['grid corrected'], lw=1.2, label=\"core sigma / g' (true keV)\")\n",
"axes[1].set_ylim(0, 0.4); axes[1].set_xlabel('E_true [keV]'); axes[1].set_ylabel('keV')\n",
"axes[1].set_title('MC: energy resolution'); axes[1].legend(fontsize=8); axes[1].grid(alpha=.3)\n",
"figure.tight_layout()\n",
"for name in LINES:\n",
" e = line_centroid(name)\n",
" print(f\"{name}: g - E = {(correction.forward(np.array([e]))[0] - e)*1000:+.0f} meV, \"\n",
" f\"g' = {correction.slope(e):.2f}, MC core sigma {correction.mc_sigma(e):.3f} keV\")"
]
},
{
"cell_type": "markdown",
"id": "dd0b6fa5",
"metadata": {},
"source": [
"## 2. Spectra\n",
"\n",
"Per frame, 50 eV bins; grey = outside the 3-30 keV training range."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "5b61a197",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:20.564617Z",
"iopub.status.busy": "2026-09-19T09:10:20.564520Z",
"iopub.status.idle": "2026-09-19T09:10:25.475927Z",
"shell.execute_reply": "2026-09-19T09:10:25.475651Z"
}
},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1400x850 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"histograms = {name: np.histogram(values, EDGES)[0] for name, values in energies.items()}\n",
"figure, axes = plt.subplots(2, 1, figsize=(14, 8.5))\n",
"for axis, log in zip(axes, (True, False)):\n",
" for name, counts in histograms.items():\n",
" axis.plot(CENTRES, counts / frames_of[name], lw=1, color=COLOURS[name], label=name)\n",
" for name in LINES:\n",
" axis.axvline(line_centroid(name), color='k', lw=.4, ls='--')\n",
" axis.axvspan(0, 3, color='0.9'); axis.axvspan(30, 40, color='0.9')\n",
" axis.set_xlabel('energy [keV]'); axis.set_ylabel('photons / frame / 50 eV')\n",
" axis.grid(alpha=.3)\n",
" if log:\n",
" axis.set_yscale('log'); axis.set_xlim(2, 33); axis.set_ylim(1e-4, None)\n",
" axis.legend(fontsize=9); axis.set_title(RUN)\n",
" else:\n",
" axis.set_xlim(*SPECTRUM_ZOOM)\n",
"figure.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "7cce07c1",
"metadata": {},
"source": [
"## 3. Line positions and energy resolution\n",
"\n",
"Grid lines: Gaussian core + a wide Gaussian at the same position (the tails of\n",
"section 1) + an erfc low-energy step, on an exp(polynomial) background.\n",
"Classical lines: Gaussian + step. Ka is fitted as the tabulated doublet, so the\n",
"position is the intensity-weighted centroid. `sigma` is the core width; FWHM is\n",
"that of the whole line shape."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "07db24a1",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:25.477272Z",
"iopub.status.busy": "2026-09-19T09:10:25.477178Z",
"iopub.status.idle": "2026-09-19T09:10:25.617926Z",
"shell.execute_reply": "2026-09-19T09:10:25.617645Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" table | position [keV] | core sigma [keV] | FWHM [keV] | MC sigma\n",
" | direct corr. 3x3 | direct corr. 3x3 | grid 3x3 ratio |\n",
"Mo_Ka 17.44 | 17.47 17.48 17.52 | 0.327 0.327 0.464 | 0.82 1.09 0.75 | 0.275\n",
"Mo_Kb 19.64 | 19.66 19.67 19.74 | 0.353 0.352 0.492 | 0.83 1.16 0.72 | 0.290\n",
"Ag_Ka 22.10 | 22.10 22.11 22.17 | 0.378 0.378 0.489 | 0.95 1.15 0.82 | 0.301\n",
"grid FWHM: direct output\n"
]
}
],
"source": [
"fits = {}\n",
"for variant, counts in histograms.items():\n",
" for group in FIT_GROUPS:\n",
" fits[variant, group['label']] = fit_lines(\n",
" CENTRES, counts, group['alpha'], group.get('beta'), window=group['window'],\n",
" beta_ratio_fixed=group.get('ratio_fixed'),\n",
" fit_stretch=group.get('fit_stretch', True), extra_peaks=group.get('extra', ()),\n",
" tails=variant != 'classical 3x3')\n",
"\n",
"\n",
"def peak(variant, name):\n",
" group = next(g for g in FIT_GROUPS\n",
" if name in g['alpha'] or name in [e[0] for e in g.get('extra', ())])\n",
" return fits[variant, group['label']]['lines'][name]\n",
"\n",
"\n",
"def fwhm_of(variant, name):\n",
" \"\"\"FWHM of the line shape; for the extra peaks (no doublet) from sigma.\"\"\"\n",
" line = peak(variant, name)\n",
" return line.get('fwhm', 2.3548 * line['sigma'])\n",
"\n",
"\n",
"print(f\"{'':8s} {'table':>6s} | {'position [keV]':^26s} | {'core sigma [keV]':^26s} | \"\n",
" f\"{'FWHM [keV]':^20s} | MC sigma\")\n",
"print(f\"{'':8s} {'':>6s} | {'direct':>8s} {'corr.':>8s} {'3x3':>8s} | \"\n",
" f\"{'direct':>8s} {'corr.':>8s} {'3x3':>8s} | {'grid':>6s} {'3x3':>6s} {'ratio':>6s} |\")\n",
"for name in PEAKS:\n",
" table = f\"{line_centroid(name):6.2f}\" if name in LINES else f\"{'':6s}\"\n",
" positions = [peak(v, name)['mu'] for v in energies]\n",
" sigmas = [peak(v, name)['sigma'] for v in energies]\n",
" grid_fwhm = fwhm_of('grid direct', name)\n",
" classical_fwhm = fwhm_of('classical 3x3', name)\n",
" mc = correction.mc_sigma(line_centroid(name)) if name in LINES else \\\n",
" correction.mc_sigma(peak('grid corrected', name)['mu'])\n",
" print(f\"{name:8s} {table} | \" + ' '.join(f'{p:8.2f}' for p in positions) + ' | ' +\n",
" ' '.join(f'{s:8.3f}' for s in sigmas) +\n",
" f\" | {grid_fwhm:6.2f} {classical_fwhm:6.2f} {grid_fwhm/classical_fwhm:6.2f} | {mc:.3f}\")\n",
"print('grid FWHM: direct output')"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "cc95eb5a",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:25.619193Z",
"iopub.status.busy": "2026-09-19T09:10:25.619065Z",
"iopub.status.idle": "2026-09-19T09:10:26.626942Z",
"shell.execute_reply": "2026-09-19T09:10:26.626658Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAACkYAAAJOCAYAAADoTSlEAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3gU5frG8e9ueg8QCBBC770FqVIEBKUpCIJBEBsSUI7HgtgL2LERUIogARSkKQpIl947SAkQEmogBNLb7vz+4GeOSC/JJJv7c11cx53dnb1n9MzDu/vM+1oMwzAQEREREREREREREREREREREREREXEAVrMDiIiIiIiIiIiIiIiIiIiIiIiIiIjcLWqMFBERERERERERERERERERERERERGHocZIEREREREREREREREREREREREREXEYaowUEREREREREREREREREREREREREYehxkgRERERERERERERERERERERERERcRhqjBQRERERERERERERERERERERERERh6HGSBERERERERERERERERERERERERFxGGqMFBERERERERERERERERERERERERGHocZIEREREREREREREREREREREREREXEYaoyUAq9jx46MGTPmms9369aNd95556b398/Xr169mlKlSt1hQhERkUvKli3LvHnzcmz/AwcO5NVXX73j/UyePJm6deveeSAREZEb0Hgud7zzzjt069bN7BgiIvmWxnL5W6tWrfjyyy/NjiEi4pA0phMRkfxC4zqR/EmNkVLgLVy4kEGDBuXIvlu0aMHx48dzZN9RUVFYLBYuXLhwS+8rW7YsFouFQ4cOXbY9LCwMi8Vy21/y/fuHspSUFDp27Mi9997LxYsXb2ufIiKSu7799ls+/vjjHP+cIUOGEBwcjK+vL0FBQQwdOpSMjIybeu/ChQupVasWhQoVonDhwrRr147du3ff9Gf/XT/LlCmD3W6/7LmaNWtisVjYsWPHrRxOtn//WHb48GHKly/PCy+8gGEYt7VPERG5Po3nLtF4TkSkYNNYTmM5EZH8qqCN6f7Wpk0bPDw8iI+Pv6Mc/25+sdlsDBgwgOrVq3PixIk72reIiOSugjCu+1tycjK+vr7cc889t/zef+vfvz9Dhw7Nfnz27FkaNGjAI488ctPHJY5NjZFSYBmGgc1mMzVDVlaWKZ9bpUoV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 truncated
"text/plain": [
"<Figure size 2640x600 with 12 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"shown = ('grid direct', 'classical 3x3')\n",
"figure, axes = plt.subplots(2, len(FIT_GROUPS) * 2, figsize=(4.4 * len(FIT_GROUPS) * 2, 6),\n",
" gridspec_kw=dict(height_ratios=[3, 1]), squeeze=False)\n",
"for column, (group, variant) in enumerate((g, v) for g in FIT_GROUPS for v in shown):\n",
" fit = fits[variant, group['label']]\n",
" x = fit['centres']\n",
" axes[0, column].plot(x, fit['counts'], 'k.', ms=2.5)\n",
" axes[0, column].plot(x, fit['model'](x), color=COLOURS[variant], lw=1.3)\n",
" for part, curve in fit['components'](x).items():\n",
" axes[0, column].plot(x, curve, lw=.7, ls='--', label=part)\n",
" axes[0, column].set_yscale('log')\n",
" axes[0, column].set_ylim(max(fit['counts'].min() * 0.5, 1), fit['counts'].max() * 2)\n",
" axes[0, column].set_title(f\"{variant}: {group['label']}\", fontsize=9)\n",
" axes[0, column].legend(fontsize=6)\n",
" axes[1, column].plot(x, (fit['counts'] - fit['model'](x)) / np.sqrt(np.maximum(fit['counts'], 1)),\n",
" '.', ms=2.5, color=COLOURS[variant])\n",
" axes[1, column].axhline(0, color='k', lw=.6)\n",
" axes[1, column].set_xlabel('energy [keV]'); axes[1, column].set_ylabel('pull')\n",
"figure.tight_layout()"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "8327278e",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:26.628157Z",
"iopub.status.busy": "2026-09-19T09:10:26.628054Z",
"iopub.status.idle": "2026-09-19T09:10:26.732058Z",
"shell.execute_reply": "2026-09-19T09:10:26.731776Z"
}
},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 750x440 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"figure, axis = plt.subplots(figsize=(7.5, 4.4))\n",
"for offset, variant in zip((-0.08, 0.0, 0.08), energies):\n",
" axis.errorbar([line_centroid(n) + offset for n in LINES],\n",
" [peak(variant, n)['sigma'] for n in LINES],\n",
" [peak(variant, n)['sigma_err'] for n in LINES],\n",
" fmt='o', ms=5, capsize=3, color=COLOURS[variant], label=variant)\n",
"e = np.linspace(min(line_centroid(n) for n in LINES) - 1, max(line_centroid(n) for n in LINES) + 1, 200)\n",
"e = e[(e >= 5) & (e <= 30)]\n",
"axis.plot(e, correction.mc_sigma(e), 'k--', lw=1, label='MC core sigma (raw)')\n",
"axis.plot(e, correction.mc_sigma(e) / correction.slope(e), 'k:', lw=1, label=\"MC / g' (true keV)\")\n",
"axis.set_xlabel('line energy [keV]'); axis.set_ylabel('core sigma [keV]'); axis.set_ylim(0, None)\n",
"axis.set_title('energy resolution'); axis.legend(fontsize=8); axis.grid(alpha=.3)\n",
"figure.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "fadc5f4d",
"metadata": {},
"source": [
"## 4. Reconstruction efficiency\n",
"\n",
"Without truth, three handles on the 10 000 frames of the classical reference:\n",
"\n",
"* **isolated photons**: classical clusters with no other cluster within 4 px\n",
" that have a grid photon within 1.5 px. Above 5 keV the 2.5 keV classical\n",
" threshold finds every isolated photon, so this is the absolute efficiency.\n",
" The MC equivalent is photons with no neighbour within 4 px.\n",
"* **pile-up**: classical clusters holding two or more grid photons within 2 px.\n",
"* **energy balance**: summed reconstructed energy / deposited energy per frame\n",
" (calibrated frame minus its baseline)."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "af106e1d",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:26.733295Z",
"iopub.status.busy": "2026-09-19T09:10:26.733190Z",
"iopub.status.idle": "2026-09-19T09:10:27.674956Z",
"shell.execute_reply": "2026-09-19T09:10:27.674675Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"isolated photons >= 5 keV found: 99.97 % (3-5 keV: 92.9 %; MC: 99.96 %)\n",
"grid / classical photon count: 1.091\n",
"classical clusters with >= 2 grid photons: 8.5 % (Poisson, second photon within 2 px: 8.3 %)\n",
"energy balance: grid 0.9968, classical 0.9980\n"
]
},
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 750x420 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"match = reference['classical_match']\n",
"found = match >= 0\n",
"isolated = reference['isolation_classical'] > 4.0\n",
"above5 = classical[:, 3] >= 5\n",
"edges_e = np.arange(2.5, 30.01, 0.5)\n",
"which = np.digitize(classical[:, 3], edges_e) - 1\n",
"fraction = np.array([found[isolated & (which == i)].mean() if (isolated & (which == i)).sum() > 200\n",
" else np.nan for i in range(len(edges_e) - 1)])\n",
"\n",
"mc = json.load(open(EXPERIMENT / 'eval_summary.json'))['1%']['pair']\n",
"far = [i for i, edge in enumerate(mc['edges']) if edge >= 4.0]\n",
"mc_isolated = sum(mc['n_match'][i] for i in far) / sum(mc['n_true'][i] for i in far)\n",
"\n",
"figure, axis = plt.subplots(figsize=(7.5, 4.2))\n",
"axis.plot(0.5 * (edges_e[1:] + edges_e[:-1]), fraction * 100, 'o-', ms=3,\n",
" color=COLOURS['grid direct'], label='isolated classical clusters found by the grid')\n",
"axis.axhline(mc_isolated * 100, color='k', ls='--', lw=1, label='MC, isolated photons, 1 %')\n",
"axis.axvspan(0, 3, color='0.9'); axis.set_ylim(90, 100.5); axis.set_xlim(2.5, 30)\n",
"axis.set_xlabel('classical 3x3 energy [keV]'); axis.set_ylabel('%')\n",
"axis.set_title('detection efficiency'); axis.legend(fontsize=8); axis.grid(alpha=.3)\n",
"figure.tight_layout()\n",
"\n",
"multiplicity = reference['grid_per_classical']\n",
"occupancy = summary['occupancy_valid_region']\n",
"balance = reference['balance'][:, 1].sum()\n",
"low = (classical[:, 3] >= 3) & ~above5\n",
"print(f\"isolated photons >= 5 keV found: {found[isolated & above5].mean()*100:.2f} % \"\n",
" f\"(3-5 keV: {found[isolated & low].mean()*100:.1f} %; MC: {mc_isolated*100:.2f} %)\")\n",
"print(f\"grid / classical photon count: {len(grid_subset) / len(classical):.3f}\")\n",
"print(f\"classical clusters with >= 2 grid photons: {(multiplicity >= 2).mean()*100:.1f} % \"\n",
" f\"(Poisson, second photon within 2 px: {(1 - np.exp(-occupancy * np.pi * 4))*100:.1f} %)\")\n",
"print(f\"energy balance: grid {grid_subset[:, 3].sum() / balance:.4f}, \"\n",
" f\"classical {classical[:, 3].sum() / balance:.4f}\")"
]
},
{
"cell_type": "markdown",
"id": "16e3c938",
"metadata": {},
"source": [
"## Conclusions\n",
"\n",
"* **Lines.** All resolved; the grid puts them within 0.03 keV of the tables,\n",
" the classical 3x3 sum 0.06-0.10 keV high (the MOENCH014 LUT reads 0.3-0.5 %\n",
" high here). The MC-based correction changes nothing at these energies\n",
" (<= 12 meV).\n",
"* **Energy resolution.** FWHM 0.82 / 0.83 / 0.95 keV for Mo Ka / Mo Kb /\n",
" Ag Ka against 1.09 / 1.16 / 1.15 keV for the classical sum, 18-28 %\n",
" narrower. The core widths (0.33-0.38 keV) are 19-26 % above the MC\n",
" (0.28-0.30 keV): at these energies the data carry an extra term of ~1 % of E\n",
" (pixel-to-pixel gain residuals of the calibration, not in the MC). The same\n",
" holds for the previous 5-30 keV model.\n",
"* **Efficiency.** 99.97 % of the isolated photons above 5 keV (MC 99.96 %).\n",
" The grid finds 9.1 % more photons than the classical finder: pile-up the 3x3\n",
" finder merges (8.5 % of its clusters hold two grid photons, 8.3 % expected\n",
" from Poisson), plus low-energy photons that the 3-30 keV model now reports.\n",
" Energy balance 0.997."
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "a8e470c1",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:27.676156Z",
"iopub.status.busy": "2026-09-19T09:10:27.676067Z",
"iopub.status.idle": "2026-09-19T09:10:27.708535Z",
"shell.execute_reply": "2026-09-19T09:10:27.708203Z"
}
},
"outputs": [],
"source": [
"output = dict(\n",
" run=RUN, epoch=summary['epoch'], n_frames=n_frames,\n",
" photons_per_frame=summary['photons_per_frame'], occupancy=occupancy,\n",
" lines={name: {variant: dict(mu=float(peak(variant, name)['mu']),\n",
" sigma=float(peak(variant, name)['sigma']),\n",
" fwhm=float(fwhm_of(variant, name)))\n",
" for variant in energies}\n",
" for name in PEAKS},\n",
" efficiency_isolated_above5keV=float(found[isolated & above5].mean()),\n",
" efficiency_isolated_mc=mc_isolated,\n",
" grid_over_classical_count=len(grid_subset) / len(classical),\n",
" energy_balance_grid=float(grid_subset[:, 3].sum() / balance),\n",
")\n",
"with open(GRID / RUN / 'xrf_energy_analysis.json', 'w') as handle:\n",
" json.dump(output, handle, indent=1)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "pytorch_nightly",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.18"
}
},
"nbformat": 4,
"nbformat_minor": 5
}