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>
80 lines
3.1 KiB
Python
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.')
|