Skip to content

Commit 7899c03

Browse files
daniloeflDanilo Ferreira de Limatakluyver
authored
TOFAnalogResponse: use thresholding to obtain a background-free response function (#511)
* Improved counting mode response estimate by summing selected spectra. * Updated change log. * Added test without scan and counting photo-electrons. * Use split_by_steps to group data by trainIds. * Re-organize the code to move counting step into special function and symmetrize things a bit more. --------- Co-authored-by: Danilo Ferreira de Lima <danilo.enoque.ferreira.de.lima@xfel.de> Co-authored-by: Thomas Kluyver <thomas.kluyver@xfel.eu>
1 parent 769ee6b commit 7899c03

3 files changed

Lines changed: 47 additions & 13 deletions

File tree

docs/changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ Changed:
116116
- [CookieboxCalibration][extra.applications.CookieboxCalibration] and
117117
[TOFAnalogResponse][extra.applications.TOFAnalogResponse] delegate
118118
parallelization to the `AdqRawChannel` class (!509).
119+
- [TOFAnalogResponse][extra.applications.TOFAnalogResponse] averages analog pulses using thresholding
120+
from `AdqRawChannel` to clean up the data (!511).
119121

120122
## [2025.1]
121123

src/extra/applications/cookiebox_deconvolve.py

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,36 @@ def shift_h(self, data, h_axis):
437437
this_h = np.interp(h_axis, xi, this_h)
438438
return this_h
439439

440+
def _count_photo_electrons(self, tof_part, parallel):
441+
"""
442+
Given an `AdqRawChannel` object with monochromated data, count photo-electrons
443+
and build a response function.
444+
445+
Args:
446+
tof_part: The `AdqRawChannel` object to use for counting the electrons.
447+
parallel: Argument used to parallelize `pulse_edges`.
448+
449+
Returns: The response function aligned to this monochromated peak.
450+
"""
451+
logging.info("Calling pulse_edges for selected trains ...")
452+
# get peak positions where photo-electrons were found
453+
tof_data = tof_part.pulse_edges(pulse_dim='pulseIndex',
454+
threshold=self.count_threshold,
455+
parallel=parallel).reset_index()
456+
# use the peaks identified to select trains
457+
good_trains = np.unique(tof_data.loc[:,'trainId'].to_numpy())
458+
# create an index of good elements
459+
idx = tof_data.loc[:, ['trainId', 'pulseIndex']].set_index(['trainId', 'pulseIndex']).index
460+
logging.info("Summing good trains ...")
461+
# select only trains with a photo-electron detected ("good trains") to do this faster
462+
analog_data = -tof_part.select_trains(by_id[good_trains]).pulse_data(pulse_dim="pulseIndex", parallel=parallel)
463+
# select the train-pulses of interest
464+
this_tof_data = analog_data.sel(pulse=idx).mean("pulse")
465+
# apply an roi
466+
if self.roi is not None:
467+
this_tof_data = this_tof_data.isel(sample=self.roi)
468+
return this_tof_data
469+
440470
def setup(self,
441471
tof: AdqRawChannel,
442472
scan: Scan=None,
@@ -466,25 +496,23 @@ def setup(self,
466496
this_tof_data = -tof.pulse_data(pulse_dim="pulseIndex", parallel=parallel).unstack("pulse")
467497
logging.info("Averaging over mono settings ...")
468498
for k, e in enumerate(scan.positions):
469-
data += [this_tof_data.sel(trainId=scan.positions_train_ids[k]).mean("trainId").mean("pulseIndex").to_numpy()]
499+
d = this_tof_data.sel(trainId=scan.positions_train_ids[k])
500+
if self.roi is not None:
501+
d = d.isel(sample=self.roi)
502+
d = d.mean("trainId").mean("pulseIndex").to_numpy()
503+
data += [d]
470504
else:
471505
# count photo-electrons by histogramming peak positions
472-
for k, e in enumerate(scan.positions):
473-
tof_data = tof.select_trains(by_id[scan.positions_train_ids[k]]).pulse_edges(pulse_dim='pulseIndex', threshold=self.count_threshold, parallel=parallel).reset_index()
474-
this_tof_data, _ = np.histogram(tof_data.edge, bins=bins, weights=-tof_data.amplitude)
475-
this_tof_data = xr.DataArray(this_tof_data, dims=('sample'), coords={'sample': bins[:-1]})
476-
if self.roi is not None:
477-
this_tof_data = this_tof_data.isel(sample=self.roi)
478-
data += [this_tof_data.to_numpy()]
506+
for tof_part in scan.split_by_steps(tof):
507+
this_tof_data = self._count_photo_electrons(tof_part, parallel=parallel)
508+
data += [this_tof_data.to_numpy()]
479509
else:
480510
if self.count_threshold is None or self.count_threshold >= 0:
481511
this_tof_data = -tof.pulse_data(pulse_dim="pulseIndex", parallel=parallel).mean('pulse')
512+
if self.roi is not None:
513+
this_tof_data = this_tof_data.isel(sample=self.roi)
482514
else:
483-
tof_data = tof.pulse_edges(pulse_dim='pulseIndex', threshold=self.count_threshold, parallel=parallel).reset_index()
484-
this_tof_data, _ = np.histogram(tof_data.edge, bins=bins, weights=-tof_data.amplitude)
485-
this_tof_data = xr.DataArray(this_tof_data, dims=('sample'), coords={'sample': bins[:-1]})
486-
if self.roi is not None:
487-
this_tof_data = this_tof_data.isel(sample=self.roi)
515+
this_tof_data = self._count_photo_electrons(tof, parallel=parallel)
488516
data += [this_tof_data.to_numpy()]
489517

490518
for d in data:

tests/test_applications_cookiebox.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,10 @@ def test_deconvolve(mock_sqs_etof_calibration_run, tmp_path):
441441
tof_response_noscan = TOFAnalogResponse(roi=slice(75, None), n_samples=150)
442442
tof_response_noscan.setup(tof_channel[0])
443443

444+
# setup TOFAnalogResponse without Scan and counting of photo-electrons
445+
tof_response_noscan_count = TOFAnalogResponse(roi=slice(75, None), n_samples=150, count_threshold=-20)
446+
tof_response_noscan_count.setup(tof_channel[0])
447+
444448
# create calibration object to read data in the appropriate format
445449
energy_axis = np.linspace(965, 1070, 160)
446450
xgm = XGM(mock_sqs_etof_calibration_run, pulse_energy)

0 commit comments

Comments
 (0)