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

+549
View File
@@ -0,0 +1,549 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "d50894bb",
"metadata": {},
"source": [
"# Knife edge, 0 filter — GridBased against eta interpolation\n",
"\n",
"The highest flux point of the scan: **10.8 % occupancy** (flat field, inside\n",
"the ESF ROI), 14.4x the 3-filter point and above where the cluster chain was ever\n",
"operated.\n",
"\n",
"No cluster-based reconstruction exists at this flux point: the beamtime\n",
"only wrote cluster files for the 1-, 2- and 3-filter measurements\n",
"(`Samples/Measurement/2603MaxIV_*Filters_12keV`), so neither the CNN\n",
"chain nor eta interpolation has a 0-filter image. Only the GridBased reconstruction\n",
"is analysed here; the classical chains are compared against GridBased in the other three notebooks.\n",
"\n",
"\n",
"**Edge width (revised 2026-09-28).** The ESF is binned and fitted exactly as\n",
"`Plotting/fig_knife_edge.py --fit odr` does it: `generate_presampled_esf()` and\n",
"`get_odr_fitting()` from `DeepLearning/src/funcs_postprocess.py` (bin content\n",
"Σk/Σf), symmetric ±4σ window, **ESF bin width 0.3 µm**. The ODR model is\n",
"`p0/2 (1 + erf((x - p1)/(sqrt(2) sigma))) + p3`, so the fit gives **sigma**, the\n",
"width of the Gaussian LSF, which is what the tables quote, with\n",
"`MTF50 [lp/mm] = 1000 sqrt(ln 2 / 2) / (pi sigma)`; MTF50 and MTF(200 lp/mm) are\n",
"also measured from the LSF (`Plotting/knife_edge_numbers.py`: first crossing,\n",
"central-difference correction), and the relative DQE uses the latter. Only the\n",
"flattened GridBased reconstruction is analysed. Same processing as\n",
"`KnifeEdge_poly2to20keV_Moench040.ipynb`.\n",
"\n",
"**Occupancy** is that of the flat field inside the ESF ROI: the reconstructed\n",
"photons per 25 µm pixel per frame over `ROI // 10` (x 33–46, y 26–49 of the\n",
"99 × 99 frame), the pixels over which `DeepLearning/Notebooks/KnifeEdge_3filters_pos0.ipynb`\n",
"sums the deposited energy (0.705 % at 3 filters from the energy, 0.747 % here\n",
"from the photons of the 12 keV model).\n",
"\n",
"*Until 2026-09-28 this notebook took the fitted sigma for sqrt(2) sigma (so its\n",
"MTF50 was sqrt(2) too high), used 0.5 µm bins, and binned the ESF with an older\n",
"copy of `generate_presampled_esf` (inverse-variance mean of k/f, biased low in\n",
"the toe, sigma ~3 % too small). The previous version is in\n",
"`Backup_260928_pre_sigma_fix/`.*\n",
"\n",
"The ROI, the oversampled ESF and the erf fit are the ones from\n",
"`DeepLearning/Notebooks/KnifeEdge_3filters_pos0.ipynb`; the edge sits at the\n",
"same place at all three flux points, so the same ROI holds throughout and the\n",
"three notebooks are directly comparable.\n",
"\n",
"Run in the `pytorch_nightly` environment (ROOT + scipy + jupyter)."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "3975e8f9",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:08.904496Z",
"iopub.status.busy": "2026-09-28T08:23:08.904413Z",
"iopub.status.idle": "2026-09-28T08:23:10.494059Z",
"shell.execute_reply": "2026-09-28T08:23:10.493725Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Welcome to JupyROOT 6.28/10\n"
]
}
],
"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, '/home/xie_x1/MLXID/DeepLearning/src') # the code Plotting/ uses\n",
"sys.path.insert(0, '/home/xie_x1/MLXID/Plotting')\n",
"from ROOT import TCanvas, gROOT\n",
"gROOT.SetBatch(True)\n",
"from funcs_postprocess import generate_presampled_esf, get_odr_fitting\n",
"from knife_edge_numbers import mtf as mtf_from_lsf # first crossing, sinc-corrected\n",
"\n",
"GRID = Path('/mnt/sls_det_storage/moench_data/MLXID/InferenceResults/GridBased')\n",
"PUBLISHED = Path('/home/xie_x1/MLXID/DeepLearning/InferenceResults')\n",
"ETA = Path('/home/xie_x1/MLXID/EtaInterpolation/Results')\n",
"\n",
"EDGE_RUN, FLAT_RUN = 'Edge_0filters_12keV', 'Flat_0filters_12keV'\n",
"BIN_UM = 2.5 # super-pixel = 25 um / 10\n",
"ROI = (330, 465, 260, 490) # x1, x2, y1, y2, as published\n",
"CROP = slice(10, -10) # 1010 -> 990, as published\n",
"BIN_WIDTH_UM = 0.3 # presampled ESF bin, as Plotting/ and the manuscript\n",
"\n",
"plt.rcParams.update({'figure.dpi': 110, 'image.cmap': 'gray',\n",
" 'image.origin': 'lower', 'image.interpolation': 'nearest'})"
]
},
{
"cell_type": "markdown",
"id": "8c496448",
"metadata": {},
"source": [
"## 1. Load every reconstruction of the same measurement"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "4bcf2cfb",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:10.495691Z",
"iopub.status.busy": "2026-09-28T08:23:10.495537Z",
"iopub.status.idle": "2026-09-28T08:23:10.618415Z",
"shell.execute_reply": "2026-09-28T08:23:10.618180Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"GridBased model epoch 135, 3,500,000 frames, occupancy 10.770 % (flat field, ESF ROI)\n",
" sub-pixel excess rms 18.3 % -> 1.6 % after flattening\n",
"\n",
"variant edge photons flat photons relative\n",
"GridBased flat 6.8684e+08 1.1910e+09 1.000\n"
]
}
],
"source": [
"def crop(image):\n",
" \"\"\"The published frames are cropped by 10 super-pixels on each side.\"\"\"\n",
" return image[CROP, CROP] if image.shape[0] == 1010 else image\n",
"\n",
"\n",
"for run in (EDGE_RUN, FLAT_RUN):\n",
" if not (GRID / run / 'summary.json').exists():\n",
" raise SystemExit(\n",
" f'{run} has not been reconstructed yet. Run:\\n'\n",
" f' python scripts/05_infer_measurement.py '\n",
" f'--config Configs/infer_measurement_12keV.yaml --run {run}')\n",
"\n",
"grid_summary = json.load(open(GRID / EDGE_RUN / 'summary.json'))\n",
"flat_summary = json.load(open(GRID / FLAT_RUN / 'summary.json'))\n",
"\n",
"\n",
"def grid_image(run, flattened):\n",
" name = 'binned_image_flattened.npy' if flattened else 'binned_image.npy'\n",
" return np.load(GRID / run / name).astype(float)\n",
"\n",
"\n",
"variants = {\n",
" 'GridBased flat': (grid_image(EDGE_RUN, True), grid_image(FLAT_RUN, True)),\n",
"}\n",
"\n",
"# This flux point was never clustered: only 2603MaxIV_{Edge,Flat}{1,2,3}Filters\n",
"# exist under Samples/Measurement, so there is no CNN and no eta image to load\n",
"# here. (Until 2026-09-17 this cell loaded the *1-filter* eta image, which is a\n",
"# different measurement at 2.4x the flux and was not comparable with anything.)\n",
"\n",
"ORDER = ['GridBased flat']\n",
"variants = {name: (crop(e), crop(f)) for name, (e, f) in variants.items()}\n",
"variants = {name: variants[name] for name in ORDER}\n",
"\n",
"def flat_occupancy(run):\n",
" # reconstructed photons per 25 um pixel per frame in the flat field, inside the\n",
" # ESF ROI: ROI // 10 in pixels of the 99 x 99 frame, both ends included as in\n",
" # DeepLearning/Notebooks; +1 because the 101 x 101 frame is cropped by 1 px\n",
" summary = json.load(open(GRID / run / 'summary.json'))\n",
" native = np.load(GRID / run / 'native_image.npy') / summary['n_frames']\n",
" x0, x1, y0, y1 = (v // 10 for v in ROI)\n",
" return float(native[y0 + 1:y1 + 2, x0 + 1:x1 + 2].mean())\n",
"\n",
"\n",
"occupancy = flat_occupancy(FLAT_RUN)\n",
"print(f\"GridBased model epoch {grid_summary['epoch']}, \"\n",
" f\"{grid_summary['n_frames']:,} frames, \"\n",
" f\"occupancy {occupancy*100:.3f} % (flat field, ESF ROI)\")\n",
"print(f\" sub-pixel excess rms {grid_summary['subpixel_nonuniformity_before']*100:.1f} % -> \"\n",
" f\"{grid_summary['subpixel_nonuniformity_after']*100:.1f} % after flattening\\n\")\n",
"print(f\"{'variant':16s} {'edge photons':>14} {'flat photons':>14} relative\")\n",
"reference = variants['GridBased flat'][0].sum()\n",
"for name, (e, f) in variants.items():\n",
" print(f'{name:16s} {e.sum():14.4e} {f.sum():14.4e} {e.sum()/reference:8.3f}')"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "21d95658",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:10.619670Z",
"iopub.status.busy": "2026-09-28T08:23:10.619469Z",
"iopub.status.idle": "2026-09-28T08:23:10.852109Z",
"shell.execute_reply": "2026-09-28T08:23:10.851858Z"
}
},
"outputs": [
{
"data": {
"image/png": "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1tZWzJkzB6NHj8brr7+OSZMmYcCAAR3G/t///V+cddZZ2HXXXXHFFVfA5/PhwQcfrJkrffLJJ+Pee+/FtGnTcOKJJwIA/vCHP+Doo4/G7bffjjlz5nT+IKqw/fbbY9ddd8WLL75oe/1Pf/oT3G531dd33313DB48uOp4BxxwAG666Sacf/75OProo00IbzQatR33zjvvYPLkyfjBD36AGTNm4O9//zsWLFiAlpYWPPvss53O+ac//SkaGxvxxBNP4KabbsLAgQMB2MOLAWDevHlIJBI4+eSTEY1Gce+99+KCCy7AkCFDcMIJJ5jjeuNzAgDpdBoHHnggxo0bh6uuugorV67E//7v/+Lb3/42Xn/9dey+++5djtEZhx9+OOLxOC699FI0Nzfj17/+NQ4//HA888wzOPTQQ3s01vr163HQQQehUqng9NNPxw477ICWlha89957+POf/4yTTz7Zdvwdd9yB1atX47TTTsOgQYPwhz/8AT/72c8Qi8VsYdN9+e+lMyqVSlVxDwD19fXm+Y0aNQr/+te/sGDBApx77rndLmRXbey6uroOKTDdpVAoYOXKlSYHnCxatAjZbBbf//73EYlEqp7rdrsxe/ZsnH766Xj88cex2267bdQcFEVRFEVRlK8gVg+56667LADWyy+/bOVyOeuEE06w3G639atf/arDsSNGjLAAWA888IDt9WuuucYCYD333HPmtZdfftkCYN11110dxjjwwANtr2WzWWvbbbe1JkyYYOVyOdt7jz76qAXAuvvuu7u8lz322MPaYYcdrPXr19te/9vf/ma53W5r/vz55rXvf//7FgDriSeeqHovI0aMMK+1tLRYkUjE2nHHHa1EImFez+Vy1t57720BsC6//HLz+sKFCy0A1o033thhjt/97neteDxuG6cnnHnmmZbL5bLWrl1rWZZlffrppxYAcz+ffPKJZVmW9cUXX1gul8s699xzzbnVngnPl/OXALBcLpf12muv2V4//fTTLQDWv/71ry7nfPnll1sArE8//bTDe/z8jR071vbsU6mUNWDAAGu//fYzr/XW5+TAAw+0AFhnnnmm7fU33njDcrlc1iGHHGJeq/U5rnVfJ510kgXAOvLII61yuWxe/+yzz6xwOGztvPPOVqVSMa9X+/dw4IEH2j5/f/zjHy0A1kMPPdTpfXGu2267rdXc3GxeL5fL1ujRo60hQ4bYju+rfy+dwf+G1Pp69dVXbfPw+/0WAGv48OHWD37wA+uWW26x/vGPf1Qdm8+12tczzzzT5dz4PJ9++mnryy+/tNauXWstWbLEOvrooy0A1gknnGA7/sILL7QAWI8//nin47755psWAOvYY4/tcC15v4qiKIqiKMrWxUaHnbe0tOCwww7DwoUL8eijj+Kcc86petx2222HmTNn2l77zne+A6A9TLSnvPjii/j8888xa9YsJJNJrFu3znwdeOCBiEQieO655zod4/3338c//vEPnHDCCcZZ49eoUaOw8847mzEqlQoWLlyIMWPGYPr06bZxzjvvvA7O7/PPP490Oo0zzjjDVrgpEAjg/PPP7zCX++67D6FQCDNmzLDNY926dTj66KORSCTw+uuvb9RaHXLIIbAsy4Qw/+lPf4LH48EVV1wBj8eDP/3pTwCAl156CZZl1QzZ7Qn77bcfvvWtb9le29Rn7uSss86yhe1GIhHst99+tvF743MimTdvnu33CRMm4NBDD8VLL72ElpaWTbqfn/zkJ7YokBEjRuDEE0/Ehx9+iHfffbdHY9FtXbx4Mdra2ro8/pRTTkFDQ4P53e1245BDDsGaNWuQSqUA9O2/l67Ybrvt8MILL1T9ampqMsfts88+eOutt3DqqaeiVCrh3nvvxdlnn4099tgD48eP75AiwHutNu7ee+/d7fl997vfxaBBgzB48GBMmDABTz75JP7f//t/HXK2+SwYHVALvt+dZ6coiqIoiqJsPWx02PmsWbOQSCTwwgsvdBoWu+OOO3Z4jWGn69ev36hrL1u2DABwxhln4Iwzzqh6DPOWuxrjmmuuwTXXXFP1GM79iy++QDKZxJgxYzocEwwGMWrUKFtbrE8++QQAsOuuu3Y4fvTo0VXnks1msf3229ecb1f3U4uDDjrIhJifcMIJePHFF7Hnnntihx12wPjx4/Hiiy/itNNOw4svvgiPx4MDDzxwo64j6Ytn3t1ryPF743NC6uvrsd1223V4fcyYMXjhhRfwySefYM899+zWWNWo9tniax999BG++c1vdnusAw44AKeccgp+97vf4cEHH8See+6JiRMn4rjjjsN+++3X4fiunlc0Gu3Tfy9dEQqFuh16v9tuu2HBggUAgFWrVuEvf/kL7r33XixevBhTpkzBBx98YNtocLlcPQ7rd3LTTTdh9913Rzabxeuvv47rr78en3/+eYew9Xg8DqBrUd1dka4oiqIoiqJsXWy0+J4xYwZ+97vf4corr8Q+++xTszWPrPTrxNrI9kiVSgUAcPXVV2PChAlVj5F/YHc2xtlnn42jjjqq6jHdbfVT6z66m3NaqVRQV1eHxx57rOYxG5v7WV9Line truncated
"text/plain": [
"<Figure size 1650x506 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, axes = plt.subplots(1, len(ORDER) if len(ORDER) < 4 else 3, figsize=(15, 4.6))\n",
"extent = [0, 990 * BIN_UM, 0, 990 * BIN_UM]\n",
"shown = ORDER if len(ORDER) < 4 else ['eta', 'cluster <=3ph', 'GridBased flat']\n",
"for ax, name in zip(np.atleast_1d(axes), shown):\n",
" image = variants[name][0]\n",
" im = ax.imshow(image, extent=extent, vmax=np.percentile(image, 99.5))\n",
" ax.add_patch(plt.Rectangle((ROI[0]*BIN_UM, ROI[2]*BIN_UM),\n",
" (ROI[1]-ROI[0])*BIN_UM, (ROI[3]-ROI[2])*BIN_UM,\n",
" edgecolor='red', facecolor='none', lw=1.4))\n",
" ax.set_title(name); ax.set_xlabel('x [um]'); ax.set_ylabel('y [um]')\n",
" plt.colorbar(im, ax=ax, fraction=0.046)\n",
"fig.suptitle('knife edge, with the published ESF ROI')\n",
"fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "782d9a37",
"metadata": {},
"source": [
"## 2. Edge spread function\n",
"\n",
"`generate_presampled_esf` fits an erf to every row of the ROI, fits a straight\n",
"line through the edge positions, and re-samples the ROI onto a common axis;\n",
"`get_odr_fitting` then fits the erf with x and y errors inside |x - p1| <= 4 sigma\n",
"(the fit of `Plotting/fig_knife_edge.py --fit odr`)."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "7a741198",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:10.853265Z",
"iopub.status.busy": "2026-09-28T08:23:10.853179Z",
"iopub.status.idle": "2026-09-28T08:23:11.402245Z",
"shell.execute_reply": "2026-09-28T08:23:11.401964Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Final Edge Midpoint Fit: slope = -0.0858, intercept = 433.2022, chi2/ndf = 0.0302\n",
"variant sigma [um] chi2/ndf MTF50 MTF50 LSF MTF(200) LSF rows\n",
"GridBased flat 1.473 +- 0.017 1.30 127.2 133.0 0.222 100%\n"
]
}
],
"source": [
"def analyse(edge_image, flat_image, label):\n",
" esf = generate_presampled_esf(edge_image, flat_image, ROI,\n",
" pixel_size_um=BIN_UM, bin_width_um=BIN_WIDTH_UM,\n",
" chi2_threshold=15.0, residual_tolerance_px=1.0)\n",
" x, z = esf['numpy']['x'], esf['numpy']['z']\n",
" odr = get_odr_fitting(x, z, esf['numpy']['x_err'], esf['numpy']['z_err'], n_sigma=4.0)\n",
" sigma, sigma_err = odr['sigma_um'], odr['sigma_err_um'] # sigma of the Gaussian LSF\n",
" try:\n",
" mtf50_lsf, mtf200_lsf = mtf_from_lsf(dict(x=x, z=z, pars=odr['beta']))\n",
" except Exception:\n",
" mtf50_lsf = mtf200_lsf = np.nan\n",
" n_fit = int(odr['mask'].sum())\n",
" ndf = max(n_fit - len(odr['beta']), 1)\n",
" x_fit = x[odr['mask']]\n",
" return dict(label=label, x=x, z=z, sigma=sigma, sigma_err=sigma_err, sigma_gauss=sigma,\n",
" chi2_ndf=odr['chi2_ndf'], chi2=odr['chi2_ndf'] * ndf, ndf=ndf,\n",
" mtf50=1000 * np.sqrt(np.log(2) / 2) / (np.pi * sigma),\n",
" mtf10=1000 * np.sqrt(np.log(10) / 2) / (np.pi * sigma),\n",
" mtf50_lsf=mtf50_lsf, mtf200_lsf=mtf200_lsf,\n",
" rows_kept=esf['valid_lines_ratio'],\n",
" edge_slope=esf['edge_slope'], edge_centre=odr['beta'][1],\n",
" fit_points=n_fit, fit_range=(x_fit.min(), x_fit.max()),\n",
" photons=float(edge_image.sum()), odr=odr)\n",
"\n",
"\n",
"results = {name: analyse(edge, flat, name) for name, (edge, flat) in variants.items()}\n",
"\n",
"print(f\"{'variant':16s} {'sigma [um]':>16} {'chi2/ndf':>9} {'MTF50':>7} {'MTF50 LSF':>9} \"\n",
" f\"{'MTF(200) LSF':>12} {'rows':>6}\")\n",
"for name, r in results.items():\n",
" print(f\"{name:16s} {r['sigma']:7.3f} +- {r['sigma_err']:.3f} {r['chi2_ndf']:9.2f} {r['mtf50']:7.1f} \"\n",
" f\"{r['mtf50_lsf']:9.1f} {r['mtf200_lsf']:12.3f} {r['rows_kept']*100:5.0f}%\")"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "f14649cd",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:11.403441Z",
"iopub.status.busy": "2026-09-28T08:23:11.403279Z",
"iopub.status.idle": "2026-09-28T08:23:11.709352Z",
"shell.execute_reply": "2026-09-28T08:23:11.709131Z"
}
},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1375x528 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"COLOURS = {'eta': '#a0451c', 'cluster 1ph': '#6b6b6b', 'cluster <=2ph': '#7b5aa6',\n",
" 'cluster <=3ph': '#4b2a6b', 'GridBased raw': '#4cbcac',\n",
" 'GridBased flat': '#10695f'}\n",
"frequency = np.linspace(0, 400, 2000)\n",
"fig, ax = plt.subplots(1, 2, figsize=(12.5, 4.8))\n",
"for name, r in results.items():\n",
" ax[0].plot(r['x'], r['z'], lw=1.1, color=COLOURS[name],\n",
" label=f\"{name}: $\\\\sigma$ = {r['sigma']:.3f} $\\\\pm$ {r['sigma_err']:.3f} $\\\\mu$m\")\n",
" sigma_mm = r['sigma_gauss'] / 1000.0\n",
" ax[1].plot(frequency, np.exp(-2 * np.pi**2 * sigma_mm**2 * frequency**2),\n",
" lw=1.6, color=COLOURS[name], label=f\"{name} ({r['mtf50']:.0f} lp/mm)\")\n",
"ax[0].set_xlim(-9, 9); ax[0].set_ylim(-0.05, 1.35)\n",
"ax[0].set_xlabel(r'position across the edge [$\\mu$m]')\n",
"ax[0].set_ylabel('relative intensity')\n",
"ax[0].legend(fontsize=8); ax[0].grid(alpha=.3); ax[0].set_title('oversampled ESF')\n",
"ax[1].axvline(200, color='k', ls=':', lw=.8, label='2.5 um Nyquist')\n",
"ax[1].axhline(0.5, color='grey', lw=.5)\n",
"ax[1].set_xlim(0, 400); ax[1].set_ylim(0, 1.05)\n",
"ax[1].set_xlabel('spatial frequency [lp/mm]'); ax[1].set_ylabel('MTF')\n",
"ax[1].legend(fontsize=8); ax[1].grid(alpha=.3); ax[1].set_title('MTF from the fitted ESF')\n",
"fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "f81b1689",
"metadata": {},
"source": [
"## 3. Where the GridBased sub-pixel phase stands"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "be944928",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:11.710528Z",
"iopub.status.busy": "2026-09-28T08:23:11.710438Z",
"iopub.status.idle": "2026-09-28T08:23:12.378356Z",
"shell.execute_reply": "2026-09-28T08:23:12.378123Z"
}
},
"outputs": [
{
"data": {
"image/png": "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bTjnnHPwwQ9+EFdddRU2bdqESy+9FKOjo/j5z3+Oo446Cg888ADe9KY34bzzzsOHP/xhVUef+9zncN111+Gyyy7D6173Ovzxj3/EZz7zGRSLRVx++eXKjnq9jvPPPx+f+cxncPXVV6O9vR2rVq3C8ccfjyVLluDHP/4xTNPEV77yFZx00kl47rnnkEwmJ5VHQoacf/75WLhwIS6//PItngcAQ0ND6OjowLXXXov29nasWLECV155Jfr7+/GjH/0IAPDAAw/gNa95DT796U/jvPPOA0AskK0dB4C///u/xw9/+EN88YtfxLHHHov7778fn//855FOp3HRRRfF6v69730vLr74Ylx++eW4+uqr8c53vhNr165FZ2fnS7bZttJdeuml+P73v48vf/nLOOKII3DLLbfgn//5n7evU2hoaGho7LfQc5ztw4YNG3DwwQfj/PPPRz6fxxNPPIErrrgCpVIJX/rSl7Y6X6jVarv8fN/SnKYZLzV32pmy/PKXv8Tf/d3f4atf/apir7zUcQB497vfjXvvvRdXXHEFFi1ahDvvvBPvfe970d7ejlNPPVVdv7+/HxdffDEuvfRStLS04NJLL8VZZ52FF154AY7jbLUNtifdBz/4Qfz5z3/G17/+dcyaNQvf//738fjjj293O2toaGhoaGhoaGi8LBFqaOxB3HrrrSGAcPXq1erY6tWrQwDhrbfeusU0vu+HruuGBx98cPiFL3xBHb/iiivCbDa72647Pj4e5nK52DXCMAyvv/76MJ1Oh0NDQ2EYhuHdd98dAggfeeSRMAzDcOnSpaFhGOF//dd/qTSu64YzZ84Md2ZIXXHFFSGAWH6e54WzZ88Ozz333DAMwzAIgnDWrFnqu+COO+4IDcMIn3nmGXUsm82GV1xxhfo+ODgYOo4Tfu5zn4ul/ehHPxpms9mwWCzG7Ljlllti511wwQXhnDlzwkqloo4NDAyE2Ww2vP7661+ybIccckj4/ve/P3bs/e9/f3jIIYdsNY3ruuFPf/rT0LbtsFQqqeMAwn/7t3+bdP6Wjj///POhYRjh97///djxz33uc+GUKVNC3/djZb7jjjvUOStXrgwBhD/5yU/UsRNPPDE87bTT1PftSTc8PBymUqnwy1/+csyGE088cVLf1NDQ0NA48KDnODuGIAhC13XDr3zlK2FfX1/sty3NF3bH8715TrM1NM+dwnDys397yrK19t/a8T//+c8hgPAPf/hD7PjZZ58dHn300er7+9///knzvT/+8Y8hgPCvf/2rOjZr1qzwk5/85A6le/bZZ0MA4Y033qjO8TwvnDt37i63uYaGhoaGhoaGhsaBDB2qS2O/wLJly3DWWWeht7cXlmXBcRw899xzWLFixR675v3334+JiQmcffbZ8DxP/b3pTW9CpVLBM888s8V0Dz/8MMIwVKEyAMC2bZx55pm7ZE9jfpZl4YwzzsCDDz4IAFixYgXWrl2Ld7/73TFbTzzxRBiGgUcffXSr+T700EOK0dKIc889F6VSCX/7299ix9/2trfFvt91110488wzYdu2um57ezsOP/xwPPLII7tUZoBCblx33XVYvHgx0uk0HMfB+eefD8/zsGrVqp3K83//938BAO985ztj9XXSSSehv78f69atU+eapok3v/nN6vv8+fORSCSwfv36l7zGttI9/fTTqFarOOOMM2LpdrWfaGhoaGgcWHglz3Gq1SquuOIKzJ8/H8lkEo7j4Itf/CI2bdqEiYmJHc5vR57vwOQ5za5gd5cFoDlWR0cH3vSmN00qz9/+9jf4vq/OnTp1Kg455BD1XRg425qvbCudzOUa5yuWZcVClGpoaGhoaGhoaGi8EqFDdWnscxSLRbzlLW9Bd3c3rr32WsyaNQupVAof/vCHUa1W99h1h4aGAFCYhi2h+eVbsGnTJjiOMynkQ29v707bsqX8enp6sGnTppitjQsZ22MrAIyOjgIApkyZEjsu30dGRtSxTCaDbDYbO29oaAjXXXfdJE0UAEin01u97vbiuuuuw2c/+1lccskleOMb34j29nY88sgj+OQnP7nT7T80NIQwDNHV1bXF39etW4dZs2YBoDIkEonY747jbPPa20onbdfd3R07p6enZ/sLoqGhoaFxQOOVPsf5/Oc/jx/84Ae44oor8KpXvQptbW24/fbb8a//+q+oVqvI5XI7lN+OPN+3NKfZFezusgBUnpGRka2G2tq0aROmT58OAGhra4v9JnOQbfWjbaWTNm9tbY2dp+crGhoaGhoaGhoar3TojRONfY4HHngA69evx29/+1scfvjh6nihUFAvi3sCHR0dAIBf/vKXmDFjxqTfRcizGX19fXBdF6Ojo7GFhc2bN++0LVvKb2BgQMW/Flu/853v4Nhjj52UfurUqVvNW9Ju3rwZ06ZNU8f7+/tjvwPYoghoR0cHTjvtNHziE5+Y9Fs+n3/Jcm0Pbr31Vpxxxhm4+uqr1bGlS5fuUp4dHR0wDAP33nvvpM0NADj44IN3Kf/tgbTd4OBgrH0GBgb2+LU1NDQ0NPYPvNLnOLfeeis+9rGP4fOf/7w6dscdd+xLine truncated
"text/plain": [
"<Figure size 1650x462 with 5 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"before = np.load(GRID / FLAT_RUN / 'subpixel_phase_before.npy')\n",
"after = np.load(GRID / FLAT_RUN / 'subpixel_phase_after.npy')\n",
"\n",
"fig, axes = plt.subplots(1, 3, figsize=(15, 4.2))\n",
"for ax, histogram, title in ((axes[0], before, 'before flattening'),\n",
" (axes[1], after, 'after flattening')):\n",
" im = ax.imshow(histogram.T / histogram.mean(), origin='lower', extent=[0, 1, 0, 1],\n",
" vmin=0.9, vmax=1.1, cmap='viridis')\n",
" ax.set_title(f'flat field, {title}', fontsize=10)\n",
" ax.set_xlabel('sub-pixel x'); ax.set_ylabel('sub-pixel y')\n",
" plt.colorbar(im, ax=ax, fraction=0.046)\n",
"for histogram, title in ((before, 'before'), (after, 'after')):\n",
" profile = histogram.sum(axis=1)\n",
" axes[2].plot(np.linspace(0, 1, len(profile)), profile / profile.mean(), lw=1, label=title)\n",
"axes[2].axhline(1, color='k', lw=.6); axes[2].set_xlabel('sub-pixel x')\n",
"axes[2].set_ylabel('relative density'); axes[2].legend(); axes[2].grid(alpha=.3)\n",
"fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "1cc3ca80",
"metadata": {},
"source": [
"## 4. Relative DQE"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "87158001",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:12.379977Z",
"iopub.status.busy": "2026-09-28T08:23:12.379792Z",
"iopub.status.idle": "2026-09-28T08:23:12.483482Z",
"shell.execute_reply": "2026-09-28T08:23:12.483250Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"variant QE rel MTF(200) DQE rel (relative to GridBased flat)\n",
"GridBased flat 1.000 0.222 0.049\n"
]
}
],
"source": [
"# DQE(f) is proportional to QE * MTF(f)^2. Every chain sees the same beam, so\n",
"# the ratio of detected photons is the ratio of QE and the relative DQE is well\n",
"# defined even without the absolute incident flux.\n",
"def mtf_at(frequency_lpmm, sigma_gauss_um):\n",
" sigma_mm = sigma_gauss_um / 1000.0\n",
" return float(np.exp(-2 * np.pi**2 * sigma_mm**2 * frequency_lpmm**2))\n",
"\n",
"\n",
"FREQUENCY = 200.0 # lp/mm, the Nyquist of the 2.5 um bins\n",
"REFERENCE = 'GridBased flat'\n",
"reference_photons = results[REFERENCE]['photons']\n",
"print(f\"{'variant':16s} {'QE rel':>8} {'MTF(200)':>9} {'DQE rel':>9} (relative to {REFERENCE})\")\n",
"relative_dqe = {}\n",
"for name, r in results.items():\n",
" qe = r['photons'] / reference_photons\n",
" mtf200 = r['mtf200_lsf'] # measured from the LSF, as in the poly notebook\n",
" relative_dqe[name] = qe * mtf200**2\n",
" print(f'{name:16s} {qe:8.3f} {mtf200:9.3f} {relative_dqe[name]:9.3f}')\n"
]
},
{
"cell_type": "markdown",
"id": "facc1139",
"metadata": {},
"source": [
"## 5. Summary"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "575e8e9c",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:12.484541Z",
"iopub.status.busy": "2026-09-28T08:23:12.484461Z",
"iopub.status.idle": "2026-09-28T08:23:12.588463Z",
"shell.execute_reply": "2026-09-28T08:23:12.588198Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{\n",
" \"GridBased flat\": {\n",
" \"sigma_um\": 1.473126548845613,\n",
" \"sigma_err_um\": 0.016628349232745622,\n",
" \"mtf50_lsf_lpmm\": 133.0356249682,\n",
" \"mtf200_lsf\": 0.22230766940089752,\n",
" \"chi2_ndf\": 1.2970189194700865,\n",
" \"mtf50_lpmm\": 127.2060606579405,\n",
" \"mtf10_lpmm\": 231.8477669544846,\n",
" \"rows_kept\": 1.0,\n",
" \"photons\": 686836853.0\n",
" }\n",
"}\n",
"\n",
"written to /mnt/sls_det_storage/moench_data/MLXID/InferenceResults/GridBased/Edge_0filters_12keV/knife_edge_comparison.json\n"
]
}
],
"source": [
"summary = {\n",
" 'measurement': 'Edge/Flat_0filters_12keV',\n",
" 'occupancy_percent': occupancy * 100, # flat field, ESF ROI\n",
" 'roi_superpixel': list(ROI),\n",
" 'bin_um': BIN_UM,\n",
" 'bin_width_um': BIN_WIDTH_UM,\n",
" 'convention': 'sigma = ODR p2 (Gaussian LSF); MTF50 = 1000 sqrt(ln 2 / 2) / (pi sigma); '\n",
" 'ESF from DeepLearning/src, bin width 0.3 um',\n",
" 'grid_model_epoch': grid_summary['epoch'],\n",
" 'grid_frames': grid_summary['n_frames'],\n",
" 'grid_subpixel_before': grid_summary['subpixel_nonuniformity_before'],\n",
" 'grid_subpixel_after': grid_summary['subpixel_nonuniformity_after'],\n",
" 'variants': {name: dict(sigma_um=float(r['sigma']), sigma_err_um=float(r['sigma_err']),\n",
" mtf50_lsf_lpmm=float(r['mtf50_lsf']),\n",
" mtf200_lsf=float(r['mtf200_lsf']),\n",
" chi2_ndf=float(r['chi2_ndf']),\n",
" mtf50_lpmm=float(r['mtf50']), mtf10_lpmm=float(r['mtf10']),\n",
" rows_kept=float(r['rows_kept']), photons=float(r['photons']))\n",
" for name, r in results.items()},\n",
" 'relative_dqe_200lpmm': relative_dqe,\n",
"}\n",
"print(json.dumps(summary['variants'], indent=1))\n",
"out = GRID / EDGE_RUN / 'knife_edge_comparison.json'\n",
"json.dump(summary, open(out, 'w'), indent=1)\n",
"print(f'\\nwritten to {out}')"
]
}
],
"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
}
+569
View File
@@ -0,0 +1,569 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "116b7266",
"metadata": {},
"source": [
"# Knife edge, 1 filter — GridBased against eta interpolation\n",
"\n",
"The second-highest flux point of the scan: **4.45 % occupancy** (flat field,\n",
"inside the ESF ROI), 6.0x the 3-filter point and above where the cluster chain was ever\n",
"operated.\n",
"\n",
"The cluster-based CNN images for this run exist only for older model\n",
"versions and without the eta-flattening step, so they would not be\n",
"comparable with the other two notebooks and are left out. Eta\n",
"interpolation is available and is kept as the classical reference.\n",
"\n",
"\n",
"**Edge width (revised 2026-09-28).** The ESF is binned and fitted exactly as\n",
"`Plotting/fig_knife_edge.py --fit odr` does it: `generate_presampled_esf()` and\n",
"`get_odr_fitting()` from `DeepLearning/src/funcs_postprocess.py` (bin content\n",
"Σk/Σf), symmetric ±4σ window, **ESF bin width 0.3 µm**. The ODR model is\n",
"`p0/2 (1 + erf((x - p1)/(sqrt(2) sigma))) + p3`, so the fit gives **sigma**, the\n",
"width of the Gaussian LSF, which is what the tables quote, with\n",
"`MTF50 [lp/mm] = 1000 sqrt(ln 2 / 2) / (pi sigma)`; MTF50 and MTF(200 lp/mm) are\n",
"also measured from the LSF (`Plotting/knife_edge_numbers.py`: first crossing,\n",
"central-difference correction), and the relative DQE uses the latter. Only the\n",
"flattened GridBased reconstruction is analysed. Same processing as\n",
"`KnifeEdge_poly2to20keV_Moench040.ipynb`.\n",
"\n",
"**Occupancy** is that of the flat field inside the ESF ROI: the reconstructed\n",
"photons per 25 µm pixel per frame over `ROI // 10` (x 33–46, y 26–49 of the\n",
"99 × 99 frame), the pixels over which `DeepLearning/Notebooks/KnifeEdge_3filters_pos0.ipynb`\n",
"sums the deposited energy (0.705 % at 3 filters from the energy, 0.747 % here\n",
"from the photons of the 12 keV model).\n",
"\n",
"*Until 2026-09-28 this notebook took the fitted sigma for sqrt(2) sigma (so its\n",
"MTF50 was sqrt(2) too high), used 0.5 µm bins, and binned the ESF with an older\n",
"copy of `generate_presampled_esf` (inverse-variance mean of k/f, biased low in\n",
"the toe, sigma ~3 % too small). The previous version is in\n",
"`Backup_260928_pre_sigma_fix/`.*\n",
"\n",
"The ROI, the oversampled ESF and the erf fit are the ones from\n",
"`DeepLearning/Notebooks/KnifeEdge_3filters_pos0.ipynb`; the edge sits at the\n",
"same place at all three flux points, so the same ROI holds throughout and the\n",
"three notebooks are directly comparable.\n",
"\n",
"Run in the `pytorch_nightly` environment (ROOT + scipy + jupyter)."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "3975e8f9",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:03.371301Z",
"iopub.status.busy": "2026-09-28T08:23:03.371220Z",
"iopub.status.idle": "2026-09-28T08:23:05.007618Z",
"shell.execute_reply": "2026-09-28T08:23:05.007252Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Welcome to JupyROOT 6.28/10\n"
]
}
],
"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, '/home/xie_x1/MLXID/DeepLearning/src') # the code Plotting/ uses\n",
"sys.path.insert(0, '/home/xie_x1/MLXID/Plotting')\n",
"from ROOT import TCanvas, gROOT\n",
"gROOT.SetBatch(True)\n",
"from funcs_postprocess import generate_presampled_esf, get_odr_fitting\n",
"from knife_edge_numbers import mtf as mtf_from_lsf # first crossing, sinc-corrected\n",
"\n",
"GRID = Path('/mnt/sls_det_storage/moench_data/MLXID/InferenceResults/GridBased')\n",
"PUBLISHED = Path('/home/xie_x1/MLXID/DeepLearning/InferenceResults')\n",
"ETA = Path('/home/xie_x1/MLXID/EtaInterpolation/Results')\n",
"\n",
"EDGE_RUN, FLAT_RUN = 'Edge_1filters_12keV', 'Flat_1filters_12keV'\n",
"BIN_UM = 2.5 # super-pixel = 25 um / 10\n",
"ROI = (330, 465, 260, 490) # x1, x2, y1, y2, as published\n",
"CROP = slice(10, -10) # 1010 -> 990, as published\n",
"BIN_WIDTH_UM = 0.3 # presampled ESF bin, as Plotting/ and the manuscript\n",
"\n",
"plt.rcParams.update({'figure.dpi': 110, 'image.cmap': 'gray',\n",
" 'image.origin': 'lower', 'image.interpolation': 'nearest'})"
]
},
{
"cell_type": "markdown",
"id": "8c496448",
"metadata": {},
"source": [
"## 1. Load every reconstruction of the same measurement"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "4bcf2cfb",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:05.009178Z",
"iopub.status.busy": "2026-09-28T08:23:05.008836Z",
"iopub.status.idle": "2026-09-28T08:23:05.131827Z",
"shell.execute_reply": "2026-09-28T08:23:05.131438Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"GridBased model epoch 135, 7,000,000 frames, occupancy 4.450 % (flat field, ESF ROI)\n",
" sub-pixel excess rms 19.8 % -> 3.0 % after flattening\n",
"\n",
"variant edge photons flat photons relative\n",
"eta 2.3085e+08 3.9229e+08 0.407\n",
"GridBased flat 5.6697e+08 9.8283e+08 1.000\n"
]
}
],
"source": [
"def crop(image):\n",
" \"\"\"The published frames are cropped by 10 super-pixels on each side.\"\"\"\n",
" return image[CROP, CROP] if image.shape[0] == 1010 else image\n",
"\n",
"\n",
"for run in (EDGE_RUN, FLAT_RUN):\n",
" if not (GRID / run / 'summary.json').exists():\n",
" raise SystemExit(\n",
" f'{run} has not been reconstructed yet. Run:\\n'\n",
" f' python scripts/05_infer_measurement.py '\n",
" f'--config Configs/infer_measurement_12keV.yaml --run {run}')\n",
"\n",
"grid_summary = json.load(open(GRID / EDGE_RUN / 'summary.json'))\n",
"flat_summary = json.load(open(GRID / FLAT_RUN / 'summary.json'))\n",
"\n",
"\n",
"def grid_image(run, flattened):\n",
" name = 'binned_image_flattened.npy' if flattened else 'binned_image.npy'\n",
" return np.load(GRID / run / name).astype(float)\n",
"\n",
"\n",
"variants = {\n",
" 'eta': (np.load(ETA / '2603MaxIV_Edge1Filters_12keV_fromClusters.npy'),\n",
" np.load(ETA / '2603MaxIV_Flat1Filters_12keV_fromClusters.npy')),\n",
" 'GridBased flat': (grid_image(EDGE_RUN, True), grid_image(FLAT_RUN, True)),\n",
"}\n",
"\n",
"# The cluster-based CNN images for this flux point exist only for older model\n",
"# versions (folders 260512_1ph.. / 260511_2ph.. / 260529_3ph..) and without the\n",
"# eta-flattening step, so they are not comparable with the other two notebooks\n",
"# and are left out. Eta interpolation is available and is kept.\n",
"\n",
"ORDER = ['eta', 'GridBased flat']\n",
"variants = {name: (crop(e), crop(f)) for name, (e, f) in variants.items()}\n",
"variants = {name: variants[name] for name in ORDER}\n",
"\n",
"def flat_occupancy(run):\n",
" # reconstructed photons per 25 um pixel per frame in the flat field, inside the\n",
" # ESF ROI: ROI // 10 in pixels of the 99 x 99 frame, both ends included as in\n",
" # DeepLearning/Notebooks; +1 because the 101 x 101 frame is cropped by 1 px\n",
" summary = json.load(open(GRID / run / 'summary.json'))\n",
" native = np.load(GRID / run / 'native_image.npy') / summary['n_frames']\n",
" x0, x1, y0, y1 = (v // 10 for v in ROI)\n",
" return float(native[y0 + 1:y1 + 2, x0 + 1:x1 + 2].mean())\n",
"\n",
"\n",
"occupancy = flat_occupancy(FLAT_RUN)\n",
"print(f\"GridBased model epoch {grid_summary['epoch']}, \"\n",
" f\"{grid_summary['n_frames']:,} frames, \"\n",
" f\"occupancy {occupancy*100:.3f} % (flat field, ESF ROI)\")\n",
"print(f\" sub-pixel excess rms {grid_summary['subpixel_nonuniformity_before']*100:.1f} % -> \"\n",
" f\"{grid_summary['subpixel_nonuniformity_after']*100:.1f} % after flattening\\n\")\n",
"print(f\"{'variant':16s} {'edge photons':>14} {'flat photons':>14} relative\")\n",
"reference = variants['GridBased flat'][0].sum()\n",
"for name, (e, f) in variants.items():\n",
" print(f'{name:16s} {e.sum():14.4e} {f.sum():14.4e} {e.sum()/reference:8.3f}')"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "21d95658",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:05.133060Z",
"iopub.status.busy": "2026-09-28T08:23:05.132975Z",
"iopub.status.idle": "2026-09-28T08:23:05.482855Z",
"shell.execute_reply": "2026-09-28T08:23:05.482542Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABUkAAAH1CAYAAADLW38kAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOy9eXxcZdn//5lJMvuWrTu0WMRSKULVsgktuxRkERUqKgUVERUXig8PChR+bsimwuPCIpuyCAqirSBI4RFl+eICYvvIDm1p0zTJZPaZZOb8/ujrc+eaO2cmaZrQNrner1deSc6c5T73Ocm5zuf+XNftcRzHgaIoiqIoiqIoiqIoiqIoygTFu70boCiKoiiKoiiKoiiKoiiKsj1RkVRRFEVRFEVRFEVRFEVRlAmNiqSKoiiKoiiKoiiKoiiKokxoVCRVFEVRFEVRFEVRFEVRFGVCoyKpoiiKoiiKoiiKoiiKoigTGhVJFUVRFEVRFEVRFEVRFEWZ0KhIqiiKoiiKoiiKoiiKoijKhEZFUkVRFEVRFEVRFEVRFEVRJjQqkiqKoiiKoiiKoiiKoiiKMqFRkVRRFEVRFEVRFEVRFEVRlAmNiqSKoig7Kbfccgs8Hg8ee+yxUd3vY489Bo/Hg1tuuaVqeS6Xw7nnnotdd90VDQ0NmDVr1qgedzRYtGjRDtmubaXWNamHx+PB0qVLx6xNY3X/bS+WLl0Kj8cz7PVnzZqFRYsWVS0b6f03kus7GozXvxdFURRFURRFGQkqkiqKoijD4vLLL8e1116LU045Bbfccgt+8IMfbO8mTWiSySSWL18+piLlY489huXLlyOZTI7ZMZSdg1mzZsHj8dT8Wr58edX6r7/+Oj7/+c9jzz33RDgcRiKRwB577IFTTz0Vv/nNb6rWXbRoUc397r777kO2bfny5VXbeL1eJBIJHHzwwbj99tvrbvvQQw/hpJNOwtSpU+Hz+dDe3o6jjz4ad911FxzHqXmsJ554YuhOUxRFURRFUXYqGrd3AxRFUZQdi0MOOQT5fB5NTU1Vyx9++GHMmzcPV1xxxXZq2cTF7Zokk0lceumlADDI0ThaPPbYY7j00kuxdOlSJBKJMTnGeOKPf/yjq7A2Xpg8eTKuvPJK18/23ntv8/Pf//53HHLIISiXyzjttNPwxS9+EQDw8ssvY8WKFchkMvjwhz9ctb3X68Wtt946aL/RaHTY7bvooouwxx57oFwu480338SNN96IT33qU1i/fj0uuOCCqnUrlQrOPvts3HDDDZg9ezbOOusszJw5Ex0dHbjnnnuwZMkS3HHHHbj77rsRDAaH3QZFURRFURRl50VFUkVRFKUKr9eLQCAwaPnGjRux6667bocWKbWuibJj4fP5tncTxpRIJIJPfOITQ663fPlyZLNZ3H///TjhhBOqPrvmmmuwbt26Qdt4PJ5h7bseRx11FD7wgQ+Y388880zsscceuPzyy3H++eejoaHBfPatb30LN9xwA04++WT88pe/hN/vN59dcMEFWLZsGa6++mp88YtfxE033bRN7VIURVEURVF2DjTdXlEUZZzxk5/8BI2NjfjYxz6GYrEIYKB+4osvvogTTjgB8XgckUgExx57LF555ZWq7e36iEwvfe211/D444+7ptf+4x//wEc+8hFMmjQJPp8P73jHO3DBBRcgl8sNu92vvPIKli5dimnTpsHn82HGjBk455xzsHnz5kHrvvzyyzjppJMQj8cRjUZx1FFH4bnnnqu579tvvx177703/H4/ZsyYga9//etYs2aNa5qw4zi44YYbsGDBAoTDYYTDYRx44IG4//77h30uNuvXr4fH48F5551Xtfyss86Cx+PB5z73uarlX/va1+DxeNDR0QFg8DW55ZZbsNtuuwEALr30UnNN3OpLPvPMMzjssMMQiUSQSCSwZMkSdHZ2DtnmRYsWGafqbrvtVjOt2nEc/OAHP8Aee+wBv9+Pd7zjHbjmmmtc97mt9wlraL7xxhs4+eST0dzcjHA4jCOPPBL//Oc/q9atV+eT9/Trr78+6LOuri6ceeaZaG9vRzAYxIEHHohHH310q9onWbNmDZYsWYJddtkFfr8fkyZNwkEHHYSbb77ZdR+8VwOBAKZPn45vfOMbKJfLg9Yby7+XbeXFF18EABx++OGun8+YMWPMji2ZPn069txzTySTyap7fvPmzfje976H6dOn49Zbb60SSIEtgu0VV1yB9773vbj55pvx73//+21pr6IoiqIoirJ9USepoijKOMFxHPz3f/83Lr/8cnz1q1/FVVddVTURzfr163HIIYfg+OOPx+WXX46XXnoJ1157LY4//nj861//gtfrPm724Q9/GLvvvju++tWvoq2tDd/4xjcADKTXPvjggzjxxBOxyy674Etf+hImT56M5557DldffTX+8pe/YNWqVWhsrP+4+ec//4lFixYhFArhzDPPxMyZM/HSSy/hJz/5Cf70pz/hmWeeQTweBwCsXbsWBx54IJLJJM4++2zsueeeePLJJ7Fo0SK0trYO2vf//M//4Itf/CLmzJmDSy+9FE1NTbjzzjtr1vI844wzcNttt+GEE07AaaedBgD4zW9+g5NOOgk/+clPcPbZZ9e/EC5Mnz4dc+bMwSOPPFK1/E9/+hO8Xq/r8r322guTJ0923d8hhxyCa665Bl/96ldx0kknmdTlSCRStd5zzz2HY445Bp/61Kdwyimn4G9/+xtuvPFG9PT04MEHH6zb5m984xtoaWnBfffdh2uuuQZtbW0AqtOqAeDCCy9EKpXCGWecgUgkgttuuw1f+9rXMHXqVJx66qlmvdG4TwAgm81i4cKF2HffffGtb30La9euxf/8z//g4IMPxpNPPom99tpryH3U4+ijj0YsFsNFF12E7u5u/PSnP8XRRx+NP/zhDzjiiCO2al9dXV049NBDUalU8LnPfQ677bYbenp68K9//QuPP/44zjjjjKr1f/azn2H9+vX4zGc+g/b2dvzmN7/Bd77zHUSj0ap08bH8e6lHpVJxFWEBIJFImOs3e/Zs/Oc//8GNN96IL3/5y8OeEMtt3/F4fFDpj+FSKpWwdu1aU6OUrFixAvl8Hp/85CcRDoddt/V6vTjrrLPwuc99Dr/+9a/x7ne/e0RtUBRFURRFUXYiHEVRFGWn5Oabb3YAOKtWrXIKhYJz6qmnOl6v1/nhD384aN2ZM2c6AJw77rijavl3v/tdB4Dz0EMPmWWrVq1yADg333zzoH0sXLiwalk+n3emTJniLFiwwCkUClWf3XPPPQ4A55ZbbhnyXPbZZx9nt912c7q6uqqWP/XUU47X63WWL19uln3yk590ADj33Xef67nMnDnTLOvp6XHC4bDzjne8w0mlUmZ5oVBw3v/+9zsAnEsuucQsv//++x0AztVXXz2ojccdd5wTi8Wq9rM1fOELX3A8Ho/T0dHhOI7jvPbaaw4Acz6vvvqq4ziOs2nTJsfj8Thf/vKXzbZu14Tby/ZLADgej8d54oknqpZ/7nOfcwA4//nPf4Zs8yWXXOIAcF577bVBn/H+23vvvauufSaTcVpbW50DDjjALBut+2ThwoUOAOcLX/hC1fKnn37a8Xg8zuGHH26W1bqPa53X6aef7gBwPvShDznlctksf/31151QKOS8853vdCqVilnu9vewcOHCqvvvt7/9rQPAueuuu+qeF9s6ZcoUp7u72ywvl8vOnnvu6UydOrVq/bH6e6kH/4fU+vrzn/9c1Q6fz+cAcHbddVfnU5/6lHPttdc6//znP133zevq9vWHP/xhyLbxev7+9793Ojs7nY6ODueZZ55xTjrpJAeAc+qpp1atf9555zkAnF//+td19/vss886AJyTTz550LHk+SqKoiiKoijjA023VxRF2cnp6enBUUcdhfvvvx/33HMPzj33XNf1pk2bhiVLllQtO/LIIwEMpMduLY888gg2btyIpUuXIp1OY/PmzeZr4cKFCIfDeOihh+ru44UXXsA///lPnHrqqcapxq/Zs2fjne98p9lHpVLB/fffj7lz5+LEE0+s2s9XvvKVQU7KP/7xj8hmszjnnHOqJoDx+/346le/Oqgtt99+O4LBIE455ZSqdmzevBknnXQSUqkUnnzyyRH11eGHHw7HcUzq9p/+9Cc0NDTg0ksvRUNDA/70pz8BAB599FE4jlMzVXlrOOCAA3DQQQdVLdvWa27zxS9+sSpdORwO44ADDqja/2jcJ5ILL7yw6vcFCxbgiCOOwKOPPoqenp5tOp///u//rnJVz5w5E6eddhpeeuklPP/881u1L7oXV65cid7e3iHXP/PMM9Hc3Gx+93q9OPzww7FhwwZkMhkAY/v3MhTTpk3Dww8/7Po1b948s95+++2Hv//97/j0pz+N/v5+3HbbbfjSl76EffbZB/Pnzx9UGoHn6rbf97///cNu33HHHYf29nZMnjwZCxYswAMPPIDPf/7zg2qK8lrQbVsLfj6ca6coiqIoiqLs/Gi6vaIoyk7O0qVLkUql8PDDD9dNB37HO94xaBnTbbu6ukZ07DVr1gAAzjnnHJxzzjmu67Cu5lD7+O53v4vvfve7ruuw7Zs2bUI6ncbcuXMHrRMIBDB79mwkk0mz7NVXXwUAzJkzZ9D6e+65p2tb8vk8pk+fXrO9Q51PLQ499FCTWn/qqafikUcewXvf+17stttumD9/Ph555BF85jOfwSOPPIKGhgYsXLhwRMeRjMU1H+4x5P5H4z4hiUQC06ZNG7R87ty5ePjhh/Hqq6/ive9977D25YbbvcVlL7/8Mt7znvcMe1+HHHIIzjzzTPz85z/HnXfeife+97048MAD8ZGPfAQHHHDAoPWHul6RSGRM/16GIhgMDrvkwLvf/W7ceOONAIB169bhL3/5C2677TasXLkSixcvxr///e8qQdjj8Wx1OQOba665BnvttRfy+TyefPJJXHnlldi4ceOgdP1Line truncated
"text/plain": [
"<Figure size 1650x506 with 4 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, axes = plt.subplots(1, len(ORDER) if len(ORDER) < 4 else 3, figsize=(15, 4.6))\n",
"extent = [0, 990 * BIN_UM, 0, 990 * BIN_UM]\n",
"shown = ORDER if len(ORDER) < 4 else ['eta', 'cluster <=3ph', 'GridBased flat']\n",
"for ax, name in zip(np.atleast_1d(axes), shown):\n",
" image = variants[name][0]\n",
" im = ax.imshow(image, extent=extent, vmax=np.percentile(image, 99.5))\n",
" ax.add_patch(plt.Rectangle((ROI[0]*BIN_UM, ROI[2]*BIN_UM),\n",
" (ROI[1]-ROI[0])*BIN_UM, (ROI[3]-ROI[2])*BIN_UM,\n",
" edgecolor='red', facecolor='none', lw=1.4))\n",
" ax.set_title(name); ax.set_xlabel('x [um]'); ax.set_ylabel('y [um]')\n",
" plt.colorbar(im, ax=ax, fraction=0.046)\n",
"fig.suptitle('knife edge, with the published ESF ROI')\n",
"fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "782d9a37",
"metadata": {},
"source": [
"## 2. Edge spread function\n",
"\n",
"`generate_presampled_esf` fits an erf to every row of the ROI, fits a straight\n",
"line through the edge positions, and re-samples the ROI onto a common axis;\n",
"`get_odr_fitting` then fits the erf with x and y errors inside |x - p1| <= 4 sigma\n",
"(the fit of `Plotting/fig_knife_edge.py --fit odr`)."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "7a741198",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:05.484058Z",
"iopub.status.busy": "2026-09-28T08:23:05.483960Z",
"iopub.status.idle": "2026-09-28T08:23:06.360573Z",
"shell.execute_reply": "2026-09-28T08:23:06.360259Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Final Edge Midpoint Fit: slope = -0.0859, intercept = 432.5449, chi2/ndf = 0.0615\n",
"Final Edge Midpoint Fit: slope = -0.0852, intercept = 432.6254, chi2/ndf = 0.0434\n",
"variant sigma [um] chi2/ndf MTF50 MTF50 LSF MTF(200) LSF rows\n",
"eta 1.506 +- 0.023 1.83 124.4 130.0 0.218 100%\n",
"GridBased flat 1.388 +- 0.020 1.82 135.0 141.3 0.251 100%\n"
]
}
],
"source": [
"def analyse(edge_image, flat_image, label):\n",
" esf = generate_presampled_esf(edge_image, flat_image, ROI,\n",
" pixel_size_um=BIN_UM, bin_width_um=BIN_WIDTH_UM,\n",
" chi2_threshold=15.0, residual_tolerance_px=1.0)\n",
" x, z = esf['numpy']['x'], esf['numpy']['z']\n",
" odr = get_odr_fitting(x, z, esf['numpy']['x_err'], esf['numpy']['z_err'], n_sigma=4.0)\n",
" sigma, sigma_err = odr['sigma_um'], odr['sigma_err_um'] # sigma of the Gaussian LSF\n",
" try:\n",
" mtf50_lsf, mtf200_lsf = mtf_from_lsf(dict(x=x, z=z, pars=odr['beta']))\n",
" except Exception:\n",
" mtf50_lsf = mtf200_lsf = np.nan\n",
" n_fit = int(odr['mask'].sum())\n",
" ndf = max(n_fit - len(odr['beta']), 1)\n",
" x_fit = x[odr['mask']]\n",
" return dict(label=label, x=x, z=z, sigma=sigma, sigma_err=sigma_err, sigma_gauss=sigma,\n",
" chi2_ndf=odr['chi2_ndf'], chi2=odr['chi2_ndf'] * ndf, ndf=ndf,\n",
" mtf50=1000 * np.sqrt(np.log(2) / 2) / (np.pi * sigma),\n",
" mtf10=1000 * np.sqrt(np.log(10) / 2) / (np.pi * sigma),\n",
" mtf50_lsf=mtf50_lsf, mtf200_lsf=mtf200_lsf,\n",
" rows_kept=esf['valid_lines_ratio'],\n",
" edge_slope=esf['edge_slope'], edge_centre=odr['beta'][1],\n",
" fit_points=n_fit, fit_range=(x_fit.min(), x_fit.max()),\n",
" photons=float(edge_image.sum()), odr=odr)\n",
"\n",
"\n",
"results = {name: analyse(edge, flat, name) for name, (edge, flat) in variants.items()}\n",
"\n",
"print(f\"{'variant':16s} {'sigma [um]':>16} {'chi2/ndf':>9} {'MTF50':>7} {'MTF50 LSF':>9} \"\n",
" f\"{'MTF(200) LSF':>12} {'rows':>6}\")\n",
"for name, r in results.items():\n",
" print(f\"{name:16s} {r['sigma']:7.3f} +- {r['sigma_err']:.3f} {r['chi2_ndf']:9.2f} {r['mtf50']:7.1f} \"\n",
" f\"{r['mtf50_lsf']:9.1f} {r['mtf200_lsf']:12.3f} {r['rows_kept']*100:5.0f}%\")"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "5b1a1427",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:06.361862Z",
"iopub.status.busy": "2026-09-28T08:23:06.361744Z",
"iopub.status.idle": "2026-09-28T08:23:06.749047Z",
"shell.execute_reply": "2026-09-28T08:23:06.748786Z"
}
},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1375x528 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"COLOURS = {'eta': '#a0451c', 'cluster 1ph': '#6b6b6b', 'cluster <=2ph': '#7b5aa6',\n",
" 'cluster <=3ph': '#4b2a6b', 'GridBased raw': '#4cbcac', 'GridBased flat': '#10695f'}\n",
"frequency = np.linspace(0, 400, 2000)\n",
"fig, ax = plt.subplots(1, 2, figsize=(12.5, 4.8))\n",
"for name, r in results.items():\n",
" ax[0].plot(r['x'], r['z'], lw=1.1, color=COLOURS[name],\n",
" label=f\"{name}: $\\\\sigma$ = {r['sigma']:.3f} $\\\\pm$ {r['sigma_err']:.3f} $\\\\mu$m\")\n",
" sigma_mm = r['sigma_gauss'] / 1000.0\n",
" ax[1].plot(frequency, np.exp(-2 * np.pi**2 * sigma_mm**2 * frequency**2),\n",
" lw=1.6, color=COLOURS[name], label=f\"{name} ({r['mtf50']:.0f} lp/mm)\")\n",
"ax[0].set_xlim(-9, 9); ax[0].set_ylim(-0.05, 1.35)\n",
"ax[0].set_xlabel(r'position across the edge [$\\mu$m]')\n",
"ax[0].set_ylabel('relative intensity')\n",
"ax[0].legend(fontsize=8); ax[0].grid(alpha=.3); ax[0].set_title('oversampled ESF')\n",
"ax[1].axvline(200, color='k', ls=':', lw=.8, label='2.5 um Nyquist')\n",
"ax[1].axhline(0.5, color='grey', lw=.5)\n",
"ax[1].set_xlim(0, 400); ax[1].set_ylim(0, 1.05)\n",
"ax[1].set_xlabel('spatial frequency [lp/mm]'); ax[1].set_ylabel('MTF')\n",
"ax[1].legend(fontsize=8); ax[1].grid(alpha=.3); ax[1].set_title('MTF from the fitted ESF')\n",
"fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "01c38bd2",
"metadata": {},
"source": [
"## 3. Where the GridBased sub-pixel phase stands"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "da7da667",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:06.750216Z",
"iopub.status.busy": "2026-09-28T08:23:06.750108Z",
"iopub.status.idle": "2026-09-28T08:23:07.141786Z",
"shell.execute_reply": "2026-09-28T08:23:07.141541Z"
}
},
"outputs": [
{
"data": {
"image/png": "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V155JcbGxnDppZdidnYWX/3qV/H85z8fP/7xj/Gyl70MF154Id71rnfZOrr44otx7bXX4rLLLsNLXvISfO9738MHPvABLC4u4iMf+YjNR6vVwpvf/GZ84AMfwMc//nEMDAzgsccew+mnn46TTz4Zn//85+G6Lv7qr/4KL3/5y/Hggw+iWCzuUB6GDHnzm9+M448/Hh/5yEd2uh8ATE1NYXBwENdccw0GBgbw0EMP4YorrsD4+Dg++9nPAgB+/OMf4zd+4zfwR3/0R7jwwgsBGBXIk20HgD/+4z/GP//zP+PDH/4wTjvtNNx+++344Ac/iHK5jHe/+925un/LW96C97///fjIRz6Cj3/843jDG96ATZs2YWho6CnbbFfHXXrppfjMZz6Dj33sY3juc5+Lr33ta/iLv/iL3esUCoVCoThooWOc3cPWrVtx3HHH4c1vfjN6enrwi1/8Apdffjmq1So++tGPPul4odls7vXzfWdjmk481dhpT8ryjW98A69//etx1VVXWfXKU20HgN/93d/FD3/4Q1x++eU44YQTcOutt+Itb3kLBgYG8JrXvMaef3x8HO9///tx6aWXore3F5deeinOO+88PProowiC4EnbYHeOe/vb347vf//7+Ou//musXbsWn/nMZ3DXXXftdjsrFAqFQqFQKBSHJVKFYj/ixhtvTAGkjz/+uN32+OOPpwDSG2+8cafHxHGchmGYHnfccemHPvQhu/3yyy9PK5XKPjvvwsJC2t3dnTtHmqbpddddl5bL5XRqaipN0zS97bbbUgDpT3/60zRN0/S+++5LHcdJ/8//+T/2mDAM06OOOirdk0vq8ssvTwHk0ouiKF23bl36pje9KU3TNE2SJF27dq39Ttxyyy2p4zjpPffcY7dVKpX08ssvt98nJyfTIAjSiy++OHfs7//+76eVSiVdXFzM5eNrX/tabr+LLrooXb9+fVqv1+22iYmJtFKppNddd91Tlu2kk05K3/rWt+a2vfWtb01POumkJz0mDMP0y1/+cur7flqtVu12AOnf/M3f7LD/zrY/8sgjqeM46Wc+85nc9osvvjhdtWpVGsdxrsy33HKL3efhhx9OAaRf+tKX7LYzzjgjPfvss+333Tlueno6LZVK6cc+9rFcHs4444wd+qZCoVAoDj3oGOfpIUmSNAzD9K/+6q/S0dHR3G87Gy/si+d755jmydA5dkrTHZ/9u1OWJ2v/J9v+/e9/PwWQfuc738ltP//889NTTjnFfn/rW9+6w3jve9/7Xgog/X//7//ZbWvXrk3f+973Pq3j7r333hRA+sUvftHuE0VRevTRR+91mysUCoVCoVAoFIcyNFSX4qDA/fffj/POOw8rV66E53kIggAPPvggHnroof12zttvvx1LS0s4//zzEUWR/feyl70M9Xod99xzz06Pu+OOO5CmqQ2VAQC+7+Pcc8/dq/y0p+d5Hl73utfhJz/5CQDgoYcewqZNm/C7v/u7ubyeccYZcBwHd95555Om+9///d9W0dKON73pTahWq/j5z3+e2/7a17429/273/0uzj33XPi+b887MDCAX/u1X8NPf/rTvSozYEJuXHvttTjxxBNRLpcRBAHe/OY3I4oiPPbYY3uU5r//+78DAN7whjfk6uvlL385xsfHsXnzZruv67p4xSteYb9v2LABhUIBW7Zsecpz7Oq4u+++G41GA6973etyx+1tP1EoFArFoYVn8hin0Wjg8ssvx4YNG1AsFhEEAT784Q9jbGwMS0tLTzu9p/N8B3Yc0+wN9nVZADPGGhwcxMte9rIdyvPzn/8ccRzbfVevXo2TTjrJfqcCZ1fjlV0dx7Fc+3jF87xciFKFQqFQKBQKheKZCA3VpVh2LC4u4lWvehVGRkZwzTXXYO3atSiVSnjXu96FRqOx3847NTUFwIRp2Bk6X76JsbExBEGwQ8iHlStX7nFedpbeihUrMDY2lstr+0TG7uQVAGZnZwEAq1atym3n95mZGbutq6sLlUolt9/U1BSuvfbaHTxRAKBcLj/peXcX1157Lf78z/8cl1xyCV760pdiYGAAP/3pT/He9753j9t/amoKaZpieHh4p79v3rwZa9euBWDKUCgUcr8HQbDLc+/qOLbdyMhIbp8VK1bsfkEUCoVCcUjjmT7G+eAHP4h/+qd/wuWXX44XvOAF6O/vx80334y//Mu/RKPRQHd399NK7+k833c2ptkb7OuyAKY8MzMzTxpqa2xsDGvWrAEA9Pf3537jGGRX/WhXx7HN+/r6cvvpeEWhUCgUCoVC8UyHLpwolh0//vGPsWXLFnzzm9/Er/3ar9nt8/Pz9mVxf2BwcBAA8I1vfANHHnnkDr/TyLMTo6OjCMMQs7OzuYmF7du373FedpbexMSEjX/NvP7DP/wDTjvttB2OX7169ZOmzWO3b9+OI444wm4fHx/P/Q5gpyagg4ODOPvss/Ge97xnh996enqesly7gxtvvBGve93r8PGPf9xuu++++/YqzcHBQTiOgx/+8Ic7LG4AwHHHHbdX6e8O2HaTk5O59pmYmNjv51YoFArFwYFn+hjnxhtLine truncated
"text/plain": [
"<Figure size 1650x462 with 5 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"before = np.load(GRID / FLAT_RUN / 'subpixel_phase_before.npy')\n",
"after = np.load(GRID / FLAT_RUN / 'subpixel_phase_after.npy')\n",
"\n",
"fig, axes = plt.subplots(1, 3, figsize=(15, 4.2))\n",
"for ax, histogram, title in ((axes[0], before, 'before flattening'),\n",
" (axes[1], after, 'after flattening')):\n",
" im = ax.imshow(histogram.T / histogram.mean(), origin='lower', extent=[0, 1, 0, 1],\n",
" vmin=0.9, vmax=1.1, cmap='viridis')\n",
" ax.set_title(f'flat field, {title}', fontsize=10)\n",
" ax.set_xlabel('sub-pixel x'); ax.set_ylabel('sub-pixel y')\n",
" plt.colorbar(im, ax=ax, fraction=0.046)\n",
"for histogram, title in ((before, 'before'), (after, 'after')):\n",
" profile = histogram.sum(axis=1)\n",
" axes[2].plot(np.linspace(0, 1, len(profile)), profile / profile.mean(), lw=1, label=title)\n",
"axes[2].axhline(1, color='k', lw=.6); axes[2].set_xlabel('sub-pixel x')\n",
"axes[2].set_ylabel('relative density'); axes[2].legend(); axes[2].grid(alpha=.3)\n",
"fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "0d8f058f",
"metadata": {},
"source": [
"## 4. Relative DQE"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "c3b2ff8f",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:07.143232Z",
"iopub.status.busy": "2026-09-28T08:23:07.143111Z",
"iopub.status.idle": "2026-09-28T08:23:07.246698Z",
"shell.execute_reply": "2026-09-28T08:23:07.246438Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"variant QE rel MTF(200) DQE rel (relative to GridBased flat)\n",
"eta 0.407 0.218 0.019\n",
"GridBased flat 1.000 0.251 0.063\n",
"\n",
"highest relative DQE at 200 lp/mm: GridBased flat (3.24x eta)\n"
]
}
],
"source": [
"# DQE(f) is proportional to QE * MTF(f)^2. Every chain sees the same beam, so\n",
"# the ratio of detected photons is the ratio of QE and the relative DQE is well\n",
"# defined even without the absolute incident flux.\n",
"def mtf_at(frequency_lpmm, sigma_gauss_um):\n",
" sigma_mm = sigma_gauss_um / 1000.0\n",
" return float(np.exp(-2 * np.pi**2 * sigma_mm**2 * frequency_lpmm**2))\n",
"\n",
"\n",
"FREQUENCY = 200.0 # lp/mm, the Nyquist of the 2.5 um bins\n",
"REFERENCE = 'GridBased flat'\n",
"reference_photons = results[REFERENCE]['photons']\n",
"print(f\"{'variant':16s} {'QE rel':>8} {'MTF(200)':>9} {'DQE rel':>9} (relative to {REFERENCE})\")\n",
"relative_dqe = {}\n",
"for name, r in results.items():\n",
" qe = r['photons'] / reference_photons\n",
" mtf200 = r['mtf200_lsf'] # measured from the LSF, as in the poly notebook\n",
" relative_dqe[name] = qe * mtf200**2\n",
" print(f'{name:16s} {qe:8.3f} {mtf200:9.3f} {relative_dqe[name]:9.3f}')\n",
"best = max(relative_dqe, key=relative_dqe.get)\n",
"print(f\"\\nhighest relative DQE at {FREQUENCY:.0f} lp/mm: {best} \"\n",
" f\"({relative_dqe[best]/relative_dqe['eta']:.2f}x eta)\")"
]
},
{
"cell_type": "markdown",
"id": "28adb593",
"metadata": {},
"source": [
"## 5. Summary"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "ff568b19",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:07.247817Z",
"iopub.status.busy": "2026-09-28T08:23:07.247707Z",
"iopub.status.idle": "2026-09-28T08:23:07.351773Z",
"shell.execute_reply": "2026-09-28T08:23:07.351532Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{\n",
" \"eta\": {\n",
" \"sigma_um\": 1.5064047956840743,\n",
" \"sigma_err_um\": 0.02338135508989506,\n",
" \"mtf50_lsf_lpmm\": 130.01783547238728,\n",
" \"mtf200_lsf\": 0.21821247141503744,\n",
" \"chi2_ndf\": 1.831632850088404,\n",
" \"mtf50_lpmm\": 124.39592974355975,\n",
" \"mtf10_lpmm\": 226.7259781499331,\n",
" \"rows_kept\": 0.9956521739130435,\n",
" \"photons\": 230851827.0\n",
" },\n",
" \"GridBased flat\": {\n",
" \"sigma_um\": 1.388201297480562,\n",
" \"sigma_err_um\": 0.020239692729912443,\n",
" \"mtf50_lsf_lpmm\": 141.25277630731097,\n",
" \"mtf200_lsf\": 0.2506173773815928,\n",
" \"chi2_ndf\": 1.8199081902180196,\n",
" \"mtf50_lpmm\": 134.98807807583214,\n",
" \"mtf10_lpmm\": 246.03139430216834,\n",
" \"rows_kept\": 1.0,\n",
" \"photons\": 566973421.0\n",
" }\n",
"}\n",
"\n",
"written to /mnt/sls_det_storage/moench_data/MLXID/InferenceResults/GridBased/Edge_1filters_12keV/knife_edge_comparison.json\n"
]
}
],
"source": [
"summary = {\n",
" 'measurement': 'Edge/Flat_1filters_12keV',\n",
" 'occupancy_percent': occupancy * 100, # flat field, ESF ROI\n",
" 'roi_superpixel': list(ROI),\n",
" 'bin_um': BIN_UM,\n",
" 'bin_width_um': BIN_WIDTH_UM,\n",
" 'convention': 'sigma = ODR p2 (Gaussian LSF); MTF50 = 1000 sqrt(ln 2 / 2) / (pi sigma); '\n",
" 'ESF from DeepLearning/src, bin width 0.3 um',\n",
" 'grid_model_epoch': grid_summary['epoch'],\n",
" 'grid_frames': grid_summary['n_frames'],\n",
" 'grid_subpixel_before': grid_summary['subpixel_nonuniformity_before'],\n",
" 'grid_subpixel_after': grid_summary['subpixel_nonuniformity_after'],\n",
" 'variants': {name: dict(sigma_um=float(r['sigma']), sigma_err_um=float(r['sigma_err']),\n",
" mtf50_lsf_lpmm=float(r['mtf50_lsf']),\n",
" mtf200_lsf=float(r['mtf200_lsf']),\n",
" chi2_ndf=float(r['chi2_ndf']),\n",
" mtf50_lpmm=float(r['mtf50']), mtf10_lpmm=float(r['mtf10']),\n",
" rows_kept=float(r['rows_kept']), photons=float(r['photons']))\n",
" for name, r in results.items()},\n",
" 'relative_dqe_200lpmm': relative_dqe,\n",
"}\n",
"print(json.dumps(summary['variants'], indent=1))\n",
"out = GRID / EDGE_RUN / 'knife_edge_comparison.json'\n",
"json.dump(summary, open(out, 'w'), indent=1)\n",
"print(f'\\nwritten to {out}')"
]
}
],
"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
}
+625
View File
@@ -0,0 +1,625 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "06536dc1",
"metadata": {},
"source": [
"# Knife edge, 2 filters — GridBased against the published chains\n",
"\n",
"Same measurement, same ROI, same ESF construction and same erf fit as\n",
"`DeepLearning/Notebooks/KnifeEdge_2filters.ipynb`, so the only thing that\n",
"differs between the rows of the final table is the reconstruction itself.\n",
"The flat field sits at **1.81 % occupancy** inside the ESF ROI, 2.4x the 3-filter point.\n",
"\n",
"\n",
"**Edge width (revised 2026-09-28).** The ESF is binned and fitted exactly as\n",
"`Plotting/fig_knife_edge.py --fit odr` does it: `generate_presampled_esf()` and\n",
"`get_odr_fitting()` from `DeepLearning/src/funcs_postprocess.py` (bin content\n",
"Σk/Σf), symmetric ±4σ window, **ESF bin width 0.3 µm**. The ODR model is\n",
"`p0/2 (1 + erf((x - p1)/(sqrt(2) sigma))) + p3`, so the fit gives **sigma**, the\n",
"width of the Gaussian LSF, which is what the tables quote, with\n",
"`MTF50 [lp/mm] = 1000 sqrt(ln 2 / 2) / (pi sigma)`; MTF50 and MTF(200 lp/mm) are\n",
"also measured from the LSF (`Plotting/knife_edge_numbers.py`: first crossing,\n",
"central-difference correction), and the relative DQE uses the latter. Only the\n",
"flattened GridBased reconstruction is analysed. Same processing as\n",
"`KnifeEdge_poly2to20keV_Moench040.ipynb`.\n",
"\n",
"**Occupancy** is that of the flat field inside the ESF ROI: the reconstructed\n",
"photons per 25 µm pixel per frame over `ROI // 10` (x 33–46, y 26–49 of the\n",
"99 × 99 frame), the pixels over which `DeepLearning/Notebooks/KnifeEdge_3filters_pos0.ipynb`\n",
"sums the deposited energy (0.705 % at 3 filters from the energy, 0.747 % here\n",
"from the photons of the 12 keV model).\n",
"\n",
"*Until 2026-09-28 this notebook took the fitted sigma for sqrt(2) sigma (so its\n",
"MTF50 was sqrt(2) too high), used 0.5 µm bins, and binned the ESF with an older\n",
"copy of `generate_presampled_esf` (inverse-variance mean of k/f, biased low in\n",
"the toe, sigma ~3 % too small). The previous version is in\n",
"`Backup_260928_pre_sigma_fix/`.*\n",
"\n",
"The ROI, the oversampled ESF and the erf fit are the ones from\n",
"`DeepLearning/Notebooks/KnifeEdge_3filters_pos0.ipynb`; the edge sits at the\n",
"same place at all three flux points, so the same ROI holds throughout and the\n",
"three notebooks are directly comparable.\n",
"\n",
"Run in the `pytorch_nightly` environment (ROOT + scipy + jupyter)."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "338eb757",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:56.593670Z",
"iopub.status.busy": "2026-09-28T08:22:56.593588Z",
"iopub.status.idle": "2026-09-28T08:22:58.252767Z",
"shell.execute_reply": "2026-09-28T08:22:58.252376Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Welcome to JupyROOT 6.28/10\n"
]
}
],
"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, '/home/xie_x1/MLXID/DeepLearning/src') # the code Plotting/ uses\n",
"sys.path.insert(0, '/home/xie_x1/MLXID/Plotting')\n",
"from ROOT import TCanvas, gROOT\n",
"gROOT.SetBatch(True)\n",
"from funcs_postprocess import generate_presampled_esf, get_odr_fitting\n",
"from knife_edge_numbers import mtf as mtf_from_lsf # first crossing, sinc-corrected\n",
"\n",
"GRID = Path('/mnt/sls_det_storage/moench_data/MLXID/InferenceResults/GridBased')\n",
"PUBLISHED = Path('/home/xie_x1/MLXID/DeepLearning/InferenceResults')\n",
"ETA = Path('/home/xie_x1/MLXID/EtaInterpolation/Results')\n",
"\n",
"EDGE_RUN, FLAT_RUN = 'Edge_2filters_12keV', 'Flat_2filters_12keV'\n",
"BIN_UM = 2.5 # super-pixel = 25 um / 10\n",
"ROI = (330, 465, 260, 490) # x1, x2, y1, y2, as published\n",
"CROP = slice(10, -10) # 1010 -> 990, as published\n",
"BIN_WIDTH_UM = 0.3 # presampled ESF bin, as Plotting/ and the manuscript\n",
"\n",
"plt.rcParams.update({'figure.dpi': 110, 'image.cmap': 'gray',\n",
" 'image.origin': 'lower', 'image.interpolation': 'nearest'})"
]
},
{
"cell_type": "markdown",
"id": "bdc32295",
"metadata": {},
"source": [
"## 1. Load every reconstruction of the same measurement"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "c09dbe26",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:58.254687Z",
"iopub.status.busy": "2026-09-28T08:22:58.254484Z",
"iopub.status.idle": "2026-09-28T08:22:58.475103Z",
"shell.execute_reply": "2026-09-28T08:22:58.474753Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"GridBased model epoch 135, 7,000,000 frames, occupancy 1.808 % (flat field, ESF ROI)\n",
" sub-pixel excess rms 21.6 % -> 3.8 % after flattening\n",
"\n",
"variant edge photons flat photons relative\n",
"eta 1.5670e+08 2.7062e+08 0.710\n",
"cluster 1ph 1.5670e+08 2.7062e+08 0.710\n",
"cluster <=2ph 2.0504e+08 3.5599e+08 0.929\n",
"cluster <=3ph 2.2076e+08 3.8444e+08 1.000\n",
"GridBased flat 2.3053e+08 4.0162e+08 1.044\n"
]
}
],
"source": [
"def crop(image):\n",
" \"\"\"The published frames are cropped by 10 super-pixels on each side.\"\"\"\n",
" return image[CROP, CROP] if image.shape[0] == 1010 else image\n",
"\n",
"\n",
"for run in (EDGE_RUN, FLAT_RUN):\n",
" if not (GRID / run / 'summary.json').exists():\n",
" raise SystemExit(\n",
" f'{run} has not been reconstructed yet. Run:\\n'\n",
" f' python scripts/05_infer_measurement.py '\n",
" f'--config Configs/infer_measurement_12keV.yaml --run {run}')\n",
"\n",
"grid_summary = json.load(open(GRID / EDGE_RUN / 'summary.json'))\n",
"flat_summary = json.load(open(GRID / FLAT_RUN / 'summary.json'))\n",
"\n",
"\n",
"def grid_image(run, flattened):\n",
" name = 'binned_image_flattened.npy' if flattened else 'binned_image.npy'\n",
" return np.load(GRID / run / name).astype(float)\n",
"\n",
"\n",
"variants = {\n",
" 'eta': (np.load(ETA / '2603MaxIV_Edge2Filters_12keV_fromClusters.npy'),\n",
" np.load(ETA / '2603MaxIV_Flat2Filters_12keV_fromClusters.npy')),\n",
" 'GridBased flat': (grid_image(EDGE_RUN, True), grid_image(FLAT_RUN, True)),\n",
"}\n",
"\n",
"EXPERIMENT = {1: '1ph_260512_12keV_v260511_06',\n",
" 2: '2ph_260610_12keV_v260610_01',\n",
" 3: '3ph_260611_12keV_v260611_00'}\n",
"KNIFE, FLAT = 'KnifeEdge_2filters', 'FlatField_2filters'\n",
"\n",
"\n",
"def published_frame(run, n_photons):\n",
" name = f'{n_photons}Photon_ML_superFrame_etaInterpolated.npy'\n",
" return np.load(PUBLISHED / run / EXPERIMENT[n_photons] / 'augX1' / name).astype(float)\n",
"\n",
"\n",
"cluster_edge = {n: published_frame(KNIFE, n) for n in (1, 2, 3)}\n",
"cluster_flat = {n: published_frame(FLAT, n) for n in (1, 2, 3)}\n",
"variants.update({\n",
" 'cluster 1ph': (cluster_edge[1], cluster_flat[1]),\n",
" 'cluster <=2ph': (cluster_edge[1] + cluster_edge[2], cluster_flat[1] + cluster_flat[2]),\n",
" 'cluster <=3ph': (cluster_edge[1] + cluster_edge[2] + cluster_edge[3],\n",
" cluster_flat[1] + cluster_flat[2] + cluster_flat[3]),\n",
"})\n",
"\n",
"ORDER = ['eta', 'cluster 1ph', 'cluster <=2ph', 'cluster <=3ph', 'GridBased flat']\n",
"variants = {name: (crop(e), crop(f)) for name, (e, f) in variants.items()}\n",
"variants = {name: variants[name] for name in ORDER}\n",
"\n",
"def flat_occupancy(run):\n",
" # reconstructed photons per 25 um pixel per frame in the flat field, inside the\n",
" # ESF ROI: ROI // 10 in pixels of the 99 x 99 frame, both ends included as in\n",
" # DeepLearning/Notebooks; +1 because the 101 x 101 frame is cropped by 1 px\n",
" summary = json.load(open(GRID / run / 'summary.json'))\n",
" native = np.load(GRID / run / 'native_image.npy') / summary['n_frames']\n",
" x0, x1, y0, y1 = (v // 10 for v in ROI)\n",
" return float(native[y0 + 1:y1 + 2, x0 + 1:x1 + 2].mean())\n",
"\n",
"\n",
"occupancy = flat_occupancy(FLAT_RUN)\n",
"print(f\"GridBased model epoch {grid_summary['epoch']}, \"\n",
" f\"{grid_summary['n_frames']:,} frames, \"\n",
" f\"occupancy {occupancy*100:.3f} % (flat field, ESF ROI)\")\n",
"print(f\" sub-pixel excess rms {grid_summary['subpixel_nonuniformity_before']*100:.1f} % -> \"\n",
" f\"{grid_summary['subpixel_nonuniformity_after']*100:.1f} % after flattening\\n\")\n",
"print(f\"{'variant':16s} {'edge photons':>14} {'flat photons':>14} relative\")\n",
"reference = variants['cluster <=3ph'][0].sum()\n",
"for name, (e, f) in variants.items():\n",
" print(f'{name:16s} {e.sum():14.4e} {f.sum():14.4e} {e.sum()/reference:8.3f}')"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "8c5580a3",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:58.476333Z",
"iopub.status.busy": "2026-09-28T08:22:58.476173Z",
"iopub.status.idle": "2026-09-28T08:22:58.963183Z",
"shell.execute_reply": "2026-09-28T08:22:58.962866Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABmcAAAH2CAYAAACFuVjoAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOy9eZgU1dn+f3fP1j29zcIwsiijEAXiIOIrKi4gogRQwSUi0ShqgrjGBRNDVDA/jSGuib6uKO6CGiUquIu+cYl+TYxRIa64oQI6W89Mz16/P7juM0+dqe7p2WF4Ptc1FzPV1VWnTjX93PVsx+c4jgNFURRFURRFURRFURRFURRFURSlV/D39QAURVEURVEURVEURVEURVEURVG2JzQ4oyiKoiiKoiiKoiiKoiiKoiiK0otocEZRFEVRFEVRFEVRFEVRFEVRFKUX0eCMoiiKoiiKoiiKoiiKoiiKoihKL6LBGUVRFEVRFEVRFEVRFEVRFEVRlF5EgzOKoiiKoiiKoiiKoiiKoiiKoii9iAZnFEVRFEVRFEVRFEVRFEVRFEVRehENziiKoiiKoiiKoiiKoiiKoiiKovQiGpxRFEVRFEVRFEVRFEVRFEVRFEXpRTQ4oyiKoiiKoiiKoiiKoiiKoiiK0otocEZRFEVRFGU75u6774bP58PLL7/crcd9+eWX4fP5cPfdd7u219bW4txzz8VOO+2EjIwMlJSUdOt5u4NJkyZtlePqKsnuSSp8Ph/mzp3bY2Pqqc9fXzF37lz4fL609y8pKcGkSZNc2zr7+evM/e0O+uv/F0VRFEVRFEXpaTQ4oyiKoiiKovQaS5YswY033ojZs2fj7rvvxg033NDXQ9quqaiowOLFi3s0OPLyyy9j8eLFqKio6LFzKNsGJSUl8Pl8SX8WL17s2v/zzz/HGWecgVGjRiEUCiEvLw+77rorjj/+eDz22GOufSdNmpT0uCNGjGh3bIsXL3a9x+/3Iy8vDwceeCDuu+++lO999tlncdRRR2HQoEHIzs5GUVERpk6diuXLl8NxnKTnevXVV9ufNEVRFEVRFKXfktnXA1AURVEURVH6HwcddBASiQSysrJc259//nmUlpbi6quv7qORbb943ZOKigpcfvnlANCmgqO7ePnll3H55Zdj7ty5yMvL65Fz9Ceee+45T4d+f6G4uBjXXHON52tjxowxv//rX//CQQcdhObmZpxwwgk4++yzAQCffPIJVq1aherqahx99NGu9/v9ftxzzz1tjhuJRNIe36WXXopdd90Vzc3N+PLLL7F06VKcdNJJ2LBhAy6++GLXvi0tLZg/fz7uuOMODB8+HPPmzcOwYcOwceNGPPLII5gzZw4efPBBrFixAsFgMO0xKIqiKIqiKNsHGpxRFEVRFEVRuh2/349AINBm+3fffYeddtqpD0akJLsnytZFdnZ2Xw+hRwmHwzjxxBPb3W/x4sWoqanBypUrMXPmTNdr119/Pb7++us27/H5fGkdOxWHHXYYDjjgAPP3qaeeil133RVLlizBRRddhIyMDPPaFVdcgTvuuAPHHHMMHnjgAeTk5JjXLr74YixYsADXXXcdzj77bNx5551dGpeiKIqiKIrS/9C2ZoqiKIqiKEobbrnlFmRmZuK4445DfX09gNb1MT766CPMnDkTsVgM4XAYM2bMwKeffup6v73+Bdv4rF+/Hq+88opnG6N33nkHxx57LAYOHIjs7GzssssuuPjii1FbW5v2uD/99FPMnTsXgwcPRnZ2NoYOHYozzzwT33//fZt9P/nkExx11FGIxWKIRCI47LDD8O677yY99n333YcxY8YgJycHQ4cOxa9//WusW7fOsx2T4zi44447MH78eIRCIYRCIUyYMAErV65M+1psNmzYAJ/PhwsvvNC1fd68efD5fDj99NNd2y+44AL4fD5s3LgRQNt7cvfdd2PnnXcGAFx++eXmnnitH/LWW29h8uTJCIfDyMvLw5w5c7B58+Z2xzxp0iRTmbPzzjsnbV/lOA5uuOEG7LrrrsjJycEuu+yC66+/3vOYXf2ccI2UL774Ascccwzy8/MRCoVw6KGH4t///rdr31TruPAz/fnnn7d57YcffsCpp56KoqIiBINBTJgwAS+99FKHxidZt24d5syZgx133BE5OTkYOHAg9t9/fyxbtszzGPysBgIBDBkyBL/73e/Q3NzcZr+e/P/SVT766CMAwCGHHOL5+tChQ3vs3JIhQ4Zg1KhRqKiocH3mv//+e/zxj3/EkCFDcM8997gCM8CWQNHVV1+NvfbaC8uWLcMHH3zQK+NVFEVRFEVRth20ckZRFEVRFEUxOI6D3/72t1iyZAnOP/98XHvtta4Fzjds2ICDDjoIRx55JJYsWYKPP/4YN954I4488ki899578Pu9c3+OPvpojBgxAueffz4GDBiA3/3udwBa2xg988wzmDVrFnbccUecc845KC4uxrvvvovrrrsOr732GtasWYPMzNTS9d///jcmTZqE3NxcnHrqqRg2bBg+/vhj3HLLLXjxxRfx1ltvIRaLAQC++uorTJgwARUVFZg/fz5GjRqFN954A5MmTUJhYWGbY//v//4vzj77bIwcORKXX345srKy8NBDDyVdq+WUU07Bvffei5kzZ+KEE04AADz22GM46qijcMstt2D+/Pmpb4QHQ4YMwciRI/HCCy+4tr/44ovw+/2e23fffXcUFxd7Hu+ggw7C9ddfj/PPPx9HHXWUaREVDodd+7377ruYNm0aTjrpJMyePRv//Oc/sXTpUpSXl+OZZ55JOebf/e53KCgowOOPP47rr78eAwYMAOBuXwUACxcuRFVVFU455RSEw2Hce++9uOCCCzBo0CAcf/zxZr/u+JwAQE1NDSZOnIg999wTV1xxBb766iv87//+Lw488EC88cYb2H333ds9RiqmTp2KaDSKSy+9FGVlZbj11lsxdepUPP3005gyZUqHjvXDDz/g4IMPRktLC04//XTsvPPOKC8vx3vvvYdXXnkFp5xyimv/2267DRs2bMAvfvELFBUV4bHHHsMf/vAHRCIRV1uunvz/koqWlhbP4A8A5OXlmfs3fPhwfPjhh1i6dCl+9atfub6HUuF17Fgs1qbFYro0NDTgq6++MmvQkFWrViGRSODnP/85QqGQ53v9fj/mzZuH008/HX/961/x4x//uFNjUBRFURRFUfopjqIoiqIoirLdsmzZMgeAs2bNGqeurs45/vjjHb/f7/z5z39us++wYcMcAM6DDz7o2n7VVVc5AJxnn33WbFuzZo0DwFm2bFmbY0ycONG1LZFIODvssIMzfvx4p66uzvXaI4884gBw7r777navZezYsc7OO+/s/PDDD67t//jHPxy/3+8sXrzYbPv5z3/uAHAef/xxz2sZNmyY2VZeXu6EQiFnl112caqqqsz2uro6Z++993YAOIsWLTLbV65c6QBwrrvuujZjPPzww51oNOo6Tkc466yzHJ/P52zcuNFxHMdZv369A8Bcz2effeY4juNs2rTJ8fl8zq9+9SvzXq97wvfL8UsAOD6fz3n11Vdd208//XQHgPPhhx+2O+ZFixY5AJz169e3eY2fvzFjxrjufXV1tVNYWOjst99+Zlt3fU4mTpzoAHDOOuss1/Y333zT8fl8ziGHHGK2JfscJ7uuk08+2QHgHHHEEU5zc7PZ/vnnnzu5ubnOj370I6elpcVs9/r/MHHiRNfn729/+5sDwFm+fHnK6+JYd9hhB6esrMxsb25udkaNGuUMGjTItX9P/X9JBb9Dkv38/e9/d40jOzvbAeDstNNOzkknneTceOONzr///W/PY/O+ev08/fTT7Y6N9/Opp55yNm/e7GzcuNF56623nKOOOsoB4Bx//PGu/S+88EIHgPPXv/415XHffvttB4BzzDHHtDmXvF5FURRFURRl+0PbmimKoiiKoigoLy/HYYcdhpUrV+KRRx7Bueee67nf4MGDMWfOHNe2Qw89FEBrG6KO8sILL+C7777D3LlzEY/H8f3335ufiRMnIhQK4dlnn015jPfffx///ve/cfzxx5vMfP4MHz4cP/rRj8wxWlpasHLlSowePRqzZs1yHee8885rUzny3HPPoaamBmeeeaZrYfGcnBycf/75bcZy3333IRgMYvbs2a5xfP/99zjqqKNQVVWFN954o1Nzdcghh8BxHNMi68UXX0RGRgYuv/xyZGRk4MUXXwQAvPTSS3AcJ2lLqI6w3377Yf/993dt6+o9tzn77LNdbaFCoRD2228/1/G743MiWbhwoevv8ePHY8qUKXjppZdQXl7epev57W9/66oiGzZsGE444QR8/PHH+M9//tOhY7FaY/Xq1aisrGx3/1NPPRX5+fnmb7/fj0MOOQTffvstqqurAfTs/5f2GDx4MJ5//nnPn9LSUrPfPvvsg3/961847bTT0NTUhHvvvRfnnHMOxo4di3HjxrVpQcdr9Tru3nvvnfb4Dj/8cBQVFaG4uBjjx4/HE088gTPOOKPNmjG8F6wuSgZfT+feKYqiKIqiKNsX2tZMURRFURRFwdy5c1FVVYXnn38+ZdulXXbZpc02tjX64YcfOnXudevWAQDOPPNMnHnmmZ77cN2U9o5x1VVX4aqrrvLch2PftGkT4vE4Ro8e3WafQCCA4cOHo6Kiwmz77LPPAAAjR45ss/+oUaM8x5JIJDBkyJCk423vepJx8MEHmxZmxx9/PF544QXstdde2HnnnTFu3Di88MIL+MUvfoEXXngBGRkZmDhxYqfOI+mJe57uOeTxu+NzQvLy8jB48OA220ePHo3nn38en332Gfbaa6+0juWF12eL2z755BPsscceaR/roIMOwqmnnoq77roLDz30EPbaay9MmDABxx57LPbbb782+7d3v8LhcI/+f2mPYDCYdmu3H//4x1i6dCkA4Ouvv8Zrr72Ge++9F6tXr8b06dPxwQcfuAJLine truncated
"text/plain": [
"<Figure size 1650x506 with 6 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, axes = plt.subplots(1, len(ORDER) if len(ORDER) < 4 else 3, figsize=(15, 4.6))\n",
"extent = [0, 990 * BIN_UM, 0, 990 * BIN_UM]\n",
"shown = ORDER if len(ORDER) < 4 else ['eta', 'cluster <=3ph', 'GridBased flat']\n",
"for ax, name in zip(np.atleast_1d(axes), shown):\n",
" image = variants[name][0]\n",
" im = ax.imshow(image, extent=extent, vmax=np.percentile(image, 99.5))\n",
" ax.add_patch(plt.Rectangle((ROI[0]*BIN_UM, ROI[2]*BIN_UM),\n",
" (ROI[1]-ROI[0])*BIN_UM, (ROI[3]-ROI[2])*BIN_UM,\n",
" edgecolor='red', facecolor='none', lw=1.4))\n",
" ax.set_title(name); ax.set_xlabel('x [um]'); ax.set_ylabel('y [um]')\n",
" plt.colorbar(im, ax=ax, fraction=0.046)\n",
"fig.suptitle('knife edge, with the published ESF ROI')\n",
"fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "1e889d01",
"metadata": {},
"source": [
"## 2. Edge spread function\n",
"\n",
"`generate_presampled_esf` fits an erf to every row of the ROI, fits a straight\n",
"line through the edge positions, and re-samples the ROI onto a common axis;\n",
"`get_odr_fitting` then fits the erf with x and y errors inside |x - p1| <= 4 sigma\n",
"(the fit of `Plotting/fig_knife_edge.py --fit odr`)."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "e36d7502",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:58.964776Z",
"iopub.status.busy": "2026-09-28T08:22:58.964610Z",
"iopub.status.idle": "2026-09-28T08:23:00.677476Z",
"shell.execute_reply": "2026-09-28T08:23:00.677078Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Final Edge Midpoint Fit: slope = -0.0858, intercept = 432.3990, chi2/ndf = 0.0751\n",
"Final Edge Midpoint Fit: slope = -0.0854, intercept = 432.2740, chi2/ndf = 0.0576\n",
"Final Edge Midpoint Fit: slope = -0.0855, intercept = 432.4402, chi2/ndf = 0.0479\n",
"Final Edge Midpoint Fit: slope = -0.0850, intercept = 432.2537, chi2/ndf = 0.0406\n",
"Final Edge Midpoint Fit: slope = -0.0858, intercept = 432.5887, chi2/ndf = 0.0361\n",
"variant sigma [um] chi2/ndf MTF50 MTF50 LSF MTF(200) LSF rows\n",
"eta 1.483 +- 0.024 1.61 126.4 131.7 0.187 100%\n",
"cluster 1ph 1.352 +- 0.022 1.44 138.6 144.8 0.229 100%\n",
"cluster <=2ph 1.372 +- 0.022 1.50 136.5 142.7 0.223 100%\n",
"cluster <=3ph 1.386 +- 0.018 1.02 135.2 139.7 0.244 100%\n",
"GridBased flat 1.346 +- 0.019 1.15 139.2 143.8 0.259 100%\n"
]
}
],
"source": [
"def analyse(edge_image, flat_image, label):\n",
" esf = generate_presampled_esf(edge_image, flat_image, ROI,\n",
" pixel_size_um=BIN_UM, bin_width_um=BIN_WIDTH_UM,\n",
" chi2_threshold=15.0, residual_tolerance_px=1.0)\n",
" x, z = esf['numpy']['x'], esf['numpy']['z']\n",
" odr = get_odr_fitting(x, z, esf['numpy']['x_err'], esf['numpy']['z_err'], n_sigma=4.0)\n",
" sigma, sigma_err = odr['sigma_um'], odr['sigma_err_um'] # sigma of the Gaussian LSF\n",
" try:\n",
" mtf50_lsf, mtf200_lsf = mtf_from_lsf(dict(x=x, z=z, pars=odr['beta']))\n",
" except Exception:\n",
" mtf50_lsf = mtf200_lsf = np.nan\n",
" n_fit = int(odr['mask'].sum())\n",
" ndf = max(n_fit - len(odr['beta']), 1)\n",
" x_fit = x[odr['mask']]\n",
" return dict(label=label, x=x, z=z, sigma=sigma, sigma_err=sigma_err, sigma_gauss=sigma,\n",
" chi2_ndf=odr['chi2_ndf'], chi2=odr['chi2_ndf'] * ndf, ndf=ndf,\n",
" mtf50=1000 * np.sqrt(np.log(2) / 2) / (np.pi * sigma),\n",
" mtf10=1000 * np.sqrt(np.log(10) / 2) / (np.pi * sigma),\n",
" mtf50_lsf=mtf50_lsf, mtf200_lsf=mtf200_lsf,\n",
" rows_kept=esf['valid_lines_ratio'],\n",
" edge_slope=esf['edge_slope'], edge_centre=odr['beta'][1],\n",
" fit_points=n_fit, fit_range=(x_fit.min(), x_fit.max()),\n",
" photons=float(edge_image.sum()), odr=odr)\n",
"\n",
"\n",
"results = {name: analyse(edge, flat, name) for name, (edge, flat) in variants.items()}\n",
"\n",
"print(f\"{'variant':16s} {'sigma [um]':>16} {'chi2/ndf':>9} {'MTF50':>7} {'MTF50 LSF':>9} \"\n",
" f\"{'MTF(200) LSF':>12} {'rows':>6}\")\n",
"for name, r in results.items():\n",
" print(f\"{name:16s} {r['sigma']:7.3f} +- {r['sigma_err']:.3f} {r['chi2_ndf']:9.2f} {r['mtf50']:7.1f} \"\n",
" f\"{r['mtf50_lsf']:9.1f} {r['mtf200_lsf']:12.3f} {r['rows_kept']*100:5.0f}%\")"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "55ce79f2",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:00.678708Z",
"iopub.status.busy": "2026-09-28T08:23:00.678621Z",
"iopub.status.idle": "2026-09-28T08:23:01.069086Z",
"shell.execute_reply": "2026-09-28T08:23:01.068788Z"
}
},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1375x528 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"COLOURS = {'eta': '#a0451c', 'cluster 1ph': '#6b6b6b', 'cluster <=2ph': '#7b5aa6',\n",
" 'cluster <=3ph': '#4b2a6b', 'GridBased raw': '#4cbcac', 'GridBased flat': '#10695f'}\n",
"frequency = np.linspace(0, 400, 2000)\n",
"fig, ax = plt.subplots(1, 2, figsize=(12.5, 4.8))\n",
"for name, r in results.items():\n",
" ax[0].plot(r['x'], r['z'], lw=1.1, color=COLOURS[name],\n",
" label=f\"{name}: $\\\\sigma$ = {r['sigma']:.3f} $\\\\pm$ {r['sigma_err']:.3f} $\\\\mu$m\")\n",
" sigma_mm = r['sigma_gauss'] / 1000.0\n",
" ax[1].plot(frequency, np.exp(-2 * np.pi**2 * sigma_mm**2 * frequency**2),\n",
" lw=1.6, color=COLOURS[name], label=f\"{name} ({r['mtf50']:.0f} lp/mm)\")\n",
"ax[0].set_xlim(-9, 9); ax[0].set_ylim(-0.05, 1.35)\n",
"ax[0].set_xlabel(r'position across the edge [$\\mu$m]')\n",
"ax[0].set_ylabel('relative intensity')\n",
"ax[0].legend(fontsize=8); ax[0].grid(alpha=.3); ax[0].set_title('oversampled ESF')\n",
"ax[1].axvline(200, color='k', ls=':', lw=.8, label='2.5 um Nyquist')\n",
"ax[1].axhline(0.5, color='grey', lw=.5)\n",
"ax[1].set_xlim(0, 400); ax[1].set_ylim(0, 1.05)\n",
"ax[1].set_xlabel('spatial frequency [lp/mm]'); ax[1].set_ylabel('MTF')\n",
"ax[1].legend(fontsize=8); ax[1].grid(alpha=.3); ax[1].set_title('MTF from the fitted ESF')\n",
"fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "6ff2c3c0",
"metadata": {},
"source": [
"## 3. Where the GridBased sub-pixel phase stands"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "30d4c001",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:01.070347Z",
"iopub.status.busy": "2026-09-28T08:23:01.070263Z",
"iopub.status.idle": "2026-09-28T08:23:01.586719Z",
"shell.execute_reply": "2026-09-28T08:23:01.586384Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABk4AAAHKCAYAAABFSbckAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOy9ebgdZZU1vmo6453nezMPhCQEHAG1VSZxQkT0QxBptNXPtp21GwRsBexu8OlGpAe62x4U9SfKoDafgoqtaKtgI/OQhJA5N7nzvecOZ6zp98fe+z2pJJBwM4e9nifPya1T9dY7VdU+9a61lxXHcQyFQqFQKBQKhUKhUCgUCoVCoVAoFAoF7MNdAYVCoVAoFAqFQqFQKBQKhUKhUCgUiiMFunCiUCgUCoVCoVAoFAqFQqFQKBQKhULB0IUThUKhUCgUCoVCoVAoFAqFQqFQKBQKhi6cKBQKhUKhUCgUCoVCoVAoFAqFQqFQMHThRKFQKBQKhUKhUCgUCoVCoVAoFAqFgqELJwqFQqFQKBQKhUKhUCgUCoVCoVAoFAxdOFEoFAqFQqFQKBQKhUKhUCgUCoVCoWDowolCoVAoFAqFQqFQKBQKhUKhUCgUCgVDF04UCoVCoVAoFAqFQqFQKBQKhUKhUCgYunCiUCgUCoVCoVAoFAqFQqFQKBQKhULB0IUTxUHHV7/6VcyfPx+O4+Ad73gHNm/eDMuycOedd76gcn71q1/huuuuOyzntSwLDz300PPud8MNN8CyrBdUNgBcc801aGhoeMHHPRceeeQRvOpVr0Iul4NlWSgUCges7BeKTZs24ayzzkJjYyMsy8Jjjz2G97///Vi1atULLuuaa67B/fffv8/bDyROP/10vO1tbzuo51AoFArF0QeNcQ4M9hQvHIrnu2BPsdNsnv2FQgHXXHMNVq9evU/bDzQWLlyIj3/84wf1HAqFQqFQKBQKxYsF7uGugOLYxtq1a/HZz34WV1xxBc4991x0dHTMuqxf/epXuOGGG3DVVVfN6ry9vb144IEHsGzZslnX4WjAxz72MYRhiLvvvhvZbBaNjY2HrS5XXXUVNm7ciDvvvBPNzc371ffXXnstGhoa8JrXvGafth9I/PM//zMcxzlo5SsUCoXi6IPGOAcOe4oXDsXzXXCgYqdCoYBrr70Wq1atwsqVK/e6/UDjhz/8IVpbWw9a+QqFQqFQKBQKxYsJunCiOKhYu3YtAOD//t//i8WLFwMANm/efFjOCwCvetWrDvq5DzfWrFmDT37ykzjjjDP2u6xKpYJMJrNfdXnd616HN73pTftdl8OJg/mSQ6FQKBRHJzTGOXA4FPHC88U0BzJ2Opx42ctedriroFAoFAqFQqFQHDPQVF2Kg4b3v//9OP/88wEAS5YsgWVZuOWWW/a477e+9S289rWvRVtbG1pbW3H66afjwQcfNN9fc801uPbaa1EsFmFZFizLwumnn/6CzvtcaSxuueUWnHTSSchkMpgzZw4+//nPIwiC523b1NQULr30UjQ2NqKzsxOXX375Xo/ZG/7whz/glFNOQSaTwYoVK/DjH/94t33uvvtunHrqqchms+js7MSf/dmfoVgsAqin25icnMRf/dVfJfooiiJcd911WLRoEdLpNI477jjcdNNNibIlZdiDDz6IV7/61chkMvjHf/xHAPRC4bzzzkNzczPy+TzOOeccbNiw4TnbIn39+OOP49vf/jYsy8LChQv3uO/AwAA+8IEPYPHixchmszjuuONw1VVXoVqtmn0kPchll11mxl/au6ftABDHMW644QYsW7YM6XQaixcvxle/+tU9tvmJJ57Aa1/7WuRyOaxatQo/+9nPEvvtmq5jX4+r1Wr45Cc/iba2NjQ3N+ODH/wgvvnNb8KyrEPyck2hUCgUBwca4+wb1q5di4suugjz5s1DLpfDypUr8ZWvfAVRFAF47njhQD3f9xTT7Izni51m05ZFixYBAC644AJT7+fbDgDVahVXXXUVFixYgHQ6jRUrVuDWW29NnFvSnP7qV7/Cy172MuTzeZxyyil4+OGHE/vtmqprX4+bnJzEJZdckhjzL3/5ywc1PZtCoVAoFAqFQnGkQxUnioOGL3zhCzj++ONx1VVX4Qc/+AF6e3uxZMkS86J/Z2zevBmXXnoplixZglqthltvvRWvf/3r8cQTT2DZsmX40Ic+hP7+ftx666345S9/CQBoamra7/PeeOONuPzyy/GZz3wGX/nKV7BmzRp8/vOfRxiG+PKXv/ycbfvABz6An/3sZ/jyl7+MRYsW4eabb8bjjz8+y54CfN/HhRdeiD//8z/HokWL8C//8i84//zz8eijjxo/kDvvvBMXXngh/uRP/gTXXnstBgYGcMUVV2BiYgLf+9738PKXvxwPPPAAzjzzTFx88cX40Ic+ZProsssuw0033YSrrroKr3vd6/Dzn/8cn/nMZzA9PY0vfOELph61Wg3vfe978ZnPfAbXX389WltbsXHjRrzmNa/BqlWrcMstt8C2bfzN3/wNzjrrLDzzzDNIp9O7tUdShrz3ve/F8uXL8YUvfGGP+wHA6Ogo2tracOONN6K1tRXr1q3DNddcg8HBQXz9618HADzwwAN49atfjU984hO4+OKLAZAK5Lm2A8CnPvUp/Md//Ac+//nP49RTT8X999+Pz33uc8hms/jIRz6S6PtLLrkEn/zkJ/GFL3wB119/Pd71rndhy5YtaG9vf94x29txV1xxBb72ta/hS1/6El760pfi9ttvx1/+5V/u26RQKBQKxRELjXH2Ddu3b8fxxx+P9773vWhsbMRjjz2Gq6++GsViEV/84hefM16oVqv7/XzfU0yzK54vdppNW37wgx/gne98J6677jqjXnm+7QDw7ne/G7/97W9x9dVXY8WKFbjnnntwySWXoLW1FW95y1vM+QcHB/HJT34SV1xxBZqamnDFFVfg/PPPx4YNG+B53nOOwb4c9yd/8if45S9/ib/927/FggUL8LWvfQ2PPPLIPo+zQqFQKBQKhUJxTCJWKA4i7rjjjhhAvGnTJrNt06ZNMYD4jjvu2OMxYRjGvu/Hxx9/fHzllVea7VdffXWcz+cP2HmnpqbihoaGxDniOI5vvvnmOJvNxqOjo3Ecx/F9990XA4j/8Ic/xHEcx6tXr44ty4r/8z//0xzj+348f/78eDaX1NVXXx0DSJQXBEG8cOHC+D3veU8cx3EcRVG8YMEC87fg7rvvji3Lip966imzLZ/Px1dffbX5e2RkJPY8L77ssssSx374wx+O8/l8PD09najH7bffntjv0ksvjRctWhSXy2WzbXh4OM7n8/HNN9/8vG074YQT4ve9732Jbe973/viE0444TmP8X0//s53vhO7rhsXi0WzHUD8d3/3d7vtv6ft69evjy3Lir/2ta8ltl922WVxT09PHIZhos1333232efZZ5+NAcTf/va3zbbTTjstPuecc8zf+3Lc2NhYnMlk4i996UuJOpx22mm7zU2FQqFQHH3QGOeFIYqi2Pf9+G/+5m/i3t7exHd7ihcOxPN915jmubBr7BTHuz/796UtzzX+z7X9l7/8ZQwg/tnPfpbYfsEFF8Qnn3yy+ft973vfbvHez3/+8xhA/Jvf/MZsW7BgQfyxj33sBR339NNPxwDib33rW2afIAjixYsX7/eYKxQKhUKhUCgURzM0VZfiiMCaNWtw/vnno7u7G47jwPM8PPPMM1i3bt1BO+f999+PmZkZXHDBBQiCwPw788wzUS6X8dRTT+3xuAcffBBxHJtUGQDgui7OO++8/arPzuU5joO3v/3t+P3vfw8AWLduHbZs2YJ3v/vdibqedtppsCwLDz300HOW+7//+79G0bIz3vOe96BYLOLRRx9NbH/rW9+a+Pvee+/FeeedB9d1zXlbW1vxkpe8BH/4wx/2q80Apdy46aabsHLlSmSzWXieh/e+970IggAbN26cVZn//d//DQB417veleivs846C4ODg9i2bZvZ17ZtvOENbzB/L126FKlUCv39/c97jr0d9+STT6JSqeDtb3974rj9nScKhUKhOLrwYo5xKpUKrr76aixduhTpdBqe5+Hzn/88BgYGMDMz84LLeyHPd2D3mGZ/cKDbAlCM1dbWhjPPPHO39jz66KMIw9Ds29fXhxNOOMH8LQqcvcUreztOYrmd4xXHcRIpShUKhUKhUCgUihcjNFWX4rBjenoab3zjG9HZ2Ykbb7wRCxYsQCaTwYc+9CFUKpWDdt7R0VEAlKZhT9j1x7dgYGAAnuftlvKhu7t71nXZU3ldXV0YGBhI1HXnFxn7UlcAmJiYAAD09PQktsvf4+PjZlsul0M+n0/sNzo6iptuumk3TxQAyGazz3nefcVNN92Ev/iLv8Dll1+OM844A62trfjDH/6Aj33sY7Me/9HRUcRxjI6Ojj1+v23bNixYsAAAtSGVSiW+9zxvr+fe23Eydp2dnYl9urq69r0hCoVCoTiq8WKPcT73uc/h3//933H11VfjFa94BVpaWnDXXXfhr//6r1GpVNDQ0PCCynshz/c9xTT7gwPdFoDaMz4+/pyptgYGBjB37lwAQEtLS+I7iUH2No/2dpyMeXNzc2I/jVcUCoVCoVAoFC926MKJ4rDjgQceQH9/P3784x/jJS95idk+OTlpfiweDLS1tQEAfvCDH2DevHm7fS9Gnruit7cXvu9jYmIi8WJhaGho1nXZU3nDw8Mm/7XU9Z/+6Z9w6qmn7nZ8X1/fc5Ytxw4NDWHOnDlm++DgYOJ7AHs0AW1ra8M555yDj370o7t919jY+Lzt2hfccccdePvb347rr7/ebFu9evV+ldnW1gbLsvDb3/52t8UNADj++OP3q/x9gYzdyMhIYnyGh4cP+rkVCoVCcWTgxR7j3HHHHfjTP/1TfO5znzPb7r777lmVBbyw5/uBNjY/0G0BqD2dnZ2455579vj9oVi8kDGLine truncated
"text/plain": [
"<Figure size 1650x462 with 5 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"before = np.load(GRID / FLAT_RUN / 'subpixel_phase_before.npy')\n",
"after = np.load(GRID / FLAT_RUN / 'subpixel_phase_after.npy')\n",
"\n",
"fig, axes = plt.subplots(1, 3, figsize=(15, 4.2))\n",
"for ax, histogram, title in ((axes[0], before, 'before flattening'),\n",
" (axes[1], after, 'after flattening')):\n",
" im = ax.imshow(histogram.T / histogram.mean(), origin='lower', extent=[0, 1, 0, 1],\n",
" vmin=0.9, vmax=1.1, cmap='viridis')\n",
" ax.set_title(f'flat field, {title}', fontsize=10)\n",
" ax.set_xlabel('sub-pixel x'); ax.set_ylabel('sub-pixel y')\n",
" plt.colorbar(im, ax=ax, fraction=0.046)\n",
"for histogram, title in ((before, 'before'), (after, 'after')):\n",
" profile = histogram.sum(axis=1)\n",
" axes[2].plot(np.linspace(0, 1, len(profile)), profile / profile.mean(), lw=1, label=title)\n",
"axes[2].axhline(1, color='k', lw=.6); axes[2].set_xlabel('sub-pixel x')\n",
"axes[2].set_ylabel('relative density'); axes[2].legend(); axes[2].grid(alpha=.3)\n",
"fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "9837906d",
"metadata": {},
"source": [
"## 4. Relative DQE"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "460617ee",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:01.588078Z",
"iopub.status.busy": "2026-09-28T08:23:01.587992Z",
"iopub.status.idle": "2026-09-28T08:23:01.691826Z",
"shell.execute_reply": "2026-09-28T08:23:01.691526Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"variant QE rel MTF(200) DQE rel (relative to cluster <=3ph)\n",
"eta 0.710 0.187 0.025\n",
"cluster 1ph 0.710 0.229 0.037\n",
"cluster <=2ph 0.929 0.223 0.046\n",
"cluster <=3ph 1.000 0.244 0.060\n",
"GridBased flat 1.044 0.259 0.070\n",
"\n",
"highest relative DQE at 200 lp/mm: GridBased flat (2.82x eta)\n"
]
}
],
"source": [
"# DQE(f) is proportional to QE * MTF(f)^2. Every chain sees the same beam, so\n",
"# the ratio of detected photons is the ratio of QE and the relative DQE is well\n",
"# defined even without the absolute incident flux.\n",
"def mtf_at(frequency_lpmm, sigma_gauss_um):\n",
" sigma_mm = sigma_gauss_um / 1000.0\n",
" return float(np.exp(-2 * np.pi**2 * sigma_mm**2 * frequency_lpmm**2))\n",
"\n",
"\n",
"FREQUENCY = 200.0 # lp/mm, the Nyquist of the 2.5 um bins\n",
"REFERENCE = 'cluster <=3ph'\n",
"reference_photons = results[REFERENCE]['photons']\n",
"print(f\"{'variant':16s} {'QE rel':>8} {'MTF(200)':>9} {'DQE rel':>9} (relative to {REFERENCE})\")\n",
"relative_dqe = {}\n",
"for name, r in results.items():\n",
" qe = r['photons'] / reference_photons\n",
" mtf200 = r['mtf200_lsf'] # measured from the LSF, as in the poly notebook\n",
" relative_dqe[name] = qe * mtf200**2\n",
" print(f'{name:16s} {qe:8.3f} {mtf200:9.3f} {relative_dqe[name]:9.3f}')\n",
"best = max(relative_dqe, key=relative_dqe.get)\n",
"print(f\"\\nhighest relative DQE at {FREQUENCY:.0f} lp/mm: {best} \"\n",
" f\"({relative_dqe[best]/relative_dqe['eta']:.2f}x eta)\")"
]
},
{
"cell_type": "markdown",
"id": "9d317fe5",
"metadata": {},
"source": [
"## 5. Summary"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "764dbd8d",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:23:01.692963Z",
"iopub.status.busy": "2026-09-28T08:23:01.692876Z",
"iopub.status.idle": "2026-09-28T08:23:01.799221Z",
"shell.execute_reply": "2026-09-28T08:23:01.798889Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{\n",
" \"eta\": {\n",
" \"sigma_um\": 1.482743287676068,\n",
" \"sigma_err_um\": 0.023612628870164847,\n",
" \"mtf50_lsf_lpmm\": 131.67127170307347,\n",
" \"mtf200_lsf\": 0.18708166715602986,\n",
" \"chi2_ndf\": 1.6091412712838244,\n",
" \"mtf50_lpmm\": 126.38103081415967,\n",
" \"mtf10_lpmm\": 230.34405458447614,\n",
" \"rows_kept\": 1.0,\n",
" \"photons\": 156697901.0\n",
" },\n",
" \"cluster 1ph\": {\n",
" \"sigma_um\": 1.3517498054839803,\n",
" \"sigma_err_um\": 0.02228742348935663,\n",
" \"mtf50_lsf_lpmm\": 144.76471476919644,\n",
" \"mtf200_lsf\": 0.22898527725431547,\n",
" \"chi2_ndf\": 1.4377846589189212,\n",
" \"mtf50_lpmm\": 138.62818723482948,\n",
" \"mtf10_lpmm\": 252.66591450992405,\n",
" \"rows_kept\": 1.0,\n",
" \"photons\": 156697890.0\n",
" },\n",
" \"cluster <=2ph\": {\n",
" \"sigma_um\": 1.3724427091990845,\n",
" \"sigma_err_um\": 0.021926901151490465,\n",
" \"mtf50_lsf_lpmm\": 142.69021685073372,\n",
" \"mtf200_lsf\": 0.2230447974394754,\n",
" \"chi2_ndf\": 1.5018077194468944,\n",
" \"mtf50_lpmm\": 136.53803096715998,\n",
" \"mtf10_lpmm\": 248.85636282080927,\n",
" \"rows_kept\": 1.0,\n",
" \"photons\": 205040034.0\n",
" },\n",
" \"cluster <=3ph\": {\n",
" \"sigma_um\": 1.3855387111593214,\n",
" \"sigma_err_um\": 0.017612306263149854,\n",
" \"mtf50_lsf_lpmm\": 139.7355710728802,\n",
" \"mtf200_lsf\": 0.2440890142229673,\n",
" \"chi2_ndf\": 1.0195693691410581,\n",
" \"mtf50_lpmm\": 135.24748433227265,\n",
" \"mtf10_lpmm\": 246.50419222530726,\n",
" \"rows_kept\": 1.0,\n",
" \"photons\": 220757025.0\n",
" },\n",
" \"GridBased flat\": {\n",
" \"sigma_um\": 1.3461043903325456,\n",
" \"sigma_err_um\": 0.01889890619496615,\n",
" \"mtf50_lsf_lpmm\": 143.7567164975865,\n",
" \"mtf200_lsf\": 0.25887598776406284,\n",
" \"chi2_ndf\": 1.1515016338976902,\n",
" \"mtf50_lpmm\": 139.2095787481861,\n",
" \"mtf10_lpmm\": 253.72556782676153,\n",
" \"rows_kept\": 1.0,\n",
" \"photons\": 230530601.0\n",
" }\n",
"}\n",
"\n",
"written to /mnt/sls_det_storage/moench_data/MLXID/InferenceResults/GridBased/Edge_2filters_12keV/knife_edge_comparison.json\n"
]
}
],
"source": [
"summary = {\n",
" 'measurement': 'Edge/Flat_2filters_12keV',\n",
" 'occupancy_percent': occupancy * 100, # flat field, ESF ROI\n",
" 'roi_superpixel': list(ROI),\n",
" 'bin_um': BIN_UM,\n",
" 'bin_width_um': BIN_WIDTH_UM,\n",
" 'convention': 'sigma = ODR p2 (Gaussian LSF); MTF50 = 1000 sqrt(ln 2 / 2) / (pi sigma); '\n",
" 'ESF from DeepLearning/src, bin width 0.3 um',\n",
" 'grid_model_epoch': grid_summary['epoch'],\n",
" 'grid_frames': grid_summary['n_frames'],\n",
" 'grid_subpixel_before': grid_summary['subpixel_nonuniformity_before'],\n",
" 'grid_subpixel_after': grid_summary['subpixel_nonuniformity_after'],\n",
" 'variants': {name: dict(sigma_um=float(r['sigma']), sigma_err_um=float(r['sigma_err']),\n",
" mtf50_lsf_lpmm=float(r['mtf50_lsf']),\n",
" mtf200_lsf=float(r['mtf200_lsf']),\n",
" chi2_ndf=float(r['chi2_ndf']),\n",
" mtf50_lpmm=float(r['mtf50']), mtf10_lpmm=float(r['mtf10']),\n",
" rows_kept=float(r['rows_kept']), photons=float(r['photons']))\n",
" for name, r in results.items()},\n",
" 'relative_dqe_200lpmm': relative_dqe,\n",
"}\n",
"print(json.dumps(summary['variants'], indent=1))\n",
"out = GRID / EDGE_RUN / 'knife_edge_comparison.json'\n",
"json.dump(summary, open(out, 'w'), indent=1)\n",
"print(f'\\nwritten to {out}')"
]
}
],
"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
}
+620
View File
@@ -0,0 +1,620 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "d54c3f3e",
"metadata": {},
"source": [
"# Knife edge, 3 filters, pos0 — GridBased against the published chains\n",
"\n",
"Same measurement, same ROI, same ESF construction and same erf fit as\n",
"`DeepLearning/Notebooks/KnifeEdge_3filters_pos0.ipynb`, so the only thing that\n",
"differs between the rows of the final table is the reconstruction itself:\n",
"\n",
"| | |\n",
"|---|---|\n",
"| eta | classical eta interpolation from clusters |\n",
"| cluster 1ph / ≤2ph / ≤3ph | cluster finder + the 1/2/3-photon CNNs, summed |\n",
"| GridBased flattened | this repo, sub-pixel phase remapped (Rosenblatt) |\n",
"\n",
"The ESF code is the published one in `DeepLearning/src/funcs_postprocess.py`,\n",
"the same that `Plotting/` uses.\n",
"\n",
"**Edge width (revised 2026-09-28).** The ESF is binned and fitted exactly as\n",
"`Plotting/fig_knife_edge.py --fit odr` does it: `generate_presampled_esf()` and\n",
"`get_odr_fitting()` from `DeepLearning/src/funcs_postprocess.py` (bin content\n",
"Σk/Σf), symmetric ±4σ window, **ESF bin width 0.3 µm**. The ODR model is\n",
"`p0/2 (1 + erf((x - p1)/(sqrt(2) sigma))) + p3`, so the fit gives **sigma**, the\n",
"width of the Gaussian LSF, which is what the tables quote, with\n",
"`MTF50 [lp/mm] = 1000 sqrt(ln 2 / 2) / (pi sigma)`; MTF50 and MTF(200 lp/mm) are\n",
"also measured from the LSF (`Plotting/knife_edge_numbers.py`: first crossing,\n",
"central-difference correction), and the relative DQE uses the latter. Only the\n",
"flattened GridBased reconstruction is analysed. Same processing as\n",
"`KnifeEdge_poly2to20keV_Moench040.ipynb`.\n",
"\n",
"**Occupancy** is that of the flat field inside the ESF ROI: the reconstructed\n",
"photons per 25 µm pixel per frame over `ROI // 10` (x 33–46, y 26–49 of the\n",
"99 × 99 frame), the pixels over which `DeepLearning/Notebooks/KnifeEdge_3filters_pos0.ipynb`\n",
"sums the deposited energy (0.705 % at 3 filters from the energy, 0.747 % here\n",
"from the photons of the 12 keV model).\n",
"\n",
"*Until 2026-09-28 this notebook took the fitted sigma for sqrt(2) sigma (so its\n",
"MTF50 was sqrt(2) too high), used 0.5 µm bins, and binned the ESF with an older\n",
"copy of `generate_presampled_esf` (inverse-variance mean of k/f, biased low in\n",
"the toe, sigma ~3 % too small). The previous version is in\n",
"`Backup_260928_pre_sigma_fix/`.* The published MTF50\n",
"of 149 lp/mm corresponds to sigma = 1.26 µm.\n",
"\n",
"Run in the `pytorch_nightly` environment (ROOT + scipy + jupyter)."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "bbf5c366",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:50.007938Z",
"iopub.status.busy": "2026-09-28T08:22:50.007862Z",
"iopub.status.idle": "2026-09-28T08:22:51.585942Z",
"shell.execute_reply": "2026-09-28T08:22:51.585615Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Welcome to JupyROOT 6.28/10\n"
]
}
],
"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, '/home/xie_x1/MLXID/DeepLearning/src') # the code Plotting/ uses\n",
"sys.path.insert(0, '/home/xie_x1/MLXID/Plotting')\n",
"from ROOT import TCanvas, gROOT\n",
"gROOT.SetBatch(True)\n",
"from funcs_postprocess import generate_presampled_esf, get_odr_fitting\n",
"from knife_edge_numbers import mtf as mtf_from_lsf # first crossing, sinc-corrected\n",
"\n",
"GRID = Path('/mnt/sls_det_storage/moench_data/MLXID/InferenceResults/GridBased')\n",
"PUBLISHED = Path('/home/xie_x1/MLXID/DeepLearning/InferenceResults')\n",
"ETA = Path('/home/xie_x1/MLXID/EtaInterpolation/Results')\n",
"\n",
"EDGE_RUN, FLAT_RUN = 'Edge_3filters_pos0_12keV', 'Flat_3filters_pos0_12keV'\n",
"BIN_UM = 2.5 # super-pixel = 25 um / 10\n",
"ROI = (330, 465, 260, 490) # x1, x2, y1, y2, as published\n",
"CROP = slice(10, -10) # 1010 -> 990, as published\n",
"BIN_WIDTH_UM = 0.3 # presampled ESF bin, as Plotting/ and the manuscript\n",
"\n",
"plt.rcParams.update({'figure.dpi': 110, 'image.cmap': 'gray',\n",
" 'image.origin': 'lower', 'image.interpolation': 'nearest'})"
]
},
{
"cell_type": "markdown",
"id": "c749780c",
"metadata": {},
"source": [
"## 1. Load every reconstruction of the same measurement"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "3b145751",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:51.587508Z",
"iopub.status.busy": "2026-09-28T08:22:51.587285Z",
"iopub.status.idle": "2026-09-28T08:22:51.804809Z",
"shell.execute_reply": "2026-09-28T08:22:51.804521Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"GridBased model epoch 135, 14,000,000 frames, occupancy 0.747 % (flat field, ESF ROI)\n",
" sub-pixel excess rms 22.63 % -> 4.30 % after flattening\n",
"\n",
"variant edge photons flat photons relative to <=3ph\n",
"eta 1.6099e+08 2.7823e+08 0.855\n",
"cluster 1ph 1.6099e+08 2.7823e+08 0.855\n",
"cluster <=2ph 1.8464e+08 3.1950e+08 0.981\n",
"cluster <=3ph 1.8828e+08 3.2591e+08 1.000\n",
"GridBased flat 1.9083e+08 3.3060e+08 1.014\n"
]
}
],
"source": [
"def crop(image):\n",
" \"\"\"The published frames are cropped by 10 super-pixels on each side.\"\"\"\n",
" return image[CROP, CROP] if image.shape[0] == 1010 else image\n",
"\n",
"\n",
"def published(run, experiment, n_photons):\n",
" name = f'{n_photons}Photon_ML_superFrame_etaInterpolated.npy'\n",
" return np.load(PUBLISHED / run / experiment / 'augX1' / name).astype(float)\n",
"\n",
"\n",
"EXPERIMENT = {1: '1ph_260512_12keV_v260511_06',\n",
" 2: '2ph_260610_12keV_v260610_01',\n",
" 3: '3ph_260611_12keV_v260611_00'}\n",
"KNIFE, FLAT = 'KnifeEdge_3filters_pos0', 'FlatField_3filters_pos0'\n",
"\n",
"cluster_edge = {n: published(KNIFE, EXPERIMENT[n], n) for n in (1, 2, 3)}\n",
"cluster_flat = {n: published(FLAT, EXPERIMENT[n], n) for n in (1, 2, 3)}\n",
"\n",
"grid_summary = json.load(open(GRID / EDGE_RUN / 'summary.json'))\n",
"flat_summary = json.load(open(GRID / FLAT_RUN / 'summary.json'))\n",
"\n",
"\n",
"def grid_image(run, flattened):\n",
" name = 'binned_image_flattened.npy' if flattened else 'binned_image.npy'\n",
" return np.load(GRID / run / name).astype(float)\n",
"\n",
"\n",
"variants = {\n",
" 'eta': (np.load(ETA / '2603MaxIV_Edge3Filters_pos0_12keV_fromClusters.npy'),\n",
" np.load(ETA / '2603MaxIV_FlatField3Filters_pos0_12keV_fromClusters.npy')),\n",
" 'cluster 1ph': (cluster_edge[1], cluster_flat[1]),\n",
" 'cluster <=2ph': (cluster_edge[1] + cluster_edge[2], cluster_flat[1] + cluster_flat[2]),\n",
" 'cluster <=3ph': (cluster_edge[1] + cluster_edge[2] + cluster_edge[3],\n",
" cluster_flat[1] + cluster_flat[2] + cluster_flat[3]),\n",
" 'GridBased flat': (grid_image(EDGE_RUN, True),\n",
" grid_image(FLAT_RUN, True)),\n",
"}\n",
"variants = {name: (crop(edge), crop(flat)) for name, (edge, flat) in variants.items()}\n",
"\n",
"def flat_occupancy(run):\n",
" # reconstructed photons per 25 um pixel per frame in the flat field, inside the\n",
" # ESF ROI: ROI // 10 in pixels of the 99 x 99 frame, both ends included as in\n",
" # DeepLearning/Notebooks; +1 because the 101 x 101 frame is cropped by 1 px\n",
" summary = json.load(open(GRID / run / 'summary.json'))\n",
" native = np.load(GRID / run / 'native_image.npy') / summary['n_frames']\n",
" x0, x1, y0, y1 = (v // 10 for v in ROI)\n",
" return float(native[y0 + 1:y1 + 2, x0 + 1:x1 + 2].mean())\n",
"\n",
"\n",
"occupancy = flat_occupancy(FLAT_RUN)\n",
"print(f\"GridBased model epoch {grid_summary['epoch']}, \"\n",
" f\"{grid_summary['n_frames']:,} frames, \"\n",
" f\"occupancy {occupancy*100:.3f} % (flat field, ESF ROI)\")\n",
"print(f\" sub-pixel excess rms {grid_summary['subpixel_nonuniformity_before']*100:.2f} % -> \"\n",
" f\"{grid_summary['subpixel_nonuniformity_after']*100:.2f} % after flattening\\n\")\n",
"print(f\"{'variant':16s} {'edge photons':>14} {'flat photons':>14} relative to <=3ph\")\n",
"reference = variants['cluster <=3ph'][0].sum()\n",
"for name, (edge, flat) in variants.items():\n",
" print(f'{name:16s} {edge.sum():14.4e} {flat.sum():14.4e} {edge.sum()/reference:8.3f}')"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "7e4f857c",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:51.805974Z",
"iopub.status.busy": "2026-09-28T08:22:51.805891Z",
"iopub.status.idle": "2026-09-28T08:22:52.219065Z",
"shell.execute_reply": "2026-09-28T08:22:52.218827Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABmcAAAH2CAYAAACFuVjoAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOydeZgU1bn/v92z9d49MwzIoqAQg0RQ8YqKC4ioAVTcIhCN4oa7xu3GGBX0F2OMa6LXFcVdXKJEA24IeqMxek2MUSHuCxJFlll6umef+v3B8z3z1pnqnp4dZt7P88zDTHd11alTRb/ferfjcxzHgaIoiqIoiqIoiqIoiqIoiqIoitIj+Ht7AIqiKIqiKIqiKIqiKIqiKIqiKP0JDc4oiqIoiqIoiqIoiqIoiqIoiqL0IBqcURRFURRFURRFURRFURRFURRF6UE0OKMoiqIoiqIoiqIoiqIoiqIoitKDaHBGURRFURRFURRFURRFURRFURSlB9HgjKIoiqIoiqIoiqIoiqIoiqIoSg+iwRlFURRFURRFURRFURRFURRFUZQeRIMziqIoiqIoiqIoiqIoiqIoiqIoPYgGZxRFURRFURRFURRFURRFURRFUXoQDc4oiqIoiqIoiqIoiqIoiqIoiqL0IBqcURRFURRF6cfcf//98Pl8ePXVV7t0v6+++ip8Ph/uv/9+1+vpdBrnnXcetttuO+Tl5WHEiBFdetyuYPLkyVvkuDpLpmuSDZ/Ph7lz53bbmLrr/ust5s6dC5/Pl/P2I0aMwOTJk12vdfT+68j17Qr66v8XRVEURVEUReluNDijKIqiKIqi9BjXXXcdbr31VsyaNQv3338/brnllt4eUr+moqICCxYs6NbgyKuvvooFCxagoqKi246hbB2MGDECPp8v48+CBQtc23/55Zc488wzsdNOOyEcDiORSGDHHXfE7Nmz8fTTT7u2nTx5csb9jho1qs2xLViwwPUZv9+PRCKB/fbbDw899FDWz7744os48sgjMXjwYBQWFqKsrAyHHHIIFi9eDMdxMh7r9ddfb3vSFEVRFEVRlD5Lfm8PQFEURVEURel77L///qipqUFBQYHr9Zdffhljx47F9ddf30sj6794XZOKigpcddVVANCqgqOrePXVV3HVVVdh7ty5SCQS3XKMvsRLL73k6dDvKwwaNAg33HCD53vjxo0zv//jH//A/vvvj6amJhx33HE455xzAACffvopli5diurqahx11FGuz/v9fjzwwAOt9huNRnMe3xVXXIEdd9wRTU1N+Prrr7Fw4UKccMIJWLt2LS699FLXts3NzTjjjDNwzz33YOTIkZg3bx6GDx+OdevW4cknn8ScOXPw6KOP4vHHH0cwGMx5DIqiKIqiKEr/QIMziqIoiqIoSpfj9/sRCARavf7dd99hu+2264URKZmuibJlUVhY2NtD6FYikQiOP/74NrdbsGABUqkUlixZgpkzZ7reu/nmm/HNN9+0+ozP58tp39k4+OCDse+++5q/Tz75ZOy444647rrrcMkllyAvL8+89+tf/xr33HMPjj76aDzyyCMoKioy71166aW4+OKLcdNNN+Gcc87Bvffe26lxKYqiKIqiKH0PbWumKIqiKIqitOKOO+5Afn4+jj32WNTV1QFoWR/j448/xsyZMxGPxxGJRDBjxgx89tlnrs/b61+wjc8XX3yB1157zbON0bvvvotjjjkGAwcORGFhIXbYYQdceumlSKfTOY/7s88+w9y5czFkyBAUFhZi2LBhOOuss7Bhw4ZW23766ac48sgjEY/HEY1GcfDBB+O9997LuO+HHnoI48aNQ1FREYYNG4b//u//xurVqz3bMTmOg3vuuQcTJkxAOBxGOBzGxIkTsWTJkpzPxWbt2rXw+Xy46KKLXK/PmzcPPp8Pp59+uuv1Cy+8ED6fD+vWrQPQ+prcf//92H777QEAV111lbkmXuuHvP3225gyZQoikQgSiQTmzJmD9evXtznmyZMnm8qc7bffPmP7KsdxcMstt2DHHXdEUVERdthhB9x8882e++zsfcI1Ur766iscffTRKC4uRjgcxkEHHYR//vOfrm2zrePCe/rLL79s9d7GjRtx8skno6ysDMFgEBMnTsSKFSvaNT7J6tWrMWfOHGy77bYoKirCwIEDsc8++2DRokWe++C9GggEMHToUPzqV79CU1NTq+268/9LZ/n4448BAAceeKDn+8OGDeu2Y0uGDh2KnXbaCRUVFa57fsOGDfjtb3+LoUOH4oEHHnAFZoDNgaLrr78eu+++OxYtWoQPP/ywR8arKIqiKIqibD1o5YyiKIqiKIpicBwHv/zlL3HdddfhggsuwI033uha4Hzt2rXYf//9cfjhh+O6667DJ598gltvvRWHH3443n//ffj93rk/Rx11FEaNGoULLrgAAwYMwK9+9SsALW2MXnjhBRxxxBHYdtttce6552LQoEF47733cNNNN+GNN97AypUrkZ+fXbr+85//xOTJkxEKhXDyySdj+PDh+OSTT3DHHXfglVdewdtvv414PA4AWLNmDSZOnIiKigqcccYZ2GmnnfDmm29i8uTJKC0tbbXv//mf/8E555yD0aNH46qrrkJBQQEee+yxjGu1nHTSSXjwwQcxc+ZMHHfccQCAp59+GkceeSTuuOMOnHHGGdkvhAdDhw7F6NGjsXz5ctfrr7zyCvx+v+frO++8MwYNGuS5v/333x8333wzLrjgAhx55JGmRVQkEnFt995772HatGk44YQTMGvWLPz973/HwoULUV5ejhdeeCHrmH/1q1+hpKQEzzzzDG6++WYMGDAAgLt9FQBcdtllqKqqwkknnYRIJIIHH3wQF154IQYPHozZs2eb7briPgGAVCqFSZMmYbfddsOvf/1rrFmzBv/zP/+D/fbbD2+++SZ23nnnNveRjUMOOQSxWAxXXHEFNm3ahDvvvBOHHHIInn/+eUydOrVd+9q4cSMOOOAANDc34/TTT8f222+P8vJyvP/++3jttddw0kknuba/6667sHbtWpx66qkoKyvD008/jd/85jeIRqOutlzd+f8lG83NzZ7BHwBIJBLm+o0cORIfffQRFi5ciPPPP9/1PZQNr33H4/FWLRZzpb6+HmvWrDFr0JClS5eipqYGP/vZzxAOhz0/6/f7MW/ePJx++un44x//iB/96EcdGoOiKIqiKIrSR3EURVEURVGUfsuiRYscAM7KlSud2tpaZ/bs2Y7f73d+//vft9p2+PDhDgDn0Ucfdb1+7bXXOgCcF1980by2cuVKB4CzaNGiVvuYNGmS67Wamhpnm222cSZMmODU1ta63nvyyScdAM7999/f5rnsuuuuzvbbb+9s3LjR9frf/vY3x+/3OwsWLDCv/exnP3MAOM8884znuQwfPty8Vl5e7oTDYWeHHXZwqqqqzOu1tbXOHnvs4QBw5s+fb15fsmSJA8C56aabWo3x0EMPdWKxmGs/7eHss892fD6fs27dOsdxHOeLL75wAJjz+fzzzx3HcZzvv//e8fl8zvnnn28+63VN+Hk5fgkAx+fzOa+//rrr9dNPP90B4Hz00Udtjnn+/PkOAOeLL75o9R7vv3HjxrmufXV1tVNaWursvffe5rWuuk8mTZrkAHDOPvts1+tvvfWW4/P5nAMPPNC8luk+znReJ554ogPAOeyww5ympibz+pdffumEQiHnBz/4gdPc3Gxe9/r/MGnSJNf996c//ckB4CxevDjreXGs22yzjbNp0ybzelNTk7PTTjs5gwcPdm3fXf9fssHvkEw/f/nLX1zjKCwsdAA42223nXPCCSc4t956q/PPf/7Tc9+8rl4/zz//fJtj4/X885//7Kxfv95Zt26d8/bbbztHHnmkA8CZPXu2a/uLLrrIAeD88Y9/zLrfd955xwHgHH300a2OJc9XURRFURRF6X9oWzNFURRFURQF5eXlOPjgg7FkyRI8+eSTOO+88zy3GzJkCObMmeN67aCDDgLQ0oaovSxfvhzfffcd5s6di2QyiQ0bNpifSZMmIRwO48UXX8y6jw8++AD//Oc/MXv2bJOZz5+RI0fiBz/4gdlHc3MzlixZgjFjxuCII45w7efnP/95q8qRl156CalUCmeddZZrYfGioiJccMEFrcby0EMPIRgMYtasWa5xbNiwAUceeSSqqqrw5ptvdmiuDjzwQDiOY1pkvfLKK8jLy8NVV12FvLw8vPLKKwCAFStWwHGcjC2h2sPee++NffbZx/VaZ6+5zTnnnONqCxUOh7H33nu79t8V94nksssuc/09YcIETJ06FStWrEB5eXmnzueXv/ylq4ps+PDhOO644/DJJ5/gX//6V7v2xWqNZcuWobKyss3tTz75ZBQXF5u//X4/DjzwQHz77beorq4G0L3/X9piyJAhePnllz1/xo4da7bbc8898Y9//AOnnHIKGhsb8eCDD+Lcc8/FrrvuivHjx7dqQcdz9drvHnvskfP4Dj30UJSVlWHQoEGYMGECnn32WZx55pmt1ozhtWB1USb4fi7XTlEURVEURelfaFszRVEURVEUBXPnzkVVVRVefvnlrG2Xdthhh1avsa3Rxo0bO3Ts1atXAwDOOussnHXWWZ7bcN2UtvZx7bXX4tprr/XchmP//vvvkUwmMWbMmFbbBAIBjBw5EhUVFea1zz//HAAwevToVtvvtNNOnmOpqanB0KFDM463rfPJxAEHHGBamM2ePRvLly/H7rvvju233x7jx4/H8uXLceqpp2L58uXIy8vDpEmTOnQcSXdc81yPIfffFfcJSSQSGDJkSKvXx4wZg5dffhmff/45dt9995z25YXXvcXXPv30U+yyyy4572v//ffHySefjPvuuw+PPfYYdt99d0ycOBHHHHMM9t5771bbt3W9IpFIt/5/aYtgMJhza7cf/ehHWLhwIQDgm2++wRtvvIEHH3wQy5Ytw/Tp0/Hhhx+Line truncated
"text/plain": [
"<Figure size 1650x506 with 6 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, axes = plt.subplots(1, 3, figsize=(15, 4.6))\n",
"extent = [0, 990 * BIN_UM, 0, 990 * BIN_UM]\n",
"for ax, name in zip(axes, ('eta', 'cluster <=3ph', 'GridBased flat')):\n",
" image = variants[name][0]\n",
" im = ax.imshow(image, extent=extent, vmax=np.percentile(image, 99.5))\n",
" ax.add_patch(plt.Rectangle((ROI[0]*BIN_UM, ROI[2]*BIN_UM),\n",
" (ROI[1]-ROI[0])*BIN_UM, (ROI[3]-ROI[2])*BIN_UM,\n",
" edgecolor='red', facecolor='none', lw=1.4))\n",
" ax.set_title(name); ax.set_xlabel('x [um]'); ax.set_ylabel('y [um]')\n",
" plt.colorbar(im, ax=ax, fraction=0.046)\n",
"fig.suptitle('knife edge, with the published ESF ROI')\n",
"fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "8b8c560d",
"metadata": {},
"source": [
"## 2. Edge spread function\n",
"\n",
"`generate_presampled_esf` fits an erf to every row of the ROI, fits a straight\n",
"line through the edge positions, and re-samples the ROI onto a common axis;\n",
"`get_odr_fitting` then fits the erf with x and y errors inside |x - p1| <= 4 sigma\n",
"(the fit of `Plotting/fig_knife_edge.py --fit odr`)."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "ae7f236f",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:52.220504Z",
"iopub.status.busy": "2026-09-28T08:22:52.220382Z",
"iopub.status.idle": "2026-09-28T08:22:53.851025Z",
"shell.execute_reply": "2026-09-28T08:22:53.850763Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Final Edge Midpoint Fit: slope = -0.0866, intercept = 433.2062, chi2/ndf = 0.0458\n",
"Final Edge Midpoint Fit: slope = -0.0859, intercept = 432.9570, chi2/ndf = 0.0451\n",
"Final Edge Midpoint Fit: slope = -0.0858, intercept = 433.0080, chi2/ndf = 0.0419\n",
"Final Edge Midpoint Fit: slope = -0.0859, intercept = 433.0672, chi2/ndf = 0.0473\n",
"Final Edge Midpoint Fit: slope = -0.0860, intercept = 433.1036, chi2/ndf = 0.0451\n",
"variant sigma [um] chi2/ndf MTF50 MTF50 LSF MTF(200) LSF rows\n",
"eta 1.536 +- 0.019 1.06 122.0 127.4 0.170 100%\n",
"cluster 1ph 1.355 +- 0.016 0.69 138.3 140.7 0.225 100%\n",
"cluster <=2ph 1.380 +- 0.016 0.81 135.8 138.1 0.221 100%\n",
"cluster <=3ph 1.381 +- 0.017 0.85 135.7 136.8 0.229 100%\n",
"GridBased flat 1.360 +- 0.019 1.10 137.8 140.3 0.252 100%\n"
]
}
],
"source": [
"def analyse(edge_image, flat_image, label):\n",
" esf = generate_presampled_esf(edge_image, flat_image, ROI,\n",
" pixel_size_um=BIN_UM, bin_width_um=BIN_WIDTH_UM,\n",
" chi2_threshold=15.0, residual_tolerance_px=1.0)\n",
" x, z = esf['numpy']['x'], esf['numpy']['z']\n",
" odr = get_odr_fitting(x, z, esf['numpy']['x_err'], esf['numpy']['z_err'], n_sigma=4.0)\n",
" sigma, sigma_err = odr['sigma_um'], odr['sigma_err_um'] # sigma of the Gaussian LSF\n",
" try:\n",
" mtf50_lsf, mtf200_lsf = mtf_from_lsf(dict(x=x, z=z, pars=odr['beta']))\n",
" except Exception:\n",
" mtf50_lsf = mtf200_lsf = np.nan\n",
" n_fit = int(odr['mask'].sum())\n",
" ndf = max(n_fit - len(odr['beta']), 1)\n",
" x_fit = x[odr['mask']]\n",
" return dict(label=label, x=x, z=z, sigma=sigma, sigma_err=sigma_err, sigma_gauss=sigma,\n",
" chi2_ndf=odr['chi2_ndf'], chi2=odr['chi2_ndf'] * ndf, ndf=ndf,\n",
" mtf50=1000 * np.sqrt(np.log(2) / 2) / (np.pi * sigma),\n",
" mtf10=1000 * np.sqrt(np.log(10) / 2) / (np.pi * sigma),\n",
" mtf50_lsf=mtf50_lsf, mtf200_lsf=mtf200_lsf,\n",
" rows_kept=esf['valid_lines_ratio'],\n",
" edge_slope=esf['edge_slope'], edge_centre=odr['beta'][1],\n",
" fit_points=n_fit, fit_range=(x_fit.min(), x_fit.max()),\n",
" photons=float(edge_image.sum()), odr=odr)\n",
"\n",
"\n",
"results = {name: analyse(edge, flat, name) for name, (edge, flat) in variants.items()}\n",
"\n",
"print(f\"{'variant':16s} {'sigma [um]':>16} {'chi2/ndf':>9} {'MTF50':>7} {'MTF50 LSF':>9} \"\n",
" f\"{'MTF(200) LSF':>12} {'rows':>6}\")\n",
"for name, r in results.items():\n",
" print(f\"{name:16s} {r['sigma']:7.3f} +- {r['sigma_err']:.3f} {r['chi2_ndf']:9.2f} {r['mtf50']:7.1f} \"\n",
" f\"{r['mtf50_lsf']:9.1f} {r['mtf200_lsf']:12.3f} {r['rows_kept']*100:5.0f}%\")"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "bc41a64f",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:53.852183Z",
"iopub.status.busy": "2026-09-28T08:22:53.852031Z",
"iopub.status.idle": "2026-09-28T08:22:54.250652Z",
"shell.execute_reply": "2026-09-28T08:22:54.250431Z"
}
},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1375x528 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, ax = plt.subplots(1, 2, figsize=(12.5, 4.8))\n",
"colours = {'eta': '#a0451c', 'cluster 1ph': '#6b6b6b', 'cluster <=2ph': '#7b5aa6',\n",
" 'cluster <=3ph': '#4b2a6b', 'GridBased raw': '#4cbcac', 'GridBased flat': '#10695f'}\n",
"frequency = np.linspace(0, 400, 2000)\n",
"for name, r in results.items():\n",
" ax[0].plot(r['x'], r['z'], lw=1.1, color=colours[name],\n",
" label=f\"{name}: $\\\\sigma$ = {r['sigma']:.3f} $\\\\pm$ {r['sigma_err']:.3f} $\\\\mu$m\")\n",
" sigma_mm = r['sigma_gauss'] / 1000.0\n",
" ax[1].plot(frequency, np.exp(-2 * np.pi**2 * sigma_mm**2 * frequency**2),\n",
" lw=1.6, color=colours[name], label=f\"{name} ({r['mtf50']:.0f} lp/mm)\")\n",
"ax[0].set_xlim(-9, 9); ax[0].set_ylim(-0.05, 1.35)\n",
"ax[0].set_xlabel(r'position across the edge [$\\mu$m]')\n",
"ax[0].set_ylabel('relative intensity')\n",
"ax[0].legend(fontsize=8); ax[0].grid(alpha=.3); ax[0].set_title('oversampled ESF')\n",
"ax[1].axvline(200, color='k', ls=':', lw=.8, label='2.5 um Nyquist')\n",
"ax[1].axhline(0.5, color='grey', lw=.5)\n",
"ax[1].set_xlim(0, 400); ax[1].set_ylim(0, 1.05)\n",
"ax[1].set_xlabel('spatial frequency [lp/mm]'); ax[1].set_ylabel('MTF')\n",
"ax[1].legend(fontsize=8); ax[1].grid(alpha=.3); ax[1].set_title('MTF from the fitted ESF')\n",
"fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "61548cb0",
"metadata": {},
"source": [
"## 3. Where the GridBased sub-pixel phase stands\n",
"\n",
"Under the uniform illumination of the flat field the reconstructed positions\n",
"should fill the pixel evenly; the Rosenblatt remap in\n",
"`05_infer_measurement.py` makes them do so."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "79ecd95a",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:54.251748Z",
"iopub.status.busy": "2026-09-28T08:22:54.251666Z",
"iopub.status.idle": "2026-09-28T08:22:54.735190Z",
"shell.execute_reply": "2026-09-28T08:22:54.734959Z"
}
},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1650x462 with 5 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"before = np.load(GRID / FLAT_RUN / 'subpixel_phase_before.npy')\n",
"after = np.load(GRID / FLAT_RUN / 'subpixel_phase_after.npy')\n",
"\n",
"fig, axes = plt.subplots(1, 3, figsize=(15, 4.2))\n",
"for ax, histogram, title in ((axes[0], before, 'before flattening'),\n",
" (axes[1], after, 'after flattening')):\n",
" im = ax.imshow(histogram.T / histogram.mean(), origin='lower', extent=[0, 1, 0, 1],\n",
" vmin=0.9, vmax=1.1, cmap='viridis')\n",
" ax.set_title(f'flat field, {title}', fontsize=10)\n",
" ax.set_xlabel('sub-pixel x'); ax.set_ylabel('sub-pixel y')\n",
" plt.colorbar(im, ax=ax, fraction=0.046)\n",
"for histogram, title in ((before, 'before'), (after, 'after')):\n",
" profile = histogram.sum(axis=1)\n",
" axes[2].plot(np.linspace(0, 1, len(profile)), profile / profile.mean(), lw=1, label=title)\n",
"axes[2].axhline(1, color='k', lw=.6); axes[2].set_xlabel('sub-pixel x')\n",
"axes[2].set_ylabel('relative density'); axes[2].legend(); axes[2].grid(alpha=.3)\n",
"fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "5dc0762d",
"metadata": {},
"source": [
"## 4. Relative DQE"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "5e11e1e3",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:54.736713Z",
"iopub.status.busy": "2026-09-28T08:22:54.736582Z",
"iopub.status.idle": "2026-09-28T08:22:54.840950Z",
"shell.execute_reply": "2026-09-28T08:22:54.840626Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"variant QE rel MTF(200) DQE rel (relative to cluster <=3ph)\n",
"eta 0.855 0.170 0.025\n",
"cluster 1ph 0.855 0.225 0.043\n",
"cluster <=2ph 0.981 0.221 0.048\n",
"cluster <=3ph 1.000 0.229 0.052\n",
"GridBased flat 1.014 0.252 0.064\n",
"\n",
"highest relative DQE at 200 lp/mm: GridBased flat (1.23x the cluster <=3ph chain, 2.60x eta)\n"
]
}
],
"source": [
"# DQE(f) is proportional to QE * MTF(f)^2. Every chain sees the same beam, so\n",
"# the ratio of detected photons is the ratio of QE and the relative DQE is well\n",
"# defined even without the absolute incident flux.\n",
"def mtf_at(frequency_lpmm, sigma_gauss_um):\n",
" sigma_mm = sigma_gauss_um / 1000.0\n",
" return float(np.exp(-2 * np.pi**2 * sigma_mm**2 * frequency_lpmm**2))\n",
"\n",
"FREQUENCY = 200.0 # lp/mm, the Nyquist of the 2.5 um bins\n",
"reference_photons = results['cluster <=3ph']['photons']\n",
"print(f\"{'variant':16s} {'QE rel':>8} {'MTF(200)':>9} {'DQE rel':>9} \"\n",
" f\"(relative to cluster <=3ph)\")\n",
"relative_dqe = {}\n",
"for name, r in results.items():\n",
" qe = r['photons'] / reference_photons\n",
" mtf200 = r['mtf200_lsf'] # measured from the LSF, as in the poly notebook\n",
" relative_dqe[name] = qe * mtf200**2\n",
" print(f'{name:16s} {qe:8.3f} {mtf200:9.3f} {relative_dqe[name]:9.3f}')\n",
"best = max(relative_dqe, key=relative_dqe.get)\n",
"print(f\"\\nhighest relative DQE at {FREQUENCY:.0f} lp/mm: {best} \"\n",
" f\"({relative_dqe[best]/relative_dqe['cluster <=3ph']:.2f}x the cluster <=3ph chain, \"\n",
" f\"{relative_dqe[best]/relative_dqe['eta']:.2f}x eta)\")"
]
},
{
"cell_type": "markdown",
"id": "a0cbe193",
"metadata": {},
"source": [
"## 5. Summary"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "72edb58e",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:54.842114Z",
"iopub.status.busy": "2026-09-28T08:22:54.842030Z",
"iopub.status.idle": "2026-09-28T08:22:54.948033Z",
"shell.execute_reply": "2026-09-28T08:22:54.947608Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{\n",
" \"eta\": {\n",
" \"sigma_um\": 1.5357788997826887,\n",
" \"sigma_err_um\": 0.018934140987143105,\n",
" \"mtf50_lsf_lpmm\": 127.41391534878392,\n",
" \"mtf200_lsf\": 0.16986274278517122,\n",
" \"chi2_ndf\": 1.0632259342404946,\n",
" \"mtf50_lpmm\": 122.01666864663473,\n",
" \"mtf10_lpmm\": 222.38949945174375,\n",
" \"rows_kept\": 1.0,\n",
" \"photons\": 160989360.0\n",
" },\n",
" \"cluster 1ph\": {\n",
" \"sigma_um\": 1.3553236105888296,\n",
" \"sigma_err_um\": 0.015582837583920595,\n",
" \"mtf50_lsf_lpmm\": 140.72272920365313,\n",
" \"mtf200_lsf\": 0.22534741293893693,\n",
" \"chi2_ndf\": 0.6948283846340041,\n",
" \"mtf50_lpmm\": 138.26264344931204,\n",
" \"mtf10_lpmm\": 251.999668656873,\n",
" \"rows_kept\": 1.0,\n",
" \"photons\": 160989340.0\n",
" },\n",
" \"cluster <=2ph\": {\n",
" \"sigma_um\": 1.380244936199657,\n",
" \"sigma_err_um\": 0.016190473494226774,\n",
" \"mtf50_lsf_lpmm\": 138.11611497098744,\n",
" \"mtf200_lsf\": 0.2208935552975036,\n",
" \"chi2_ndf\": 0.8064499904260305,\n",
" \"mtf50_lpmm\": 135.76621091995148,\n",
" \"mtf10_lpmm\": 247.44963146296,\n",
" \"rows_kept\": 1.0,\n",
" \"photons\": 184639952.0\n",
" },\n",
" \"cluster <=3ph\": {\n",
" \"sigma_um\": 1.381207746488057,\n",
" \"sigma_err_um\": 0.016615366693369205,\n",
" \"mtf50_lsf_lpmm\": 136.79352035267127,\n",
" \"mtf200_lsf\": 0.22868136720959276,\n",
" \"chi2_ndf\": 0.8533928535430366,\n",
" \"mtf50_lpmm\": 135.6715712069733,\n",
" \"mtf10_lpmm\": 247.2771396335164,\n",
" \"rows_kept\": 1.0,\n",
" \"photons\": 188275709.0\n",
" },\n",
" \"GridBased flat\": {\n",
" \"sigma_um\": 1.3596210744597628,\n",
" \"sigma_err_um\": 0.018755935321852032,\n",
" \"mtf50_lsf_lpmm\": 140.32690640620402,\n",
" \"mtf200_lsf\": 0.2518136955393898,\n",
" \"chi2_ndf\": 1.104983605454407,\n",
" \"mtf50_lpmm\": 137.8256255727252,\n",
" \"mtf10_lpmm\": 251.20315300123687,\n",
" \"rows_kept\": 0.9956521739130435,\n",
" \"photons\": 190825531.0\n",
" }\n",
"}\n",
"\n",
"written to /mnt/sls_det_storage/moench_data/MLXID/InferenceResults/GridBased/Edge_3filters_pos0_12keV/knife_edge_comparison.json\n"
]
}
],
"source": [
"summary = {\n",
" 'measurement': 'Edge/Flat_3filters_pos0_12keV',\n",
" 'roi_superpixel': list(ROI),\n",
" 'bin_um': BIN_UM,\n",
" 'bin_width_um': BIN_WIDTH_UM,\n",
" 'convention': 'sigma = ODR p2 (Gaussian LSF); MTF50 = 1000 sqrt(ln 2 / 2) / (pi sigma); '\n",
" 'ESF from DeepLearning/src, bin width 0.3 um',\n",
" 'grid_model_epoch': grid_summary['epoch'],\n",
" 'grid_frames': grid_summary['n_frames'],\n",
" 'occupancy_percent': occupancy * 100, # flat field, ESF ROI\n",
" 'grid_subpixel_before': grid_summary['subpixel_nonuniformity_before'],\n",
" 'grid_subpixel_after': grid_summary['subpixel_nonuniformity_after'],\n",
" 'variants': {name: dict(sigma_um=float(r['sigma']), sigma_err_um=float(r['sigma_err']),\n",
" mtf50_lsf_lpmm=float(r['mtf50_lsf']),\n",
" mtf200_lsf=float(r['mtf200_lsf']),\n",
" chi2_ndf=float(r['chi2_ndf']),\n",
" mtf50_lpmm=float(r['mtf50']), mtf10_lpmm=float(r['mtf10']),\n",
" rows_kept=float(r['rows_kept']),\n",
" photons=float(r['photons']))\n",
" for name, r in results.items()},\n",
" 'relative_dqe_200lpmm': relative_dqe,\n",
"}\n",
"print(json.dumps(summary['variants'], indent=1))\n",
"out = GRID / EDGE_RUN / 'knife_edge_comparison.json'\n",
"json.dump(summary, open(out, 'w'), indent=1)\n",
"print(f'\\nwritten to {out}')"
]
}
],
"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
}
@@ -0,0 +1,897 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "b3ef09b8",
"metadata": {},
"source": [
"# Knife edge, 12 keV, MOENCH040 — polychromatic 2–20 keV GridBased model\n",
"\n",
"The polychromatic GridBased network retrained for the 650 µm **MOENCH040** on\n",
"flat **2–20 keV** frames (`[32, 64, 96, 96]`, α(E,t)/β(E,t) charge-cloud table of\n",
"MOENCH040 @150 V), run over the same 2603 MaxIV knife-edge scan as the 12 keV\n",
"monochromatic model: four flux points (0–3 filters), edge and flat field each.\n",
"Reconstruction settings are those of the 12 keV runs (same ROI readout,\n",
"pedestal, column common mode, self-flattening); the only difference is the model\n",
"(and FP16 with an FP32 head, as for every poly model).\n",
"\n",
"**Edge width.** The ESF and its fit are the ones of `Plotting/fig_knife_edge.py\n",
"--fit odr`: `generate_presampled_esf()` and `get_odr_fitting()` from\n",
"`DeepLearning/src/funcs_postprocess.py`, ROI `(330, 465, 260, 490)` on the\n",
"990 × 990 super-frame, symmetric ±4σ window, **ESF bin width 0.3 µm**. The ODR\n",
"model is `p0/2 · (1 + erf((x − p1)/(√2 σ))) + p3`, so the fit gives **σ**, the\n",
"width of the Gaussian LSF, which is what the tables quote, with\n",
"**MTF50 = 1000 √(ln2 / 2) / (π σ) lp/mm** (the Gaussian's), plus MTF50 and\n",
"MTF(200 lp/mm) measured from the LSF by FFT with the first-crossing and\n",
"central-difference corrections of `Plotting/knife_edge_numbers.py`. Only the\n",
"flattened GridBased reconstructions are analysed.\n",
"\n",
"**Occupancy** is that of the flat field inside the ESF ROI: the reconstructed\n",
"photons per 25 µm pixel per frame over `ROI // 10`, the pixels over which\n",
"`DeepLearning/Notebooks/KnifeEdge_3filters_pos0.ipynb` sums the deposited energy.\n",
"The flux points are labelled with the 12 keV model's value, as in the\n",
"single-flux notebooks.\n",
"\n",
"The 12 keV model's images are re-fitted here with the same code, and so are the\n",
"2026-09-17 GridBased knife-edge notebooks since 2026-09-28: the numbers agree\n",
"digit for digit with `KnifeEdge_{0,1,2,3}filters*.ipynb`."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "4d16f0f1",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:35.617146Z",
"iopub.status.busy": "2026-09-28T08:22:35.616976Z",
"iopub.status.idle": "2026-09-28T08:22:37.557915Z",
"shell.execute_reply": "2026-09-28T08:22:37.557583Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Welcome to JupyROOT 6.28/10\n",
"poly model : 260927_poly2to20keV_Moench040_c96_00 tag 'poly2to20'\n",
"12 keV model: 260831_12keV_Moench040_01\n"
]
}
],
"source": [
"import json\n",
"import os\n",
"import sys\n",
"from pathlib import Path\n",
"\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"from scipy.optimize import curve_fit\n",
"\n",
"sys.path.insert(0, '/home/xie_x1/MLXID/DeepLearning/src') # the code Plotting/ uses\n",
"sys.path.insert(0, '/home/xie_x1/MLXID/Plotting')\n",
"from ROOT import gROOT\n",
"gROOT.SetBatch(True)\n",
"from funcs_postprocess import generate_presampled_esf, get_odr_fitting\n",
"from knife_edge_numbers import mtf as mtf_from_lsf # first crossing, sinc-corrected\n",
"\n",
"GRID = Path('/mnt/sls_det_storage/moench_data/MLXID/InferenceResults/GridBased')\n",
"PUBLISHED = Path('/home/xie_x1/MLXID/DeepLearning/InferenceResults')\n",
"ETA = Path('/home/xie_x1/MLXID/EtaInterpolation/Results')\n",
"GRIDBASED = Path('/home/xie_x1/MLXID/GridBased')\n",
"OVERNIGHT = GRIDBASED / 'Results/overnight_poly2to20keV_Moench040'\n",
"\n",
"POLY_TAG = os.environ.get('GRID_POLY_TAG', 'poly2to20')\n",
"POLY_EXPERIMENT = GRIDBASED / (os.environ.get('GRID_POLY_EXPERIMENT')\n",
" or (OVERNIGHT / 'state/experiment').read_text().strip())\n",
"MONO_EXPERIMENT = GRIDBASED / 'Results/260831_12keV_Moench040_01'\n",
"\n",
"BIN_UM = 2.5 # super-pixel = 25 um / 10\n",
"ROI = (330, 465, 260, 490) # x1, x2, y1, y2 on the 990 x 990 frame, as published\n",
"CROP = 10 # 1010 -> 990\n",
"BIN_WIDTH_UM = 0.3 # presampled ESF bin\n",
"N_SIGMA = 4.0 # symmetric ODR window\n",
"FLUX = [('3filters_pos0', '3 filters'), ('2filters', '2 filters'),\n",
" ('1filters', '1 filter'), ('0filters', '0 filters')]\n",
"MONO, POLY = 'GridBased 12 keV', 'GridBased 2-20 keV'\n",
"\n",
"plt.rcParams.update({'figure.dpi': 110, 'image.cmap': 'gray', 'image.origin': 'lower',\n",
" 'image.interpolation': 'nearest'})\n",
"print('poly model :', POLY_EXPERIMENT.name, ' tag', repr(POLY_TAG))\n",
"print('12 keV model:', MONO_EXPERIMENT.name)"
]
},
{
"cell_type": "markdown",
"id": "3b2ee3ef",
"metadata": {},
"source": [
"## 1. The reconstructions"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "aeb7d57f",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:37.559180Z",
"iopub.status.busy": "2026-09-28T08:22:37.559038Z",
"iopub.status.idle": "2026-09-28T08:22:40.056391Z",
"shell.execute_reply": "2026-09-28T08:22:40.056105Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"missing: nothing\n",
"\n",
"flux point occupancy model own occ. frames edge photons flat photons frame/s sub-pixel rms\n",
"3 filters 0.75% GridBased 2-20 keV 0.76% 14,000,000 1.9590e+08 3.3831e+08 7603 23.4 -> 3.1 %\n",
"3 filters 0.75% GridBased 12 keV 0.75% 14,000,000 1.9083e+08 3.3060e+08 7647 22.6 -> 4.3 %\n",
"2 filters 1.81% GridBased 2-20 keV 1.85% 7,000,000 2.3612e+08 4.1102e+08 7664 22.2 -> 2.8 %\n",
"2 filters 1.81% GridBased 12 keV 1.81% 7,000,000 2.3053e+08 4.0162e+08 7123 21.6 -> 3.9 %\n",
"1 filter 4.45% GridBased 2-20 keV 4.58% 7,000,000 5.8278e+08 1.0097e+09 7682 20.0 -> 2.2 %\n",
"1 filter 4.45% GridBased 12 keV 4.45% 7,000,000 5.6697e+08 9.8283e+08 6537 19.8 -> 3.0 %\n",
"0 filters 10.77% GridBased 2-20 keV 11.15% 3,500,000 7.0822e+08 1.2280e+09 7185 18.6 -> 1.3 %\n",
"0 filters 10.77% GridBased 12 keV 10.77% 3,500,000 6.8684e+08 1.1910e+09 6176 18.3 -> 1.6 %\n"
]
}
],
"source": [
"def crop(image):\n",
" return image[CROP:-CROP, CROP:-CROP] if image.shape[0] == 1010 else image\n",
"\n",
"\n",
"def run_dir(run, tag):\n",
" return GRID / (run + (f'_{tag}' if tag else ''))\n",
"\n",
"\n",
"def summary_of(run, tag):\n",
" path = run_dir(run, tag) / 'summary.json'\n",
" return json.load(open(path)) if path.exists() else None\n",
"\n",
"\n",
"def grid_pair(filters, tag, flattened):\n",
" name = 'binned_image_flattened.npy' if flattened else 'binned_image.npy'\n",
" edge, flat = run_dir(f'Edge_{filters}_12keV', tag), run_dir(f'Flat_{filters}_12keV', tag)\n",
" if not ((edge / name).exists() and (flat / name).exists()):\n",
" return None\n",
" return crop(np.load(edge / name).astype(float)), crop(np.load(flat / name).astype(float))\n",
"\n",
"\n",
"ETA_FILES = {'3filters_pos0': ('2603MaxIV_Edge3Filters_pos0_12keV_fromClusters.npy',\n",
" '2603MaxIV_FlatField3Filters_pos0_12keV_fromClusters.npy'),\n",
" '2filters': ('2603MaxIV_Edge2Filters_12keV_fromClusters.npy',\n",
" '2603MaxIV_Flat2Filters_12keV_fromClusters.npy'),\n",
" '1filters': ('2603MaxIV_Edge1Filters_12keV_fromClusters.npy',\n",
" '2603MaxIV_Flat1Filters_12keV_fromClusters.npy')}\n",
"CNN = {1: '1ph_260512_12keV_v260511_06', 2: '2ph_260610_12keV_v260610_01',\n",
" 3: '3ph_260611_12keV_v260611_00'}\n",
"CNN_DIRS = {'3filters_pos0': ('KnifeEdge_3filters_pos0', 'FlatField_3filters_pos0'),\n",
" '2filters': ('KnifeEdge_2filters', 'FlatField_2filters')}\n",
"\n",
"\n",
"def cnn_pair(filters, up_to):\n",
" knife_dir, flat_dir = CNN_DIRS[filters]\n",
" edge = flat = 0\n",
" for n in range(1, up_to + 1):\n",
" name = f'{n}Photon_ML_superFrame_etaInterpolated.npy'\n",
" edge = edge + crop(np.load(PUBLISHED / knife_dir / CNN[n] / 'augX1' / name).astype(float))\n",
" flat = flat + crop(np.load(PUBLISHED / flat_dir / CNN[n] / 'augX1' / name).astype(float))\n",
" return edge, flat\n",
"\n",
"\n",
"variants, missing = {}, []\n",
"for filters, label in FLUX:\n",
" v = {}\n",
" for name, tag, flattened in ((f'{POLY}, flattened', POLY_TAG, True),\n",
" (f'{MONO}, flattened', '', True)):\n",
" pair = grid_pair(filters, tag, flattened)\n",
" if pair is None:\n",
" missing.append(f'{label}: {name}')\n",
" else:\n",
" v[name] = pair\n",
" if filters in ETA_FILES:\n",
" v['eta'] = tuple(np.load(ETA / f).astype(float) for f in ETA_FILES[filters])\n",
" if filters in CNN_DIRS:\n",
" for n in (1, 2, 3):\n",
" v[f'cluster CNN <={n}ph'] = cnn_pair(filters, n)\n",
" variants[filters] = v\n",
"\n",
"def flat_occupancy(filters, tag):\n",
" # reconstructed photons per 25 um pixel per frame in the flat field, inside the\n",
" # ESF ROI: ROI // 10 in pixels of the 99 x 99 frame, both ends included as in\n",
" # DeepLearning/Notebooks; +1 because the 101 x 101 frame is cropped by 1 px\n",
" run = run_dir(f'Flat_{filters}_12keV', tag)\n",
" if not (run / 'native_image.npy').exists():\n",
" return np.nan\n",
" native = np.load(run / 'native_image.npy') / json.load(open(run / 'summary.json'))['n_frames']\n",
" x0, x1, y0, y1 = (v // 10 for v in ROI)\n",
" return float(native[y0 + 1:y1 + 2, x0 + 1:x1 + 2].mean())\n",
"\n",
"\n",
"# the flux point is labelled with the 12 keV model's value, the same number as in\n",
"# KnifeEdge_{0,1,2,3}filters*.ipynb; the 2-20 keV model counts 2-3 % more photons\n",
"occupancy = {f: flat_occupancy(f, '') for f, _ in FLUX}\n",
"print('missing:', missing or 'nothing')\n",
"print(f\"\\n{'flux point':10s} {'occupancy':>9s} {'model':20s} {'own occ.':>8s} {'frames':>11s} {'edge photons':>13s} \"\n",
" f\"{'flat photons':>13s} {'frame/s':>8s} {'sub-pixel rms':>14s}\")\n",
"for filters, label in FLUX:\n",
" for model, tag in ((POLY, POLY_TAG), (MONO, '')):\n",
" e, f = summary_of(f'Edge_{filters}_12keV', tag), summary_of(f'Flat_{filters}_12keV', tag)\n",
" if e is None or f is None:\n",
" continue\n",
" print(f\"{label:10s} {occupancy[filters]*100:8.2f}% {model:20s} {flat_occupancy(filters, tag)*100:7.2f}% \"\n",
" f\"{e['n_frames']:11,d} \"\n",
" f\"{e['n_photons']:13.4e} {f['n_photons']:13.4e} {e['frames_per_second']:8.0f} \"\n",
" f\"{f['subpixel_nonuniformity_before']*100:5.1f} -> {f['subpixel_nonuniformity_after']*100:4.1f} %\")"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "c5b40483",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:40.057689Z",
"iopub.status.busy": "2026-09-28T08:22:40.057511Z",
"iopub.status.idle": "2026-09-28T08:22:40.832086Z",
"shell.execute_reply": "2026-09-28T08:22:40.831793Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABwQAAAN7CAYAAABLVi50AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOy9d5hV1dn+f5/pM+dMoWMFK6CiEhUECwQVgw0sEbGB8bXEXmNLRP1pfC2xt2gU29eaCFGDGEVRg71gsKBRsQZUFKb32b8/eO917r1mnzNnYICZ4flc11zM7LPL2msf1r2ftlYsCIIAhmEYhmEYhmEYhmEYhmEYhmEYhmF0S7LWdgMMwzAMwzAMwzAMwzAMwzAMwzAMw1h9WEDQMAzDMAzDMAzDMAzDMAzDMAzDMLoxFhA0DMMwDMMwDMMwDMMwDMMwDMMwjG6MBQQNwzAMwzAMwzAMwzAMwzAMwzAMoxtjAUHDMAzDMAzDMAzDMAzDMAzDMAzD6MZYQNAwDMMwDMMwDMMwDMMwDMMwDMMwujEWEDQMwzAMwzAMwzAMwzAMwzAMwzCMbowFBA3DMAzDMAzDMAzDMAzDMAzDMAyjG2MBQcMwDMMwDMMwDMMwDMMwDMMwDMPoxlhA0DAMwzAMwzAMwzAMwzAMwzAMwzC6MRYQNAzDMAzDMNZ57r33XsRiMcydO7dDzzt37lzEYjHce++9oe01NTU47bTTsPHGGyM7OxsDBw7s0Ot2BGPGjOmU7TJSk+r7lopLLrkEsVgMX3755Wpt15qkvd/bNdEHAwcOxJgxY1bb+Q3DMAzDMAzDMDLBAoKGYRiGYRiGsYa56qqrcPPNN2PSpEm49957ccMNN6ztJnUp/vOf/+CSSy7BLrvsgv79+yMej2OrrbbCaaedhsWLF2d8HgaDon7OOeec1XgHxppk5syZuOSSS1brNS655BLMnDlztV4jU7788suU32v++MkP8+bNw4EHHohNN90UBQUF6NOnD7bbbjuccMIJeO+990L7pjvv//zP/7TZvjFjxoSOycnJQb9+/TBx4kS8+uqrKY9rbGzEnXfeidGjR6NXr17Iz8/HRhtthMmTJ+ONN95Iea2cnJy2O80wDMMwDMMw1gHszdgwDMMwDMMwVhO77747amtrkZubG9r+3HPPYejQobjmmmvWUsu6NnfffTduuukm7Lvvvvj1r3+NoqIivP7667jtttvw//7f/8O8efMwePDgjM93/fXXo3fv3qFtW221VUc3u9Px+9//Hueffz7y8/PXdlM6jH/+858IgiC0bebMmbjvvvtWa1Dw0ksvxZQpUzBx4sTVdo32MmbMGBx77LGRnw0ZMsT9/uc//xknnngi+vfvj6OPPhqbb745li9fjoULF+KJJ57AFltsgWHDhoWO33rrrXH++ee3Ou/mm2+eUduysrJw3333AQDq6+vxwQcf4K677sKsWbMwZ84c7LbbbqH9ly5div333x+vv/46dt11V1xwwQXo0aMHPvvsM9x///145JFHcNlll+EPf/hDRtc3DMMwDMMwjHURCwgahmEYhmEYxmoiKysLBQUFrbYvWbIEG2+88VpoUffgkEMOwXnnnYcePXq4bccffzx23nlnnHDCCbj44ovx2GOPZXy+iRMnrpPTo+bk5HS76qm8vLy13YROw2abbYYjjzwy7T5NTU244IILkEgk8NZbb2HDDTcMfd7Y2Iiff/651XH9+/dv89zpiMVirY4fPXo0DjzwQFx99dWtAoKTJk3C66+/jmuvvRZnn3126LMLL7wQ++67Ly6++GIMHDgQRx111Eq3yzAMwzAMwzC6MzZlqGEYhmEYhmGk4Pbbb0dOTg4OPfRQ1NfXA0iuB/bpp59iwoQJKC0tRSKRwL777ovPP/88dLy/phunqFy0aBFeeuklN2WeVi699957OOSQQ9C3b1/k5eVh0003xfnnn4+ampqM2/35559j6tSpWH/99ZGXl4cNN9wQJ510EpYuXdpq388++wwHHnggSktLUVxcjHHjxuH9999Pee4HHngA2267LfLz87Hhhhvid7/7HT7++ONW9wEAQRDgrrvuwvDhwxGPxxGPxzFq1KhVnlpxxx13DAUDyWGHHQYA+Pe//93uc1ZWVqKxsbHdx6Vbgy5qPTt+fz744APsvffeKCkpQa9evfA///M/qK6uRhAEuOaaa7D55psjPz8fW2+9Nf7xj3+0u1233347hgwZgoKCAgwcOBCXXXYZmpqa2mw7t3366ae4+OKLMWDAAOTn52PIkCF46KGHWl1n9uzZGDt2LPr27YuCggJsuOGG2GeffdJO/ZiO4447Djk5OVi+fLnb9t///tf9X9EpYZcvX47s7GycdNJJbpvf5wMHDnSVaDpNpb/OYkNDQ0b368P/4wBw3333ha7hk8mYwbZcffXV2HbbbVFYWIiSkhLsueeeePnll9tsT3tZunQpli1bhkGDBrUKBgJAbm4u+vXr1+HXjWLPPfcEsGJKYGXWrFl44YUXMGHChFbBQAAoLi7Go48+isLCQlxwwQUr9f/YMAzDMAzDMNYFulc6qGEYhmEYhmF0AEEQ4IILLsBVV12FM888E3/6059CDv7vvvsOu+++Ow444ABcddVV+M9//oObb74ZBxxwABYsWICsrOi8u4MOOgibb745zjzzTPTu3RsXXXQRAGDbbbcFsCK4MnHiRGy00UY49dRT0a9fP7z//vu47rrrMG/ePLz44ottVnTNnz8fY8aMQVFREX7zm99gwIAB+M9//oPbb78dc+bMwZtvvonS0lIAwDfffINRo0Zh+fLlOPHEEzFkyBC89tprGDNmDHr16tXq3LfeeitOOeUUDB48GJdeeilyc3Px8MMPt1qPjBxzzDG4//77MWHCBBxxxBEAgCeeeAIHHnggbr/9dpx44onpH0Q7+e677wAAffv2bddx2223HSoqKpCVlYVhw4bhd7/7HQ499NAObZvy3XffYezYsTjkkENw4IEH4rXXXsPdd9+N2tpa9OjRA//6179wwgknIDs7GzfeeCMOOuggfPrppxgwYEBG57/lllvw7bff4sQTT0TPnj0xY8YMTJs2DV988UWrQFgqpkyZglgshtNOOw1ZWVm47bbbcMQRR2DTTTfFzjvvDAB4+eWXsd9++2GrrbbCueeei169emHJkiWYN28e5s+fj1GjRrW7b/bcc0/85S9/wYsvvogDDzwQADBnzhz3/+/55593FWAvvvgiWlpasMcee6Q83w033IDrrrsOr7zyCh544AG33W9bJvcbxZAhQ/DAAw/gqKOOwm677Ybjjz8+cr9Mx4ympibss88+eOmllzB58mSceOKJqKmpwYMPPoixY8di5syZ2G+//TLoyRVTcUYlAcRiMff/u1+/fkgkEvjwww/x6quvZvzMGhsbI8/tT73bHj777DMAaDX2PP744wCA3/72tymPXW+99TBx4kQ8/PDDeO2117D77ruvdDsMwzAMwzAMo9sSGIZhGIZhGMY6zvTp0wMAwYsvvhjU1dUFhx12WJCVlRXceOONrfYdMGBAACB46KGHQtuvvPLKAEDw7LPPum0vvvhiACCYPn16q3OMHj06tK22tjbo379/MHz48KCuri702eOPPx4ACO69994272X77bcPNtlkk+Cnn34KbX/99deDrKys4JJLLnHbjjrqqABAMGPGjMh7GTBggNu2bNmyIB6PB5tuumlQUVHhttfV1QU77bRTACCYNm2a2z5z5swAQHDddde1auN+++0XlJSUhM7TERx00EGR/Z2K66+/Pvif//mfYPr06cGTTz4Z/OlPf3LP98orr8zoHNOmTQsABIsWLWr12ejRo0N9GATJ78/DDz8c2j5hwoQgFosF22+/fVBfX++2v/feewGA4IILLmizLfy+FRUVBV9++aXb3tzcHOy///4BgOCVV15J23ZuGz9+fNDc3Oy2f/3110Fubm4wefJkt+3MM88MAARLlixps22Z8sMPPwSxWCw46aST3LYpU6YE2223XbDtttsGRx99tNt+8sknB1lZWaHvelSfT5kyJUhl+rbnftMBIJgyZUrkZ+0ZM2644YYAQPDEE0+E9m1oaAiGDRsWbLLJJm22ZdGiRQGAlD/Z2dmh/a+//nr32XbbbReccsopwX333Rd8/fXXKe811U9tbW2b7Rs9enSQnZ0d/Pjjj8GPP/4YfPvtt8Hs2bODbbbZJgAQ3HHHHaH9d9hhhwBAqzHN59prrw0ABDfffHOraxmGYRiGYRiGEQQ2ZahhGIZhGIZh/B/Lli3DuHHjMHPmTDz++OM47bTTIvdbf/31MXny5NC2vfbaC8CKaQFXhueffx5LlizB1KlTUVlZiaVLl7qf0aNHIx6P49lnn017jg8++ADz58/HYYcdhpaWltA5NttsM2yxxRbuHC0tLZg5cya22morTJw4MXSeM844A4lEIrTtn//8J6qrq3HSSSehuLjYbc/Pz8eZZ57Zqi0PPPAACgsLMWnSpFA7li5digMPPBAVFRV47bXXVqqvovjjH/+IJ554AhMnTsSUKVMyOuaMM87AXXfdhalTp2L//ffHWWedhQULFmDTTTfFxRdfjG+//bbD2qesv/76bnpTMnr0aARBgJNOOim0Dt7222+PkpKSdn2vjjzyyFA1YVZWFs4//3wAwN/+9reMznHmmWeGKl032mgjDBo0KNSOsrIyACsquPzpSFeWPn36YOjQoZgzZ47bNmfOHOyxxx7YY489Wm3ffvvt0bNnz1W+bib3uypkOmY88MADGDhwIHbbbbfQ/5ny8nIccMABWLRoUcZtGj9+PJ577rlWP//85z9D+51xxhmYNWsW9t9/fyxatAi33HILpkyZggEDBuDggw/GTz/91Orcw4YNizx3pms4Njc3o0+fPujTpw823HBD/OpXv8J///tfXH/99TjhhBNC+5aXlwOAq2xOBT/n/oZhGIZhGIZLine truncated
"text/plain": [
"<Figure size 1848x902 with 8 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"shown = [(f, l) for f, l in FLUX if f'{POLY}, flattened' in variants[f]]\n",
"fig, axes = plt.subplots(2, max(len(shown), 1), figsize=(4.2 * max(len(shown), 1), 8.2), squeeze=False)\n",
"extent = [0, 990 * BIN_UM, 0, 990 * BIN_UM]\n",
"for column, (filters, label) in enumerate(shown):\n",
" for row, model in enumerate((MONO, POLY)):\n",
" ax = axes[row, column]\n",
" name = f'{model}, flattened'\n",
" if name not in variants[filters]:\n",
" ax.axis('off'); continue\n",
" image = variants[filters][name][0]\n",
" ax.imshow(image, extent=extent, vmax=np.percentile(image, 99.5))\n",
" ax.add_patch(plt.Rectangle((ROI[0] * BIN_UM, ROI[2] * BIN_UM), (ROI[1] - ROI[0]) * BIN_UM,\n",
" (ROI[3] - ROI[2]) * BIN_UM, edgecolor='red', facecolor='none', lw=1.2))\n",
" ax.set_title(f'{label} ({occupancy[filters]*100:.2f} %)\\n{name}', fontsize=9)\n",
" ax.set_xlabel('x [um]'); ax.set_ylabel('y [um]')\n",
"fig.suptitle('knife edge, 2.5 um bins, with the ESF ROI')\n",
"fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "19689795",
"metadata": {},
"source": [
"## 2. Edge spread function and ODR fit\n",
"\n",
"`generate_presampled_esf` fits an erf to every row of the ROI, fits a straight\n",
"line through the edge positions, and resamples the knife/flat ratio onto a\n",
"0.3 µm grid across the edge (bin content Σk/Σf). `get_odr_fitting` fits the ESF\n",
"with x and y errors inside |x − p1| ≤ 4σ, iterating the window."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "02f502b8",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:40.833998Z",
"iopub.status.busy": "2026-09-28T08:22:40.833910Z",
"iopub.status.idle": "2026-09-28T08:22:45.700834Z",
"shell.execute_reply": "2026-09-28T08:22:45.700450Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Final Edge Midpoint Fit: slope = -0.0871, intercept = 433.5999, chi2/ndf = 0.0389\n",
"Final Edge Midpoint Fit: slope = -0.0860, intercept = 433.1036, chi2/ndf = 0.0451\n",
"Final Edge Midpoint Fit: slope = -0.0866, intercept = 433.2062, chi2/ndf = 0.0458\n",
"Final Edge Midpoint Fit: slope = -0.0859, intercept = 432.9570, chi2/ndf = 0.0451\n",
"Final Edge Midpoint Fit: slope = -0.0858, intercept = 433.0080, chi2/ndf = 0.0419\n",
"Final Edge Midpoint Fit: slope = -0.0859, intercept = 433.0672, chi2/ndf = 0.0473\n",
"Final Edge Midpoint Fit: slope = -0.0855, intercept = 432.4900, chi2/ndf = 0.0377\n",
"Final Edge Midpoint Fit: slope = -0.0858, intercept = 432.5887, chi2/ndf = 0.0361\n",
"Final Edge Midpoint Fit: slope = -0.0858, intercept = 432.3990, chi2/ndf = 0.0751\n",
"Final Edge Midpoint Fit: slope = -0.0854, intercept = 432.2740, chi2/ndf = 0.0576\n",
"Final Edge Midpoint Fit: slope = -0.0855, intercept = 432.4402, chi2/ndf = 0.0479\n",
"Final Edge Midpoint Fit: slope = -0.0850, intercept = 432.2537, chi2/ndf = 0.0406\n",
"Final Edge Midpoint Fit: slope = -0.0853, intercept = 432.6611, chi2/ndf = 0.0386\n",
"Final Edge Midpoint Fit: slope = -0.0852, intercept = 432.6254, chi2/ndf = 0.0434\n",
"Final Edge Midpoint Fit: slope = -0.0859, intercept = 432.5449, chi2/ndf = 0.0615\n",
"Final Edge Midpoint Fit: slope = -0.0860, intercept = 433.2953, chi2/ndf = 0.0337\n",
"Final Edge Midpoint Fit: slope = -0.0858, intercept = 433.2022, chi2/ndf = 0.0302\n"
]
}
],
"source": [
"def analyse(edge, flat):\n",
" esf = generate_presampled_esf(edge, flat, ROI, pixel_size_um=BIN_UM,\n",
" bin_width_um=BIN_WIDTH_UM, chi2_threshold=15.0,\n",
" residual_tolerance_px=1.0)\n",
" n = esf['numpy']\n",
" fit = get_odr_fitting(n['x'], n['z'], n['x_err'], n['z_err'], n_sigma=N_SIGMA)\n",
" sigma, sigma_err = fit['sigma_um'], fit['sigma_err_um']\n",
" try:\n",
" mtf50_lsf, mtf200_lsf = mtf_from_lsf(dict(x=n['x'], z=n['z'], pars=fit['beta']))\n",
" except Exception:\n",
" mtf50_lsf = mtf200_lsf = np.nan\n",
" return dict(x=n['x'], z=n['z'], z_err=n['z_err'], fit=fit,\n",
" sigma=sigma, sigma_err=sigma_err,\n",
" chi2_ndf=fit['chi2_ndf'], n_fit=int(fit['mask'].sum()),\n",
" mtf50=1000 * np.sqrt(np.log(2) / 2) / (np.pi * sigma),\n",
" mtf200=float(np.exp(-2 * np.pi**2 * (sigma / 1000)**2 * 200.0**2)),\n",
" mtf50_lsf=mtf50_lsf, mtf200_lsf=mtf200_lsf,\n",
" rows_kept=esf['valid_lines_ratio'], photons=float(edge.sum()))\n",
"\n",
"\n",
"results = {}\n",
"for filters, label in FLUX:\n",
" results[filters] = {name: analyse(edge, flat) for name, (edge, flat) in variants[filters].items()}"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "022aa946",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:45.702053Z",
"iopub.status.busy": "2026-09-28T08:22:45.701965Z",
"iopub.status.idle": "2026-09-28T08:22:45.805285Z",
"shell.execute_reply": "2026-09-28T08:22:45.804879Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"=== 3 filters, occupancy 0.75 % (flat field, ESF ROI) ===\n",
" sigma [um] chi2/ndf MTF50 [lp/mm] MTF50 LSF MTF(200) LSF edge photons\n",
"GridBased 2-20 keV, flattened 1.420 +- 0.021 1.34 131.9 134.4 0.227 1.9590e+08\n",
"GridBased 12 keV, flattened 1.360 +- 0.019 1.10 137.8 140.3 0.252 1.9083e+08\n",
"eta 1.536 +- 0.019 1.06 122.0 127.4 0.170 1.6099e+08\n",
"cluster CNN <=1ph 1.355 +- 0.016 0.69 138.3 140.7 0.225 1.6099e+08\n",
"cluster CNN <=2ph 1.380 +- 0.016 0.81 135.8 138.1 0.221 1.8464e+08\n",
"cluster CNN <=3ph 1.381 +- 0.017 0.85 135.7 136.8 0.229 1.8828e+08\n",
"\n",
"=== 2 filters, occupancy 1.81 % (flat field, ESF ROI) ===\n",
" sigma [um] chi2/ndf MTF50 [lp/mm] MTF50 LSF MTF(200) LSF edge photons\n",
"GridBased 2-20 keV, flattened 1.391 +- 0.018 1.00 134.7 139.8 0.254 2.3612e+08\n",
"GridBased 12 keV, flattened 1.346 +- 0.019 1.15 139.2 143.8 0.259 2.3053e+08\n",
"eta 1.483 +- 0.024 1.61 126.4 131.7 0.187 1.5670e+08\n",
"cluster CNN <=1ph 1.352 +- 0.022 1.44 138.6 144.8 0.229 1.5670e+08\n",
"cluster CNN <=2ph 1.372 +- 0.022 1.50 136.5 142.7 0.223 2.0504e+08\n",
"cluster CNN <=3ph 1.386 +- 0.018 1.02 135.2 139.7 0.244 2.2076e+08\n",
"\n",
"=== 1 filter, occupancy 4.45 % (flat field, ESF ROI) ===\n",
" sigma [um] chi2/ndf MTF50 [lp/mm] MTF50 LSF MTF(200) LSF edge photons\n",
"GridBased 2-20 keV, flattened 1.551 +- 0.028 3.37 120.9 130.5 0.227 5.8278e+08\n",
"GridBased 12 keV, flattened 1.388 +- 0.020 1.82 135.0 141.3 0.251 5.6697e+08\n",
"eta 1.506 +- 0.023 1.83 124.4 130.0 0.218 2.3085e+08\n",
"\n",
"=== 0 filters, occupancy 10.77 % (flat field, ESF ROI) ===\n",
" sigma [um] chi2/ndf MTF50 [lp/mm] MTF50 LSF MTF(200) LSF edge photons\n",
"GridBased 2-20 keV, flattened 1.856 +- 0.037 7.36 101.0 113.1 0.173 7.0822e+08\n",
"GridBased 12 keV, flattened 1.473 +- 0.017 1.30 127.2 133.0 0.222 6.8684e+08\n"
]
}
],
"source": [
"header = (f\"{'':24s} {'sigma [um]':>16s} {'chi2/ndf':>8s} \"\n",
" f\"{'MTF50 [lp/mm]':>13s} {'MTF50 LSF':>9s} {'MTF(200) LSF':>12s} {'edge photons':>13s}\")\n",
"for filters, label in FLUX:\n",
" print(f'\\n=== {label}, occupancy {occupancy[filters]*100:.2f} % (flat field, ESF ROI) ===')\n",
" print(header)\n",
" for name, r in results[filters].items():\n",
" print(f\"{name:24s} {r['sigma']:7.3f} +- {r['sigma_err']:.3f} {r['chi2_ndf']:8.2f} \"\n",
" f\"{r['mtf50']:13.1f} {r['mtf50_lsf']:9.1f} {r['mtf200_lsf']:12.3f} {r['photons']:13.4e}\")"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "7b595528",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:45.806910Z",
"iopub.status.busy": "2026-09-28T08:22:45.806826Z",
"iopub.status.idle": "2026-09-28T08:22:46.448997Z",
"shell.execute_reply": "2026-09-28T08:22:46.448709Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAB4IAAAHKCAYAAADmRIbLAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOzdd3hT1eMG8Dfde5cCbWmZBQptaVmyoWxFhoBsUHCADEUFNyAqCMIPUXDgV5YKCojIkiEgIBsFZK8W2lI66J5pkvP7o+SSNEmbtmmblvfzPDyaO8495+bevL333CETQggQEREREREREREREREREVGNYVHVFSAiIiIiIiIiIiIiIiIiItNiRzARERERERERERERERERUQ3DjmAiIiIiIiIiIiIiIiIiohqGHcFERERERERERERERERERDUMO4KJiIiIiIiIiIiIiIiIiGoYdgQTEREREREREREREREREdUw7AgmIiIiIiIiIiIiIiIiIqph2BFMRERERERERERERERERFTDsCOYiIiIiIiIiIiIiIiIiKiGYUcwEREREREREREREREREVENw45gIiIiIiIiIiIiIiIiIqIahh3BREREREREREREREREREQ1DDuCiYiIiIgeY4cOHYJMJsOaNWsqpPwJEyZAJpNVSNlVITU1FV5eXli4cGFVV8XsrVmzBjKZDIcOHTJq+oreFtWOHTsGCwsLHD9+vEKXYwo1bf8xJDk5GePGjUPdunUhk8nQrVs3AJW3TRSntHUw1+3enHEdEBERERFVHHYEExERERFRlVN3BBT3Lzo6WmuexMREvPXWW2jZsiVcXFzg7OyMBg0aYPDgwfjf//5ndNk//PCD0fWcN28erK2tMX36dJ1xv/76K9q3bw9HR0e4u7tjwIABuHDhglHlJicn4/nnn0doaCg8PT1hZ2eHBg0aYOTIkTh//rzR9aOSdejQAX379sWrr74KIURVV8ekcnJysHjxYrRr1w5ubm7SdjRp0iRcuXJF7zxF9w9LS0u4uroiKCgIw4cPx08//QS5XF7ifDKZDA4ODmjZsiU+/PBD5ObmGl3v119/HT///DNefvllrF+/Hu+++67BaaOjozF37lycO3fO6PJNzRzqUNlUKhXat28PmUyGnj17VnV1iIiIiIjISFZVXQEiIiIiIiK1oUOHYuDAgXrHeXt7S/9/9+5d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 truncated
"text/plain": [
"<Figure size 1936x462 with 4 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"colours = {f'{POLY}, flattened': '#c0392b', f'{MONO}, flattened': '#1f618d', 'eta': '#7d6608',\n",
" 'cluster CNN <=1ph': '#7f8c8d', 'cluster CNN <=2ph': '#8e44ad', 'cluster CNN <=3ph': '#4a235a'}\n",
"done = [(f, l) for f, l in FLUX if f'{POLY}, flattened' in results[f]]\n",
"fig, axes = plt.subplots(1, max(len(done), 1), figsize=(4.4 * max(len(done), 1), 4.2), squeeze=False)\n",
"for ax, (filters, label) in zip(axes[0], done):\n",
" for name in (f'{MONO}, flattened', f'{POLY}, flattened'):\n",
" if name not in results[filters]:\n",
" continue\n",
" r = results[filters][name]\n",
" centre = r['fit']['beta'][1]\n",
" ax.errorbar(r['x'] - centre, r['z'], yerr=r['z_err'], fmt='.', ms=3, lw=.6,\n",
" color=colours[name], label=f\"{name}: $\\\\sigma$ = {r['sigma']:.3f} $\\\\pm$ {r['sigma_err']:.3f} um\")\n",
" grid = np.linspace(-N_SIGMA * r['sigma'], N_SIGMA * r['sigma'], 300)\n",
" ax.plot(grid, r['fit']['model'](grid + centre), color=colours[name], lw=1.2)\n",
" ax.set_xlim(-8, 8); ax.set_ylim(-0.1, 1.4)\n",
" ax.set_title(f'{label} ({occupancy[filters]*100:.2f} %)', fontsize=10)\n",
" ax.set_xlabel('distance from the fitted edge [um]'); ax.set_ylabel('knife / flat')\n",
" ax.grid(alpha=.3); ax.legend(fontsize=7, loc='upper left')\n",
"fig.suptitle(f'presampled ESF ({BIN_WIDTH_UM} um bins) and ODR fit within $\\\\pm${N_SIGMA:g} sigma')\n",
"fig.tight_layout()"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "5d493d1f",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:46.450231Z",
"iopub.status.busy": "2026-09-28T08:22:46.450132Z",
"iopub.status.idle": "2026-09-28T08:22:46.793813Z",
"shell.execute_reply": "2026-09-28T08:22:46.793491Z"
}
},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1430x506 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, axes = plt.subplots(1, 2, figsize=(13, 4.6))\n",
"order = [f'{POLY}, flattened', f'{MONO}, flattened', 'eta', 'cluster CNN <=1ph', 'cluster CNN <=3ph']\n",
"for name in order:\n",
" points = [(occupancy[f] * 100, results[f][name]) for f, _ in FLUX if name in results[f]]\n",
" if not points:\n",
" continue\n",
" occ = np.array([p[0] for p in points])\n",
" style = dict(color=colours[name], marker='o', ls='-' if 'GridBased' in name else ':', lw=1.2, ms=5)\n",
" axes[0].errorbar(occ, [p[1]['sigma'] for p in points], yerr=[p[1]['sigma_err'] for p in points],\n",
" label=name, capsize=2, **style)\n",
" axes[1].plot(occ, [p[1]['mtf50'] for p in points], label=name, **style)\n",
"for ax, ylabel in zip(axes, ('LSF sigma [um]', 'MTF50 of the fitted Gaussian [lp/mm]')):\n",
" ax.set_xscale('log'); ax.set_xlabel('occupancy, flat field in the ESF ROI [%]')\n",
" ax.set_ylabel(ylabel); ax.grid(alpha=.3, which='both')\n",
"axes[0].legend(fontsize=8)\n",
"fig.suptitle('edge width against flux')\n",
"fig.tight_layout()"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "ea39bb23",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:46.795017Z",
"iopub.status.busy": "2026-09-28T08:22:46.794926Z",
"iopub.status.idle": "2026-09-28T08:22:47.276187Z",
"shell.execute_reply": "2026-09-28T08:22:47.275894Z"
}
},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1936x429 with 4 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"frequency = np.linspace(0, 400, 800)\n",
"fig, axes = plt.subplots(1, max(len(done), 1), figsize=(4.4 * max(len(done), 1), 3.9), squeeze=False)\n",
"for ax, (filters, label) in zip(axes[0], done):\n",
" for name in (f'{MONO}, flattened', f'{POLY}, flattened'):\n",
" if name in results[filters]:\n",
" sigma_mm = results[filters][name]['sigma'] / 1000\n",
" ax.plot(frequency, np.exp(-2 * np.pi**2 * sigma_mm**2 * frequency**2), color=colours[name],\n",
" label=f\"{name}: {results[filters][name]['mtf50']:.0f} lp/mm\")\n",
" ax.axvline(200, color='k', ls=':', lw=.8)\n",
" ax.axhline(0.5, color='grey', lw=.5)\n",
" ax.set_title(f'{label} ({occupancy[filters]*100:.2f} %)', fontsize=10)\n",
" ax.set_xlabel('spatial frequency [lp/mm]'); ax.set_ylabel('MTF (Gaussian from the fit)')\n",
" ax.set_ylim(0, 1.02); ax.grid(alpha=.3); ax.legend(fontsize=7)\n",
"fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "5d9c96d1",
"metadata": {},
"source": [
"### Relative DQE at 200 lp/mm\n",
"\n",
"Both models see the same beam at each flux point, so the ratio of the photons\n",
"they detect is the ratio of their quantum efficiencies, and DQE ∝ QE · MTF² is\n",
"comparable without the absolute flux. MTF(200) here is the one measured from the\n",
"LSF."
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "e261c1be",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:47.277410Z",
"iopub.status.busy": "2026-09-28T08:22:47.277320Z",
"iopub.status.idle": "2026-09-28T08:22:47.380917Z",
"shell.execute_reply": "2026-09-28T08:22:47.380567Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"flux point occupancy photons 2-20 / 12 keV MTF(200) 12 keV MTF(200) 2-20 DQE(200) 2-20 / 12 keV\n",
"3 filters 0.75% 1.0266 0.252 0.227 0.837\n",
"2 filters 1.81% 1.0242 0.259 0.254 0.982\n",
"1 filter 4.45% 1.0279 0.251 0.227 0.846\n",
"0 filters 10.77% 1.0311 0.222 0.173 0.625\n"
]
}
],
"source": [
"print(f\"{'flux point':10s} {'occupancy':>9s} {'photons 2-20 / 12 keV':>22s} {'MTF(200) 12 keV':>15s} \"\n",
" f\"{'MTF(200) 2-20':>13s} {'DQE(200) 2-20 / 12 keV':>23s}\")\n",
"relative_dqe = {}\n",
"for filters, label in FLUX:\n",
" r = results[filters]\n",
" a, b = r.get(f'{POLY}, flattened'), r.get(f'{MONO}, flattened')\n",
" if a is None or b is None:\n",
" continue\n",
" qe = a['photons'] / b['photons']\n",
" relative_dqe[filters] = qe * (a['mtf200_lsf'] / b['mtf200_lsf'])**2\n",
" print(f\"{label:10s} {occupancy[filters]*100:8.2f}% {qe:22.4f} {b['mtf200_lsf']:15.3f} \"\n",
" f\"{a['mtf200_lsf']:13.3f} {relative_dqe[filters]:23.3f}\")"
]
},
{
"cell_type": "markdown",
"id": "9de8f3c7",
"metadata": {},
"source": [
"## 3. What the energy head reports at 12 keV\n",
"\n",
"The polychromatic model also measures every photon's energy. On a\n",
"monochromatic beam the spectrum shows where it puts the 12 keV line, how wide it\n",
"is, and how much ends up off the line: charge lost at the halo / edges below, and\n",
"pile-up (two photons in one cell) above — the model cannot report more than one\n",
"photon per cell or much beyond its 20 keV range."
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "70897b20",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:47.382133Z",
"iopub.status.busy": "2026-09-28T08:22:47.382048Z",
"iopub.status.idle": "2026-09-28T08:22:48.050311Z",
"shell.execute_reply": "2026-09-28T08:22:48.050022Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"run line [keV] core sigma [keV] < 10.5 keV 10.5-13.5 > 13.5 keV\n",
"Edge_3filters_pos0 12.045 0.408 4.32% 94.84% 0.84%\n",
"Flat_3filters_pos0 12.055 0.407 4.01% 95.15% 0.84%\n",
"Edge_2filters 12.010 0.422 4.91% 93.08% 2.01%\n",
"Flat_2filters 12.006 0.422 4.85% 93.11% 2.04%\n",
"Edge_1filters 11.910 0.458 7.44% 87.38% 5.19%\n",
"Flat_1filters 11.891 0.459 7.44% 87.34% 5.21%\n",
"Edge_0filters 11.642 0.572 13.41% 72.68% 13.90%\n",
"Flat_0filters 11.637 0.574 13.48% 72.46% 14.05%\n"
]
},
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABYsAAAHZCAYAAADHf2T1AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOzdd3gU1dfA8e/upvcOCSGVHkLvvQmIhfqTXgRUFAFFQFERFBsvVlRQUaoUBURQERQJCNKr9B5IqOm97e68f2BWQhIIkM3OkvN5njzK1DN7cmdOZmfu1SiKoiCEEEIIIYQQQgghhBCiXNNaOgAhhBBCCCGEEEIIIYQQlic3i4UQQgghhBBCCCGEEELIzWIhhBBCCCGEEEIIIYQQcrNYCCGEEEIIIYQQQgghBHKzWAghhBBCCCGEEEIIIQRys1gIIYQQQgghhBBCCCEEcrNYCCGEEEIIIYQQQgghBHKzWAghhBBCCCGEEEIIIQRys1gIIYQQQgghhBBCCCEEcrNYCCGEEEIIIYQQQgghBHKzWIgHwoIFC9BoNGzevLlUt7t582Y0Gg0LFiwoMD0zM5OxY8cSFBSETqcjJCSkVPdbGtq1a6fKuMxp2rRpaDQaoqOjS7T8g/4ZDRs2DI1GY+kwzO5u8y6EEEKUZ/Hx8QwZMoSAgAA0Gg3t2rUDICQkxPT/Zen8+fP06NEDX19fNBoNw4YNAyjw//fibusgSx3/nUh9W5DUt0KIsiA3i4UQd23GjBl89tln9O3blwULFvDJJ59YOiSrlJubyzfffEOXLl2oUKECdnZ2uLm5UbduXUaPHs3OnTvLPKZ27dqh0WgK/Hh4eBAZGclbb71FcnJymcdkboqisGTJEgYOHEj16tVxdnbG39+f9u3bs3r1ahRFKfG2tmzZwpgxY6hbty4eHh54enrSsGFDPvroI7Kysopc58KFCwwYMABfX18cHR2pW7cuc+fOLa3Du2tPPPEEGo2GHTt23Ha5Xr16odFoOHDgQBlFJoQQQty/l156ie+//55Ro0axePFiXnvttVLZ7ubNm5k2bdpd10rDhg1jy5YtvPzyyyxevJhnnnmmVOKxFKlv1UHq24KkvhXi7thYOgAhhHq1adOGrKwsbG1tC0z/448/iIyMZObMmRaKzPpdvHiRxx9/nEOHDtGyZUvGjBlDQEAAWVlZnDhxgl9++YXZs2ezadMm2rdvX6Jtvv7667zyyivY29vfV2xarZaFCxcCNwrNxMREfv75Z6ZOncratWvZvXs3Wu2D811jTk4OgwYNolatWvTo0YNq1aoRHx/Pt99+S69evXj22WeZPXt2ibb18ssvEx0dTY8ePXj22WfJy8tjzZo1vPTSSyxZsoS///4bBwcH0/KxsbE0a9aMlJQUXnjhBUJDQ/n55595+umnuXjxItOnTzfXYRfrqaeeYsWKFXz77bc0b968yGWuX7/OL7/8QoMGDahfv34ZRyiEEELcuz/++IMuXbrwxhtvlOp2N2/ezJtvvsmwYcPw8PAo0To5OTls3bqV559/ngkTJhSYl5WVhU6nK9UYzU3qW/WQ+rYgqW+FuEuKEMLqzZ8/XwGUqKioMtlfaGio0rZt2zLZ171q27atEhwcbOkwipSdna3Url1b0el0yrJly4pcxmAwKPPnz1d27Nhxx+2lp6ffUxxFfUZt27ZVdDpdoWWNRqPSqFEjBVAOHDhwT/sra0OHDlVKcpnLy8tTNm7cWGh6enq6UrVqVQVQjh49WqJ9btq0ScnNzS00vX///gqgfPHFFwWmDx48WAGUVatWFZjes2dPRafTKadOnbrjPqdOnaoAyvnz50sU450YjUYlJCREcXFxUdLS0opc5v/+7/8UQJk9e3ap7FMIIYQoKxqNRhk6dGih6cHBwfdV397L9fjChQsKoEydOvWe91ucktZB+e73+KW+LRtS394bqW+FuDsPzldnQohC5syZg42NDU888QQ5OTnAf/2RnTp1iu7du+Pu7o6LiwuPPPIIZ8+eLbD+rX0W5/cddf78ebZs2WJ6jWvatGmmdQ4cOECfPn3w8/PDzs6OsLAwXnnlFTIzM0sc99mzZxk2bBgBAQHY2dkRGBjIc889R3x8fKFlz5w5Q8+ePXF3d8fV1ZXOnTtz6NChYre9ePFi6tSpg729PYGBgUyaNInjx48XOg648dTB3LlzadKkCc7Ozjg7O9OiRQt++umnEh9LUb799luOHDnChAkT6NevX5HLaLVahg0bRrNmzUzTbs7HV199RZ06dXBwcGD06NFA8X173e1nVBSNRkOFChUAsLOzM01PS0tjypQpNGvWDF9fX+zs7AgJCWHs2LFFvtK3ZMkSmjdvjpeXF46OjgQHB9O7d29OnDhRYLlr164xZswYQkJCsLOzo0KFCgwaNKjIfsuuXbvG0KFD8fb2xtnZmZYtWxIVFVXiY7OxsaFjx46Fpjs7O/Poo48C8M8//5RoW+3bty/0JD5gyvPN28nMzGTlypWEhobSq1evAsuPHz8eg8HAkiVLSnwcN1MUhddeew2NRsMLL7yA0WgESva5ajQaRowYQXp6Oj/88EOR2583bx5OTk4MGDDgnuITQgghylp+X6+KorBw4UJTHXvr2Bw3+/333+nfvz/h4eE4Ojri5uZG27Zt+fXXXwss165dO958800AQkNDi6yRb9WuXTuCg4MBePPNN03r5I9BUlyfxVFRUTz88MN4enpib29PzZo1mTFjBgaDoUSfw759++jUqRPOzs54enrSu3fvUukXVupbqW9B6lshHhTSDYUQDyBFUZg8eTIzZszgxRdf5MMPPywwEMKlS5do06YNjz/+ODNmzOD06dN89tlnPP744xw+fLjYV7B69epFlSpVePHFF/Hx8TH18VanTh0A1q9fT48ePahcuTJjxoyhQoUKHDp0iI8++oi///6bqKgobGxuf9o5ePAg7dq1w8nJieHDhxMcHMzp06eZM2cOf/75J7t378bd3R2AmJgYWrRoQXJyMqNGjaJmzZrs2LGDdu3a4e3tXWjbX3zxBc8//zw1atTgzTffxNbWlmXLlhU7MOCTTz7JokWL6N69OwMHDgTgxx9/pGfPnsyZM4dRo0bdPhHFyC9Qnn766Xta/9NPP+XatWs8/fTTVKpUCVdX12KXvdvPKF/+jXnl39f01q5dy/r16+nYsSO1atUyLXfp0iW+/vprevXqRd++fXFwcGD37t3MmTOHbdu2sWvXLlNxuWTJEgYNGkTLli2ZOnUqLi4uXLp0iU2bNnH69Glq1KhRIOb09HRGjBhBtWrVuHTpEnPmzOH3339n7969BAUFATeK+datW3PmzBmGDRtG48aNOXr0KI8++ijh4eH39Pne7NKlSwD4+fmV+nYOHz5MVlZWka/CNW3aFJ1Ox+7du+96X7m5uYwYMYKlS5fy0Ucf8eKLLwJ397k++eSTTJs2jW+//Zbhw4cX2P62bds4ceIEQ4cONbVFIYQQQu2eeeYZOnXqxODBg2ndurWpDmvRokWx6yxYsIBr164xaNAgAgMDiYuLY+HChTz66KOsWLGCPn36APDaa6/h5eXF6tWr+fjjj/Hx8QH+q5GL8tprr9GjRw9efPFFevbsabqxVrNmzWLXmTdvHiNHjqR+/fq88soreHh48PfffzN58mQOHDjA8uXLb/sZ7N+/nzZt2qDT6Rg7diyBgYH8/vvvtGvXjoyMjNuueydS30p9C1LfCvHAsOBTzUKIUnJzNxTZ2dlKv379FK1Wq3z66aeFlg0ODlYAZenSpQWmv/feewqgbNiwwTQtKipKAZT58+cX2satr6llZWUpFStWVJo0aaJkZ2cXmLdixQoFUBYsWHDHY6lXr54SGhqqJCQkFJi+c+dORavVKtOmTTNNy3/FafXq1UUey82voCUlJSnOzs5KWFiYkpqaapqenZ2tNG7cuNArgD/99JMCKB999FGhGB999FHFzc2twHbuhre3t+Lm5lZoutFoVOLi4gr83PyaVH4+PD09lWvXrhVav6jXte7mM1KUG6/pAUX+PPnkk4Vym5OTU+RraXPnzlUAZcWKFaZpPXv2VFxdXYtc/mY9evRQPD09lbNnzxaYfu7cOcXFxUUZNmyYadqUKVMUQPn4448LLLts2TJT3Pdq3759io2NjVKlSpU7xnw7qampSkhIiGJra6ucPHnSNH3lypUKoEyaNKnI9fz9/ZXIyMg7bv/mvCcnJyvt27dXHBwcCnz2inJ3n6ui3Pg9B5Rjx44VmD5s2DAFULZu3XrH2IQQQgi1AUrcDUVRXSFkZGQo1apVUyIiIgpMv5fX5s+fP19sNxS3xnnlyhXFwcFB6dGjh2I0GgssO3PmTAVQNm/ebJpWVHcFrVu3VrRarbJv374C05955hkFuK9uKKS+lfpWUaS+FeJBId1QCPEASUpKonPnzvz000+sWLGCsWPHFrlcQEAA/fv3LzDtoYceAuDUqVP3tO+NGzdy9epVhg0bRlpaGvHx8aaftm3b4uzszIYNG267jSNHjnDw4EH69euH0WgssI3w8HCqVq1q2obRaOSnn34yDdpwsxdeeAEXF5cC037//XcyMjJ47rnnCjypYG9vb/pm+maLFy/G0dGRvn37FogjPj6enj17kpqaesfRdIuTkpKCm5tboekJCQn4+voW+Hn++ecLLTdkyJASPQlwt59RPq1Wyx9//GH6+f777xk9ejTfffcdgwYNKvCao52dnenJCr1eT3JyMvHx8abX3m4e8drDw4PMzEx+/vnnYkdgTklJYe3atXTr1g03N7cCn7urqyvNmjUr8Hu0atUqPDw8eO655wpsp1+/flStWvWOn1Fxrly5Qs+ePdHpdHz33XdFvnpXEnq9nr59+xIdHc3//d//Ua1aNdO8/K5ZihuwxcHB4a66b7l48SItW7bk0KFDbNy40fS0E9z95wo3BgKBG08x5UtLS2PFihXUqFGDVq1alTgLine truncated
"text/plain": [
"<Figure size 1430x484 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"def gauss(x, a, mu, sig):\n",
" return a * np.exp(-0.5 * ((x - mu) / sig)**2)\n",
"\n",
"\n",
"def line_fit(centres, counts, guess=12.0, half_window=0.8):\n",
" # Gaussian core of the line; weighted mean / rms of the window if the fit fails\n",
" near = np.abs(centres - guess) < 2\n",
" peak = centres[near][np.argmax(counts[near])]\n",
" window = np.abs(centres - peak) < half_window\n",
" x, y = centres[window], counts[window]\n",
" try:\n",
" pars, _ = curve_fit(gauss, x, y, p0=(y.max(), peak, 0.3), maxfev=20000)\n",
" return pars[1], abs(pars[2])\n",
" except Exception:\n",
" mean = np.average(x, weights=y)\n",
" return mean, np.sqrt(np.average((x - mean)**2, weights=y))\n",
"\n",
"\n",
"fig, axes = plt.subplots(1, 2, figsize=(13, 4.4))\n",
"print(f\"{'run':30s} {'line [keV]':>11s} {'core sigma [keV]':>16s} {'< 10.5 keV':>10s} \"\n",
" f\"{'10.5-13.5':>9s} {'> 13.5 keV':>10s}\")\n",
"spectra = {}\n",
"for filters, label in FLUX:\n",
" for kind, ax in (('Edge', axes[0]), ('Flat', axes[1])):\n",
" path = run_dir(f'{kind}_{filters}_12keV', POLY_TAG) / 'energy_spectrum.npy'\n",
" if not path.exists():\n",
" continue\n",
" centres, counts = np.load(path)\n",
" total = counts.sum()\n",
" mu, sig = line_fit(centres, counts)\n",
" low, mid, high = (counts[centres < 10.5].sum() / total, counts[(centres >= 10.5) & (centres <= 13.5)].sum() / total,\n",
" counts[centres > 13.5].sum() / total)\n",
" spectra[f'{kind}_{filters}'] = dict(line_keV=mu, core_sigma_keV=sig, below=low, line=mid, above=high)\n",
" print(f\"{kind + '_' + filters:30s} {mu:11.3f} {sig:16.3f} {low*100:9.2f}% {mid*100:8.2f}% {high*100:9.2f}%\")\n",
" ax.semilogy(centres, counts / total / np.diff(centres)[0], lw=1,\n",
" label=f'{label} ({occupancy[filters]*100:.2f} %)')\n",
"for ax, kind in zip(axes, ('knife edge', 'flat field')):\n",
" ax.set_xlim(0, 30); ax.set_ylim(1e-6, 5)\n",
" ax.set_xlabel('reconstructed photon energy [keV]'); ax.set_ylabel('photons / keV (normalised)')\n",
" ax.set_title(f'{kind}, {POLY}'); ax.grid(alpha=.3, which='both'); ax.legend(fontsize=8)\n",
"fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "1c638bc8",
"metadata": {},
"source": [
"## 4. Fake photons on dark runs (1000 frames each, pedestal of another run)"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "50b9539e",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:48.051536Z",
"iopub.status.busy": "2026-09-28T08:22:48.051448Z",
"iopub.status.idle": "2026-09-28T08:22:48.232125Z",
"shell.execute_reply": "2026-09-28T08:22:48.231785Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"run model frames photons per frame\n",
"Pedestal_1filters_12keV GridBased 2-20 keV 1000 106 0.1060\n",
"Pedestal_1filters_12keV GridBased 12 keV 1000 0 0.0000\n",
"Pedestal_2filters_12keV GridBased 2-20 keV 1000 105 0.1050\n",
"Pedestal_3filters_pos0_12keV GridBased 2-20 keV 1000 73 0.0730\n"
]
}
],
"source": [
"print(f\"{'run':30s} {'model':20s} {'frames':>7s} {'photons':>8s} {'per frame':>10s}\")\n",
"dark = {}\n",
"for run in ('Pedestal_1filters_12keV', 'Pedestal_2filters_12keV', 'Pedestal_3filters_pos0_12keV'):\n",
" for model, tag in ((POLY, POLY_TAG), (MONO, ''), (MONO, 'darktest')):\n",
" s = summary_of(run, tag)\n",
" if s is None:\n",
" continue\n",
" dark[f'{run}_{tag}'] = s['n_photons']\n",
" print(f\"{run:30s} {model:20s} {s['n_frames']:7d} {s['n_photons']:8d} {s['n_photons']/s['n_frames']:10.4f}\")"
]
},
{
"cell_type": "markdown",
"id": "eec26108",
"metadata": {},
"source": [
"## 5. The model on Monte Carlo\n",
"\n",
"Fixed-occupancy evaluation of the new checkpoint (`scripts/04_evaluate.py`,\n",
"threshold 0.3) and its energy response (`scripts/07_energy_response.py`,\n",
"occupancy ≤ 2.5 %), both on MOENCH040 2–20 keV frames the network never saw."
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "4271e8af",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:48.233350Z",
"iopub.status.busy": "2026-09-28T08:22:48.233268Z",
"iopub.status.idle": "2026-09-28T08:22:48.349056Z",
"shell.execute_reply": "2026-09-28T08:22:48.348722Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"occupancy efficiency purity RMS [um] sigma68 [um] E sigma68 [keV]\n",
" 0.5% 99.54% 98.44% 1.171 0.775 0.348\n",
" 1% 99.25% 98.02% 1.257 0.808 0.364\n",
" 2% 98.62% 97.53% 1.400 0.858 0.391\n",
" 5% 96.65% 95.85% 1.859 1.041 0.524\n",
" 10% 93.61% 93.09% 2.650 1.461 0.925\n",
"\n",
"energy response (epoch 182):\n",
"E_true [keV] line - E [meV] core sigma [keV] efficiency\n",
" 2.375 70 0.202 95.58%\n",
" 3.875 8 0.304 97.77%\n",
" 5.875 -5 0.319 98.39%\n",
" 7.875 -7 0.339 98.64%\n",
" 9.875 1 0.381 99.41%\n",
" 11.875 -5 0.391 99.52%\n",
" 14.875 -13 0.388 99.77%\n",
" 17.875 -4 0.386 99.64%\n",
" 19.375 -85 0.317 99.77%\n"
]
}
],
"source": [
"eval_path = POLY_EXPERIMENT / 'eval_summary.json'\n",
"if eval_path.exists():\n",
" ev = json.load(open(eval_path))\n",
" print(f\"{'occupancy':>9s} {'efficiency':>10s} {'purity':>8s} {'RMS [um]':>9s} {'sigma68 [um]':>12s} {'E sigma68 [keV]':>15s}\")\n",
" for key, value in ev.items():\n",
" m = value['summary']\n",
" print(f\"{key:>9s} {m['efficiency']*100:9.2f}% {m['purity']*100:7.2f}% {m['rms_1d_um']:9.3f} \"\n",
" f\"{m['sigma68_1d_um']:12.3f} {m['energy_sigma68_keV']:15.3f}\")\n",
"else:\n",
" print('no eval_summary.json in', POLY_EXPERIMENT)\n",
"response_path = POLY_EXPERIMENT / 'energy_response.npz'\n",
"if response_path.exists():\n",
" r = np.load(response_path)\n",
" print(f\"\\nenergy response (epoch {int(r['epoch'])}):\")\n",
" print(f\"{'E_true [keV]':>12s} {'line - E [meV]':>14s} {'core sigma [keV]':>16s} {'efficiency':>10s}\")\n",
" for energy in (2.5, 4.0, 6.0, 8.0, 10.0, 12.0, 15.0, 18.0, 19.5):\n",
" i = int(np.argmin(np.abs(r['table_centre'] - energy)))\n",
" print(f\"{r['table_centre'][i]:12.3f} {(r['table_core_mu'][i]-r['table_centre'][i])*1000:14.0f} \"\n",
" f\"{r['table_core_sigma'][i]:16.3f} {r['table_efficiency'][i]*100:9.2f}%\")"
]
},
{
"cell_type": "markdown",
"id": "0c49b3fa",
"metadata": {},
"source": [
"## 6. Summary"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "89072280",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T08:22:48.350375Z",
"iopub.status.busy": "2026-09-28T08:22:48.350271Z",
"iopub.status.idle": "2026-09-28T08:22:48.475704Z",
"shell.execute_reply": "2026-09-28T08:22:48.475399Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"flux point occupancy sigma, 12 keV model [um] sigma, 2-20 keV model [um] MTF50 12 keV / 2-20 keV\n",
"3 filters 0.75% 1.360 +- 0.019 1.420 +- 0.021 138 / 132 lp/mm\n",
"2 filters 1.81% 1.346 +- 0.019 1.391 +- 0.018 139 / 135 lp/mm\n",
"1 filter 4.45% 1.388 +- 0.020 1.551 +- 0.028 135 / 121 lp/mm\n",
"0 filters 10.77% 1.473 +- 0.017 1.856 +- 0.037 127 / 101 lp/mm\n",
"\n",
"written to /home/xie_x1/MLXID/GridBased/Results/overnight_poly2to20keV_Moench040/report/knife_edge_poly2to20keV_summary.json\n"
]
}
],
"source": [
"summary = dict(\n",
" poly_experiment=POLY_EXPERIMENT.name, poly_tag=POLY_TAG, mono_experiment=MONO_EXPERIMENT.name,\n",
" roi_superpixel=list(ROI), bin_um=BIN_UM, esf_bin_width_um=BIN_WIDTH_UM, odr_window_sigma=N_SIGMA,\n",
" convention='sigma = ODR p2 (Gaussian LSF); MTF50 = 1000 sqrt(ln 2 / 2) / (pi sigma)',\n",
" occupancy_flat_esf_roi=occupancy,\n",
" occupancy_flat_esf_roi_poly={f: flat_occupancy(f, POLY_TAG) for f, _ in FLUX},\n",
" results={f: {name: {k: float(r[k]) for k in ('sigma', 'sigma_err', 'chi2_ndf', 'mtf50',\n",
" 'mtf50_lsf', 'mtf200_lsf', 'photons', 'rows_kept')}\n",
" for name, r in results[f].items()} for f, _ in FLUX},\n",
" relative_dqe_200lpmm=relative_dqe, spectra=spectra, dark_photons=dark, missing=missing)\n",
"out = OVERNIGHT / 'report' / 'knife_edge_poly2to20keV_summary.json'\n",
"out.parent.mkdir(parents=True, exist_ok=True)\n",
"json.dump(summary, open(out, 'w'), indent=1)\n",
"\n",
"print(f\"{'flux point':10s} {'occupancy':>9s} {'sigma, 12 keV model [um]':>24s} {'sigma, 2-20 keV model [um]':>26s} \"\n",
" f\"{'MTF50 12 keV / 2-20 keV':>24s}\")\n",
"for filters, label in FLUX:\n",
" a, b = results[filters].get(f'{POLY}, flattened'), results[filters].get(f'{MONO}, flattened')\n",
" if a is None or b is None:\n",
" continue\n",
" print(f\"{label:10s} {occupancy[filters]*100:8.2f}% {b['sigma']:15.3f} +- {b['sigma_err']:.3f} \"\n",
" f\"{a['sigma']:17.3f} +- {a['sigma_err']:.3f} {b['mtf50']:12.0f} / {a['mtf50']:.0f} lp/mm\")\n",
"print(f'\\nwritten to {out}')"
]
}
],
"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
}
@@ -0,0 +1,661 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "86bf6c9c",
"metadata": {},
"source": [
"# Siemens star, 15 keV, MOENCH040 — polychromatic 2–20 keV GridBased model\n",
"\n",
"SOLEIL 2504 beamtime, MOENCH040 @150 V, 15 keV, 20 M frames of the full chip\n",
"(2000 files). The two regions of `DataProcess/Configs.py` are reconstructed:\n",
"\n",
"| region | x [px] | y [px] |\n",
"|---|---|---|\n",
"| `SiemenStarLowerLeft` | 130–240 | 110–220 |\n",
"| `SiemenStarLowerRight` | 235–345 (the notebooks use 234–344) | 110–220 |\n",
"\n",
"Both share the same rows, so one crop of the frame, x 126–349 and y 106–224\n",
"(the regions plus the network's halo R = 4), is read and reconstructed in one\n",
"pass; its valid region is x 130–345, y 110–220. Reconstruction: the 2–20 keV\n",
"MOENCH040 model, FP16 with an FP32 head, masked column common mode (median over the pixels without charge) over the\n",
"118 rows of the crop, self-flattening of the sub-pixel phase with one Rosenblatt table per\n",
"local-occupancy class (tables from the first 3·10⁸ photons, every photon\n",
"remapped; one table for the whole crop left a 25 µm grid in the dim star of\n",
"LowerLeft, whose occupancy is ~8x below the rest). The run is processed as four parts of 500\n",
"files; this notebook sums the parts it finds.\n",
"\n",
"For comparison, the published cluster-based reconstructions of the same regions\n",
"(`SiementStar_LowerLeft/Right.ipynb`): eta interpolation (single-photon clusters)\n",
"and the 1/2/3-photon CNNs summed (`≤3ph`), all at 2.5 µm."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "5e4befe5",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T10:33:40.244153Z",
"iopub.status.busy": "2026-09-28T10:33:40.244075Z",
"iopub.status.idle": "2026-09-28T10:33:40.777351Z",
"shell.execute_reply": "2026-09-28T10:33:40.777052Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"parts found: 4 of 4 ['SiemensStar_15keV_part0', 'SiemensStar_15keV_part1', 'SiemensStar_15keV_part2', 'SiemensStar_15keV_part3']\n",
"model 260927_poly2to20keV_Moench040_c96_00 (epoch 182), 20,000,000 frames, 1.6259e+10 photons (813.0 per frame in the 23650 px valid region)\n",
" SiemensStar_15keV_part0 files [0, 499, 1] 5,000,000 frames occupancy 3.29 % (illuminated 5.58 %) 2434 frame/s sub-pixel rms 17.2 -> 1.2 %\n",
" SiemensStar_15keV_part1 files [500, 999, 1] 5,000,000 frames occupancy 3.45 % (illuminated 5.88 %) 1970 frame/s sub-pixel rms 17.3 -> 1.1 %\n",
" SiemensStar_15keV_part2 files [1000, 1499, 1] 5,000,000 frames occupancy 3.50 % (illuminated 5.96 %) 2325 frame/s sub-pixel rms 17.3 -> 1.1 %\n",
" SiemensStar_15keV_part3 files [1500, 1999, 1] 5,000,000 frames occupancy 3.51 % (illuminated 5.97 %) 2391 frame/s sub-pixel rms 17.3 -> 1.1 %\n"
]
}
],
"source": [
"import json\n",
"import os\n",
"from pathlib import Path\n",
"\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"from scipy.optimize import curve_fit\n",
"\n",
"GRID = Path('/mnt/sls_det_storage/moench_data/MLXID/InferenceResults/GridBased')\n",
"PUBLISHED = Path('/home/xie_x1/MLXID/DeepLearning/InferenceResults')\n",
"ETA = Path('/home/xie_x1/MLXID/EtaInterpolation/Results')\n",
"OVERNIGHT = Path('/home/xie_x1/MLXID/GridBased/Results/overnight_poly2to20keV_Moench040')\n",
"POLY_TAG = os.environ.get('GRID_POLY_TAG', 'poly2to20')\n",
"RUNS = [r.strip() for r in os.environ.get(\n",
" 'GRID_STAR_RUNS', ','.join(f'SiemensStar_15keV_part{i}' for i in range(4))).split(',')]\n",
"CNN = {1: '1ph_260616_15keV_v260511_00', 2: '2ph_260622_15keV_v260610_00',\n",
" 3: '3ph_260623_15keV_v260611_00'}\n",
"B = 10 # 2.5 um bins per 25 um pixel\n",
"# region -> (x0, x1, y0, y1) in chip pixels: the whole region, the zoom of the\n",
"# published notebooks, and the 50 x 50 px FRC box (SiementStar_LowerLeft.ipynb's\n",
"# [1200:2100, 1400:2300][200:-200, 220:-180]; the same box around the right star)\n",
"REGIONS = {'LowerLeft': dict(roi=(130, 240, 110, 220), zoom=(140, 190, 170, 210), frc=(162, 212, 140, 190)),\n",
" 'LowerRight': dict(roi=(234, 344, 110, 220), zoom=(284, 324, 160, 200), frc=(263, 313, 140, 190))}\n",
"\n",
"plt.rcParams.update({'figure.dpi': 110, 'image.origin': 'lower', 'image.interpolation': 'nearest'})\n",
"\n",
"\n",
"def run_dir(run):\n",
" return GRID / (run + (f'_{POLY_TAG}' if POLY_TAG else ''))\n",
"\n",
"\n",
"parts = [r for r in RUNS if (run_dir(r) / 'binned_image_flattened.npy').exists()]\n",
"print(f'parts found: {len(parts)} of {len(RUNS)} {parts}')\n",
"summaries = [json.load(open(run_dir(r) / 'summary.json')) for r in parts]\n",
"frames = np.array([s['n_frames'] for s in summaries], float)\n",
"flattened = sum(np.load(run_dir(r) / 'binned_image_flattened.npy').astype(float) for r in parts)\n",
"raw = sum(np.load(run_dir(r) / 'binned_image.npy').astype(float) for r in parts)\n",
"analog = sum(np.load(run_dir(r) / 'analog_image.npy') * n for r, n in zip(parts, frames)) / frames.sum()\n",
"spectrum_centres = np.load(run_dir(parts[0]) / 'energy_spectrum.npy')[0]\n",
"spectrum = sum(np.load(run_dir(r) / 'energy_spectrum.npy')[1] for r in parts)\n",
"crop_x0, _, crop_y0, _ = summaries[0]['crop']\n",
"print(f\"model {Path(summaries[0]['checkpoint']).parent.parent.name} (epoch {summaries[0]['epoch']}), \"\n",
" f\"{frames.sum():,.0f} frames, {sum(s['n_photons'] for s in summaries):.4e} photons \"\n",
" f\"({sum(s['n_photons'] for s in summaries)/frames.sum():.1f} per frame in the \"\n",
" f\"{summaries[0]['valid_pixels']} px valid region)\")\n",
"for s in summaries:\n",
" print(f\" {s['run']:28s} files {s['signal_files']} {s['n_frames']:>10,d} frames \"\n",
" f\"occupancy {s['occupancy_valid_region']*100:.2f} % (illuminated {s['occupancy_exposed']*100:.2f} %) \"\n",
" f\"{s['frames_per_second']:.0f} frame/s sub-pixel rms {s['subpixel_nonuniformity_before']*100:.1f} -> \"\n",
" f\"{s['subpixel_nonuniformity_after']*100:.1f} %\")"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "54a0702c",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T10:33:40.778887Z",
"iopub.status.busy": "2026-09-28T10:33:40.778584Z",
"iopub.status.idle": "2026-09-28T10:33:40.970833Z",
"shell.execute_reply": "2026-09-28T10:33:40.970515Z"
}
},
"outputs": [],
"source": [
"def cut_grid(image, box, factor=B):\n",
" # chip-pixel box -> the GridBased crop's binned image\n",
" x0, x1, y0, y1 = box\n",
" return image[(y0 - crop_y0) * factor:(y1 - crop_y0) * factor,\n",
" (x0 - crop_x0) * factor:(x1 - crop_x0) * factor]\n",
"\n",
"\n",
"def cut_chip(image, box, factor=B):\n",
" # chip-pixel box -> a full-chip super-frame (4000 x 4000 at 2.5 um)\n",
" x0, x1, y0, y1 = box\n",
" return image[y0 * factor:y1 * factor, x0 * factor:x1 * factor]\n",
"\n",
"\n",
"def published(region):\n",
" eta = np.load(ETA / f'SiemenStar{region}_fromClusters.npy', mmap_mode='r')\n",
" single = cnn = None\n",
" for n, experiment in CNN.items():\n",
" path = (PUBLISHED / f'SiemenStar{region}' / experiment / 'augX1'\n",
" / f'{n}Photon_ML_superFrame_etaInterpolated.npy')\n",
" if path.exists():\n",
" image = np.asarray(np.load(path, mmap_mode='r'), float)\n",
" single = image if n == 1 else single\n",
" cnn = image if cnn is None else cnn + image\n",
" return eta, single, cnn\n",
"\n",
"\n",
"images = {}\n",
"for region, boxes in REGIONS.items():\n",
" eta, single, cnn = published(region)\n",
" images[region] = {}\n",
" for key, box in boxes.items():\n",
" images[region][key] = {\n",
" 'native readout (25 um)': cut_grid(analog, box, factor=1),\n",
" 'eta interpolation': np.asarray(cut_chip(eta, box), float),\n",
" 'cluster CNN 1ph': cut_chip(single, box) if single is not None else None,\n",
" 'cluster CNN <=3ph': cut_chip(cnn, box) if cnn is not None else None,\n",
" 'GridBased 2-20 keV': cut_grid(flattened, box),\n",
" }\n",
" images[region]['roi_raw_grid'] = cut_grid(raw, boxes['roi'])"
]
},
{
"cell_type": "markdown",
"id": "43f9cd97",
"metadata": {},
"source": [
"## 1. The two regions"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "29d6aaff",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T10:33:40.972383Z",
"iopub.status.busy": "2026-09-28T10:33:40.972301Z",
"iopub.status.idle": "2026-09-28T10:33:41.986287Z",
"shell.execute_reply": "2026-09-28T10:33:41.985991Z"
}
},
"outputs": [
{
"data": {
"image/png": "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C+U2T548WS7T3usBAD788EOcfPLJ0Ov1SEtLw1VXXYWamhooFIpm1SFbeg8AXn31VZx88skwmUwwGo3o378/Xn/99WbL0fVXXl6Oyy67DA6HA3q9HgMHDsS3337brv1vDbPZLIV1B4MQAvX19S0em0NBCIF77rkHCoUCN998M2KxGID2PXMVCgWuuOIKNDQ04M0332zx91944QUYDAZcfPHFh2V7GYZhGIZhGIZhGIZhCK5cxzAMwzAMwzAMwzDMYaVjx44AgEWLFuHhhx8+pMpfJSUlGDBgABoaGnDFFVegS5cu2L9/P/7zn//g448/xjfffIO8vLyE79x1110IBoP45z//CZVKhQULFmDMmDF46623cMUVV+CCCy7AqFGjsHHjRrz00ktISUnBwoULf9M67777btTX12PSpEkwmUx4+eWXceuttyIzMxMXXnghgCax4bBhw+ByuTB58mRkZmaiqqoK3333HTZu3IhRo0a1eSwuuOACrF27FldffTV69+6NYDCIXbt2Yc2aNQAAl8uFxYsX45ZbboHT6cQ999wjv+tyuQAAy5Ytw5YtWzBu3Djk5+fD4/HgzTffxMSJE1FVVYXbb7+92XovueQSaDQaXH/99TAajejatetBnMGWiUajGDFiBNauXYuRI0fi+uuvR3l5Of7zn//g1FNPxQcffIDBgwcDaKpWtXr1avzwww+yzeOaNWugVCrR0NCAL7/8Eqeddpp8H0gUdL7wwgu48sor0bt3b9x5552wWq3YsGED7rrrLnz//ffNRE4NDQ047bTTcNJJJ2HmzJnwer0tVshqjUgkghEjRmD9+vW46KKLcO2118Ln8+GVV17BkCFDsHz5cowcORKnn346Fi9ejAceeABut1uKMXNzc1FSUoLLLrsMp512Gq6++upDPs5arRannXYa1q5di1gsBqVSmXD8Pv/8cwSDQaSkpMj3LRYL+vbtm/A7CxcuxP79+3HllVfC5XLhnXfewezZs2E2m3HnnXfK5T788EOMHj0aFosFkydPRnp6OlatWoVp06bhiy++wMqVK+U2/JZj1x4WLFiAiooKXH311cjOzobZbAZwcNfDe++9h3PPPReZmZm48847YbPZ8O677+Kss85q3wkAMG3aNNx///3o1asX7r33XgDAK6+8gosuugi7d+/G3XffnbB8Y2MjTjvtNPTt2xf3338/Kioq8Nhjj+Gss87C7t275X78XlitVtTX10OtVmPAgAGYNm1aq4LpAxEKhXDFFVfgtddew6OPPopbbrkFwME9cydNmoQZM2bg+eefx+WXX57w+//973/x888/Y8KECbBYLL9txxmGYRiGYRiGYRiGYZIRDMMwDMMwDMMwDMMwh5GioiJhtVoFAJGeni4uuOAC8cgjj4gvv/xSxGKxZstPmDBBJLsoxo4dK2w2m9i1a1fC+7t37xYmk0lMnDhRvrdo0SIBQPTq1UsEAgH5/vfffy8ACIVCId54442E3xkzZozQaDTC6/Ue1nU2NDQIh8Mh/v73v8v3FixYIACIL7/8ss3j1hJ1dXVCoVCIa6+99oDL5ufni4EDB7b4WUNDQ7P3otGoOP3004XVahWhUEi+P336dAFAnH766SIcDrd7W/Pz80XHjh3bXOb5558XAMQ///nPhPd/+eUXkZKSIjp37iyi0agQQohNmzYJAGLOnDlyuYEDB4qRI0cKm80mpk2bJt8/55xzhM1mk98tKysTOp1OjB07ttk1N3fuXAFArFu3LuF3AYi77767xe0GIIYOHdrmvs2fP18AEO+8807C+6FQSPTu3VsUFhYmvD9w4ECRn5/f4romTJjQ5rraA+3n119/LYQQwu/3C51OJy677DIBQHz66adCCCEikYiwWCxizJgx8rtr164VAERGRoaoqamR70ejUdGtWzeRmZkp34tEIqKgoEAYDAaxe/fuhG2YNGmSACAWL14s36N7Z+3atfK9gz12LUHbbLPZREVFRcJnB3M9RCIRkZeXJ1JTU0VpaWnCvo8ZM6bF85P83vbt24VSqRR9+vQRPp9Pvt/Q0CCOP/54oVKpRFFRkXyfrr/Zs2cn/O6SJUsEALFw4cID7n97OdC1fMcdd4gbb7xRLF68WCxfvlzMmjVLOJ1OoVAoxKuvvtquddAzpKioSNTV1YnBgwcLnU4nli5dmrDcwTxzhRBi5MiRAoDYunVrwvsTJ04UAMTnn3/eru1jGIZhGIZhGIZhGIY5GLgtLMMwDMMwDMMwDMMwh5WCggL873//w4033giDwYA333wTt912G/r3748uXbrIKmOt4fF48N5772HEiBFITU2F2+2Wf2azGf3798dHH33U7HvXX3+9rMQFACeeeCJSU1ORmZmJCy64IGHZgQMHIhwOy9aDh2udRqMRf//732VrXKCpAhQALF++HH6//4DHLx69Xo+UlBR89dVX2L1790F9Nx6j0Sj/7/f7UV1djZqaGpx55pmoq6vDL7/80uw7t9xyyyFVHWyLt99+G0BTVa94unTpgosvvhg7duzAjz/+CADo06cPbDabvF58Ph82btyIM844A4MGDZLvRyILine truncated
"text/plain": [
"<Figure size 2530x506 with 5 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAACc8AAAIPCAYAAACvu7RHAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOydd5hT1fb3v+l10iaZzhQGGECKVBGlgwVBULEXsF97uWKXol7FdkW92AtiAQtiAawIiAqiCNLr9J4pSSYz6TnvH7xr/5LMwAwIYlmf55kHcnJyzj51r73Wd60tkyRJAsMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMP8g5Af6wYwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwDMMwzB8Ni+cYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmGYfxwsnmMYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmH+cbB4jmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhvnHweI5hmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEY5h8Hi+cYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmGYfxwsnmMYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmH+cbB4jmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhvnHweI5hmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEY5h8Hi+cYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmGYfxwsnmMYhmEYhmEYhmEY5qgxbdo0yGSyY90MhjlizJ8/HzKZDKtWrTrsbYwcORK5ublHrE1/BEfiuA/ErFmzIJPJUFxcfMS3fSwoLi6GTCbDrFmzjnVT/jRwX8AwDMMwDMMwDMMwzJ8VFs8xDMMwDMMwDMMwDHNI1NbW4u6770bv3r1hMpmQlJSEzp0746yzzsJrr712rJv3t2Tu3LmYP3/+Mdt/bm4uunTpcsz2fzSRyWRxfxqNBnl5ebj66qtRXl5+rJsHl8uFWbNmHRXRGvPPZNeuXZg+fTrGjh2L5ORkyGQyXHXVVQdcf+TIka2eE/pbunTpH9jy/2Pw4MFQKpXtPqP9+/eHWq1GbW3tH9QyhmEYhmEYhmEYhmH+aiiPdQMYhmEYhmEYhmEYhvnrUFpaisGDB6Ourg5TpkzB1VdfDbVajcLCQnz99deYO3currzySrH+K6+8ghdffPEYtvjvwdy5c5Gbm4tp06Yd66b8LTnuuONw9913AwA8Hg++++47vPbaa1i+fDk2b96M5ORkse6ll16KCy64AGq1+g9pm8vlwuzZswHsFzExf35ycnLg8/mgVP45Xa9r167Fk08+iby8PAwaNAhffvllu7+x2+14+umnWy3v16/f0Whiu1x99dW45pprMH/+fNx///1trrNx40Zs3LgRZ599NlJSUv7gFjIMwzAMwzAMwzAM81fhz+nBYRiGYRiGYRiGYRjmT8mTTz6JmpoazJ07F7fcckur7xOrAKlUKqhUqj+qecxhEI1GEQgEoNPpjnVT/jACgQAUCoUQN6WlpeGSSy4R319//fVIS0vDM888g/nz5+Pf//63+E6hUEChUPzhbWaODJIkwefzQa/XH7V9yGQyaLXao7b938vEiRPR0NAAq9WK4uJi5OXltfsbg8EQ94wcay688ELcfvvteP3113Hfffe1OSXsq6++CgAHrarHMAzDMAzDMAzDMAzD07YyDMMwDMMwDMMwDNNhdu/eDQAYM2ZMm99nZWXFfZ42bVqbooaamhrcdNNNyM3NhVqtRmpqKi655BIUFxfHrTd//nzIZDKsWLECjzzyCDp37gytVou+ffvi888/BwBs374dEyZMgNlshsViwbRp0+D1en/3PleuXIm5c+eiW7du0Gg06Ny5c5uVl9atW4eJEyciIyMDGo0G6enpGD16ND777LMDnkeisbER06dPR9euXaHT6WC1WtG7d28h1iouLoZMJkNJSQlWr14dN10itXv9+vW44oorUFBQAIPBAIPBgMGDB+PNN99stb9Zs2ZBJpNh+/btuPPOO5GTkwO1Wo333nuv3bZ2BL/fj9mzZ6N79+7QarWw2WyYOHEifvnll7j1xo0bB4fDAUmSxLIff/wRMpkMSUlJCIVCYvnmzZshk8nw+OOPx21j5cqVOP3002G1WqHRaNCjRw889thjiEQiceuNHDkSubm5KCkpwQUXXAC73Q6tVtvudI9jx44FAOzZsyduOd0fidOoVlVV4dJLL0VycjIMBgOGDRuG77777oDPAABUV1eL3+h0OowYMQIbNmyI2xcJm2bPni2ufW5u7kHb3hYVFRWQyWRxQkAAuOaaayCTyXDttdfGLb/99tshk8lQU1MTt1ySpA49FwDwxRdfYNSoUTCZTNDpdDj++OMxb968uOt+MJqamnDfffehoKAAGo0GNpsNkydPxubNmzv0e7pW33zzDR599FHR5th7afHixRgxYoRoY79+/YToKpG33noLffr0gUajQVZWFu68807s2LEDMpkMs2bNEuvRcxu7DNgvVH322WfRt29f6HQ6mEwmjB49Gl9//XWrfeXm5mLkyJHYvXs3Jk2aBLPZDKPRiDPOOAP79u3r0PEfiOTkZFit1kP+XTQahcfjQTQa/V37J0KhEKZNmwa5XI4nn3xSLN+3bx+mTZuGjIwMqNVqZGVl4frrr0ddXZ1Yx2g04vzzz0dRURFWrlzZatt+vx/vvvsusrOzceqppx6R9jIMwzAMwzAMwzAM8/eEK88xDMMwDMMwDMMwDNNh8vPzAQBvvPEGHnvsscOalrCsrAxDhw6F1+vFlVdeiW7duqGiogIvvPACvvrqK/zyyy/Izs6O+80999yDQCCA6667DgqFAs888wwmTZqEDz/8EFdeeSXOO+88TJw4EWvXrsWbb74JjUaDl1566Xft895774XH48Hll18Oo9GIBQsW4Pbbb0d6ejouuOACAPvFhGPHjoXD4cANN9yA9PR0OJ1O/Prrr1i7di0mTpx40HNx3nnnYeXKlbjmmmvQr18/BAIB7Nu3DytWrAAAOBwOvPXWW7jttttgt9tx3333id86HA4AwJIlS7B161ZMmTIFOTk5cLvdeP/99zFt2jQ4nU7ccccdrfZ78cUXQ6VS4cYbb4TBYEBBQcEhXMG2iUQiGD9+PFauXIkJEybgxhtvRHV1NV544QWcfPLJ+PzzzzFq1CgA+4Vp33zzDTZt2iSmfVyxYgXkcjm8Xi/WrVuHYcOGieVAvGDz9ddfx1VXXYV+/frh7rvvhsViwQ8//IB77rkHGzduxKJFi+La5vV6MWzYMAwePBizZ89GU1MTjEbjQY9n7969ABA3ZeuBcLvdGDZsGAoLC3HFFVdgwIAB2LFjB8aPH48uXbq0+Zvm5mYMGzYMAwYMwEMPPYSamho8/fTTOO2001BYWIikpCQMHz4cTz/9NG677TacddZZOPvsswGg3ba3RWZmJrp3745vvvkmbjmd97aW9+rVC6mpqXHLO/JcAMBrr72Gq6++Grm5uZg+fTqMRiM+/PBD3Hjjjfjtt9/w8ssvH7S9Ho8HJ598Mvbu3YupU6eib9++aGxsxCuvvIITTzwRa9asQf/+/Tt07NOnT4fP58PUqVPhcDjQqVMnAPvFpLNnz8aoUaMwc+ZM6HQ6fPnll7j66quxd+9ezJkzR2xj3rx5uPHGG9G9e3fMnj0bKpUKCxcubCWiPBjTpk3DW2+9hZNPPhmPPvoompqa8Oqrr+LUU0/FggULWlV2q6iowPDhw3HmmWfisccew549e/Dcc8/hzDPPxJYtWyCX/3F50RUVFTAajfD5fNBqtRg9ejQeeuihDl+DRDweD8455xysWbMGixYtwnnnnQcA2LRpE0aOHAm9Xo8rrrgCOTk52LNnD1544QWsWLEC69evh9lsBrB/6tbXXnsNr732GkaPHh23/Q8//BAulwu33HLLH3qeGIZhGIZhGIZhGIb5CyIxDMMwDMMwDMMwDMN0kKKiIslisUgApNTUVOm8886TnnrqKWndunVSNBpttf7UqVOlRPfD5MmTJavVKu3bty9ueWFhoWQ0GqVp06aJZW+88YYEQOrTp4/k9/vF8o0bN0oAJJlMJr333ntx25k0aZKkUqmkpqamI7pPr9crJScnSyeeeKJY9swzz0gApHXr1h30vLWFy+WSZDKZ9K9//avddXNycqQRI0a0+Z3X6221LBKJSMOHD5csFosUDAbF8pkzZ0oApOHDh0uhUKjDbc3JyZHy8/MPus5rr70mAZCuu+66uOW7du2SNBqN1LVrVykSiUiSJEk///yzBEB6/PHHxXojRoyQJkyYIFmtVmnGjBli+RlnnCFZrVbx26qqKkmr1UqTJ09udc898cQTEgBp1apVcdsFIN17771ttpvOh9PplJxOp1RYWCjNnz9fslgsklKplLZs2RK3Pt0fK1euFMvuvfdeCYA0b968uHUXL14sAWj1DFCbHnnkkbjlCxculABIL730klhWVFQkAZBmzpzZZvsPhRtuuEGSyWRSTU1N3LYvvfRSCYBUWFgoSZIk1dbWSjKZTLrllltaHXdHnguXyyUZjUYpMzNTqq+vF8tDoZA0btw4CYC0Zs0asZzuy6KiIrHs1ltvlVQqVatnq7GxUcrKypJGjhzZ7vFSm7t27drqOdm4caMkk8mkm2++uc3zJJfLxfuisbFRMhgMUufOnSWPxyPW8/v90qBBg1pdn7au2YoVKyQA0oQJE6RwOCyW19bWSikpKZLFYol7Z+Xk5EgApHfffTeubY8++qgEQPryyy/bPf6OQG298sorD7jO1KlTpXvuuUdauHChtHjxYumee+6RDAaDpNFo4p61gxHbF5SVlUm9e/eWrFar9N1338Wtd/zxx0t5eXlx940kSdK6deskuVwuzZo1K2557969Ja1WKzULine truncated
"text/plain": [
"<Figure size 2530x506 with 5 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"def limits(img):\n",
" # grey levels from the central 60 % of the image: in LowerLeft the unshadowed\n",
" # strip at x > 222 is several times brighter than the star (and brighter\n",
" # still for GridBased, which does not lose its photons to pile-up)\n",
" ny, nx = img.shape\n",
" centre = img[int(.2 * ny):int(.8 * ny), int(.2 * nx):int(.8 * nx)]\n",
" centre = centre[np.isfinite(centre)]\n",
" return np.percentile(centre, 0.5), np.percentile(centre, 99.5)\n",
"\n",
"\n",
"def show(region, key, title):\n",
" box = REGIONS[region][key]\n",
" panels = [(name, img) for name, img in images[region][key].items() if img is not None]\n",
" fig, axes = plt.subplots(1, len(panels), figsize=(4.6 * len(panels), 4.6))\n",
" for ax, (name, img) in zip(axes, panels):\n",
" low, high = limits(img)\n",
" ax.imshow(img, cmap='gray', extent=[box[0], box[1], box[2], box[3]], vmin=low, vmax=high)\n",
" ax.set_title(name, fontsize=10)\n",
" ax.set_xlabel('x [pixel, 25 um]'); ax.set_ylabel('y [pixel, 25 um]')\n",
" fig.suptitle(f'Siemens star {region}, {title}, 15 keV')\n",
" fig.tight_layout()\n",
"\n",
"\n",
"for region in REGIONS:\n",
" show(region, 'roi', 'whole region')"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "b4daba25",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T10:33:41.989993Z",
"iopub.status.busy": "2026-09-28T10:33:41.989887Z",
"iopub.status.idle": "2026-09-28T10:33:42.917327Z",
"shell.execute_reply": "2026-09-28T10:33:42.917050Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAACdcAAAHtCAYAAADxptVPAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOzdd3RU1doG8Gf6TCbJlPReaAKCgoJcEAMIFgRBRUVRARV7RbFeBdRrL9i7CMrFgh0RRQRERcVesCA1hCSkJ5NMkpnM/v7ge/fNpAcpis9vLdYiM2fOnJk55+z27ncblFIKRERERERERERERERERERERERERKQZ9/UBEBEREREREREREREREREREREREf3VMLiOiIiIiIiIiIiIiIiIiIiIiIiIqAkG1xERERERERERERERERERERERERE1weA6IiIiIiIiIiIiIiIiIiIiIiIioiYYXEdERERERERERERERERERERERETUBIPriIiIiIiIiIiIiIiIiIiIiIiIiJpgcB0RERERERERERERERERERERERFREwyuIyIiIiIiIiIiIiIiIiIiIiIiImqCwXVERERERERERERERERERERERERETTC4joiIiIiIiIiIiIiIiIiIiIiIiKgJBtcRERERERERNTJlyhQYDIZ9fRhEe1UoFMKsWbOQnZ0Ns9nMa2APq6mpwWWXXYb09HSYTCZkZmbu8r6GDRv2p17/V2MwGDBlypQObbty5UoYDAY8//zz+rHNmzfDYDBg1qxZnX7vfXH//zPH+1f0V/o8nTmX9gctXQ9ERERERERE9OcxuI6IiIiIiIj2ezt27MB1112HPn36IDo6GlFRUcjOzsYJJ5yAZ599dl8f3n5pzpw5+3SAPzMzE127dt1n778nGQwGjBw5crfuc968eZg9ezaGDx+OZ599Fi+88AIAYNasWXjzzTd363sRcNddd+Hhhx/Gqaeeiueffx5z5sxpc/t9fT3R/m/lypWYNWsWysvL9/Wh7DfuvPNOnHrqqejWrRuMRiPMZnOr2z7//PMwGAwt/pswYcJePOr/eeyxx2AwGHDHHXe0ud1DDz0Eg8GABx54YC8dGREREREREdHe1XqLnoiIiIiIiGg/sHXrVgwcOBDFxcWYMGECpk2bBqvVio0bN2LZsmWYM2cOzjnnHL39008/jSeeeGIfHvH+Yc6c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 truncated
"text/plain": [
"<Figure size 2530x506 with 5 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "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odPpMG7cOFgsFjRs2PAs3mD1eL1e9O7dG5s3b0bfvn0xbtw4pKWl4a233sL111+PdevWoWvXrgAqA9M2btyI3377DS1atAAAbNq0CWq1GkVFRdi5cyc6duwojwP+AZvvvfce7r77brRo0QJPPPEEQkJCsH37dvzvf//Dr7/+WiUopaioCB07dkSbNm0wffp0FBYWIigo6IzPc+jQIQDw27K1JvLz89GxY0ckJyfjzjvvRKtWrbB//3707t0b9erVq/Y3xcXF6NixI1q1aoVnnnkG6enpeOWVV3DTTTchOTkZVqsVnTp1wiuvvILx48dj4MCBGDRoEADUWvaaWLp0aZVje/fuxYwZMxARESGPffXVV+jfvz9sNhseeOABhIeHY+3atZgyZQq+//57rFmzBmp15brQkydPonXr1sjIyMCYMWPQrFkz/P7775g/fz6++uor/PjjjwgPD/e755tvvonMzEyMGTMGQUFBePfddzFy5EjodDr873//Q8eOHfHss8/izz//xBtvvIGRI0di/fr1tT7fb7/9hk6dOsHr9WLs2LFITEzEd999h9mzZ2Pjxo34/vvvYTabce+996JHjx64/fbb0bFjR9xzzz0AgPbt21d73UaNGmHp0qVVzj+dkydPolOnTujfvz9efPFFHDx4EK+99hr69++P3bt3yzrzeDzo3bs3tm7diqFDh+K+++5DSUkJ3n//fXTr1g0rV65E3759a31eAHj99ddx4sQJ3HfffXA4HFixYgWmTp2K5OTkGoMBA2X16tV4+eWXcf/99+Ouu+7Cxo0bMWfOHPzxxx/YsGGDfJ5Aef755/F///d/6NOnD+6++27o9XocO3YMX331FdLS0vzaSSD9gxg9ejSWLFmCAQMGYPjw4QCAzz//HAMHDsRbb72F++67T577xBNP4MUXX8S1116LF154AaWlpXjvvfewatWqs66fkSNHQqVS4aGHHoJarcabb76J4cOHIzExEe3atZPnTZkyBc888wyaNWuGp556CgDw/vvvY+jQoUhOTsbkyZMBVG7P/fLLL2Pbtm1+fZXaZUFBAa6//nocOnQII0eORPPmzZGbm4v58+fjuuuuw7Zt29CyZUu/MgbaPgLtO0BlwFeHDh2wb98+DB8+HO3bt8fhw4fx5ptv4quvvsL27dvPuMXp8uXLMWLECLRr1w4rVqxASEgIPB4PevbsiePHj2Ps2LG46qqrUFhYiL/++gvffPMNHn/88bN+P/+E5s2bo6CgAGq1Gi1atMCkSZNw6623ntO1hBD43//+hxdffBHjx4/H7NmzoVKpzup9Dh48GA899BAWLVqEadOm+fW9Y8eOYcOGDejcuTPq169/Xp6fYRiGYRiGYRiGYf4Line truncated
"text/plain": [
"<Figure size 2530x506 with 5 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"for region in REGIONS:\n",
" show(region, 'zoom', 'zoom of the published notebooks')"
]
},
{
"cell_type": "markdown",
"id": "cba943fe",
"metadata": {},
"source": [
"### GridBased 2–20 keV at full size\n",
"\n",
"The flattened reconstruction of each region at 2.5 µm, and the same photons\n",
"before the sub-pixel flattening (the 25 µm pixel grid is visible there)."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "76f70e13",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T10:33:42.920209Z",
"iopub.status.busy": "2026-09-28T10:33:42.920105Z",
"iopub.status.idle": "2026-09-28T10:33:43.673514Z",
"shell.execute_reply": "2026-09-28T10:33:43.673248Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABmYAAAMwCAYAAAA6Vd9WAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOz9eZzkdXXvj79q6aruquqq7qqu3nu6Z3p2hmUQBAURDBE30BijJsGAu17jEqMQQ77BGK8oUcyNSYg3XtGLRDHJ1XhdkhhFIEGv7OPADAOz9/TeXdVVvVR3LZ/fH/17nvlUTw8MyO779XjMA7q6uurzeX/ey3md8zrnBDzP8+Tg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODwlCP4TF+Ag4ODg4ODg4ODg4ODg4ODg4ODg4ODg4PDrwpcYMbBwcHBwcHBwcHBwcHBwcHBwcHBwcHBweFpggvMODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4PE1wgRkHBwcHBwcHBwcHBwcHBwcHBwcHBwcHB4enCS4w4+Dg4ODg4ODg4ODg4ODg4ODg4ODg4ODg8DTBBWYcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHByeJrjAjIODg4ODg4ODg4ODg4ODg4ODg4ODg4ODw9MEF5hxcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHB4muACMw4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODk8TXGDGwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHhaYILzDg4ODzl+MlPfqJAIKCf/OQnj/nej3/84woEAk/9RT1NGBgY0OWXX/5MX8aTCp7nN77xjWf6UhyeIG6++WZt3bpVkUjkSV9vP/7xj/WCF7xATU1NCgQCOnDgwPNuXT9RPB/3AwcHBwcHh6cCjj9c/kxfxpMKxx+e+3iq+MPll1+uxsbGJ+3zwGqc5NmAWq2mq666Sv39/QoGgzr//PMlPT/W/eWXX66BgYFn+jIcHJ5TcIEZBweH42J6elpXX321tm/frmQyqWg0qjVr1uiNb3yj/uVf/kWe5z0t1zEwMKBAIGD/GhsbtX79en3wgx/U+Pj403INTyXm5+d1/fXX6xWveIV6enoUjUY1ODioj33sY5qfnz+hz5ientbnPvc5nX/++ero6FBjY6O2bt2qv/iLv1ClUjnm/Z7n6X/8j/+hjRs3KhqNauPGjfqrv/qrp+2ZSlK5XFZbW5vOOuusR33fC1/4QnV0dKx6H4+Gr3zlKwoEAvrZz372y1zmswKBQEDvec97npTP2rdvny699FJ1d3fri1/8om688UZJ0ic/+Ul9+9vf/qU+u1Ao6A1veIM8z9MXvvAF3Xjjjcpms7/UZ373u9/Vxz/+8VV/92Rcs4ODg4ODg8OTB8cfnh44/uD4w2PhucIfngo8FZzkycJNN92kT33qU3rVq16lr371q7rqqqt+6c883nPYuXOnPv7xjz9rglIODg7HIvxMX4CDg8OzE/fee69e/epXa2pqSr/1W7+lt7/97YrFYhoaGtK//du/6XWve53+9m//Vu9973sf87POO+88LSwsKBKJPOHrOeWUU/TRj35UklQqlbRjxw793d/9nX70ox/p/vvvVygUesKf/Uxj3759et/73qeXvvSlev/7369MJqOf/vSnuvbaa/Vf//Vf+slPfqJg8NHj6HfccYf+6I/+SBdddJGuvPJKxWIx/fCHP9QVV1yh+++/X1/72tfq3v+JT3xCH//4x/WWt7xFV1xxhW699VZ98IMfVD6f15/+6Z8+lbdraGho0Bvf+EZdf/31euSRR7R+/fpj3vPwww/rzjvv1Ac+8AGFw+7IejJw++23q1Kp6LOf/axOO+00e/2Tn/yk3vzmN+t1r3vdE/7se++9V7lcTl/+8pd/qc/x47vf/a6++MUvrhqceTKu2cHBwcHBweHJgeMPTx8cf3D84enEU8kfngo8FZzkycKPf/xjtbS06G//9m+ftMyj4z2HnTt36s/+7M90/vnnPy2ZLH//93+vWq32lH+Pg8PzCe6UcnBwOAYzMzO65JJLVKvVdPfdd2vbtm11v//TP/1T3Xrrrcrlco/6OfPz84rFYgoGg790enJXV5cuvfTSutdaW1v1iU98Qjt27ND27dt/qc9/JtHZ2an7779fJ598sr32zne+Uxs3btTHPvYxfe9739PFF1/8qJ9x0kkn6eGHH64zuN7znvfoPe95j774xS/qyiuvtM8fHR3VNddco7e97W36X//rf0mS3vGOdygUCulTn/qU3vWud6mzs/PJv9FVcOmll+r666/XTTfdpKuvvvqY39900032vl81VKtVVavVX8ohsRrGxsYkSS0tLU/q5z7Vn+3g4ODg4ODw7IXjD08vHH9w/GE1PBf5w1OBp+J62Zt+WYyNjSmVSj2vyi+ChoaGZ/oSHByec3ClzBwcHI7BF7/4RQ0NDelzn/vcMaQKvPSlL61TZJD2fcstt+hDH/qQOjs7FY/HJR2/RvT3vvc9nXrqqWpsbNTGjRvNyD9RtLe3S1KdEmp6elof/ehHdcopp6i5uVmJREIXXHCB/uu//uuYv//mN7+pM888U8lkUqlUSieffLI++clP1r2nWCzqiiuu0Nq1a60Uwx/90R9pcXGx7n1LS0u68sor7b4vvPBC7d69+4Tuo62trY5Ugd/8zd+UJD3wwAOP+Rlr165dVQWz2mf8y7/8ixYXF/X+97+/7r3vf//7tbi4qH/5l385oev2o1Kp6M1vfrOampr0/e9/X9JyuYPrr79ep556qpqampTJZPS7v/u7Ghoasr978YtfrMHBQSNQK/EP//AP2rhxo84888zHfU0nip07d+qSSy5RS0uLYrGYXvziF+vf/u3f7Pee56mtrU3/7b/9t7q/6+vrUyAQ0L59++y1m2++WYFAQDt37rTXxsbG9O53v9vKTGzYsEHXXnttXdmHAwcOKBAI6NOf/rSuv/56bdiwQdFoVHfccccJ38eJjPfAwICuvPJKSctzJhAI6PLLL1cgENDi4qK++tWvWskP6h2fKM4//3y96U1vkiRdcMEFj/kZ3/nOd3TxxRert7dX0WhU/f39uvLKK+vW1uWXX64vfvGLklRXjoTxerRrPtG1S5mHH/zgB9q+fbsaGxu1YcMGff3rXz/mmp+O/cDBwcHBweG5CMcfjsLxhxOD4w/L+FXmD34cPHhQr371q5VIJNTe3q4//MM/1NLS0jHv+8d//EedffbZisfjSqVSuvjii/Xggw/a7x+Lk/znf/6nXvaylymRSKi5uVkvf/nLdeedd9Z9x6PtTZJ0zz336OKLL1Zra6uampr0whe+UN/97ncf9f54Xj/4wQ908OBBG7OvfOUrq75/aWlJV199tc4880z7nrPOOkvf+c536t53vOfwla98Rb/9279dNw4rv+9E7oOxuP322/XHf/zH6urqUlNTk17+8pdr//79de9drcfM4+FaDzzwgC688ELFYjF1dnbqiiuu0L//+7+fcL8xB4fnIlzGjIODwzH4zne+o8bGRv3Wb/3W4/7b97///WptbdVVV12lfD5/3Pf9+Mc/1mtf+1qtX79ef/7nf65SqWQH/Wool8uanJyUJC0uLur+++/Xtddeq+3bt+ukk06y9+3bt0//+I//qDe84Q1av369ZmZm9L/+1//Sr/3ar+muu+4yovgf//EfevOb36yXvexluuaaaxQKhfTQQw/p9ttvt88qlUq64IILtHfvXr3rXe/S4OCg7rvvPn3uc5/T7t276+q4vvvd79ZXvvIVveENb9AFF1ygO++8U7/+67+uUqn0uMcQDA8PS1omXk/mZ9x9992KRqM65ZRT6t67fft2RSIR3XPPPY/rOxYXF/XGN75RP/rRj/S9731PL3vZyyQtz4W/+7u/01ve8ha9973v1ejoqL7whS/ojjvu0L333msKpt/93d/VJz7xCd155511BOrnP/+5Hn74YX3iE594Ird+QtizZ4/OOeccRaNR/cEf/IESiYRuuOEGvepVr9K3vvUtXXLJJQoEAjr33HN166232t/t379fQ0NDCgaDuvXWW7Vu3TpJ0m233aZ0Om1zcnJyUmeffbYWFxf1rne9S11dXbr99tt15ZVXanh4WH/5l39Zdz1f+9rXNDc3p3e9611KJBLHXQ+r4UTG+y//8i/1jW98QzfffLM+//nPq62tTYODg7rwwgv1tre9TWeffbbe9a53SZI6Ojoe11heddVV2rZtm/7mb/5Gf/zHf6wtW7Y86md8+ctfVigU0u///u+rtbVVP/vZz/S5z31OQ0NDRrTf/e536/Dhw/rxj39stawlKZvN6sYbbzzuNT+etStJP/vZz/Stb31L733ve/X2t79dX/rSl3TppZdq+/bt2rx58+P+zKdiP3BwcHBwcHg2w/GHZTzT9oLjD44/PJf4A6jVanrFK16h0047TZ/5zGf0n//5n7ruuus0PT2tG264wd732c9+Vh/96Ef1+te/Xm95y1s0Ozurv/3bv9U555yje+65R2vXrn1UTnLbbbfp13/919Xb26s/+ZM/Ua1W09/93d/ppS99qW699dZjgnmr7U233367Xv7yl2vbtm266qqrFI1GdfPNN+uSSy7RP/3TP+n1r3/9qvcIf/mLv/gLDQ8P6/Of/7yk5UDjaigUCrr++uv1pje9SW9961tVKpX0D//wD3rd616nH/zgB7rooosk6bicaHBwUO973/vqxsH/fY/3Pv7gD/5AjY2N+tjHPqbJyUl99rOf1e/+7u+eUCDwRLjWyMiIXvrSl6pSqegjH/mI0um0vva1r+mHP/zhY36+g8NzGp6Dg4PDCqTTae/UU0895vXZ2VlvYmLC/uXzefvdDTfc4EnyzjrrLK9cLtf93S233OJJ8m655RZ77fTTT/fa2tq8qakpe+3BBx/0QqGQt3Jr6u/v9yQd8++MM87whoeH695bKpWLine truncated
"text/plain": [
"<Figure size 1650x814 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1650x814 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"for region, boxes in REGIONS.items():\n",
" box = boxes['roi']\n",
" fig, axes = plt.subplots(1, 2, figsize=(15, 7.4))\n",
" for ax, img, name in ((axes[0], images[region]['roi']['GridBased 2-20 keV'], 'flattened'),\n",
" (axes[1], images[region]['roi_raw_grid'], 'before flattening')):\n",
" low, high = limits(img)\n",
" ax.imshow(img, cmap='gray', extent=[box[0], box[1], box[2], box[3]], vmin=low, vmax=high)\n",
" ax.set_title(f'GridBased 2-20 keV, {region}, {name}', fontsize=11)\n",
" ax.set_xlabel('x [pixel, 25 um]'); ax.set_ylabel('y [pixel, 25 um]')\n",
" fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "318433ac",
"metadata": {},
"source": [
"## 2. Profiles and FRC\n",
"\n",
"A row through the zoom (normalised to its mean), and the one-image Fourier ring\n",
"correlation as `SiementStar_LowerLeft.ipynb` computes it (`frc.one_frc` and\n",
"`frc.frc_res` on the 50 × 50 px box x 162–212, y 140–190, no window; for\n",
"LowerRight the same box around its star). The cutoffs are in that notebook's\n",
"units, so they compare with its ML 2.041 / eta 2.053 for LowerLeft."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "6162c863",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T10:33:43.677997Z",
"iopub.status.busy": "2026-09-28T10:33:43.677903Z",
"iopub.status.idle": "2026-09-28T10:33:43.883956Z",
"shell.execute_reply": "2026-09-28T10:33:43.883755Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABfYAAAHCCAYAAACt2ZiFAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOzdd3wU5dbA8d/M7iab3ulVpakgWADLpdsB21VREOEFUbBduRYsKIpiBdu1NxABr2AB7ICA5aqAilJEEAgtlPS+beZ5/9hCQtoGEnYTztfPfkxmZ555ZmY3zJw5cx5NKaUQQgghhBBCCCGEEEIIIUSDoIe6A0IIIYQQQgghhBBCCCGECJ4E9oUQQgghhBBCCCGEEEKIBkQC+0IIIYQQQgghhBBCCCFEAyKBfSGEEEIIIYQQQgghhBCiAZHAvhBCCCGEEEIIIYQQQgjRgEhgXwghhBBCCCGEEEIIIYRoQCSwL4QQQgghhBBCCCGEEEI0IBLYF0IIIYQQQgghhBBCCCEaEAnsCyGEEEIIIYQQQgghhBANiAT2hRBCCCGEEEIIIYQQQogGRAL7QgjRQG3fvp1LL72UtLQ0NE1j1KhRoe6SEEIIIYQQx6x+/frRrl27w14+PT0dTdOYMmVKnfVJCCFE4yWBfSFEWFixYgWapvHoo4+Guit1bubMmWiaxnvvvVen7Y4aNYqVK1dyzz33MHv2bG688UbWrl3LlClTSE9Pr9N1ieD99ddf3HXXXQwaNIiUlBQ0TWPs2LFVzu/xeHjqqac4+eSTsdvtJCUlcfHFF7NmzZoql3njjTc45ZRTiIqKIi0tjWuvvZYdO3bUx+YIIYQQQlTpWDiHL/uKi4vjjDPO4IUXXsAwjFB3kRUrVjBlyhTy8vJC3ZVj1qpVq7j99tvp06cP8fHxNX4fnE4nL774IqeddhpJSUkkJCTQrVs3Hn/8cYqKiirM7/F4ePLJJ+nUqRORkZG0aNGC8ePHk52dXZ+bJYRoICSwL4QQDZDT6eS7777juuuu484772TEiBGceeaZrF27locfflgC+yH0448/8swzz7Bt2zbOOOOMaudVSnHZZZdxzz33kJaWxtNPP82kSZPYtGkT55xzDt99912FZSZPnsy4ceOIi4vjueee4/bbb2fJkiWceeaZ7N69u742SwghhBDimDR+/Hhmz57Nu+++y+TJkykpKeH222/n5ptvrjDv119/zV9//XXU+rZixQoefvhhCeyH0Oeff85//vMfMjMzOfXUU2ucf9iwYdx22220adOGxx57jGnTptG2bVvuu+8+Lrzwwgrzjx49mkmTJtGxY0f+85//MHr0aN5991369OlDYWFhfWySEKIBsYa6A0II0VgVFxcTExNTL23v378fpRTJycn10n516nO7GoMhQ4aQk5NDUlIS6enptG/fvsp5Fy1axKeffsr555/PF198gaZpANx444107tyZG264gT///DMwffPmzTz++OOceuqprFixAqvV+8/4xRdfzOmnn859993Hu+++W/8bKYQQQgjRSB16rnvWWWcxYsSIwO8TJkygS5cuvPHGG0ydOpW0tLTAexEREUe1r/VNKUVpaSnR0dGh7krYGj9+PHfeeSexsbGsWLGC/v37Vznv33//zSeffMLll1/Ohx9+GJh+8803M2TIED799FM2bNjASSedBMA333zDe++9x9ChQ1m4cGFg/p49e3LppZfy9NNP88gjj9Tfxgkhwp5k7AshGpxNmzYxbNgwmjZtSmRkJMcddxx33nknBQUFgXn27NmDpmn8+9//LrfsuHHj0DSNG2+8sdz0iRMnomka+/fvD0xzuVw89dRTdOvWjaioKOLj4xk0aBDffvttuWXL1sL88MMP6dmzJ9HR0Vx88cW13ratW7cyatQoWrRoQUREBK1atWLChAlkZWUF5unXrx9t27YF4OGHHw48GtyuXTtGjx4NQP/+/QPTy9be37lzJ5s2bcLtdtfYF/+j1TNnzuS1116jW7du2O32ctlJc+bMoVevXsTGxhITE0Pv3r15//33y7Xz2GOPoWka69atC0xzu93ExsaiaRo//vhjYLpSitTUVC666KJq+3bxxRcTFRVFbm5uhfcyMjKwWq0MGzasxm2sDykpKSQlJQU17zfffAN4yyr5g/cAiYmJXHLJJfz111/88MMPgelz587FMAxuu+22QFAfoEePHvTv358FCxZQWlpa43r9n4vly5dz9tlnExMTQ2pqKqNHjyYzMzMwn2EY9O3bF7vdzq+//lqh7xaLhUsvvTSobRVCCCHEsa2xnMPHxsbSq1cvTNNk69at5d6rqsb+l19+Sa9evYiKiqJJkybccMMN5OTkVDtO1hdffEHv3r0Dy9x0002UlJSUW9fDDz8MQPv27QPn/mXr82/dupVNmzZVuz1+/tJDS5cu5fHHH6djx45ERkby1FNPAWCaJi+88EKgHGR8fDwDBgxgyZIl5dq54YYbsFqt5Z4iyMjICPRv7969gel5eXlYLBYmTJhQbd9OPvlkWrZsWWn5o59//hlN05g0aVJQ21nXmjZtSmxsbFDz5ufnA9CyZcsK77Vq1Qqg3E0Uf8LOxIkTy817ySWXcMIJJwSV0FP2c75gwQJOPfVUoqKiaNGiBXfccQfFxcWBeYuKiujcuTMpKSns2rWrXDuzZs1C0zT+9a9/BbWtQoijQzL2hRANytq1a+nTpw+GYTB+/HiOO+44vv/+e6ZPn87SpUv53//+R3R0NC1btqRz584sXbq03PLLli1D1/VKp5988sk0bdoU8NYyvOiii1i5ciXXXHNN4ET6vffeY8CAAXzyyScMHjy4XBsLFy7kueee46abbuKGG25AKVXrbevXrx/R0dH83//9H23btmXLli288sorLFu2jFWrVpGQkMD999/PpZdeyh133MFll13G5ZdfDkDr1q2ZO3cur7/+Ovfddx9dunQB4Pjjjw+sY+TIkaxcuZLt27cHPbDX888/z/79+xk3bhwtW7YkLi4OgAcffJCpU6fSrVs3Jk+eDMB7773HNddcw7Zt27jvvvsAGDRoEA888ABLly6la9euAPz0008UFxcHjsWZZ54Z2AfZ2dkMHDiw2j7ddNNNfP7558yePZvbbrut3HvvvPMOhmFwww031Lht+fn5Qd3kALDZbCQkJAQ1b7CcTidApVlQ/mk//fQT55xzDuC9cAFv5tihzjrrLJYtW8a6devo2bNnjev+7bffWLBgAaNHj2bEiBH8/PPPzJw5k1WrVrF69Wqio6OxWCzMmzeP7t27c9VVV/Hrr78SHx/PgQMHGD58OK1bt+add9457O0XQgghxLGhsZ3D+wP6KSkpNc67aNEiLrvsMpo3b86kSZNISkpi4cKFXHDBBVUu88UXX/Cf//yHG2+8kVGjRrFs2TJee+01AF599VUA7r//fpKTk/n444959tlnSU1NBaBbt26BdgYOHMiOHTtqdV1y1113UVpayvXXX09aWhqtW7cGvIkos2fP5pxzzuHxxx+nsLCQN998k/PPP59333038FTDoEGDePPNN1m+fDmXXXYZ4D1O/iSWpUuXct111wGwfPlyTNOs8dz/xhtv5LbbbuOzzz5j6NCh5d578803axzTyi83NzfosRHsdnvQAftgnXTSSbRo0YI333yTbt26MXDgQJRSfPXVV7zzzjuMGTOm3NO+P//8M7qu07t37wptnXnmmcyePZsDBw7QpEmTGtf96aefMmPGDCZMmMCYMWNYunQpzz33HH/88QdLlixB13ViY2P54IMP6NWrF1dffTXffvstVquVP//8k5tvvpnTTz89cKNHCBEmlBBChIHly5crQE2dOrXa+f7xj38oTdPUjz/+WG76ww8/XGH5m2++WWmapvbv36+UUmr79u0KUNddd50C1LZt25RSSh04cEBpmqZuv/32wLLPPfecAtRHH31Ubj0ul0v16NFDtW/fPjDN367ValUbNmyo0Od33nlHAWr27NnVblv37t1V+/btVXZ2drnpP/30k9J1XU2ZMqXCOh966KFK17V8+fJK19G3b18FqO3bt1fbF6UOHpOkpKTAPvTbvHmz0nVdnXrqqaqkpCQwvaioSJ188snKYrEE1uHxeFRCQoK66KKLAvM99NBDKikpSQ0ePFj16dMnMP3pp59WgPrtt9+q7ZthGKpNmzaqa9eu5aabpqnat2+vjj/+eGWaZo3b6N8fwbz69u1bY3uH8h+nMWPGVPr+Cy+8oIBynz3/dvTo0UMB6tZbbw1MP/nkkxVQbp/7vfbaawpQH374YY398m/T/Pnzy01/6qmnKv0efvnll0rTNHXllVcqwzDUoEGDlM1mUz/99FON6xJCCCFE43UsnMO/8sorKjMzUx04cED9/vvvaty4cQpQvXv3rrBM3759Vdu2bQO/ezwe1aZNGxUfH68yMjIC0w3DUJdccokC1PXXX1+hT1FRUWrr1q3l2j7//POVzWZTRUVFgWkPPfRQtef2bdu2VcGGffzb26FDh3LrUEqpZcuWKUANHjxYeTyewPQDBw6oJk2aqMTERFVYWBiYpmmamjBhQmC+66+/Xp1yyimqW7duauTIkYHpN998s9J1vcL1z6Hy8vJUdHS0GjJkSLnphYWFKjY2Vg0YMCCobfTvj2BeZY9LsIL5Pvzyyy/qlFNOKbcuXdcrXSY2NlY1adKk0nbuvfdeBahffvml2j75P1Oapqmff/653Hs333xzpdep/uuKO++8U5WUlKiTTz5ZJSQkVPhMCiFCT0rxCCEajMzMTL777jvOP//8ClkLd955JzExMeVqFfozIPzlTpYtW4bFYuHhhx/GYrGwbNkywFtSRClVLlNk9uzZtGvXjn/84x9kZWUFXvn5+QwdOpTt27ezefPmcn24+OKLOfHEEw9r29avX8/atWsZNmwYpmmWW+fxxx9Phw4d+Oqrrw6r7bJWrFiBUirobH3wZvkLine truncated
"text/plain": [
"<Figure size 1540x462 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, axes = plt.subplots(1, len(REGIONS), figsize=(7 * len(REGIONS), 4.2))\n",
"for ax, region in zip(np.atleast_1d(axes), REGIONS):\n",
" box = REGIONS[region]['zoom']\n",
" for name, img in images[region]['zoom'].items():\n",
" if img is None or name.startswith('native'):\n",
" continue\n",
" row = img[img.shape[0] // 2]\n",
" ax.plot(box[0] + (np.arange(len(row)) + 0.5) / B, row / row.mean(), lw=1, label=name)\n",
" ax.set_xlabel('x [pixel, 25 um]'); ax.set_ylabel('intensity / mean')\n",
" ax.set_title(f'{region}: row y = {(box[2] + box[3]) / 2:.0f} px'); ax.grid(alpha=.3); ax.legend(fontsize=8)\n",
"fig.tight_layout()"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "81b5a618",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T10:33:43.885036Z",
"iopub.status.busy": "2026-09-28T10:33:43.884951Z",
"iopub.status.idle": "2026-09-28T10:33:44.783796Z",
"shell.execute_reply": "2026-09-28T10:33:44.783573Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"DIPlib -- a quantitative image analysis library\n",
"Version 3.5.1 (Jul 5 2024)\n",
"For more information see https://diplib.org\n",
"region reconstruction FRC cutoff: median range crossed\n",
"LowerLeft eta interpolation 2.053 2.053-2.053 2/10\n",
"LowerLeft cluster CNN 1ph 2.045 2.045-2.045 1/10\n",
"LowerLeft cluster CNN <=3ph 2.050 2.020-2.079 2/10\n",
"LowerLeft GridBased 2-20 keV nan nan-nan 0/10\n",
"LowerRight eta interpolation nan nan-nan 0/10\n",
"LowerRight cluster CNN 1ph nan nan-nan 0/10\n",
"LowerRight cluster CNN <=3ph nan nan-nan 0/10\n",
"LowerRight GridBased 2-20 keV nan nan-nan 0/10\n"
]
},
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1540x462 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# one_frc splits the counts binomially with its own (unseeded) generator, so one\n",
"# call is one draw: repeat it and quote the median and the range\n",
"N_REPEAT = 10\n",
"frc_results = {}\n",
"try:\n",
" import frc\n",
" fig, axes = plt.subplots(1, len(REGIONS), figsize=(7 * len(REGIONS), 4.2))\n",
" for ax, region in zip(np.atleast_1d(axes), REGIONS):\n",
" frc_results[region] = {}\n",
" for name, img in images[region]['frc'].items():\n",
" if img is None or name.startswith('native'):\n",
" continue\n",
" square = frc.util.square_image(np.array(img, float), add_padding=False)\n",
" size = square.shape[0]\n",
" values, first = [], None\n",
" for _ in range(N_REPEAT):\n",
" curve = frc.one_frc(square)\n",
" xs = np.arange(len(curve)) / size\n",
" first = curve if first is None else first\n",
" try:\n",
" values.append(float(frc.frc_res(xs, curve, size)[0]))\n",
" except Exception: # the curve never crosses 1/7\n",
" values.append(np.nan)\n",
" values = np.array(values)\n",
" found = np.isfinite(values)\n",
" frc_results[region][name] = dict(\n",
" median=float(np.median(values[found])) if found.any() else np.nan,\n",
" low=float(values[found].min()) if found.any() else np.nan,\n",
" high=float(values[found].max()) if found.any() else np.nan,\n",
" crossings=int(found.sum()), repeats=N_REPEAT)\n",
" r = frc_results[region][name]\n",
" ax.plot(np.arange(len(first)) / size, first, lw=1,\n",
" label=f\"{name}: {r['median']:.3f} ({r['crossings']}/{N_REPEAT} crossed)\")\n",
" ax.axhline(1 / 7, color='k', lw=.6, ls=':')\n",
" ax.set_xlabel('spatial frequency (1/pixels)'); ax.set_ylabel('FRC')\n",
" ax.set_title(f'{region}, FRC box {REGIONS[region][\"frc\"]}'); ax.grid(alpha=.3); ax.legend(fontsize=8)\n",
" fig.tight_layout()\n",
" print(f\"{'region':11s} {'reconstruction':20s} {'FRC cutoff: median':>18s} {'range':>15s} {'crossed':>8s}\")\n",
" for region, values in frc_results.items():\n",
" for name, r in values.items():\n",
" print(f\"{region:11s} {name:20s} {r['median']:18.3f} {r['low']:7.3f}-{r['high']:.3f} \"\n",
" f\"{r['crossings']:5d}/{r['repeats']}\")\n",
"except Exception as error:\n",
" print('FRC skipped:', repr(error))"
]
},
{
"cell_type": "markdown",
"id": "533a651d",
"metadata": {},
"source": [
"## 3. Photon statistics\n",
"\n",
"Photons in each whole region. The cluster chains keep only clusters that fall in\n",
"the 1/2/3-photon energy windows; at this occupancy the rest is lost to pile-up\n",
"(`SiementStar_LowerLeft.ipynb`: 45 % of the photons are in single-photon\n",
"clusters)."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "47cb5a91",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T10:33:44.785013Z",
"iopub.status.busy": "2026-09-28T10:33:44.784889Z",
"iopub.status.idle": "2026-09-28T10:33:44.798000Z",
"shell.execute_reply": "2026-09-28T10:33:44.797767Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"region eta (1ph) CNN 1ph CNN <=3ph GridBased photons per frame: GridBased / CNN <=3ph\n",
"LowerLeft 1.4060e+09 1.4060e+09 1.6677e+09 3.8487e+09 192.4 / 83.4 = 2.31\n",
"LowerRight 2.7039e+09 2.7039e+09 4.4703e+09 1.3339e+10 667.0 / 223.5 = 2.98\n",
"(GridBased over 20,000,000 frames; the cluster chains used all 20 M)\n"
]
}
],
"source": [
"counts = {}\n",
"CLUSTER_FRAMES = 20e6 # the cluster chains used the whole run\n",
"print(f\"{'region':11s} {'eta (1ph)':>12s} {'CNN 1ph':>12s} {'CNN <=3ph':>12s} {'GridBased':>12s} \"\n",
" f\"photons per frame: GridBased / CNN <=3ph\")\n",
"for region in REGIONS:\n",
" c = {name: float(np.nansum(img)) for name, img in images[region]['roi'].items()\n",
" if img is not None and not name.startswith('native')}\n",
" counts[region] = c\n",
" cnn = c.get('cluster CNN <=3ph', np.nan)\n",
" print(f\"{region:11s} {c['eta interpolation']:12.4e} {c.get('cluster CNN 1ph', np.nan):12.4e} {cnn:12.4e} {c['GridBased 2-20 keV']:12.4e} \"\n",
" f\"{c['GridBased 2-20 keV'] / frames.sum():8.1f} / {cnn / CLUSTER_FRAMES:6.1f} = \"\n",
" f\"{(c['GridBased 2-20 keV'] / frames.sum()) / (cnn / CLUSTER_FRAMES):.2f}\")\n",
"print(f'(GridBased over {frames.sum():,.0f} frames; the cluster chains used all 20 M)')"
]
},
{
"cell_type": "markdown",
"id": "b2edee48",
"metadata": {},
"source": [
"## 4. Energy spectrum from the energy head"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "75e762b8",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T10:33:44.799203Z",
"iopub.status.busy": "2026-09-28T10:33:44.799120Z",
"iopub.status.idle": "2026-09-28T10:33:45.047119Z",
"shell.execute_reply": "2026-09-28T10:33:45.046919Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"15 keV line at 15.013 keV, core sigma 0.538 keV; below 13.5 keV 12.12 %, 13.5-16.5 keV 82.46 %, above 16.5 keV 5.41 %\n"
]
},
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 825x462 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"def gauss(x, a, mu, sig):\n",
" return a * np.exp(-0.5 * ((x - mu) / sig)**2)\n",
"\n",
"\n",
"total = spectrum.sum()\n",
"near = np.abs(spectrum_centres - 15) < 2\n",
"peak = spectrum_centres[near][np.argmax(spectrum[near])]\n",
"window = np.abs(spectrum_centres - peak) < 0.8\n",
"try:\n",
" (a, mu, sig), _ = curve_fit(gauss, spectrum_centres[window], spectrum[window],\n",
" p0=(spectrum[window].max(), peak, 0.3), maxfev=20000)\n",
"except Exception:\n",
" if spectrum[window].sum() > 0:\n",
" mu = np.average(spectrum_centres[window], weights=spectrum[window])\n",
" sig = np.sqrt(np.average((spectrum_centres[window] - mu)**2, weights=spectrum[window]))\n",
" else:\n",
" mu = sig = np.nan\n",
"fractions = dict(below=spectrum[spectrum_centres < 13.5].sum() / total,\n",
" line=spectrum[(spectrum_centres >= 13.5) & (spectrum_centres <= 16.5)].sum() / total,\n",
" above=spectrum[spectrum_centres > 16.5].sum() / total)\n",
"print(f'15 keV line at {mu:.3f} keV, core sigma {abs(sig):.3f} keV; below 13.5 keV {fractions[\"below\"]*100:.2f} %, '\n",
" f'13.5-16.5 keV {fractions[\"line\"]*100:.2f} %, above 16.5 keV {fractions[\"above\"]*100:.2f} %')\n",
"fig, ax = plt.subplots(figsize=(7.5, 4.2))\n",
"ax.semilogy(spectrum_centres, spectrum / total / np.diff(spectrum_centres)[0], lw=1)\n",
"ax.axvline(15, color='grey', lw=.6, ls=':')\n",
"ax.set_xlim(0, 30); ax.set_ylim(1e-6, 5)\n",
"ax.set_xlabel('reconstructed photon energy [keV]'); ax.set_ylabel('photons / keV (normalised)')\n",
"ax.set_title('Siemens star, both regions, GridBased 2-20 keV'); ax.grid(alpha=.3, which='both')\n",
"fig.tight_layout()"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "42d1185c",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T10:33:45.048244Z",
"iopub.status.busy": "2026-09-28T10:33:45.048159Z",
"iopub.status.idle": "2026-09-28T10:33:45.133257Z",
"shell.execute_reply": "2026-09-28T10:33:45.133003Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"written /home/xie_x1/MLXID/GridBased/Results/overnight_poly2to20keV_Moench040/report/siemens_star_poly2to20keV_summary.json\n"
]
}
],
"source": [
"summary = dict(parts=parts, frames=float(frames.sum()),\n",
" photons=float(sum(s['n_photons'] for s in summaries)),\n",
" model=summaries[0]['checkpoint'], epoch=summaries[0]['epoch'], crop=summaries[0]['crop'],\n",
" regions={k: v['roi'] for k, v in REGIONS.items()}, photon_counts=counts,\n",
" frc_cutoff=frc_results, line_keV=float(mu), line_sigma_keV=float(abs(sig)),\n",
" spectrum_fractions=fractions)\n",
"out = OVERNIGHT / 'report' / os.environ.get('GRID_STAR_SUMMARY', 'siemens_star_poly2to20keV_summary.json')\n",
"out.parent.mkdir(parents=True, exist_ok=True)\n",
"json.dump(summary, open(out, 'w'), indent=1)\n",
"regions_dir = GRID / f'SiemensStar_15keV_{POLY_TAG}_regions' # the two regions as their own arrays\n",
"regions_dir.mkdir(exist_ok=True)\n",
"for region in REGIONS:\n",
" np.save(regions_dir / f'{region}_binned_image_flattened.npy', images[region]['roi']['GridBased 2-20 keV'])\n",
" np.save(regions_dir / f'{region}_binned_image.npy', cut_grid(raw, REGIONS[region]['roi']))\n",
"json.dump(summary, open(regions_dir / 'summary.json', 'w'), indent=1)\n",
"print('written', out)"
]
}
],
"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
}
@@ -0,0 +1,659 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "86bf6c9c",
"metadata": {},
"source": [
"# Siemens star, 15 keV, MOENCH040 — polychromatic 2–20 keV GridBased model\n",
"\n",
"SOLEIL 2504 beamtime, MOENCH040 @150 V, 15 keV, 20 M frames of the full chip\n",
"(2000 files). The two regions of `DataProcess/Configs.py` are reconstructed:\n",
"\n",
"| region | x [px] | y [px] |\n",
"|---|---|---|\n",
"| `SiemenStarLowerLeft` | 130–240 | 110–220 |\n",
"| `SiemenStarLowerRight` | 235–345 (the notebooks use 234–344) | 110–220 |\n",
"\n",
"Both share the same rows, so one crop of the frame, x 126–349 and y 106–224\n",
"(the regions plus the network's halo R = 4), is read and reconstructed in one\n",
"pass; its valid region is x 130–345, y 110–220. Reconstruction: the 2–20 keV\n",
"MOENCH040 model, FP16 with an FP32 head, masked column common mode (median over the pixels without charge) over the\n",
"118 rows of the crop, self-flattening of the sub-pixel phase with one Rosenblatt table per\n",
"local-occupancy class (tables from the first 3·10⁸ photons, every photon\n",
"remapped; one table for the whole crop left a 25 µm grid in the dim star of\n",
"LowerLeft, whose occupancy is ~8x below the rest). The run is processed as four parts of 500\n",
"files; this notebook sums the parts it finds.\n",
"\n",
"For comparison, the published cluster-based reconstructions of the same regions\n",
"(`SiementStar_LowerLeft/Right.ipynb`): eta interpolation (single-photon clusters)\n",
"and the 1/2/3-photon CNNs summed (`≤3ph`), all at 2.5 µm."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "5e4befe5",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T09:23:42.386960Z",
"iopub.status.busy": "2026-09-28T09:23:42.386752Z",
"iopub.status.idle": "2026-09-28T09:23:42.764863Z",
"shell.execute_reply": "2026-09-28T09:23:42.764621Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"parts found: 2 of 2 ['SiemensStar_15keV_part0', 'SiemensStar_15keV_part1']\n",
"model 260927_poly2to20keV_Moench040_c96_00 (epoch 182), 10,000,000 frames, 7.9688e+09 photons (796.9 per frame in the 23650 px valid region)\n",
" SiemensStar_15keV_part0 files [0, 499, 1] 5,000,000 frames occupancy 3.29 % (illuminated 5.58 %) 2434 frame/s sub-pixel rms 17.2 -> 1.2 %\n",
" SiemensStar_15keV_part1 files [500, 999, 1] 5,000,000 frames occupancy 3.45 % (illuminated 5.88 %) 1970 frame/s sub-pixel rms 17.3 -> 1.1 %\n"
]
}
],
"source": [
"import json\n",
"import os\n",
"from pathlib import Path\n",
"\n",
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"from scipy.optimize import curve_fit\n",
"\n",
"GRID = Path('/mnt/sls_det_storage/moench_data/MLXID/InferenceResults/GridBased')\n",
"PUBLISHED = Path('/home/xie_x1/MLXID/DeepLearning/InferenceResults')\n",
"ETA = Path('/home/xie_x1/MLXID/EtaInterpolation/Results')\n",
"OVERNIGHT = Path('/home/xie_x1/MLXID/GridBased/Results/overnight_poly2to20keV_Moench040')\n",
"POLY_TAG = os.environ.get('GRID_POLY_TAG', 'poly2to20')\n",
"RUNS = [r.strip() for r in os.environ.get(\n",
" 'GRID_STAR_RUNS', ','.join(f'SiemensStar_15keV_part{i}' for i in range(4))).split(',')]\n",
"CNN = {1: '1ph_260616_15keV_v260511_00', 2: '2ph_260622_15keV_v260610_00',\n",
" 3: '3ph_260623_15keV_v260611_00'}\n",
"B = 10 # 2.5 um bins per 25 um pixel\n",
"# region -> (x0, x1, y0, y1) in chip pixels: the whole region, the zoom of the\n",
"# published notebooks, and the 50 x 50 px FRC box (SiementStar_LowerLeft.ipynb's\n",
"# [1200:2100, 1400:2300][200:-200, 220:-180]; the same box around the right star)\n",
"REGIONS = {'LowerLeft': dict(roi=(130, 240, 110, 220), zoom=(140, 190, 170, 210), frc=(162, 212, 140, 190)),\n",
" 'LowerRight': dict(roi=(234, 344, 110, 220), zoom=(284, 324, 160, 200), frc=(263, 313, 140, 190))}\n",
"\n",
"plt.rcParams.update({'figure.dpi': 110, 'image.origin': 'lower', 'image.interpolation': 'nearest'})\n",
"\n",
"\n",
"def run_dir(run):\n",
" return GRID / (run + (f'_{POLY_TAG}' if POLY_TAG else ''))\n",
"\n",
"\n",
"parts = [r for r in RUNS if (run_dir(r) / 'binned_image_flattened.npy').exists()]\n",
"print(f'parts found: {len(parts)} of {len(RUNS)} {parts}')\n",
"summaries = [json.load(open(run_dir(r) / 'summary.json')) for r in parts]\n",
"frames = np.array([s['n_frames'] for s in summaries], float)\n",
"flattened = sum(np.load(run_dir(r) / 'binned_image_flattened.npy').astype(float) for r in parts)\n",
"raw = sum(np.load(run_dir(r) / 'binned_image.npy').astype(float) for r in parts)\n",
"analog = sum(np.load(run_dir(r) / 'analog_image.npy') * n for r, n in zip(parts, frames)) / frames.sum()\n",
"spectrum_centres = np.load(run_dir(parts[0]) / 'energy_spectrum.npy')[0]\n",
"spectrum = sum(np.load(run_dir(r) / 'energy_spectrum.npy')[1] for r in parts)\n",
"crop_x0, _, crop_y0, _ = summaries[0]['crop']\n",
"print(f\"model {Path(summaries[0]['checkpoint']).parent.parent.name} (epoch {summaries[0]['epoch']}), \"\n",
" f\"{frames.sum():,.0f} frames, {sum(s['n_photons'] for s in summaries):.4e} photons \"\n",
" f\"({sum(s['n_photons'] for s in summaries)/frames.sum():.1f} per frame in the \"\n",
" f\"{summaries[0]['valid_pixels']} px valid region)\")\n",
"for s in summaries:\n",
" print(f\" {s['run']:28s} files {s['signal_files']} {s['n_frames']:>10,d} frames \"\n",
" f\"occupancy {s['occupancy_valid_region']*100:.2f} % (illuminated {s['occupancy_exposed']*100:.2f} %) \"\n",
" f\"{s['frames_per_second']:.0f} frame/s sub-pixel rms {s['subpixel_nonuniformity_before']*100:.1f} -> \"\n",
" f\"{s['subpixel_nonuniformity_after']*100:.1f} %\")"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "54a0702c",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T09:23:42.766071Z",
"iopub.status.busy": "2026-09-28T09:23:42.765944Z",
"iopub.status.idle": "2026-09-28T09:23:43.851086Z",
"shell.execute_reply": "2026-09-28T09:23:43.850546Z"
}
},
"outputs": [],
"source": [
"def cut_grid(image, box, factor=B):\n",
" # chip-pixel box -> the GridBased crop's binned image\n",
" x0, x1, y0, y1 = box\n",
" return image[(y0 - crop_y0) * factor:(y1 - crop_y0) * factor,\n",
" (x0 - crop_x0) * factor:(x1 - crop_x0) * factor]\n",
"\n",
"\n",
"def cut_chip(image, box, factor=B):\n",
" # chip-pixel box -> a full-chip super-frame (4000 x 4000 at 2.5 um)\n",
" x0, x1, y0, y1 = box\n",
" return image[y0 * factor:y1 * factor, x0 * factor:x1 * factor]\n",
"\n",
"\n",
"def published(region):\n",
" eta = np.load(ETA / f'SiemenStar{region}_fromClusters.npy', mmap_mode='r')\n",
" single = cnn = None\n",
" for n, experiment in CNN.items():\n",
" path = (PUBLISHED / f'SiemenStar{region}' / experiment / 'augX1'\n",
" / f'{n}Photon_ML_superFrame_etaInterpolated.npy')\n",
" if path.exists():\n",
" image = np.asarray(np.load(path, mmap_mode='r'), float)\n",
" single = image if n == 1 else single\n",
" cnn = image if cnn is None else cnn + image\n",
" return eta, single, cnn\n",
"\n",
"\n",
"images = {}\n",
"for region, boxes in REGIONS.items():\n",
" eta, single, cnn = published(region)\n",
" images[region] = {}\n",
" for key, box in boxes.items():\n",
" images[region][key] = {\n",
" 'native readout (25 um)': cut_grid(analog, box, factor=1),\n",
" 'eta interpolation': np.asarray(cut_chip(eta, box), float),\n",
" 'cluster CNN 1ph': cut_chip(single, box) if single is not None else None,\n",
" 'cluster CNN <=3ph': cut_chip(cnn, box) if cnn is not None else None,\n",
" 'GridBased 2-20 keV': cut_grid(flattened, box),\n",
" }\n",
" images[region]['roi_raw_grid'] = cut_grid(raw, boxes['roi'])"
]
},
{
"cell_type": "markdown",
"id": "43f9cd97",
"metadata": {},
"source": [
"## 1. The two regions"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "29d6aaff",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T09:23:43.852994Z",
"iopub.status.busy": "2026-09-28T09:23:43.852857Z",
"iopub.status.idle": "2026-09-28T09:23:45.025073Z",
"shell.execute_reply": "2026-09-28T09:23:45.024813Z"
}
},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 2530x506 with 5 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAACc8AAAIPCAYAAACvu7RHAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOydd3hU1dbG32mZmilJJp0UAoSOVBGlgwVBULEXsF97uWKXol5FxSvqxV4QC1gQC2JFQFQQRZRe0/tM2mSSmcmU/f3Bt7Yzk4QEBEFdv+fJA3PmzDn71LX3Wu9aWyGEEGAYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmGYfxDKo90AhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhmEYhvmzYfEcwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAM84+DxXMMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzDMPw4WzzEMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzD/OFg8xzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMw/zjYPEcwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAM84+DxXMMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzDMPw4WzzEMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzD/OFg8xzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMw/zjYPEcwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAMwzAM84+DxXMMwzAMwzAMwzAMwxwxpk+fDoVCcbSbwTCHjYULF0KhUGD16tWHvI1Ro0YhKyvrsLXpz+BwHHdbzJ49GwqFAgUFBYd920eDgoICKBQKzJ49+2g35ZiBbQHDMAzDMAzDMAzDMMcqLJ5jGIZhGIZhGIZhGOagqKqqwl133YU+ffrAbDYjNjYWnTt3xplnnolXXnnlaDfvb8n8+fOxcOHCo7b/rKwsdOnS5ajt/0iiUCgi/rRaLbKzs3HVVVehpKTkaDcPdXV1mD179hERrTH/THbt2oUZM2Zg3LhxiI+Ph0KhwJVXXtnm+qNGjWrxnNDf8uXL/8SW/86QIUOgVqvbfUYHDBiAmJgYVFVV/UktYxiGYRiGYRiGYRjmr4b6aDeAYRiGYRiGYRiGYZi/DkVFRRgyZAicTiemTp2Kq666CjExMcjLy8NXX32F+fPn44orrpDrv/TSS3j++eePYov/HsyfPx9ZWVmYPn360W7K35JevXrhrrvuAgC4XC58++23eOWVV7BixQps3rwZ8fHxct1LLrkE559/PmJiYv6UttXV1WHOnDkA9ouYmGOfzMxMeDweqNXHput13bp1mDdvHrKzszF48GB88cUX7f4mISEBTz75ZIvl/fv3PxJNbJerrroKV199NRYuXIj77ruv1XU2bdqETZs24ayzzkJiYuKf3EKGYRiGYRiGYRiGYf4qHJseHIZhGIZhGIZhGIZhjknmzZuHyspKzJ8/HzfffHOL76OrAGk0Gmg0mj+recwhEAqF4PP5oNfrj3ZT/jR8Ph9UKpUUNyUnJ+Piiy+W31933XVITk7GU089hYULF+Lf//63/E6lUkGlUv3pbWYOD0IIeDweGAyGI7YPhUIBnU53xLb/R5k0aRJqampgs9lQUFCA7Ozsdn9jNBojnpGjzQUXXIDbbrsNr776Ku69995Wp4R9+eWXAeCAVfUYhmEYhmEYhmEYhmF42laGYRiGYRiGYRiGYTrM7t27AQBjx45t9fv09PSIz9OnT29V1FBZWYkbb7wRWVlZiImJQVJSEi6++GIUFBRErLdw4UIoFAqsXLkSDz/8MDp37gydTod+/frhs88+AwBs374dEydOhMVigdVqxfTp0+F2u//wPletWoX58+ejW7du0Gq16Ny5c6uVl9avX49JkyYhNTUVWq0WKSkpGDNmDD755JM2zyNRW1uLGTNmoGvXrtDr9bDZbOjTp48UaxUUFEChUKCwsBBr1qyJmC6R2r1hwwZcfvnlyM3NhdFohNFoxJAhQ/D666+32N/s2bOhUCiwfft23HHHHcjMzERMTAzeeeeddtvaEbxeL+bMmYPu3btDp9MhLi4OkyZNws8//xyx3vjx42G32yGEkMt++OEHKBQKxMbGwu/3y+WbN2+GQqHAY489FrGNVatW4bTTToPNZoNWq0WPHj3w6KOPIhgMRqw3atQoZGVlobCwEOeffz4SEhKg0+nane5x3LhxAIA9e/ZELKf7I3oa1fLyclxyySWIj4+H0WjE8OHD8e2337b5DABARUWF/I1er8fIkSOxcePGiH2RsGnOnDny2mdlZR2w7a1RWloKhUIRIQQEgKuvvhoKhQLXXHNNxPLbbrsNCoUC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 truncated
"text/plain": [
"<Figure size 2530x506 with 5 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"def limits(img):\n",
" # grey levels from the central 60 % of the image: in LowerLeft the unshadowed\n",
" # strip at x > 222 is several times brighter than the star (and brighter\n",
" # still for GridBased, which does not lose its photons to pile-up)\n",
" ny, nx = img.shape\n",
" centre = img[int(.2 * ny):int(.8 * ny), int(.2 * nx):int(.8 * nx)]\n",
" centre = centre[np.isfinite(centre)]\n",
" return np.percentile(centre, 0.5), np.percentile(centre, 99.5)\n",
"\n",
"\n",
"def show(region, key, title):\n",
" box = REGIONS[region][key]\n",
" panels = [(name, img) for name, img in images[region][key].items() if img is not None]\n",
" fig, axes = plt.subplots(1, len(panels), figsize=(4.6 * len(panels), 4.6))\n",
" for ax, (name, img) in zip(axes, panels):\n",
" low, high = limits(img)\n",
" ax.imshow(img, cmap='gray', extent=[box[0], box[1], box[2], box[3]], vmin=low, vmax=high)\n",
" ax.set_title(name, fontsize=10)\n",
" ax.set_xlabel('x [pixel, 25 um]'); ax.set_ylabel('y [pixel, 25 um]')\n",
" fig.suptitle(f'Siemens star {region}, {title}, 15 keV')\n",
" fig.tight_layout()\n",
"\n",
"\n",
"for region in REGIONS:\n",
" show(region, 'roi', 'whole region')"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "b4daba25",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T09:23:45.028865Z",
"iopub.status.busy": "2026-09-28T09:23:45.028776Z",
"iopub.status.idle": "2026-09-28T09:23:46.061555Z",
"shell.execute_reply": "2026-09-28T09:23:46.061121Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAACdcAAAHtCAYAAADxptVPAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOzdd3hTZf8/8Hd20jRNmu492CAoKIgoFhAcCIKKiqICKu6J4vwqoD7ugXuLgDw4cCOiiICoqPi4xYFsWrpn2rRNmvv3B7/P/TTdRYYPvl/XxXXR5OTk5OTk3Otzf26DUkqBiIiIiIiIiIiIiIiIiIiIiIiIiDTj/j4AIiIiIiIiIiIiIiIiIiIiIiIior8bBtcRERERERERERERERERERERERERNcHgOiIiIiIiIiIiIiIiIiIiIiIiIqImGFxHRERERERERERERERERERERERE1ASD64iIiIiIiIiIiIiIiIiIiIiIiIiaYHAdERERERERERERERERERERERERURMMriMiIiIiIiIiIiIiIiIiIiIiIiJqgsF1RERERERERERERERERERERERERE0wuI6IiIiIiIiIiIiIiIiIiIiIiIioCQbXERERERERERERERERERERERERETXB4DoiIiIiIiKiRqZMmQKDwbC/D4NonwqFQpg1axays7NhNpv5G9jLampqcOWVVyI9PR0mkwmZmZm7va9hw4b9pdf/3RgMBkyZMqVD265atQoGgwEvvfSSfmzLli0wGAyYNWtWp997f9z//8rx/h39nT5PZ66lA0FLvwciIiIiIiIi+usYXEdEREREREQHvMLCQtx4443o27cvoqKi4HK5kJ2djZNPPhkvvPDC/j68A9KcOXP26wB/ZmYmunbtut/ef28yGAwYOXLkHt3nvHnzMHv2bAwfPhwvvPACFixYAACYNWsW3n777T36XgTce++9eOyxx3DGGWfgpZdewpw5c9rcfn//nujAt2rVKsyaNQvl5eX7+1AOGPfccw/OOOMMdOvWDUajEWazudVtX3rpJRgMhhb/TZgwYR8e9X89+eSTMBgMuPvuu9vc7tFHH4XBYMDDDz+8j46MiIiIiIiIaN9qvUVPREREREREdADYtm0bBg0ahOLiYkyYMAHTpk2D1WrFpk2bsHz5csyZMwfnn3++3v65557D008/vR+P+MAwZ84c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gw/i448/xhdffIGIiAgAQFVVFY488kisX78ekyZNwpAhQ7Bx40Y8+eSTWLZsGT7//HP07t077D3ff/99PPnkk7jkkktwwQUXYNGiRZgxYwbsdjvmzp2LlJQU3Hrrrdi5cyceeughjB8/Hj///DOMxrbnp+bm5mLgwIEoLCzEtGnT0K9fP/zwww947rnnsGzZMqxbtw4JCQk45ZRT0LVr12a/j379+rW43478ngCguroaQ4cOxaGHHoo77rgDBQUFePjhh3H88cdj06ZNcLlcetupU6di/vz5GDduHCZNmgQAePPNN3HyySfjqaeewsUXX9zmZxVLlizBQw89hEsvvRTnn38+Pv74Y8yZMwc//vgjli9f3u45a8t3332HxYsXY+rUqTj77LPx1Vdf4aWXXsLXX3+NdevW6WugoxYuXIizzz4bRx55JGbOnInIyEjk5ubik08+wYYNG9CzZ8+w7Y877jh4PB7cdNNNqKmpwZw5czB27Fhs2LAh7L47a9YszJ49G8OHD8fMmTPhcDjw4YcfYtq0afjzzz9xzz336G2ffvppXHLJJejevTtmzpwJi8WChQsXthmc1pqO3OuBXYFP06ZNQ2ZmJmbMmIHIyEgsXrwYl19+OX744Qc8++yzAIBbbrkFXq8Xb731Fh5++GH9m5TrsqP3k8Y6en109LcDdO6e3ZLPPvsM48aNQ0pKCpYuXaqD69sr1/alcePG6UDiXr164YorrsDFF1+824HNjz32GK6++mqcdtppuq7Rme/zqKOOQq9evbB48WI89thjcLvdet81NTVYtGgRsrKyWp3AQERERERERHRAUEREREREREQHsM2bNyuPx6MAqISEBHX66aerBx98UH355ZcqFAo1237y5MmqaXN5/PjxKjo6Wm3cuDHs8U2bNqnIyEg1ZcoU/djcuXMVANWvXz9VW1urH//uu+8UAGUwGNSrr74atp9x48Ypi8Wiqqqq9uh7+nw+FRMTo4444gj92COPPKIAqC+//LLN89aS8vJyZTAY1MUXX9zuthkZGSonJ6fF53w+X7PHGhoa1NFHH608Ho+qr6/Xj8+cOVMBUEcffbQKBAIdPtaMjAzVpUuXNrd54YUXFAB1ySWXhD3++++/K5vNprp166YaGhqUUkqtW7dOAVD33Xef3i4nJ0eNGTNGRUdHq9tuu00/fuKJJ6ro6Gj92p07dyq73a7Gjx/f7Jq7//77FQC1atWqsP0CUDfffHOLxw1AHXPMMW1+tjlz5igA6s033wx7vL6+XvXv319lZWWFPZ6Tk6MyMjJafK/Jkye3+V67o7KyUh188MHK6XSqdevW6ceHDh2qDAaDWrt2bdj2s2fPVgDUHXfcoR+79dZbFQB1773Line truncated
"text/plain": [
"<Figure size 2530x506 with 5 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "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aKxzS9aePXsCAPbt2xdwnNrHyduopqWl4dZbb0VYWBgsFgs6deqE7777rtY+AFQFb9E1JpMJXbp0wS+//BLwLAqUmDZtmvz2ZxrUUdt2hDXd88cff0S/fv3gdDphNBqRlJSEp59+GpWVldXue+zYMdx5552oV68e9Ho9YmJicNdddyEtLS3gvI0bN0KlUmHBggV455130LRpUxiNRjRu3BiLFi2S9xoyZIisx+TkZKSnpwf9jn/99ReGDBmCiIgI2W8fffRRFBYWynPomwghsHDhQlkHtW2NTOUGEHB+Td917969GDhwIGw2G0JCQtC3b18cOHCg2nmVlZV48cUX0aJFC5hMJoSGhqJnz5747rvvgnpPZV2+9dZbSEpKgtFoREJCAp566il4vd6A86kf1IRKpao1q+Vnn32G1q1bw2QyITo6GuPGjUNJSclplY8QQuC1115Dq1atZP0kJiZi+PDhyMjIqHaPuvqH8r5z585Fu3bt5BjYsWPHWrfYffXVV9GkSRO5RfPTTz9drb5OBY2je/fuxeTJkxEfHy/HoA8//LDGaz744AO0b98eISEhsFgs6NChAz766KOAcxISEuS4rWxjyjosKirCE088IcvvdDqRnJyMHTt21FreYNoHEFzfIfLz8zF+/HjUr18fBoMBERERGDp0aLXxsjZ++OEHuFwuXH755Thy5AiAquD7u+66C/Xr14fRaITL5UKbNm3wwgsvBHXPmjAYDDJw7nQpKioKkEX/lLfeegtarRY333wzKioq5DOC+Z5jxoyBEALz58+v8d7z5s0DUJWljmEYhmEYhmEYhmEuJngJGMMwDMMwDMMwDHPRkZiYCACYP38+XnjhhTPKcHLkyBF07NgRxcXFuOOOO9C4cWMcO3YMb731Fr755hv8/PPPiIuLC7jmf//7HyoqKjB27FhoNBrMnj0bAwcOxGeffYY77rgDN998M/r374+tW7di4cKFMBgMmDNnzj965qRJk1BYWIjRo0cjJCQEixYtwvjx4xEVFYUhQ4YAqAqU6dmzJ9xuN+677z5ERUUhKysLv/76K7Zu3Vrn9mk333wzNmzYgLvuugutWrVCRUUFDhw4gPXr1wMA3G43Fi9ejHHjxsHlcuGJJ56Q17rdbgDAsmXLsGvXLgwePBjx8fEoKCjAJ598glGjRiErKwuPPvpotecOHz4cOp0O999/PywWC5o0aXIaX7BmfD4f+vTpgw0bNqBfv364//77kZ6ejrfeegvXXHMN1qxZg27dugGoCkxbt24dfv/9d7Rq1QoAsH79eqjVahQXF2Pbtm3o1KmTPA4EBmy+9957uPPOO9GqVSs8/vjjsNvt2LJlC/73v//ht99+qxaUUlxcjE6dOqFdu3aYNm0aioqKEBIScsr32b9/PwAEbNlaGwUFBejUqRNSUlJw++23o02bNtizZw/69OmDhg0b1nhNSUkJOnXqhDZt2uDpp59GRkYGXnnlFVx//fVISUmB1WpF586d8corr2DcuHG44YYbMGjQIACos+y1sXjx4mrH/vzzT0yfPh2RkZHy2FdffYUBAwbAZrPhvvvuQ0REBFavXo3Jkyfjhx9+wKpVq6BWV60LPXbsGNq2bYvMzEyMGTMGLVq0wB9//IG5c+fiq6++wk8//YSIiIiAZ7755pvIysrCmDFjEBISgnfffRcjR46ETqfD//73P3Tq1AnPPPMM/vrrL7zxxhsYOXIkvv766zrf7/fff0fnzp3h8/kwduxYNGjQAN9//z1eeuklrFu3Dj/88APMZjPuvvtu9OzZE7feeis6deqEu+66CwDQsWPHGu+blJSExYsXVzv/ZI4dO4bOnTtjwIABeOGFF7Bv3z689tprGDBgAHbu3CnrzOv1ok+fPti0aROGDh2Ke+65B6WlpXj//ffRvXt3LF++HP369avzfQHg9ddfx9GjR3HPPffA6XRi2bJlmDJlClJSUmoNBgyWlStX4uWXX8a9996LO+64A+vWrcOsWbOwY8cOrF27Vr5PsDz33HP4v//7P/Tt2xd33nkn9Ho9Dh8+jK+++grp6ekB7SSY/kGMHj0aixYtwsCBAzF8+HAAwOeff44bbrgBb731Fu655x557uOPP44XXngBV155JZ5//nmUlZXhvffew4oVK067fkaOHAmVSoUHH3wQarUab775JoYPH44GDRqgQ4cO8rzJkyfj6aefRosWLfDkk08CAN5//30MHToUKSkpmDRpEoCq7blffvllbN68OaCvUrssLCzENddcg/3792PkyJFo2bIl8vLyMHfuXFx11VXYvHkzWrduHVDGYNtHsH0HqAr4uvrqq7F7924MHz4cHTt2xIEDB/Dmm2/iq6++wpYtW065xenSpUsxYsQIdOjQAcuWLYPdbofX60WvXr1w5MgRjB07FpdddhmKiorw999/49tvv8Vjjz122t/nn9CyZUsUFhZCrVajVatWmDhxIm6++eYzupcQAv/73//wwgsvYNy4cXjppZegUqlO63sOHjwYDz74IBYsWICpU6cG9L3Dhw9j7dq16NKlCxo1anRW3p9hGIZhGIZhGIZh/jULine truncated
"text/plain": [
"<Figure size 2530x506 with 5 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"for region in REGIONS:\n",
" show(region, 'zoom', 'zoom of the published notebooks')"
]
},
{
"cell_type": "markdown",
"id": "cba943fe",
"metadata": {},
"source": [
"### GridBased 2–20 keV at full size\n",
"\n",
"The flattened reconstruction of each region at 2.5 µm, and the same photons\n",
"before the sub-pixel flattening (the 25 µm pixel grid is visible there)."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "76f70e13",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T09:23:46.065314Z",
"iopub.status.busy": "2026-09-28T09:23:46.065072Z",
"iopub.status.idle": "2026-09-28T09:23:46.886751Z",
"shell.execute_reply": "2026-09-28T09:23:46.886518Z"
}
},
"outputs": [
{
"data": {
"image/png": "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P//zP/HsZz8bf/jDHyRR/MUvfoFXvvKVeNaznoUrr7wSJpMJu3fvxi233CLbajQaOOecc7B//3686U1vwuTkJP70pz/h05/+NO6///4+H9c3v/nN+MpXvoKXvexlOOecc3DHHXfgOc95DhqNxkM+h0QikQCwlng9mtv44x//CJvNhuOOO67vu9u3b4fVasWdd975kP5Gs9nEy1/+cvzyl7/Ej3/8YzzrWc8CsHYvfOELX8CrX/1qvPWtb0UymcTnPvc53HrrrbjrrruEwfS3f/u3+MhHPoI77rijL4G6/fbbsXfvXnzkIx95OId+RNizZw/OOOMM2Gw2vPvd74bb7ca1116L5z//+fje976HCy+8EAaDAc94xjNw0003ye/Nzs5iaWkJRqMRN910EzZs2AAAuPnmmxEMBuWezGazOP3009FsNvGmN70J8Xgct9xyCy677DIkEgl89rOf7dufr33ta6hWq3jTm94Et9t9yOdhPRzJ+f7sZz+Lb37zm7jhhhvwmc98BuFwGJOTkzj33HPxute9Dqeffjre9KY3AQAGBgYe0rm8/PLLceyxx+Lf/u3f8MEPfhBbtmw57Da+/OUvw2Qy4e/+7u8QCARw22234dOf/jSWlpYk0X7zm9+MxcVF/OpXvxIvawCIRCK47rrrDrnPD+XZBYDbbrsN3/ve9/DWt74Vr3/96/GlL30JF110EbZv347Nmzc/5G0+FuuBgoKCgoLCExkqf1jD4x0vqPxB5Q9PpvyB6PV6eN7znocTTjgBn/jEJ/Cb3/wGV199NfL5PK699lr53qc+9Sm8733vw0te8hK8+tWvRqVSwec//3mcccYZuPPOOzExMXHYnOTmm2/Gc57zHAwPD+NDH/oQer0evvCFL+Css87CTTfddFAzb7216ZZbbsFzn/tcHHvssbj88sths9lwww034MILL8R3vvMdvOQlL1n3GJm/fPKTn0QikcBnPvMZAGuNxvVQKpVwzTXX4BWveAVe+9rXotFo4Otf/zpe/OIX46c//SnOO+88ADhkTjQ5OYm3ve1tfedB//ce6nG8+93vht1uxwc+8AFks1l86lOfwt/+7d8eUSPwSHKtlZUVnHXWWeh0Onjve9+LYDCIr33ta/j5z3/+oNtXUHhSQ1NQUFA4AMFgUDv++OMP+rxSqWiZTEb+KxaL8rNrr71WA6CddtppWrvd7vu9G2+8UQOg3XjjjfLZiSeeqIXDYS2Xy8ln9957r2YymbQDl6axsTENwEH/nXzyyVoikej7bqPR0DqdTt9n+Xxei0aj2hve8Ab57F3vepfm9XoP+q4eV155peZwOLR777237/N/+7d/0wBoN998s6ZpmrZz504NgPa6172u73sf+MALine truncated
"text/plain": [
"<Figure size 1650x814 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "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dx18Ph8+97nPHdXR+OMf/4i3vvWtWLhwIb74xS8in8/js5/9rJCqB6NUKmFmZgYAUCgUsGPHDtxyyy044YQTsHz5cnnf/v378R//8R945zvfiYULFyKZTOJf//Vf8aY3vQlbt26VQPGhhx7CpZdeis2bN+Pmm2+GyWRCT08PnnjiCfmsfD6Ps846C/39/bjqqquwYMECvPDCC7j11lvR3d2N++67T9579dVX46677sI73/lOnHXWWXjuuefw5je/Gfl8/mWPITE+Pg5gLvB6LT/j+eefF2dZjxNOOAFWqxXbtm17Wd9RKBTw7ne/Gw8//DAeeOABbN68GcDcXPj+97+PD3zgA/jwhz+MyclJfPe738WWLVuwfft2Uf28733vw0033YTnnnuuJoB69tln0dfXh5tuuumV3Poxobe3Fxs2bIDNZsPHP/5xOJ1O3HnnnXjLW96CX/7yl7j44othMBhw+umn47HHHpO/GxgYwOjoKIxGIx577DHMnz8fAPD444/D7/fLnJyZmcEpp5yCQqGAq666Ck1NTXjiiSfwqU99CuPj44c4p//2b/+GTCaDq666Ck6n84jrAQASiQTi8TgWLFjwkvd5xx134Oqrr8Ypp5yC6667DtPT0/jABz6A1tbWw77/F7/4BSKRCK655hrU1dXh29/+Nt7+9rdjaGgIfr8f73jHO9Dd3Y177rkH3/rWt2R+vZx+x0uXLsVPf/pTXH/99fB4PPjc5z531M84lrXd0NCA2267Df/v//0/vP3tb8c73vEOAMCCBQtQX19/1Gu+55578Fd/9Vc466yz8MUvfhHVahU/+tGPsHnzZjz22GOH9DJ/73vfi3A4jC9+8Yvo6+vDd7/7XVgsFvzsZz+T9xzrZ+7Zswdnn3023G43Pv/5z8NqteKOO+6A0+k85vFUUFBQUFB4I6Hihzmo+OHYoOIHFT+8kvhBj5/97GeIx+P48Ic/DIPBgH/+53/GOeecg2effRbLli0DcGxjebSYRNM0vOMd78Dvfvc7fPCDH8TJJ5+MJ598El/84hexZ88e/OIXv6i5pn379uGSSy7BlVdeiQ996ENob2+Hpmm45JJL8OCDD+LKK6/EypUr0d/fj9tuuw3btm3Dn/70pyO28rr66qsxb9483HjjjbjqqqtwxhlnHDU+uPfeexGNRnHllVciHA5j586d+OEPf4gXX3wRTz75JAAc9Tl8+9vfxt/+7d/WjAO/7+Xex8DAAC688EJcfvnleN/73od77rkHH//4x7Fs2TKcc845L/l8jyXW+tznPoebb74Z5557Li666CL09fXhggsugN/vP+I8VVA4rqApKCgoHAZ+v19bvXr1Ia+n02ktEonIv0QiIb+78847NQDLine truncated
"text/plain": [
"<Figure size 1650x814 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"for region, boxes in REGIONS.items():\n",
" box = boxes['roi']\n",
" fig, axes = plt.subplots(1, 2, figsize=(15, 7.4))\n",
" for ax, img, name in ((axes[0], images[region]['roi']['GridBased 2-20 keV'], 'flattened'),\n",
" (axes[1], images[region]['roi_raw_grid'], 'before flattening')):\n",
" low, high = limits(img)\n",
" ax.imshow(img, cmap='gray', extent=[box[0], box[1], box[2], box[3]], vmin=low, vmax=high)\n",
" ax.set_title(f'GridBased 2-20 keV, {region}, {name}', fontsize=11)\n",
" ax.set_xlabel('x [pixel, 25 um]'); ax.set_ylabel('y [pixel, 25 um]')\n",
" fig.tight_layout()"
]
},
{
"cell_type": "markdown",
"id": "318433ac",
"metadata": {},
"source": [
"## 2. Profiles and FRC\n",
"\n",
"A row through the zoom (normalised to its mean), and the one-image Fourier ring\n",
"correlation as `SiementStar_LowerLeft.ipynb` computes it (`frc.one_frc` and\n",
"`frc.frc_res` on the 50 × 50 px box x 162–212, y 140–190, no window; for\n",
"LowerRight the same box around its star). The cutoffs are in that notebook's\n",
"units, so they compare with its ML 2.041 / eta 2.053 for LowerLeft."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "6162c863",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T09:23:46.891035Z",
"iopub.status.busy": "2026-09-28T09:23:46.890941Z",
"iopub.status.idle": "2026-09-28T09:23:47.101421Z",
"shell.execute_reply": "2026-09-28T09:23:47.101191Z"
}
},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABfYAAAHCCAYAAACt2ZiFAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOzdd3gU5fbA8e9sT28EQgsggqCCiApYLt0uWFFUQPyBiqJw5Vqwgyh2bNfeQAT1YgHsAgJ6vSqiogIivZc00rfOvL8/ZndJSNtAQjZwPs/DQ3Z2yjszu8nMmfOeV1NKKYQQQgghhBBCCCGEEEII0ShYGroBQgghhBBCCCGEEEIIIYSInAT2hRBCCCGEEEIIIYQQQohGRAL7QgghhBBCCCGEEEIIIUQjIoF9IYQQQgghhBBCCCGEEKIRkcC+EEIIIYQQQgghhBBCCNGISGBfCCGEEEIIIYQQQgghhGhEJLAvhBBCCCGEEEIIIYQQQjQiEtgXQgghhBBCCCGEEEIIIRoRCewLIYQQQgghhBBCCCGEEI2IBPaFEEIIIYQQQgghhBBCiEZEAvtCCNFIbdq0iYsuuoj09HQ0TWPkyJEN3SQhhBBCCCGOWH379qVt27YHvPzmzZvRNI1JkybVWZuEEEIcviSwL4SICkuWLEHTNB566KGGbkqdmz59Opqm8c4779TpekeOHMnSpUu58847mTlzJjfccAMrVqxg0qRJbN68uU63JSL3999/c/vttzNw4EDS0tLQNI3Ro0dXOX8gEODxxx/n+OOPx+VykZKSwvnnn8/y5curXOa1117jhBNOICYmhvT0dK666iq2bNlSH7sjhBBCCFGlI+Eavuy/hIQETjnlFJ577jl0XW/oJrJkyRImTZpEfn5+QzfliLVs2TLGjx9P7969SUxMrPH74PV6ef755znppJNISUkhKSmJrl278sgjj1BcXFxh/kAgwGOPPcYxxxyD0+mkRYsW3HjjjeTm5tbnbgkhGgkJ7AshRCPk9Xr57rvvGD58OLfddhvDhg3j1FNPZcWKFUyePFkC+w3ohx9+4Mknn2Tjxo2ccsop1c6rlOLiiy/mzjvvJD09nSeeeIKJEyeyZs0azjjjDL777rsKy9x3331cf/31JCQk8MwzzzB+/HgWLFjAqaeeyvbt2+trt4QQQgghjkg33ngjM2fO5O233+a+++6jtLSU8ePHM3bs2Arzfv311/z999+HrG1Llixh8uTJEthvQJ9//jn//ve/yc7Opnv37jXOP3ToUMaNG0dmZiYPP/wwU6dOpU2bNtx9992ce+65Fea/9tprmThxIh07duTf//431157LW+//Ta9e/emqKioPnZJCNGI2Bq6AUIIcbgqKSkhLi6uXta9Z88elFKkpqbWy/qrU5/7dTgYNGgQeXl5pKSksHnzZtq1a1flvPPnz+fTTz/l7LPP5osvvkDTNABuuOEGOnXqxHXXXcdff/0Vnr527VoeeeQRunfvzpIlS7DZzD/j559/PieffDJ33303b7/9dv3vpBBCCCHEYWr/a93TTjuNYcOGhV/fdNNNdO7cmddee40pU6aQnp4efs/hcBzSttY3pRRut5vY2NiGbkrUuvHGG7ntttuIj49nyZIl9OvXr8p5169fz9y5c7nkkkv48MMPw9PHjh3LoEGD+PTTT1m1ahXHHXccAN988w3vvPMOgwcPZt68eeH5e/TowUUXXcQTTzzBgw8+WH87J4SIepKxL4RodNasWcPQoUNp1qwZTqeTo446ittuu43CwsLwPDt27EDTNP71r3+VW/b6669H0zRuuOGGctMnTJiApmns2bMnPM3n8/H444/TtWtXYmJiSExMZODAgXz77bflli1bC/PDDz+kR48exMbGcv7559d63zZs2MDIkSNp0aIFDoeDVq1acdNNN5GTkxOep2/fvrRp0waAyZMnh7sGt23blmuvvRaAfv36haeXrb2/detW1qxZg9/vr7Etoa7V06dP55VXXqFr1664XK5y2UmzZs2iZ8+exMfHExcXR69evXjvvffKrefhhx9G0zT+/PPP8DS/3098fDyapvHDDz+EpyulaNKkCeedd161bTv//POJiYlh7969Fd7buXMnNpuNoUOH1riP9SEtLY2UlJSI5v3mm28As6xSKHgPkJyczIUXXsjff//N999/H54+e/ZsdF1n3Lhx4aA+wIknnki/fv344IMPcLvdNW439LlYvHgxp59+OnFxcTRp0oRrr72W7Ozs8Hy6rtOnTx9cLhe//vprhbZbrVYuuuiiiPZVCCGEEEe2w+UaPj4+np49e2IYBhs2bCj3XlU19r/88kt69uxJTEwMTZs25brrriMvL6/acbK++OILevXqFV5mzJgxlJaWltvW5MmTAWjXrl342r9sff4NGzawZs2aavcnJFR6aOHChTzyyCN07NgRp9PJ448/DoBhGDz33HPhcpCJiYn079+fBQsWlFvPddddh81mK9eLYOfOneH27dq1Kzw9Pz8fq9XKTTfdVG3bjj/+eFq2bFlp+aOffvoJTdOYOHFiRPtZ15o1a0Z8fHxE8xYUFADQsmXLCu+1atUKoNxDlFDCzoQJE8rNe+GFF3L00UdHlNBT9nP+wQcf0L17d2JiYmjRogW33norJSUl4XmLi4vp1KkTaWlpbNu2rdx6ZsyYgaZp/POf/4xoX4UQh4Zk7AshGpUVK1bQu3dvdF3nxhtv5KijjuK///0vTz31FAsXLuR///sfsbGxtGzZkk6dOrFw4cJyyy9atAiLxVLp9OOPP55mzZoBZi3D8847j6VLl3LllVeGL6Tfeecd+vfvz9y5c7ngggvKrWPevHk888wzjBkzhuuuuw6lVK33rW/fvsTGxvJ///d/tGnThnXr1vHSSy+xaNEili1bRlJSEvfccw8XXXQRt956KxdffDGXXHIJAK1bt2b27Nm8+uqr3H333XTu3BmA9u3bh7cxYsQIli5dyqZNmyIe2OvZZ59lz549XH/99bRs2ZKEhAQA7r//fqZMmULXrl257777AHjnnXe48sor2bhxI3fffTcAAwcO5N5772XhwoV06dIFgB9//JGSkpLwuTj11FPDxyA3N5cBAwZU26YxY8bw+eefM3PmTMaNG1fuvbfeegtd17nuuutq3LeCgoKIHnIA2O12kpKSIpo3Ul6vF6DSLKjQtB9//JEzzjgDMG9cwMwc299pp53GokWL+PPPP+nRo0eN2/7tt9/44IMPuPbaaxk2bBg//fQT06dPZ9myZfz888/ExsZitVp599136datG5dffjm//voriYmJZGVlcfXVV9O6dWveeuutA95/IYQQQhwZDrdr+FBAPy0trcZ558+fz8UXX0zz5s2ZOHEiKSkpzJs3j3POOafKZb744gv+/e9/c8MNNzBy5EgWLVrEK6+8AsDLL78MwD333ENqaioff/wxTz/9NE2aNAGga9eu4fUMGDCALVu21Oq+5Pbbb8ftdnPNNdeQnp5O69atATMRZebMmZxxxhk88sgjFBUV8frrr3P22Wfz9ttvh3s1DBw4kNdff53Fixdz8cUXA+Z5CiWxLFy4kOHDhwOwePFiDMOo8dr/hhtuYNy4cXz22WcMHjy43Huvv/56jWNahezduzfisRFcLlfEAftIHXfccbRo0YLXX3+drl27MmDAAJRSfPXVV7z11luMGjWqXG/fn376CYvFQq9evSqs69RTT2XmzJlkZWXRtGnTGrf96aefMm3aNG666SZGjRrFwoULeeaZZ/jjjz9YsGABFouF+Ph4/vOf/9CzZ0+uuOIKvv32W2w2G3/99Rdjx47l5JNPDj/oEUJECSWEEFFg8eLFClBTpkypdr5//OMfStM09cMPP5SbPnny5ArLjx07Vmmapvbs2aOUUmrTpk0KUMOHD1eA2rhxo1JKqaysLKVpmho/fnx42WeeeUYB6qOPPiq3HZ/Pp0488UTVrl278LTQem02m1q1alWFNr/11lsKUDNnzqx237p166batWuncnNzy03/8ccflcViUZMmTaqwzQceeKDSbS1evLjSbfTp00cBatOmTdW2Ral95yQlJSV8DEPWrl2rLBaL6t69uyotLQ1PLy4uVscff7yyWq3hbQQCAZWUlKTOO++88HwPPPCASklJURdccIHq3bt3ePoTTzyhAPXbb79V2zZd11VmZqbq0qVLuemGYah27dqp9u3bK8MwatzH0PGI5F+fPn1qXN/+Qudp1KhRlb7/3HPPKaDcZy+0HyeeeKIC1C233BKefvzxxyug3DEPeeWVVxSgPvzwwxrbFdqnOXPmlJv++OOPV/o9/PLLL5WmaWrIkCFK13U1cOBAZbfb1Y8//ljjtoQQQghx+DoSruFfeukllZ2drbKystTvv/+urr/+egWoXr16VVimT58+qk2bNuHXgUBAZWZmqsTERLVz587wdF3X1YUXXqgAdc0111RoU0xMjNqwYUO5dZ999tnKbrer4uLi8LQHHnig2mv7Nm3aqEjDPqH97dChQ7ltKKXUokWLFKAuuOACFQgEwtOzsrJU06ZNVXJysioqKgpP0zRN3XTTTeH5rrnmGnXCCSeorl27qhEjRoSnjx07Vlkslgr3P/vLz89XsbGxatCgQeWmFxUVqfj4eNW/f/+I9jF0PCL5V/a8RCqS78Mvv/yiTjjhhHLbslgslS4THx+vmjZtWul67rrrLgWoX375pdo2hT5Tmqapn376qdx7Y8eOrfQ+NXRfcdttt6nS0lJ1/PHHq6SkpAqfSSFEw5NSPEKIRiM7O5vvvvuOs88+u0LWwm233UZcXFy5WoWhDIhQuZNFixZhtVqZPHkyVquVRYsWAWZJEaVUuUyRmTNn0rZtW/7xj3+Qk5MT/ldQUMDgwYPZtGkTa9euLdeG888/n2OPPfaA9m3lypWsWLGCoUOHYhhGuW22b9+eDh068NVXXx3QustasmQJSqmIs/XLine truncated
"text/plain": [
"<Figure size 1540x462 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, axes = plt.subplots(1, len(REGIONS), figsize=(7 * len(REGIONS), 4.2))\n",
"for ax, region in zip(np.atleast_1d(axes), REGIONS):\n",
" box = REGIONS[region]['zoom']\n",
" for name, img in images[region]['zoom'].items():\n",
" if img is None or name.startswith('native'):\n",
" continue\n",
" row = img[img.shape[0] // 2]\n",
" ax.plot(box[0] + (np.arange(len(row)) + 0.5) / B, row / row.mean(), lw=1, label=name)\n",
" ax.set_xlabel('x [pixel, 25 um]'); ax.set_ylabel('intensity / mean')\n",
" ax.set_title(f'{region}: row y = {(box[2] + box[3]) / 2:.0f} px'); ax.grid(alpha=.3); ax.legend(fontsize=8)\n",
"fig.tight_layout()"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "81b5a618",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T09:23:47.102562Z",
"iopub.status.busy": "2026-09-28T09:23:47.102478Z",
"iopub.status.idle": "2026-09-28T09:23:48.132289Z",
"shell.execute_reply": "2026-09-28T09:23:48.132064Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"DIPlib -- a quantitative image analysis library\n",
"Version 3.5.1 (Jul 5 2024)\n",
"For more information see https://diplib.org\n",
"region reconstruction FRC cutoff: median range crossed\n",
"LowerLeft eta interpolation 2.049 2.033-2.053 5/10\n",
"LowerLeft cluster CNN 1ph 2.033 2.024-2.045 4/10\n",
"LowerLeft cluster CNN <=3ph 2.062 2.062-2.062 1/10\n",
"LowerLeft GridBased 2-20 keV 2.169 2.151-2.342 10/10\n",
"LowerRight eta interpolation nan nan-nan 0/10\n",
"LowerRight cluster CNN 1ph nan nan-nan 0/10\n",
"LowerRight cluster CNN <=3ph nan nan-nan 0/10\n",
"LowerRight GridBased 2-20 keV nan nan-nan 0/10\n"
]
},
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABfgAAAHCCAYAAACzEKg2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAABDrAAAQ6wFQlOh8AAEAAElEQVR4nOzdd3wURRvA8d9eyV16TwgthN5771V6F0SUoiCKXVRQsVAUGxaK2F6QIiqCiCJK70WagPSahARCeq9X5v3jzElIaNLh+fK5T8js7Ozs7iWZmZt5VlNKKYQQQgghhBBCCCGEEEIIcUfR3eoKCCGEEEIIIYQQQgghhBDi6skAvxBCCCGEEEIIIYQQQghxB5IBfiGEEEIIIYQQQgghhBDiDiQD/EIIIYQQQgghhBBCCCHEHUgG+IUQQgghhBBCCCGEEEKIO5AM8AshhBBCCCGEEEIIIYQQdyAZ4BdCCCGEEEIIIYQQQggh7kAywC+EEEIIIYQQQgghhBBC3IFkgF8IIYQQQgghhBBCCCGEuAPJAL8QQgghhBBCCCGEEEIIcQeSAX4hxB0tPDycXr16ERgYiKZpDB069FZX6aYrU6YMrVu3vtXVuKQJEyYQGBhIenr6ra6KENckIiICk8nEDz/8cKurIoQQQtzWWrduTZkyZf7z/hEREWiaxrhx465bne4md0Lf59FHH6VatWrY7fZbXRUhrsnWrVvR6XRs27btVldFiCLJAL8Qt5H169ejaRpvv/32ra7KdTd79mw0TePbb7+9ruUOHTqUDRs2MGbMGObNm8fjjz/O3r17GTduHBEREddc/rhx49A07aKv6Oho4N8OyPkvs9lMxYoVefHFF0lOTi6y/LS0NN5//32aNWuGn58fRqOR4OBgOnXqxNdff01ubu41n8OtdvbsWT744ANeffVVPD09nekxMTG8/vrrdOnShZCQEDRNo3379pctb9GiRbRp0wYfHx9cXV0pX758oc7N8ePHGTduHM2aNaNYsWK4u7tTtWpVnn32WWJiYq7pfJKTk5k2bRpdunQhNDQUs9lMWFgY999/P3v27CmUPyMjgwkTJtCrVy9CQ0PRNI3y5csXWbZSivnz5/PQQw9RqVIl3N3dCQkJoU2bNvz8888opa6p7v/1muebPn16off+hRYvXkzjxo1xd3fH19eX7t278/fff19TvQEWLlzIsGHDqFOnDi4uLmiaxubNmy+aPzw8nCFDhhASEoLJZKJMmTK8+OKLpKWlFZk/MTGRkSNHUrx4cUwmE5UqVeL999/HarUWyFemTBmGDRvGmDFjyM7OvubzEkIIcfe4F9ry5788PT1p0KABU6dOxWaz3eoqsn79esaNG0dKSso1lzV06NCLtv8NBkOBY1643c3NjRo1ajBhwoSLthXi4uJ4/fXXqVevHj4+Pri4uFCiRAl69erFggUL7ooB8T179jBnzhwmTpyITvfv0NPSpUsZPnw41apVw9PTk8DAQJo0acKsWbMKtbvy2e12vvrqKxo3boynpyceHh5UrVqVV155pUC+EydO8PDDD1OlShVnX6FixYqMGDGCkydPXtP52Gw2Ro4cSf369QkMDMRkMlG6dGl69OjBxo0bC+W/2nb3p59+Stu2bSlevDhms5mgoCCaNWvG3Llzr/nn6+jRo7z88su0b98ef39/NE1j+PDhV7z/Sy+9VOi9f6Gvv/6aWrVq4erqSmBgIAMHDiQyMvKa6g3w1VdfMWjQIKpVq4bBYLhkPwRg37599OnTh4CAAGebfuLEiRftV0dGRjJw4EACAwNxdXWlVq1afP3114XyNW3alE6dOvH8889fc59MiBvh4j+dQghxm8vNzWXTpk08/fTTvPTSS8702bNnM378+GueNXS+N954g4oVKxZK9/PzK/B969atGTZsGABJSUksX76cjz/+mFWrVrFr1y5cXFyceffs2UOPHj04c+YMnTt3ZuzYsfj5+REXF8eaNWt4/PHH2blzJ1999dV1OYdb5YMPPkDTNB5//PEC6UePHuWdd96hRIkSNGjQgKVLl162rKeeeorPP/+c7t27M3HiRFxdXYmKimLr1q0F8s2cOZOpU6fStWtX+vXrh5ubG3/++SczZsxg/vz5bNmyhcqVK/+n89m+fTvPPfccrVq1YsSIERQrVozjx4/zxRdf8PPPPzN//nwGDBjgzJ+QkMBbb71FUFAQderUISkp6aJl5+bm8vDDD1O1alV69epFxYoVSUhIYObMmfTp04eRI0cyY8aM/1Rv+G/XPN/p06d59dVX8fDwICMjo8g8M2fOZPjw4VSvXp3333+f3Nxcpk+fTrNmzdi8eTO1atX6z3X/7LPP+PPPP6lRowaVK1dm//79F8179OhRmjZtSnp6Oo8//jjVq1fnwIEDTJs2jQ0bNrBp0yZcXV2d+dPT02nZsiVHjx7lySefpGbNmmzatIlXXnmFAwcOMG/evALljxo1is8//5xvvvmGJ5988j+fkxBCCHGnGTlyJE2bNkUpRUxMDHPmzOG5557j0KFDfPHFFwXyrly58qYOhK1fv57x48czdOhQfHx8rkuZU6dOxdfXt0Da+YPV+e6//3569uwJQHx8PD/++CNvvfUWW7duZfny5QXyrlq1igceeID09HT69OnD4MGD8fT05MyZMyxfvpwBAwZw8uRJXnvttetyDrfK+PHjKVOmDL179y6Q/thjj+Hq6krPnj2pVq0aaWlp/PDDDwwbNozFixezdOlSNE1z5rdarfTt25fff/+dBx54wPnhS0RERKEJXdHR0Zw5c4ZevXpRokQJTCYTx44dY9asWfzwww9s2bKFGjVq/KfzsVqtbN++naZNm/LQQw/h7e3N2bNn+fbbb2nVqhWzZs3ikUcecea/2nb3jh07KFmyJJ06dSIgIIC0tDR+++03hgwZwtq1a5k9e/Z/qjfAtm3bmDx5MmFhYTRo0IAVK1Zc8b67du3i008/xcPD46IfWL3xxhu8/fbbNGvWjE8//ZT4+HimTJnC+vXrnef1X02aNImEhARq165NmTJlLvlBzebNm2nfvj1Go5GnnnqKsLAwtm3bxltvvcX27dsLvbeio6Np3LgxqampPP/884SFhbF06VJGjBjB6dOnmThxYoHyR40aRYcOHfjjjz/o0qXLfz4nIW4IJYS4baxbt04BauLEibe6KtdNRkaGUkqpb775RgFq3rx5163syMhIBai33nqrQHr+sdatW3fNx3jrrbcUoDZt2nTJfOHh4QpQw4YNK7StZ8+eClCLFi1ypsXGxqqQkBDl7u6u1qxZU2SZe/fuVZMnT75sHUNDQ1WrVq0um+9WyMjIUN7e3uqRRx4ptC0tLU3FxsY6vwdUu3btLlrW3LlzFaC++uqryx53586dKikpqVD6l19+qQDVr1+/KzyDwsLDw9XRo0cLpe/fv18ZjUZVrFgxZbPZnOk5OTnq9OnTzu9DQ0NVuXLliizbYrGo1atXF0rPyMhQFSpUUIA6ePDgf6771V7z83Xq1EnVq1dPPfzwwwpQUVFRBbYnJSUpLy8vVbJkSZWamupMj4qKUp6enqpFixb/ud5KKRUREaHy8vKUUpf/uezVq5cC1HfffVcg/bvvvlOAeueddwqkv/HGGwpQH330UYH0559/XgFF/ow2bdpUVa9e/VpOSQghxF3mXmzLp6enq5IlSyqdTqfi4uKu67Hz29cXtvUvJr99EB4efs3HHjJkSJHtnQtd7J5brVZVr149Bahdu3Y50w8fPqzc3d1VsWLF1N69e4ssc+3atWrmzJmXrSOghgwZcvmTuQUiIiKUXq9X48ePL7Rt1apVym63F0izWq2qefPmClDLli0rsG3ChAlKp9Op5cuX/+f6/Pnnnwoosk9yrdLS0lRgYKAKCwsrlP5f293n69y5swJUZGTkf65jQkKCs290qX7rhfLy8lSNGjVUr169VKtWrZRery+U5+jRo0qv16u6desqi8XiTP/rr7+UTqdTgwYN+s/1VkqpU6dOOftWl/u5rF27ttI0TW3evLlA+qRJkxSg5s+fXyB90KBBClA//fRTgfTevXsrvV6vjh07ViDdZrOpkiVLqq5du17TOQlxI0iIHiHuUEeOHGHAgAEEBwdjMpkoW7YsL730UoHwE2fOnEHTNF588cUC+44YMaLIGdWjRo1C0zRiY2OdaXl5eXzwwQfUrFkTV1dXvLy8aN++faFliOfHyPzpp59o2LAhbm5udO3a9arP7eTJkwwdOpTixYvj4uJCyZIlefLJJ0lISHDmad26NaGhoYBjdkj+ktgyZco4Z060adPGmX4r41PmL8U8fvy4M+3DDz8kJiaG9957j7Zt2xa5X61atQrdu0vZt28f9913H56ennh7e9O3b1/Cw8ML5cvJyWH8+PFUrlwZs9mMn58f3bt3Z9euXc48Z8+eJSgoiPLlyxcKaZIftujTTz+9bJ3++OMPUlNTi3wfeHp6EhQUdMXnN3HiRGrVqsVjjz0GOGZdX2z5cv369QvNtgKcM+uvJWRMmTJlilzNUb16dapXr865c+eIi4tzpptMJkqVKnVFZRsMBtq1a1co3d3dnW7dugHXVverveb55s6dy6pVq/j666/R6/VF5vnll19IS0tj+PDheHl5OdNLlixJ//792bRp0zWFzQoNDcVoNF5R3rVr1+Lq6lpgJQXAAw88gNlsZtasWQXS586di6urKyNHjiyQnv/zN3fu3ELH6NKlCwcOHODAgQNXcxpCCCEEcPe05T08PGjUqBF2u73QzNqLraZdvnw5jRo1wtXVlaCgIB577DGSkpIu2Wb/448/aNy4sXOfJ554gqysrALHGj9+PABhYWHOPsCtit+v1+udz8g6vw/wxhtvkJmZ6QxnUpQ2bdrw6KOPXvGx1q1bR7NmzXB3dycgIIBHHnmE+Pj4QvlSUlIYNWoUYWFhmEwmgoODefDBBwvUb//+/bi5udG0adNLine truncated
"text/plain": [
"<Figure size 1540x462 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# one_frc splits the counts binomially with its own (unseeded) generator, so one\n",
"# call is one draw: repeat it and quote the median and the range\n",
"N_REPEAT = 10\n",
"frc_results = {}\n",
"try:\n",
" import frc\n",
" fig, axes = plt.subplots(1, len(REGIONS), figsize=(7 * len(REGIONS), 4.2))\n",
" for ax, region in zip(np.atleast_1d(axes), REGIONS):\n",
" frc_results[region] = {}\n",
" for name, img in images[region]['frc'].items():\n",
" if img is None or name.startswith('native'):\n",
" continue\n",
" square = frc.util.square_image(np.array(img, float), add_padding=False)\n",
" size = square.shape[0]\n",
" values, first = [], None\n",
" for _ in range(N_REPEAT):\n",
" curve = frc.one_frc(square)\n",
" xs = np.arange(len(curve)) / size\n",
" first = curve if first is None else first\n",
" try:\n",
" values.append(float(frc.frc_res(xs, curve, size)[0]))\n",
" except Exception: # the curve never crosses 1/7\n",
" values.append(np.nan)\n",
" values = np.array(values)\n",
" found = np.isfinite(values)\n",
" frc_results[region][name] = dict(\n",
" median=float(np.median(values[found])) if found.any() else np.nan,\n",
" low=float(values[found].min()) if found.any() else np.nan,\n",
" high=float(values[found].max()) if found.any() else np.nan,\n",
" crossings=int(found.sum()), repeats=N_REPEAT)\n",
" r = frc_results[region][name]\n",
" ax.plot(np.arange(len(first)) / size, first, lw=1,\n",
" label=f\"{name}: {r['median']:.3f} ({r['crossings']}/{N_REPEAT} crossed)\")\n",
" ax.axhline(1 / 7, color='k', lw=.6, ls=':')\n",
" ax.set_xlabel('spatial frequency (1/pixels)'); ax.set_ylabel('FRC')\n",
" ax.set_title(f'{region}, FRC box {REGIONS[region][\"frc\"]}'); ax.grid(alpha=.3); ax.legend(fontsize=8)\n",
" fig.tight_layout()\n",
" print(f\"{'region':11s} {'reconstruction':20s} {'FRC cutoff: median':>18s} {'range':>15s} {'crossed':>8s}\")\n",
" for region, values in frc_results.items():\n",
" for name, r in values.items():\n",
" print(f\"{region:11s} {name:20s} {r['median']:18.3f} {r['low']:7.3f}-{r['high']:.3f} \"\n",
" f\"{r['crossings']:5d}/{r['repeats']}\")\n",
"except Exception as error:\n",
" print('FRC skipped:', repr(error))"
]
},
{
"cell_type": "markdown",
"id": "533a651d",
"metadata": {},
"source": [
"## 3. Photon statistics\n",
"\n",
"Photons in each whole region. The cluster chains keep only clusters that fall in\n",
"the 1/2/3-photon energy windows; at this occupancy the rest is lost to pile-up\n",
"(`SiementStar_LowerLeft.ipynb`: 45 % of the photons are in single-photon\n",
"clusters)."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "47cb5a91",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T09:23:48.133530Z",
"iopub.status.busy": "2026-09-28T09:23:48.133348Z",
"iopub.status.idle": "2026-09-28T09:23:48.148052Z",
"shell.execute_reply": "2026-09-28T09:23:48.147815Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"region eta (1ph) CNN 1ph CNN <=3ph GridBased photons per frame: GridBased / CNN <=3ph\n",
"LowerLeft 1.4060e+09 1.4060e+09 1.6677e+09 1.8519e+09 185.2 / 83.4 = 2.22\n",
"LowerRight 2.7039e+09 2.7039e+09 4.4703e+09 6.5713e+09 657.1 / 223.5 = 2.94\n",
"(GridBased over 10,000,000 frames; the cluster chains used all 20 M)\n"
]
}
],
"source": [
"counts = {}\n",
"CLUSTER_FRAMES = 20e6 # the cluster chains used the whole run\n",
"print(f\"{'region':11s} {'eta (1ph)':>12s} {'CNN 1ph':>12s} {'CNN <=3ph':>12s} {'GridBased':>12s} \"\n",
" f\"photons per frame: GridBased / CNN <=3ph\")\n",
"for region in REGIONS:\n",
" c = {name: float(np.nansum(img)) for name, img in images[region]['roi'].items()\n",
" if img is not None and not name.startswith('native')}\n",
" counts[region] = c\n",
" cnn = c.get('cluster CNN <=3ph', np.nan)\n",
" print(f\"{region:11s} {c['eta interpolation']:12.4e} {c.get('cluster CNN 1ph', np.nan):12.4e} {cnn:12.4e} {c['GridBased 2-20 keV']:12.4e} \"\n",
" f\"{c['GridBased 2-20 keV'] / frames.sum():8.1f} / {cnn / CLUSTER_FRAMES:6.1f} = \"\n",
" f\"{(c['GridBased 2-20 keV'] / frames.sum()) / (cnn / CLUSTER_FRAMES):.2f}\")\n",
"print(f'(GridBased over {frames.sum():,.0f} frames; the cluster chains used all 20 M)')"
]
},
{
"cell_type": "markdown",
"id": "b2edee48",
"metadata": {},
"source": [
"## 4. Energy spectrum from the energy head"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "75e762b8",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T09:23:48.149351Z",
"iopub.status.busy": "2026-09-28T09:23:48.149259Z",
"iopub.status.idle": "2026-09-28T09:23:48.409693Z",
"shell.execute_reply": "2026-09-28T09:23:48.409471Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"15 keV line at 15.022 keV, core sigma 0.514 keV; below 13.5 keV 11.38 %, 13.5-16.5 keV 83.31 %, above 16.5 keV 5.31 %\n"
]
},
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 825x462 with 1 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"def gauss(x, a, mu, sig):\n",
" return a * np.exp(-0.5 * ((x - mu) / sig)**2)\n",
"\n",
"\n",
"total = spectrum.sum()\n",
"near = np.abs(spectrum_centres - 15) < 2\n",
"peak = spectrum_centres[near][np.argmax(spectrum[near])]\n",
"window = np.abs(spectrum_centres - peak) < 0.8\n",
"try:\n",
" (a, mu, sig), _ = curve_fit(gauss, spectrum_centres[window], spectrum[window],\n",
" p0=(spectrum[window].max(), peak, 0.3), maxfev=20000)\n",
"except Exception:\n",
" if spectrum[window].sum() > 0:\n",
" mu = np.average(spectrum_centres[window], weights=spectrum[window])\n",
" sig = np.sqrt(np.average((spectrum_centres[window] - mu)**2, weights=spectrum[window]))\n",
" else:\n",
" mu = sig = np.nan\n",
"fractions = dict(below=spectrum[spectrum_centres < 13.5].sum() / total,\n",
" line=spectrum[(spectrum_centres >= 13.5) & (spectrum_centres <= 16.5)].sum() / total,\n",
" above=spectrum[spectrum_centres > 16.5].sum() / total)\n",
"print(f'15 keV line at {mu:.3f} keV, core sigma {abs(sig):.3f} keV; below 13.5 keV {fractions[\"below\"]*100:.2f} %, '\n",
" f'13.5-16.5 keV {fractions[\"line\"]*100:.2f} %, above 16.5 keV {fractions[\"above\"]*100:.2f} %')\n",
"fig, ax = plt.subplots(figsize=(7.5, 4.2))\n",
"ax.semilogy(spectrum_centres, spectrum / total / np.diff(spectrum_centres)[0], lw=1)\n",
"ax.axvline(15, color='grey', lw=.6, ls=':')\n",
"ax.set_xlim(0, 30); ax.set_ylim(1e-6, 5)\n",
"ax.set_xlabel('reconstructed photon energy [keV]'); ax.set_ylabel('photons / keV (normalised)')\n",
"ax.set_title('Siemens star, both regions, GridBased 2-20 keV'); ax.grid(alpha=.3, which='both')\n",
"fig.tight_layout()"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "42d1185c",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-28T09:23:48.410840Z",
"iopub.status.busy": "2026-09-28T09:23:48.410746Z",
"iopub.status.idle": "2026-09-28T09:23:48.503131Z",
"shell.execute_reply": "2026-09-28T09:23:48.502878Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"written /home/xie_x1/MLXID/GridBased/Results/overnight_poly2to20keV_Moench040/report/siemens_star_poly2to20keV_summary_50pct.json\n"
]
}
],
"source": [
"summary = dict(parts=parts, frames=float(frames.sum()),\n",
" photons=float(sum(s['n_photons'] for s in summaries)),\n",
" model=summaries[0]['checkpoint'], epoch=summaries[0]['epoch'], crop=summaries[0]['crop'],\n",
" regions={k: v['roi'] for k, v in REGIONS.items()}, photon_counts=counts,\n",
" frc_cutoff=frc_results, line_keV=float(mu), line_sigma_keV=float(abs(sig)),\n",
" spectrum_fractions=fractions)\n",
"out = OVERNIGHT / 'report' / os.environ.get('GRID_STAR_SUMMARY', 'siemens_star_poly2to20keV_summary.json')\n",
"out.parent.mkdir(parents=True, exist_ok=True)\n",
"json.dump(summary, open(out, 'w'), indent=1)\n",
"regions_dir = GRID / f'SiemensStar_15keV_{POLY_TAG}_regions' # the two regions as their own arrays\n",
"regions_dir.mkdir(exist_ok=True)\n",
"for region in REGIONS:\n",
" np.save(regions_dir / f'{region}_binned_image_flattened.npy', images[region]['roi']['GridBased 2-20 keV'])\n",
" np.save(regions_dir / f'{region}_binned_image.npy', cut_grid(raw, REGIONS[region]['roi']))\n",
"json.dump(summary, open(regions_dir / 'summary.json', 'w'), indent=1)\n",
"print('written', out)"
]
}
],
"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
}
+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": "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 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": "iVBORw0KGgoAAAANSUhEUgAAAuQAAAGuCAYAAADYjxPyAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAACBfElEQVR4nO3dd1gUV9sG8HvoTUCkK03FShMrVuw1ltjFrlFjjFGTWKKJJZZo1KgxtqioWIK9x95Q7L2LBbGAoEiRDnu+P3ydzw2grMAuwv27Li7dMzNnnnkAffbsmTOSEEKAiIiIiIg0QkvTARARERERFWUsyImIiIiINIgFORERERGRBrEgJyIiIiLSIBbkREREREQaxIKciIiIiEiDWJATEREREWkQC3IiIiIiIg1iQU5EREREpEEsyImIqMCZNGkSJEn6pGNv3bqFSZMmITQ0NNO2vn37wtnZOXfBERHlMRbkRERUqNy6dQuTJ0/OsiD/+eefsW3bNvUHRUT0ATqaDoCIiLKWmJgIIyMjjZ0/LS0NkiRBR6fw/FdRpkwZTYdARJQJR8iJiN4TEhKCHj16wNraGvr6+qhYsSL++usvpX2OHTsGSZKwYcMGjB8/Hvb29jA1NUWTJk1w9+7dTH0eOnQIjRs3hqmpKYyMjFCnTh0cPnxYaZ93UzQuXbqETp06oXjx4nLxmJKSgu+//x62trYwMjJC/fr1cfHiRTg7O6Nv374AgNDQUOjo6GDGjBmZzn/ixAlIkoRNmzZle93vrikgIADff/89SpYsCX19fdy/fz/H1xAVFYVBgwbBwcEB+vr6sLKyQp06dXDo0CGl/VauXAlPT08YGBjAwsICHTp0wO3bt7ON7R1JkjBp0qRM7e/nYdWqVejcuTMAoGHDhpAkCZIkYdWqVQCynrKSnJyMcePGwcXFBXp6eihZsiS++eYbxMTEZDpPmzZtsG/fPnh7e8PQ0BAVKlTAypUrPxo7EdGHsCAnIvqfW7duoXr16rhx4wbmzJmD3bt3o3Xr1hg+fDgmT56caf+ffvoJjx8/xvLly7Fs2TKEhITgiy++QEZGhrzP2rVr0axZM5iammL16tXYuHEjLCws0Lx580wFLQB8+eWXKFu2LDZt2oQlS5YAAPr164d58+ahX79+2LFjBzp27IgOHTooFYzOzs5o27YtlixZonR+AFi4cCHs7e3RoUOHj+Zg3LhxCAsLw5IlS7Br1y5YW1vn+Bp69eqF7du345dffsGBAwewfPlyNGnSBK9evZL3mTFjBgYMGIDKlStj69atmD9/Pq5duwYfHx+EhIR8NL6Pad26NaZPnw4A+Ouvv3D69GmcPn0arVu3znJ/IQTat2+P2bNno1evXtizZw9GjRqF1atXo1GjRkhJSVHa/+rVq/j+++8xcuRI7NixAx4eHhgwYABOnDiR69iJqAgTREQkhBCiefPmolSpUiI2NlapfdiwYcLAwEBER0cLIYQ4evSoACBatWqltN/GjRsFAHH69GkhhBAJCQnCwsJCfPHFF0r7ZWRkCE9PT1GjRg25beLEiQKA+OWXX5T2vXnzpgAgxowZo9S+YcMGAUD06dNHbnsX17Zt2+S2Z8+eCR0dHTF58uQPXvu7Y+vXr6/Urso1mJiYiBEjRmR7jtevXwtDQ8NMeQsLCxP6+vqiR48ectu7fLwPgJg4cWKmfp2cnJTysGnTJgFAHD16NNO+ffr0EU5OTvLrffv2CQBi1qxZSvsFBgYKAGLZsmVK5zEwMBCPHz+W25KSkoSFhYUYPHhwttdNRPQxHCEnIsLbaQuHDx9Ghw4dYGRkhPT0dPmrVatWSE5OxpkzZ5SOadu2rdJrDw8PAMDjx48BAMHBwYiOjkafPn2U+lMoFGjRogXOnz+PhIQEpT46duyo9Pr48eMAgC5duii1d+rUKdPcbl9fX3h6eipNsVmyZAkkScKgQYNylIf/nl+Va6hRowZWrVqFqVOn4syZM0hLS1Pq6/Tp00hKSpKnl7zj4OCARo0aZfmJQX47cuQIAGSKqXPnzjA2Ns4Uk5eXFxwdHeXXBgYGKFeunPw9JyL6FCzIiYgAvHr1Cunp6fjzzz+hq6ur9NWqVSsAwMuXL5WOKVGihNJrfX19AEBSUhIA4MWLFwDeFs//7XPmzJkQQiA6OlqpDzs7u0xxAYCNjY1Su46OTqbzA8Dw4cNx+PBh3L17F2lpafj777/RqVMn2Nra5igP/z2/KtcQGBiIPn36YPny5fDx8YGFhQV69+6NiIgIpWv57zkAwN7eXmlqi7q8evUKOjo6sLKyUmqXJAm2traZYsoq5/r6+vL3nIjoUxSeW+eJiHKhePHi0NbWRq9evfDNN99kuY+Li4tKfVpaWgIA/vzzT9SqVSvLff5baP937e13BeCLFy9QsmRJuT09PT3LArZHjx4YM2YM/vrrL9SqVQsRERHZXk9W/nt+Va7B0tIS8+bNw7x58xAWFoadO3di7NixiIyMxL59++RrCQ8Pz9TH8+fP5XNlR19fP9OcbgC5KuRLlCiB9PR0REVFKRXlQghERESgevXqn9w3EVFOsSAnIgJgZGSEhg0b4vLly/Dw8ICenl6u+6xTpw7Mzc1x69YtDBs27JP6qF+/PoC3o8/e3t5y++bNm5Genp5pfwMDAwwaNAgLFy5EcHAwvLy8UKdOnU+7AHz6NTg6OmLYsGE4fPgwTp06BQDw8fGBoaEh1q5dK6+EAgBPnz7FkSNH0KlTpw/26ezsjGvXrim1HTlyBG/evFFq++8nFR/SuHFjzJo1C2vXrsXIkSPl9i1btiAhIQGNGzf+aB9ERLnFgpyI6H/mz5+PunXrol69evj666/h7OyM+Ph43L9/H7t27ZLnG+eUiYkJ/vzzT/Tp0wfR0dHo1KkTrK2tERUVhatXryIqKgqLFy/+YB+VK1dG9+7dMWfOHGhra6NRo0a4efMm5syZAzMzM2hpZZ55OHToUMyaNQsXL17E8uXLVYr5U68hNjYWDRs2RI8ePVChQgUUK1YM58+fx759+/Dll18CAMzNzfHzzz/jp59+Qu/evdG9e3e8evUKkydPhoGBASZOnPjBWHr16oWff/4Zv/zyCxo0aIBbt25h4cKFMDMzU9rPzc0NALBs2TIUK1YMBgYGcHFxyXK6SdOmTdG8eXOMGTMGcXFxqFOnDq5du4aJEyeiSpUq6NWrV67yR0SUEyzIiYj+p1KlSrh06RJ+/fVXTJgwAZGRkTA3N4erq6s8j1xVPXv2hKOjI2bNmoXBgwcjPj4e1tbW8PLyynQjYXb8/f1hZ2eHFStW4I8//oCXlxc2btyIFi1awNzcPNP+JUuWRN26dXHt2jX06NHjk+JW9RoMDAxQs2ZNBAQEIDQ0FGlpaXB0dMSYMWMwevRoua9x48bB2toaCxYsQGBgIAwNDeHr64vp06fD1dX1g3H8+OOPiIuLw6pVqzB79mzUqFEDGzduRLt27ZT2c3Fxwbx58zB//nz4+voiIyMD/v7+WeZbkiRs374dkyZNgr+/P6ZNmwZLS0v06tUL06dPl0fbiYjykySEEJoOgoiIVBMcHIw6depg3bp1mYruyMhIODk54dtvv8WsWbM0FCEREeUUC3IiogLu4MGDOH36NKpWrQpDQ0NcvXoVv/32G8zMzHDt2jUYGBgAeDsX++HDh/j9999x5MgR3Lt3T+lGUCIiKpg4ZYWIqIAzNTXFgQMHMG/ePMTHx8PS0hItW7bEjBkz5GIcAJYvX44pU6bA2dkZ69atYzFORPSZ4Ag5EREREZEG8cFAREREREQaxIKciIiIiEiDWJATEREREWlQkbupU6FQ4Pnz5yhWrFimR0QTEREREeUFIQTi4+Nhb2+f5UPc3lfkCvLnz5/DwcFB02EQERERURHw5MkTlCpV6oP7FLmCvFixYgDeJsfU1FTD0Xy+FAoFoqKiYGVl9dF3fZQ7zLX6MNfqw1yrF/OtPsy1+hT0XMfFxcHBwUGuPT+kyBXk76apmJqasiDPBYVCgeTkZJiamhbIX4LChLlWH+ZafZhr9WK+1Ye5Vp/PJdc5mSJdcKMnIiIiIioCWJATEREREWkQC3IiIiIiIg3SeEG+aNEiuLi4wMDAAFWrVkVQUFC2+x47dgySJGX6unPnjhojJiIiIiLKOxq9qTMwMBAjRozAokWLUKdOHSxduhQtW7bErVu34OjomO1xd+/eVboh08rKSh3hEhEVWrGxsYiLi8t2u6mpKczMzNQYERFR0aHRgnzu3LkYMGAABg4cCACYN28e9u/fj8WLF2PGjBnZHmdtbQ1zc3M1RUlEVPidPHkS//77b7bbW7ZsidatW6sxIiKiokNjBXlqaiouXryIsWPHKrU3a9YMwcHBHzy2SpUqSE5ORqVKlTBhwgQ0bNgwP0MlIir06tatCw8PD6SmpuKPP/4AAIwcORJ6enoAwGViiYjykcYK8pcvXyIjIwM2NjZK7TY2NoiIiMjyGDs7OyxbtgxVq1ZFSkoKAgIC0LhxYxw7dgz169fP8piUlBSkpKTIr999JKtQKKBQKPLoaooehUIBIQRzqAbMtfoU5VwXK1YMxYoVU/r30t7eHvr6+vLrvMxLUc61JjDf6sNcq09Bz7UqcWn8wUD/XSxdCJHtAurly5dH+fLl5dc+Pj548uQJZs+enW1BPmPGDEyePDlTe1RUFJKTk3MRedGmUCgQGxsLIUSBXoy/MGCu1Ye5fvvp5TtRUVHyCHleY67Vi/lWH+ZafQp6ruPj43O8r8YKcktLS2hra2caDY+MjMw0av4htWrVwtq1a7PdPm7cOIwaNUp+/e4xplZWVvwINhcUCgUkSSqwj6stTJhr9WGuoTRCbmVlpTRCnpeYa/VivtWHuVafgp5rAwODHO+rsYJcT08PVatWxcGDB9GhQwe5/eDBg2jXrl2O+7l8+TLs7Oyy3a6vr5/lfyhaWloF8pv3OZEkiXlUE+ZafYp6rl+Line 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
}
+591
View File
@@ -0,0 +1,591 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "a4f1f029",
"metadata": {},
"source": [
"# XRF CuCrCo_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 a Cu/Cr/Co sample under a 12 keV beam; slit factor 10,\n",
"**1.28 % occupancy**, 2.0e8 photons in 100 000 frames. Cr 5.41, Co 6.93 and\n",
"Cu 8.04 keV Ka lines 1.1-1.5 keV apart with their Kb lines in between (Co Kb\n",
"0.39 keV below Cu Ka), plus the elastic line at 12 keV. Cr Ka sat 0.4 keV\n",
"above the lower edge of the previous 5-30 keV model, which biased and\n",
"compressed it; the 3-30 keV model puts it 2.4 keV inside its range.\n",
"`XRF_AgMo_1_factor_10.ipynb` has the high-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 CuCrCo_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": "865cec36",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:29.570680Z",
"iopub.status.busy": "2026-09-19T09:10:29.570598Z",
"iopub.status.idle": "2026-09-19T09:10:29.817584Z",
"shell.execute_reply": "2026-09-19T09:10:29.817269Z"
}
},
"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 = 'CuCrCo_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 = [\n",
" # one joint fit; each Kb tied to its Ka with the tabulated offset. Co Kb\n",
" # cannot be separated from Cu Ka, so its Kb/Ka ratio is fixed at 0.135.\n",
" dict(label='Cr/Co/Cu K', alpha=['Cr_Ka', 'Co_Ka', 'Cu_Ka'],\n",
" beta={'Cr_Kb': 'Cr_Ka', 'Co_Kb': 'Co_Ka', 'Cu_Kb': 'Cu_Ka'},\n",
" ratio_fixed={'Co_Kb': 0.135}, fit_stretch=False, window=(4.85, 9.6)),\n",
" dict(label='elastic', alpha=[], extra=[('elastic', 12.0)], window=(10.9, 13.2)),\n",
"]\n",
"SPECTRUM_ZOOM = (4.0, 13.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": "b782342f",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:29.819386Z",
"iopub.status.busy": "2026-09-19T09:10:29.819219Z",
"iopub.status.idle": "2026-09-19T09:10:40.369835Z",
"shell.execute_reply": "2026-09-19T09:10:40.369529Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"CuCrCo_1_factor_10: 100000 frames, 1985 photons/frame, occupancy 1.29 %, 0.7 % 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": "d8f721c4",
"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": "a9ff6281",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:40.371116Z",
"iopub.status.busy": "2026-09-19T09:10:40.370965Z",
"iopub.status.idle": "2026-09-19T09:10:40.638231Z",
"shell.execute_reply": "2026-09-19T09:10:40.637990Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Cr_Ka: g - E = -7 meV, g' = 1.00, MC core sigma 0.256 keV\n",
"Co_Ka: g - E = +0 meV, g' = 1.00, MC core sigma 0.264 keV\n",
"Cu_Ka: g - E = +2 meV, g' = 1.00, MC core sigma 0.267 keV\n"
]
},
{
"data": {
"image/png": "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z/jnn38YNWoUs2fPZvTo0Tnq9P3337N//37mzZvHP//8Q7169Thw4ABt2rQhNjaWJUuWsG3bNry9vRk0aFCB9tYLCwtjzJgxbNiwgc2bN9O/f3/eeecdZs6cme9zC+OHH35gz549LFiwgDVr1pCUlMQLL7xAXFycNs3TtCUlJYVOnTqxcuVKJk6cyN9//81rr73G119/Tf/+/QGoWrUqfn5+NG7cGDc3N/z8/PDz88PHxyfXPLds2YKbmxuNGzfWpt2yZYtOmsmTJxMeHs6SJUv466+/cHR0LFS/fPnllwwePJgGDRqwYcMGVq1aRUJCAu3atSMoKOixzy3s66c47sHIkSMxNjZm1apV/PHHHxgbGzNlyhSmTJlCjx492LZtG2PHjuWNN97gypUr2udZWVkxcuRI1qxZo1M+ZC33NjExyfUNzKMURSEjIyPHV36B7vv37wPw+eef8/fff7N8+XLc3Nzo2LFjroHjRYsWsXPnThYsWMDq1asxMDDg+eefx8/PL0fafv36Ubt2bf744w+mTZvG1q1b6d69O+np6do0Bb3vYWFh1K1blwULFrBr1y6++uorIiIiaNasGffu3ctRdm73Y+jQoWzdupXPPvuM3bt3s2zZMrp27UpMTEy+/SuEEKJsKy3jashastqsWTMCAwP55ptv2L59Oz179mT8+PFMnz49R/pPPvmEGzdusGzZMpYuXUpISAi9evUiMzNTm2b16tV069YNGxsbfvvtNzZs2EDFihXp3r17jkAbQP/+/alduzYbN25kyZIlALz++ussWLCA119/nW3btvHSSy/Rr18/nUCWi4sLvXv3ZsmSJTrlQ9YYoVq1avTr1y/fPsht3FjQNhTk7/ns2bMZNWoUDRs2ZPPmzXz33XecP3+eVq1aERISkm/98tOzZ0++/PJLIGtcmT0+7tmzZ67pFUWhb9++zJs3j6FDh/L3338zceJEfvvtNzp37qz9MD9bQd4/CSEeQxGinFm+fLkCKKdOnVIOHDigAEpgYKCiKIrSrFkzZcSIEYqiKErDhg2VDh06aJ/Xo0cPBVBSUlIKXJahoaHSuXPnfNO99tpripubm/b7rl27KqNHj1bs7OyU3377TVEURTl69KgCKLt379amc3Z2VszMzJQbN25oryUnJysVK1ZUxowZo702ZswYxcrKSiedoijKvHnzFEC5ePGioiiKEhoaqgBKrVq1lLS0NJ209erVUxo3bqykp6frXH/xxReVqlWrKpmZmfm2M1tmZqaSnp6uzJgxQ6lUqZKi0Wgem/7zzz9XHv111KFDB537k113T09PJSMjQ3v95MmTCqCsXbu2SNqyZMkSBVA2bNigc/2rr77KcX86dOigNGzY8LFty/bo6y1b9mu0ffv2OR7LrV8U5b/XeGhoqKIoihIeHq4YGRkp77zzjk66hIQEpUqVKsrAgQMfW7fCvn6K8h5kt2XYsGE66e7fv6+YmpoqgwYN0rnu5+enADp9ee3aNcXAwED59ttvtdeSk5OVSpUqKa+//vpj264oigLk+bVq1ap8n/+wjIwMJT09XenSpYvSr18/7fXsvqtWrZqSnJysvR4fH69UrFhR6dq1q/Za9n1/7733dPJes2aNAiirV69WFOXp7ntGRoaSmJioWFpaKt999532el73Q1EUxcrKSpkwYUIBe0IIIUR5UBrH1d27d1eqV6+uxMXF6Vx/++23FTMzM+X+/fuKovw3xnrhhRd00m3YsEEBFD8/P0VRFCUpKUmpWLGi0qtXL510mZmZSqNGjZTmzZtrr2X/jf7ss8900l68eFEBlI8++kjn+tq1axVAGT58uPZadr22bNmivXb79m3FyMhImT59+mPbnte4sTBtyO/v+YMHDxRzc/Mc/RYeHq6YmpoqQ4YM0V7LbawKKJ9//nmOfJ2dnXX6YePGjQqgHDhwIEfa4cOHK87Oztrvd+7cqQDK119/rZNu/fr1CqAsXbpUp5yCvH8SQuRNZhSKcq1Dhw7UqlWLX3/9lQsXLnDq1KkCzS4qqIyMjFw/ZXxUly5duH79OqGhoaSkpODr60uPHj3o1KkTe/bsAbJmGZqamtK2bVud53p7e1OzZk3t92ZmZtSpU0dnyeD27dvp1KkT1apV05kN9fzzzwNZG00/rHfv3jp7wly9epXLly9r99p7OI8XXniBiIiIXJdoPGz//v107doVW1tbDA0NMTY25rPPPiMmJoaoqKh8+6igevbsiaGhofZ7Ly8vAG1/PG1b9u/fj6WlJQMGDNC5nr1UoiD3+0m89NJLT/zcXbt2kZGRwbBhw3Taa2ZmRocOHfJcEp2tsK+f4rgHj7b/+PHjpKamMnDgQJ3rLVu2zLEUxc3NjRdffJHFixdrZwD+/vvvxMTE8Pbbbz+27dkGDhzIqVOncny98MIL+T53yZIl+Pj4YGZmhpGREcbGxuzbty/X5Tn9+/fHzMxM+721tTW9evXi8OHDOWYWPLr35cCBAzEyMtJuFVCY+56YmMhHH31E7dq1MTIywsjICCsrK5KSknKtZ26vx+bNm7NixQpmzZrF8ePHdWY2CiGEKP9Kw7gLine 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": "2d694291",
"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": "88a7f95b",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:40.639432Z",
"iopub.status.busy": "2026-09-19T09:10:40.639337Z",
"iopub.status.idle": "2026-09-19T09:10:49.511881Z",
"shell.execute_reply": "2026-09-19T09:10:49.511595Z"
}
},
"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": "02b8297f",
"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": "e00064c3",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:49.513200Z",
"iopub.status.busy": "2026-09-19T09:10:49.513104Z",
"iopub.status.idle": "2026-09-19T09:10:54.511973Z",
"shell.execute_reply": "2026-09-19T09:10:54.511727Z"
}
},
"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",
"Cr_Ka 5.41 | 5.42 5.42 5.46 | 0.245 0.245 0.398 | 0.58 0.94 0.62 | 0.256\n",
"Co_Ka 6.93 | 6.95 6.95 6.99 | 0.265 0.264 0.408 | 0.62 0.96 0.65 | 0.264\n",
"Cu_Ka 8.04 | 8.08 8.07 8.11 | 0.305 0.305 0.441 | 0.72 1.04 0.69 | 0.267\n",
"elastic | 11.99 11.99 12.05 | 0.311 0.312 0.470 | 0.73 1.11 0.66 | 0.279\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": "d7c0b45e",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:54.513111Z",
"iopub.status.busy": "2026-09-19T09:10:54.513023Z",
"iopub.status.idle": "2026-09-19T09:10:55.229598Z",
"shell.execute_reply": "2026-09-19T09:10:55.229316Z"
}
},
"outputs": [
{
"data": {
"image/png": "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 truncated
"text/plain": [
"<Figure size 1760x600 with 8 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": "48f4f57f",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:55.230821Z",
"iopub.status.busy": "2026-09-19T09:10:55.230692Z",
"iopub.status.idle": "2026-09-19T09:10:55.331040Z",
"shell.execute_reply": "2026-09-19T09:10:55.330825Z"
}
},
"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": "32c28598",
"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": "045ab180",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:55.332205Z",
"iopub.status.busy": "2026-09-19T09:10:55.332119Z",
"iopub.status.idle": "2026-09-19T09:10:56.819582Z",
"shell.execute_reply": "2026-09-19T09:10:56.819355Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"isolated photons >= 5 keV found: 99.99 % (3-5 keV: 100.0 %; MC: 99.96 %)\n",
"grid / classical photon count: 1.067\n",
"classical clusters with >= 2 grid photons: 10.2 % (Poisson, second photon within 2 px: 15.0 %)\n",
"energy balance: grid 0.9961, classical 0.9991\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": "4e5818b8",
"metadata": {},
"source": [
"## Conclusions\n",
"\n",
"* **Lines.** The grid separates Cr, Co and Cu Ka cleanly; the classical 3x3\n",
" sum shows Cu Ka only as a shoulder. Grid positions within 0.04 keV of the\n",
" tables, classical 0.05-0.07 keV high (the LUT reads ~0.9 % high at\n",
" 5-8 keV). The pile-up continuum at 13-25 keV is 5-10x lower than in the\n",
" classical spectrum.\n",
"* **Cr Ka and the training range.** With Cr Ka 2.4 keV inside the 3-30 keV\n",
" range, the model puts it at +0.01 keV with a 0.245 keV core (MC 0.256)\n",
" straight away. The previous 5-30 keV model, whose edge sat 0.4 keV below the\n",
" line, gave +0.03 keV with a compressed 0.18 keV core, and its MC correction\n",
" overshot. Here the correction changes nothing (<= 7 meV).\n",
"* **Energy resolution.** FWHM 0.58 / 0.62 / 0.72 keV for Cr / Co / Cu Ka and\n",
" 0.73 keV for the elastic line, against 0.94 / 0.96 / 1.04 / 1.11 keV for the\n",
" classical sum: 31-38 % narrower. The core widths agree with the MC within\n",
" +-5 % for Cr and Co; Cu and the elastic line are 12-14 % wider.\n",
"* **Efficiency.** 99.99 % of the isolated photons above 5 keV and 100 % at\n",
" 3-5 keV (MC 99.96 %). The grid finds 6.7 % more photons than the classical\n",
" finder (pile-up), and the energy balance is 0.996."
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "403d0ea6",
"metadata": {
"execution": {
"iopub.execute_input": "2026-09-19T09:10:56.820785Z",
"iopub.status.busy": "2026-09-19T09:10:56.820668Z",
"iopub.status.idle": "2026-09-19T09:10:56.892665Z",
"shell.execute_reply": "2026-09-19T09:10:56.892382Z"
}
},
"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
}