Files
MLXIDDemo/scripts/01_generate_mc_singlephoton.py
xie_x1andClaude Sonnet 5 2683fd3576 Initial commit: MLXIDDemo, a 12 keV end-to-end walkthrough of the MLXID pipeline
MC generation -> pile-up assembly -> CNN vs. classical eta-interpolation
(scored against MC ground truth) -> optional training -> qualitative
inference on real measurement data, self-contained and runnable from a
single conda env (see README.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 16:50:06 +02:00

80 lines
3.1 KiB
Python

"""
Generate a small single-photon MC cluster sample at 12 keV (Moench040, 150V).
Mirrors McGeneration/generate_Moench040_150V.py exactly: `parameterization()`
fits the generalized-Gaussian charge-cloud parameters alpha(t)/beta(t) from
raw per-depth-bin charge-transport simulation output (see
https://github.com/slsdetectorgroup/ChargeTransportSimulation for how that
raw output itself is produced -- data/mc_raw_simulation/ ships the 256
files needed for this one sensor/voltage/energy point, ~34 MB), then
`generateFromParameters()` draws the MC events. Only the event count is
much smaller here than production.
Output:
data/mc_params/ggdPar_Moench040_150V_12keV.npy (+ _fit.png) -- fitted
alpha(t)/beta(t) parameters, regenerated fresh each run
data/mc_samples/12keV_Moench040_150V_{thread}.npz (+ .h5)
samples: (N, 5, 5) float32 pixel clusters in keV
labels: (N, 4) float32 [x, y, z_um, energy_eV] -- MC ground truth
The beta(t) fit is a 5-parameter nonlinear fit and occasionally converges
such that it's undefined (NaN) for a handful of the shortest drift times;
singleProcess() already discards those events (~0.1-0.2% of the total,
printed per event) the same way it discards out-of-range z0 draws -- not a
bug, just how that fit behaves at its domain edge.
Needs: numpy, scipy, ROOT, h5py. Use the `mlxid_demo` conda env.
"""
import sys
from pathlib import Path
import numpy as np
sys.path.append(str(Path(__file__).resolve().parent.parent / 'src'))
import mc_generator
HERE = Path(__file__).resolve().parent.parent
RAW_SIM_DIR = HERE / 'data/mc_raw_simulation'
GGD_PARAM_DIR = HERE / 'data/mc_params'
OUTPUT_DIR = HERE / 'data/mc_samples'
### 12 keV attenuation length in Moench040 (Si), from https://henke.lbl.gov/optical_constants/atten2.html
ATTENUATION_LENGTH_12KEV_CM = 228.738
N_TOTAL_INCIDENT = 40_000 ### demo scale; production uses 10_000_000
N_THREAD = 4
mcConfig = {
'element': '12keV',
'energy': 12_000, ### eV
'attenuationLength': ATTENUATION_LENGTH_12KEV_CM,
'sensorCode': 'Moench040',
'nTotalIncident': N_TOTAL_INCIDENT,
'nThread': N_THREAD,
'Roi': [50, 350, 50, 350],
'clusterWidth': 5, ### pixel
'pixelSize': 25, ### um
'noiseEneFrame': np.zeros((400, 400)), ### no noise map shipped -> noise-free MC
'shotNoiseFactor': 0.0,
'calibrationNoise': 0.0,
'sensorThickness': 650, ### um, Moench040
'T': 273 + 20.0 + 14.8,
'hv': 150,
'depletionVoltage': 34.7,
'zBins': 128, ### depth bins the raw simulation was run in
'simulationMethod': 'simu2',
'resultsPath': str(RAW_SIM_DIR),
'ggdParOutputDir': str(GGD_PARAM_DIR),
'sampleOutputPath': str(OUTPUT_DIR),
}
if __name__ == '__main__':
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
GGD_PARAM_DIR.mkdir(parents=True, exist_ok=True)
mc_generator.init(mcConfig)
print(f'[01] Fitting alpha(t)/beta(t) from {RAW_SIM_DIR} -> {GGD_PARAM_DIR}...')
mc_generator.parameterization()
print(f'[01] Generating {N_TOTAL_INCIDENT} single-photon 12keV MC events '
f'across {N_THREAD} threads -> {OUTPUT_DIR}')
mc_generator.generateFromParameters()
print('[01] Done.')