Skip to content

Commit 04f52d2

Browse files
daniloeflDanilo Ferreira de Lima
andauthored
CookieboxCalibration: added flag to parallelize over etofs (#513)
* Expose flag to parallelize over tofs: good for machine with large number of cores only. * Updated change log. --------- Co-authored-by: Danilo Ferreira de Lima <danilo.enoque.ferreira.de.lima@xfel.de>
1 parent 2de6258 commit 04f52d2

3 files changed

Lines changed: 82 additions & 14 deletions

File tree

docs/changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ Changed:
120120
parallelization to the `AdqRawChannel` class (!509).
121121
- [TOFAnalogResponse][extra.applications.TOFAnalogResponse] averages analog pulses using thresholding
122122
from `AdqRawChannel` to clean up the data (!511).
123+
- [CookieboxCalibration][extra.applications.CookieboxCalibration] use
124+
processes to parallelize over eTOFs (!513).
123125

124126
## [2025.1]
125127

src/extra/applications/cookiebox.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,8 @@ def setup(self,
475475
scan: Scan,
476476
xgm: XGM,
477477
tof_response: Dict[int, TOFAnalogResponse]=None,
478-
parallel=None
478+
parallel=False,
479+
parallel_over_tofs=None,
479480
):
480481
"""
481482
Derive calibrations.
@@ -493,6 +494,7 @@ def setup(self,
493494
For example: `XGM(run, "SQS_DIAG1_XGMD/XGM/DOOCS")`
494495
tof_response: The response function object for deconvolution if that is desired.
495496
parallel: Whether to paralellize data reading.
497+
parallel_over_tofs: Whether to parallelize over eTOFs.
496498
"""
497499
# base properties
498500
self._run = run
@@ -524,7 +526,7 @@ def setup(self,
524526
self.update_metadata()
525527

526528
# find RoI if needed
527-
self.update_roi(parallel)
529+
self.update_roi(parallel, parallel_over_tofs)
528530

529531
# find where the peaks are per energy in each Tof
530532
self.update_fit_result()
@@ -688,7 +690,7 @@ def update_tof_settings(self):
688690
self.kwargs_adq[tof_id]["name"] = self._tof[tof_id].name
689691
self.mask = {tof_id: True for tof_id in self.kwargs_adq.keys()}
690692

691-
def update_roi(self, parallel=None):
693+
def update_roi(self, parallel=False, parallel_over_tofs=16):
692694
"""
693695
Given calibrated data, apply a selection and find RoI if needed.
694696
@@ -697,7 +699,7 @@ def update_roi(self, parallel=None):
697699
"""
698700
# average data for each energy slice
699701
logging.info("Reading calibration data ... (this takes a while)")
700-
self.select_calibration_data(parallel)
702+
self.select_calibration_data(parallel, parallel_over_tofs=parallel_over_tofs)
701703
# find RoI if needed
702704
for tof_id in self.kwargs_adq.keys():
703705
if (self.auger_start_roi[tof_id] is None
@@ -728,7 +730,7 @@ def update_calibration(self):
728730
def fast_response_correction(self, x, tof_id):
729731
return self._tof_response[tof_id].apply(x.fillna(0.0), method="nn_matrix", n_iter=100, nonneg=True)
730732

731-
def select_calibration_data(self, parallel=None):
733+
def select_calibration_data(self, parallel=False, parallel_over_tofs=None):
732734
"""
733735
Select data for calibration.
734736
"""
@@ -751,15 +753,27 @@ def select_calibration_data(self, parallel=None):
751753
correction_fn=correction_fn,
752754
parallel=parallel,
753755
)
754-
itr_gen = list(itertools.product(tof_ids, energy_ids))
755-
data_gen = map(fn, itr_gen)
756-
# organize it all in a numpy array
757-
for (d, x), (tof_id, energy_id) in zip(data_gen, itr_gen):
758-
data[tof_id] += [d]
759-
mean_xgm[tof_id] += [x]
760-
for tof_id in tof_ids:
761-
data[tof_id] = np.stack(data[tof_id], axis=0)
762-
mean_xgm[tof_id] = np.stack(mean_xgm[tof_id], axis=0)
756+
if parallel_over_tofs is not None:
757+
with ProcessPoolExecutor(max_workers=parallel_over_tofs) as p:
758+
itr_gen = list(itertools.product(tof_ids, energy_ids))
759+
data_gen = p.map(fn, itr_gen)
760+
# organize it all in a numpy array
761+
for (d, x), (tof_id, energy_id) in zip(data_gen, itr_gen):
762+
data[tof_id] += [d]
763+
mean_xgm[tof_id] += [x]
764+
for tof_id in tof_ids:
765+
data[tof_id] = np.stack(data[tof_id], axis=0)
766+
mean_xgm[tof_id] = np.stack(mean_xgm[tof_id], axis=0)
767+
else:
768+
itr_gen = list(itertools.product(tof_ids, energy_ids))
769+
data_gen = map(fn, itr_gen)
770+
# organize it all in a numpy array
771+
for (d, x), (tof_id, energy_id) in zip(data_gen, itr_gen):
772+
data[tof_id] += [d]
773+
mean_xgm[tof_id] += [x]
774+
for tof_id in tof_ids:
775+
data[tof_id] = np.stack(data[tof_id], axis=0)
776+
mean_xgm[tof_id] = np.stack(mean_xgm[tof_id], axis=0)
763777

764778
self.calibration_data = data
765779
self.calibration_mean_xgm = mean_xgm

tests/test_applications_cookiebox.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,58 @@ def test_no_parallel(mock_sqs_etof_calibration_run, tmp_path, mock_etof_mono_ene
441441
# check how well it matches
442442
assert np.allclose(ts, ts_true, rtol=1e-2, atol=1e-2)
443443

444+
# tests data reading parallelizing over etofs
445+
def test_parallel_tofs(mock_sqs_etof_calibration_run, tmp_path, mock_etof_mono_energies, mock_etof_calibration_constants):
446+
# same as above, but tests only if a crash happens in `calc_mean`
447+
# somehow parallelization means that `calc_mean` is not shown in the coverage
448+
pulse_timing = 'SQS_RR_UTC/TSYS/TIMESERVER'
449+
monochromator_energy = 'SA3_XTD10_MONO/MDL/PHOTON_ENERGY'
450+
digitizer = 'SQS_DIGITIZER_UTC4/ADC/1:network'
451+
digitizer_control = 'SQS_DIGITIZER_UTC4/ADC/1'
452+
pulse_energy = 'SQS_DIAG1_XGMD/XGM/DOOCS'
453+
mock_sqs_etof_calibration_run = mock_sqs_etof_calibration_run.select([pulse_timing,
454+
digitizer, digitizer_control,
455+
pulse_energy, f"{pulse_energy}:output",
456+
monochromator_energy], require_all=True).select_trains(np.s_[10:])
457+
channel_name = "1_A"
458+
tof_ids = [0]
459+
tof_channel = {}
460+
tof_channel[0] = AdqRawChannel(mock_sqs_etof_calibration_run,
461+
channel_name,
462+
digitizer=digitizer,
463+
first_pulse_offset=1000)
464+
scan = Scan(mock_sqs_etof_calibration_run[monochromator_energy, "actualEnergy"], resolution=2)
465+
energy_axis = np.linspace(965, 1070, 160)
466+
xgm = XGM(mock_sqs_etof_calibration_run, pulse_energy)
467+
cal = CookieboxCalibration(
468+
auger_start_roi=1,
469+
start_roi=75,
470+
stop_roi=320,
471+
)
472+
cal.setup(run=mock_sqs_etof_calibration_run, energy_axis=energy_axis, tof_settings=tof_channel,
473+
xgm=xgm,
474+
scan=scan,
475+
parallel=False,
476+
parallel_over_tofs=2,
477+
)
478+
479+
correct_energies = np.unique(mock_etof_mono_energies)
480+
correct_constants = np.array(mock_etof_calibration_constants)
481+
for tof_id in tof_ids:
482+
assert np.allclose(cal.tof_fit_result[tof_id].energy, correct_energies, rtol=1e-2, atol=1e-2)
483+
484+
energy = correct_energies
485+
486+
# get calibration curve
487+
c, e0, t0 = cal.model_params[tof_id]
488+
ts = t0 + np.sqrt(c/(energy - e0))
489+
490+
c_true, e0_true, t0_true = correct_constants
491+
ts_true = t0_true + np.sqrt(c_true/(energy - e0_true))
492+
493+
# check how well it matches
494+
assert np.allclose(ts, ts_true, rtol=1e-2, atol=1e-2)
495+
444496
def test_deconvolve(mock_sqs_etof_calibration_run, tmp_path):
445497
# use mock data and do the same as before, but with deconvolution
446498
# it should improve resolution, but lead to the same calibration constants

0 commit comments

Comments
 (0)