Skip to content

Commit 2de6258

Browse files
daniloeflDanilo Ferreira de Lima
andauthored
CookieboxCalibration: Normalize transmission by pulse energy and detect RoI robustly (#512)
* Normalize transmission by pulse energy. * Updated change log. --------- Co-authored-by: Danilo Ferreira de Lima <danilo.enoque.ferreira.de.lima@xfel.de>
1 parent 7899c03 commit 2de6258

3 files changed

Lines changed: 94 additions & 48 deletions

File tree

docs/changelog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ Fixed:
9898
- Fixed counting threshold for `CookieboxCalibration` (!498).
9999
- [CookieboxCalibration][extra.applications.CookieboxCalibration] bug fix for
100100
new `extra.applications.base.SerializableMixin` interface (!506).
101+
- [CookieboxCalibration][extra.applications.CookieboxCalibration] transmission
102+
corrected based on pulse energy (!512).
101103

102104
Changed:
103105

src/extra/applications/cookiebox.py

Lines changed: 46 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import xarray as xr
1414
import pandas as pd
1515
from scipy.signal import find_peaks
16+
from scipy.ndimage import gaussian_filter
1617

1718
from .base import SerializableMixin
1819
from .cookiebox_deconvolve import TOFAnalogResponse
@@ -47,7 +48,7 @@ def search_offset(trace: np.ndarray, sigma: float=20) -> int:
4748
peak_idx = np.argmax(smoothened)
4849
return peak_idx - 200
4950

50-
def search_roi(roi: np.ndarray) -> np.ndarray:
51+
def search_roi(roi: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
5152
"""
5253
Find highest peaks in the 1D trace.
5354
@@ -56,9 +57,21 @@ def search_roi(roi: np.ndarray) -> np.ndarray:
5657
5758
Returns: Peak position.
5859
"""
59-
import scipy
60-
p, _ = scipy.signal.find_peaks(roi, prominence=(0.25*np.max(roi), None))
61-
return p
60+
roi_smooth = gaussian_filter(roi[np.isfinite(roi)], sigma=5)
61+
p, prop = find_peaks(roi_smooth, prominence=0.25*np.max(roi_smooth), width=1, height=0)
62+
idx = np.argsort(prop["peak_heights"])[::-1]
63+
p = p[idx]
64+
w = prop["widths"][idx]
65+
if len(p) == 0:
66+
logging.info(f"Failed to find peaks.")
67+
return -1, 0
68+
if len(p) == 1:
69+
logging.info(f"Failed to find two peaks. If there is no Auger peak, set start_roi=0")
70+
return -1, 0
71+
idx = np.argsort(p[:2])
72+
p = p[:2][idx]
73+
w = w[:2][idx]
74+
return p[-1], w[-1]
6275

6376
def model(ts: np.ndarray, c: float, e0: float, t0: float) -> np.ndarray:
6477
"""
@@ -177,16 +190,13 @@ def calc_mean(itr: Tuple[int, int], scan: Scan, xgm_data: xr.DataArray, tof: Dic
177190
tof_data = tof_data.pulse_data(pulse_dim='pulseIndex', parallel=parallel)
178191

179192
# select XGM
180-
if xgm_threshold > 0:
181-
mask = xgm_data.coords["trainId"].isin(good_ids)
182-
sel_xgm_data = xgm_data[mask]
183-
tof_data = tof_data.loc[sel_xgm_data > xgm_threshold, :]
184-
tof_xgm_data = sel_xgm_data.loc[sel_xgm_data > xgm_threshold]
193+
mask = xgm_data.coords["trainId"].isin(good_ids)
194+
sel_xgm_data = xgm_data[mask]
195+
tof_data = tof_data.loc[sel_xgm_data > xgm_threshold, :]
196+
tof_xgm_data = sel_xgm_data.loc[sel_xgm_data > xgm_threshold].to_numpy()
185197

186-
out_data = -tof_data.mean('pulse')
187-
out_xgm = 0.0
188-
if xgm_threshold > 0:
189-
out_xgm = tof_xgm_data.mean('pulse').to_numpy()
198+
out_data = -tof_data.mean("pulse")
199+
out_xgm = np.mean(tof_xgm_data)
190200
else:
191201
# option 2: count photon peaks
192202
# in this case, ignore the XGM, as it is only used for cleaning the data
@@ -198,9 +208,7 @@ def calc_mean(itr: Tuple[int, int], scan: Scan, xgm_data: xr.DataArray, tof: Dic
198208
out_data, _ = np.histogram(tof_data.edge, bins=bins, weights=-tof_data.amplitude)
199209

200210
out_data = xr.DataArray(out_data, dims=('sample'), coords={'sample': bins[:-1]})
201-
out_xgm = 0.0
202-
if xgm_threshold > 0:
203-
out_xgm = xgm_data.mean('pulse').to_numpy()
211+
out_xgm = np.mean(xgm_data.to_numpy())
204212

205213
if correction_fn is not None:
206214
out_data = correction_fn[tof_id](out_data)
@@ -691,16 +699,13 @@ def update_roi(self, parallel=None):
691699
logging.info("Reading calibration data ... (this takes a while)")
692700
self.select_calibration_data(parallel)
693701
# find RoI if needed
694-
if (self.auger_start_roi is None
695-
or self.start_roi is None
696-
or self.stop_roi is None):
697-
logging.info("Finding RoI ...")
698-
logging.info("(This may fail. If it does, please provide a `auger_start_roi`, `start_roi` and `stop_roi`.)")
699-
for tof_id in self.kwargs_adq.keys():
702+
for tof_id in self.kwargs_adq.keys():
703+
if (self.auger_start_roi[tof_id] is None
704+
or self.start_roi[tof_id] is None
705+
or self.stop_roi[tof_id] is None):
706+
logging.info(f"Finding RoI for TOF {tof_id}...")
700707
self.find_roi(tof_id)
701-
logging.info(f"Auger start RoIs found: {self.auger_start_roi}")
702-
logging.info(f"Start RoIs found: {self.start_roi}")
703-
logging.info(f"Stop RoIs found: {self.stop_roi}")
708+
logging.info(f"Start RoIs found: {self.start_roi[tof_id]}")
704709

705710
def update_fit_result(self):
706711
"""
@@ -781,38 +786,32 @@ def find_roi(self, tof_id: int):
781786
Args:
782787
tof_id: The eTOF ID.
783788
"""
784-
auger = list()
785-
roi = list()
789+
p = list()
790+
w = list()
791+
N = self.calibration_data[tof_id].shape[-1]
786792
for e in range(self.calibration_data[tof_id].shape[0]):
787793
d = self.calibration_data[tof_id][e, :]
788-
peaks = search_roi(d)
789-
peaks = sorted(peaks)
790-
if len(peaks) < 2:
791-
logging.info(f"Failed to find peaks for eTOF {tof_id}, energy index {e}. "
792-
f"Check the data quality.")
794+
peak, width = search_roi(d)
795+
if peak < 0:
793796
continue
794-
auger += [peaks[0]]
795-
roi += [peaks[1]]
796-
if len(auger) == 0 or len(roi) == 0:
797+
p += [peak]
798+
w += [width]
799+
if len(p) == 0:
797800
logging.info(f"No peaks found in eTOF {tof_id}. "
798801
f"Check the data quality. "
799802
f"I will set the RoI to collect non-sense,"
800803
f" so this TOF data will be meaningless. "
801804
f"It will also be masked.")
802805
self.auger_start_roi[tof_id] = 0
803806
self.start_roi[tof_id] = 100
804-
self.stop_roi[tof_id] = 200
807+
self.stop_roi[tof_id] = N
805808
self.mask[tof_id] = False
806809
return
807-
a = min(auger)
808-
b = min(roi)
809-
dab = abs(b - a)
810-
stop_roi = max(roi) + int(dab/2)
811-
auger_start_roi = int(a - dab/2)
812-
start_roi = int(b - dab/2)
813-
self.auger_start_roi[tof_id] = auger_start_roi
814-
self.start_roi[tof_id] = start_roi
815-
self.stop_roi[tof_id] = stop_roi
810+
idx = np.argmin(p)
811+
pos = p[idx] - 2*w[idx]
812+
self.auger_start_roi[tof_id] = 0
813+
self.start_roi[tof_id] = int(pos)
814+
self.stop_roi[tof_id] = N
816815

817816
def plot_calibration_data(self):
818817
"""Plot data for checks.
@@ -966,9 +965,8 @@ def calculate_calibration_and_transmission(self, tof_id: int):
966965
detected = self.tof_fit_result[tof_id].A
967966
#produced = self.calibration_mean_xgm[tof_id] * self.tof_fit_result[tof_id].Aa * dsig_dth
968967
#produced = self.tof_fit_result[tof_id].Aa * dsig_dth
969-
produced = dsig_dth
970-
if produced == 0:
971-
produced += 1e-1
968+
produced = dsig_dth*self.calibration_mean_xgm[tof_id][mask][eidx]
969+
produced[produced == 0] = 0.1 # avoid division by zero
972970
en = detected[mask][eidx]/produced
973971
# interpolate normalization
974972
self.normalization[tof_id] = np.interp(self.energy_axis,

tests/test_applications_cookiebox.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,52 @@ def test_fit(tmp_path):
254254
for tof_id, v in correct.items():
255255
assert np.allclose(cal_read.model_params[tof_id], v, rtol=1e-2, atol=1e-2)
256256

257+
def test_detect_roi_and_fit_single_channel(mock_sqs_etof_calibration_run, tmp_path, mock_etof_mono_energies, mock_etof_calibration_constants):
258+
# use mock data
259+
pulse_timing = 'SQS_RR_UTC/TSYS/TIMESERVER'
260+
monochromator_energy = 'SA3_XTD10_MONO/MDL/PHOTON_ENERGY'
261+
digitizer = 'SQS_DIGITIZER_UTC4/ADC/1:network'
262+
digitizer_control = 'SQS_DIGITIZER_UTC4/ADC/1'
263+
pulse_energy = 'SQS_DIAG1_XGMD/XGM/DOOCS'
264+
mock_sqs_etof_calibration_run = mock_sqs_etof_calibration_run.select([pulse_timing,
265+
digitizer, digitizer_control,
266+
pulse_energy, f"{pulse_energy}:output",
267+
monochromator_energy], require_all=True).select_trains(np.s_[10:])
268+
channel_name = "1_A"
269+
tof_ids = [0]
270+
tof_channel = {}
271+
tof_channel[0] = AdqRawChannel(mock_sqs_etof_calibration_run,
272+
channel_name,
273+
digitizer=digitizer,
274+
first_pulse_offset=1000)
275+
scan = Scan(mock_sqs_etof_calibration_run[monochromator_energy, "actualEnergy"], resolution=2)
276+
energy_axis = np.linspace(965, 1070, 160)
277+
xgm = XGM(mock_sqs_etof_calibration_run, pulse_energy)
278+
cal = CookieboxCalibration(
279+
auger_start_roi=None,
280+
start_roi=None,
281+
stop_roi=None,
282+
)
283+
cal.setup(run=mock_sqs_etof_calibration_run, energy_axis=energy_axis, tof_settings=tof_channel,
284+
xgm=xgm,
285+
scan=scan)
286+
correct_energies = np.unique(mock_etof_mono_energies)
287+
correct_constants = np.array(mock_etof_calibration_constants)
288+
for tof_id in tof_ids:
289+
assert np.allclose(cal.tof_fit_result[tof_id].energy, correct_energies, rtol=1e-2, atol=1e-2)
290+
291+
energy = correct_energies
292+
293+
# get calibration curve
294+
c, e0, t0 = cal.model_params[tof_id]
295+
ts = t0 + np.sqrt(c/(energy - e0))
296+
297+
c_true, e0_true, t0_true = correct_constants
298+
ts_true = t0_true + np.sqrt(c_true/(energy - e0_true))
299+
300+
# check how well it matches
301+
assert np.allclose(ts, ts_true, rtol=1e-2, atol=1e-2)
302+
257303
def test_avg_and_fit_single_channel(mock_sqs_etof_calibration_run, tmp_path, mock_etof_mono_energies, mock_etof_calibration_constants):
258304
# use mock data
259305
pulse_timing = 'SQS_RR_UTC/TSYS/TIMESERVER'

0 commit comments

Comments
 (0)