# GridBased Grid-based X-ray reconstruction for MÖNCH. A fully-convolutional network reads a calibrated frame and predicts, for **every pixel cell**, whether a photon converted there, where inside the cell, and with what energy. The number of photons in a frame is an output of the model, so one set of weights covers any multiplicity and any flux, and those same weights run on a 32×32 training frame or on a full detector frame. Design rationale, work-package split and open questions: [`docs/WORKFLOW_DESIGN.md`](docs/WORKFLOW_DESIGN.md). **Stage 1: 12 keV monochromatic, MOENCH040 @150 V** — the point where this workflow can be compared head-to-head with the published cluster-based results (σ = 0.8 µm on MC, MTF50 = 160 lp/mm at DanMAX). Done: gate G2 on MC, knife edge at four flux points. **Stage 2 (current): polychromatic 3–30 keV, MOENCH014 @150 V** — per-photon energy from the same network. Done: α(E,t)/β(E,t) charge LUT, MC training and evaluation, XRF lines at 2603 MaxIV (energy resolution and efficiency against the classical 3×3 sum), and a scan of network sizes. Default model: `Results/260919_poly3to30keV_Moench014_c96_00`, `[32, 64, 96, 96]`. --- ## Pipeline ``` McGeneration single-photon stamps (12 keV: existing, 10 M events, noiseless 5×5) or α(E,t)/β(E,t) charge-cloud LUT (3–30 keV: scripts/01_build_charge_lut.py) │ scripts/02_generate_frames.py ← superpose stamps / sample clouds + noise ▼ MC frames + truth photon list (HDF5) │ scripts/03_train.py ← fully-convolutional GridNet ▼ model checkpoint │ scripts/04_evaluate.py ← efficiency/purity/RMS vs occupancy │ scripts/07_energy_response.py ← MC line position g(E) and width (poly) │ scripts/05_infer_measurement.py ← raw ADU → photon list → image ▼ benchmark plots, reconstructed images ``` ## Conventions * Pixel `(row r, col c)` covers `x ∈ [c, c+1)`, `y ∈ [r, r+1)`. A photon position is a float in `[0, S)`; its cell is `(⌊y⌋, ⌊x⌋)` and its in-cell offset is `(x−c−0.5, y−r−0.5) ∈ [−0.5, 0.5]`. * Energies are keV throughout; positions are pixels (25 µm) unless a name says `_um`. * **Duplicate suppression works in position space.** Every cell predicts a physical position, so two cells that point at the same place — within `eval.merge_distance` (0.1 px in the current configs) — are merged and the stronger one kept, while two cells that point at different places both survive. Predictions that merge are always less than 1 px apart and therefore in adjacent cells, so comparing the 8 neighbours is enough and the decode costs the same as a max-pool. * **Receptive-field radius R = number of 3×3 conv layers = the halo.** Cells within R of a frame border are supervised nowhere and reported nowhere, so the zero padding stays invisible to everything that is used. `scripts/00_selftest.py` checks that a 32×32 frame and the same content embedded in a larger frame give bit-identical predictions inside the halo — that is what makes train-small / infer-large exact — and that R is tight, the ring at R−1 already differing. * **One photon per cell sets a known ceiling.** For a Poisson field of occupancy λ, a single slot per 25 µm cell can represent a fraction `(1−e^−λ)/λ ≈ 1 − λ/2` of the photons: 99.5 % at 1 %, 97.5 % at 5 %, 95.2 % at 10 %, 90.6 % at 20 %, 78.7 % at 50 %. The generator reports the loss per shard as `n_same_cell`, and it follows the formula to a few per mille. The ceiling is not a hard bound on the *measured* efficiency: the offset channels are unbounded, so a cell can report a position inside its neighbour. ## Data layout One HDF5 file per shard: | dataset | shape | meaning | |---|---|---| | `frames` | (N, S, S) f4 | calibrated frame, keV | | `photons` | (M, 4) f4 | `x, y, E[keV], z[µm]`, frame coordinates | | `frame_ptr` | (N+1,) i8 | photons of frame *i* = `photons[ptr[i]:ptr[i+1]]` | | `occupancy` | (N,) f4 | occupancy each frame was thrown at | Splits draw from **disjoint stamp files** (train 0–11, val 12–13, test/eval_scan 14–15), so every single-photon event belongs to exactly one split. ## Running Training, evaluation and inference need an environment with PyTorch (`conda activate pytorch_nightly`); generation needs numpy/h5py/omegaconf. ```bash cd /home/xie_x1/MLXID/GridBased # 0. self-tests (coordinates, D4, translation equivariance, position-space merging) python scripts/00_selftest.py # 1. generate MC frames python scripts/02_generate_frames.py --config Configs/generate_frames_12keV.yaml # subsets / quick check: python scripts/02_generate_frames.py --config Configs/generate_frames_12keV.yaml --splits eval_scan python scripts/02_generate_frames.py --config Configs/generate_frames_12keV.yaml --smoke # 2. train (-u keeps progress live when the output is redirected) python -u scripts/03_train.py --config Configs/train_grid_12keV.yaml python scripts/03_train.py --config Configs/train_grid_12keV.yaml --epochs 3 --tag smoke # 3. evaluate the occupancy scan (0.5 / 1 / 2 / 5 / 10 / 20 / 50 %) python scripts/04_evaluate.py --checkpoint Results//Models/best.pth # 4. run over measured data python scripts/05_infer_measurement.py --config Configs/infer_measurement_12keV.yaml \ --run Edge_3filters_pos0_12keV # polychromatic 3-30 keV, MOENCH014: charge LUT (+ its validation), frames, training python scripts/01_build_charge_lut.py --config Configs/charge_lut_Moench014_3to30keV.yaml python scripts/01b_validate_charge_lut.py --lut data/charge_lut_Moench014_150V_3to30keV.npz \ --outdir Results/charge_lut_3to30keV python scripts/02_generate_frames.py --config Configs/generate_frames_poly3to30keV_Moench014.yaml python -u scripts/03_train.py --config Configs/train_grid_poly3to30keV_Moench014_c96.yaml python scripts/04_evaluate.py --checkpoint Results//Models/best.pth # MC energy response (line position g(E), core width, efficiency) -> energy_response.npz python scripts/07_energy_response.py --checkpoint Results//Models/best.pth # an XRF run, with its photon list for Notebooks/XRF_*.ipynb python scripts/05_infer_measurement.py --config Configs/infer_measurement_poly_Moench014.yaml \ --run AgMo_1_factor_10 --save-photons # the whole chain for several network sizes, unattended, with a comparison report # (resumable: finished stages are skipped); see Results/overnight_poly3to30keV/ setsid nohup bash scripts/overnight_poly3to30keV.sh \ > Results/overnight_poly3to30keV/driver.log 2>&1 < /dev/null & python scripts/08_compare_models.py --out --model