Tutorial for DLD sectors alignment using photon peak#

Preparation#

Import necessary libraries#

[1]:
%load_ext autoreload
%autoreload 2

from pathlib import Path
import os
import numpy as np

from sed import SedProcessor
from sed.dataset import dataset

import matplotlib.pyplot as plt
%matplotlib widget

from lmfit.models import GaussianModel

Get data paths#

If it is your beamtime, you can read the raw data and write to the processed directory. For the public data, you can not write to the processed directory.

The paths are such that if you are on Maxwell, it uses those. Otherwise, data is downloaded in the current directory from Zenodo: https://zenodo.org/records/15011781

[2]:
beamtime_dir = "/asap3/flash/gpfs/pg2/2021/data/11010004" # on Maxwell
if os.path.exists(beamtime_dir) and os.access(beamtime_dir, os.R_OK):
    path = beamtime_dir + "/raw/hdf/FL1USER3"
    buffer_path = beamtime_dir + "/processed/tutorial/"
else:
    # data_path can be defined and used to store the data in a specific location
    dataset.get("Photon_peak") # Put in Path to a storage of at least 10 GByte free space.
    path = dataset.dir
    buffer_path = path + "/processed/"
INFO - Not downloading Photon_peak data as it already exists at "/home/runner/work/sed/sed/docs/tutorial/datasets/Photon_peak".
Set 'use_existing' to False if you want to download to a new location.
INFO - Using existing data path for "Photon_peak": "/home/runner/work/sed/sed/docs/tutorial/datasets/Photon_peak"
INFO - Photon_peak data is already present.

Config setup#

Here, we get the path to the config file and set up the relevant directories. This can also be done directly in the config file.

[3]:
# pick the default configuration file for hextof@FLASH
config_file = Path('../src/sed/config/flash_example_config.yaml')
assert config_file.exists()
[4]:
# here we setup a dictionary that will be used to override the path configuration
# a few setting changes are needed as well to work with older data
config_override = {
    "core": {
        "beamtime_id": 11010004,
        "paths": {
            "raw": path,
            "processed": buffer_path
        },
    },
    "dataframe": {
        "ubid_offset": 0,
        "channels": {
            "timeStamp": {
                "index_key": "/zraw/TIMINGINFO/TIME1.BUNCH_FIRST_INDEX.1/dGroup/index",
                "dataset_key": "/zraw/TIMINGINFO/TIME1.BUNCH_FIRST_INDEX.1/dGroup/time",
            },
            "pulseId": {
                "index_key": "/zraw/FLASH.FEL/HEXTOF.DAQ/DLD1/dGroup/index",
                "dataset_key": "/zraw/FLASH.FEL/HEXTOF.DAQ/DLD1/dGroup/value",
            },
            "dldPosX": {
                "index_key": "/zraw/FLASH.FEL/HEXTOF.DAQ/DLD1/dGroup/index",
                "dataset_key": "/zraw/FLASH.FEL/HEXTOF.DAQ/DLD1/dGroup/value",
            },
            "dldPosY": {
                "index_key": "/zraw/FLASH.FEL/HEXTOF.DAQ/DLD1/dGroup/index",
                "dataset_key": "/zraw/FLASH.FEL/HEXTOF.DAQ/DLD1/dGroup/value",
            },
            "dldTimeSteps": {
                "index_key": "/zraw/FLASH.FEL/HEXTOF.DAQ/DLD1/dGroup/index",
                "dataset_key": "/zraw/FLASH.FEL/HEXTOF.DAQ/DLD1/dGroup/value",
            },
            "dldAux": {
                "index_key": "/zraw/FLASH.FEL/HEXTOF.DAQ/DLD1/dGroup/index",
                "dataset_key": "/zraw/FLASH.FEL/HEXTOF.DAQ/DLD1/dGroup/value",
            },
            "bam": {
                "index_key": "/zraw/FLASH.SDIAG/BAM.DAQ/4DBC3.HIGH_CHARGE_ARRIVAL_TIME/dGroup/index",
                "dataset_key": "/zraw/FLASH.SDIAG/BAM.DAQ/4DBC3.HIGH_CHARGE_ARRIVAL_TIME/dGroup/value",
            },
            "delayStage": {
                "index_key": "/zraw/FLASH.SYNC/LASER.LOCK.EXP/FLASH1.MOD1.PG.OSC/FMC0.MD22.1.ENCODER_POSITION.RD/dGroup/index",
                "dataset_key": "/zraw/FLASH.SYNC/LASER.LOCK.EXP/FLASH1.MOD1.PG.OSC/FMC0.MD22.1.ENCODER_POSITION.RD/dGroup/value",
            },
            "opticalDiode": {
                "format": "per_train",
                "index_key": "/uncategorised/FLASH.LASER/FLACPUPGLASER1.PULSEENERGY/PG2_incoupl/PULSEENERGY.MEAN/index",
                "dataset_key": "/uncategorised/FLASH.LASER/FLACPUPGLASER1.PULSEENERGY/PG2_incoupl/PULSEENERGY.MEAN/value",
            },
        },
    },
}

Read data#

[5]:
run_number = 40887
sp_ph_peak = SedProcessor(runs=[run_number], config=config_override, system_config=config_file, verbose=True)
sp_ph_peak.add_jitter()
INFO - Folder config loaded from: [/home/runner/work/sed/sed/docs/tutorial/sed_config.yaml]
INFO - System config loaded from: [/home/runner/work/sed/sed/docs/src/sed/config/flash_example_config.yaml]
INFO - Default config loaded from: [/opt/hostedtoolcache/Python/3.10.17/x64/lib/python3.10/site-packages/sed/config/default.yaml]
INFO - Reading files: 0 new files of 17 total.
loading complete in  0.12 s
INFO - add_jitter: Added jitter to columns ['dldPosX', 'dldPosY', 'dldTimeSteps'].

Check which channels are included in the dataframe

[6]:
sp_ph_peak.dataframe[["dldTimeSteps", "dldSectorID"]].head()
[6]:
dldTimeSteps dldSectorID
0 2390.487828 3
1 2390.865981 0
2 4275.866290 7
3 4277.303092 6
4 4421.184809 7

Data w/o correction of quadrants in time#

First, we take a look at the photon peak and apply separation by single quadrants before any corrections. We plot the data in detector time (dldTimeSteps) as well as in detector position (dldPosX and dldPosY) coordinates, with additional separation by single sectors.

[7]:
axes = ['dldSectorID', 'dldTimeSteps','dldPosX','dldPosY']
ranges = [[0,8], [2360,2460], [435,885], [445,895]]
bins = [8,700,225,225]
res_ph_peak = sp_ph_peak.compute(bins=bins, axes=axes, ranges=ranges)
[8]:
res_ph_peak['dldPosX'].attrs['unit'] = 'pixel'
res_ph_peak['dldPosY'].attrs['unit'] = 'pixel'
fig,ax = plt.subplots(1,2,figsize=(6,2.25), layout='tight')
res_ph_peak.sum(('dldSectorID','dldPosX','dldPosY')).plot(ax=ax[0])
res_ph_peak.sel(dldTimeSteps=slice(2380,2400)).mean(('dldSectorID','dldTimeSteps')).plot(ax=ax[1], robust=True)
[8]:
<matplotlib.collections.QuadMesh at 0x7f80d8f85ba0>

Just photon peak itself without surrounding background

[9]:
ph_peak = res_ph_peak.sel(dldTimeSteps=slice(2380,2400)).sum(('dldPosX','dldPosY'))
plt.figure(figsize=(6,4))
ph_peak.sum('dldSectorID').plot()
[9]:
[<matplotlib.lines.Line2D at 0x7f80e03e1330>]

Let’s check the signal (photon peak) from every single sector

[10]:
plt.figure(figsize=(6,4))
ph_peak.plot()
[10]:
<matplotlib.collections.QuadMesh at 0x7f80e01983a0>
[11]:
plt.figure(figsize=(6,4))
for i, item in enumerate(ph_peak):
    item.plot(label=f'S{i}')
    plt.legend()

Position of the photon peak#

[12]:
Gauss_mod = GaussianModel()

x=ph_peak['dldTimeSteps']
y=ph_peak.sum('dldSectorID')

pars = Gauss_mod.make_params(amplitude=200, center=2390, sigma=1)
# pars = Gauss_mod.guess(y, x=x)
out = Gauss_mod.fit(y, pars, x=x)

print(out.fit_report())
plt.figure(figsize=(6,4))
plt.plot(x,y, 'rx')
plt.plot(x,out.best_fit, "b", label="FWHM = {:.3f}".format(out.values['fwhm']))
plt.title(f'Run {run_number}, full photon peak')
plt.legend(loc="best")
plt.xlabel("dldTimeSteps [step]")
[[Model]]
    Model(gaussian)
[[Fit Statistics]]
    # fitting method   = leastsq
    # function evals   = 45
    # data points      = 141
    # variables        = 3
    chi-square         = 348712.352
    reduced chi-square = 2526.90110
    Akaike info crit   = 1107.66723
    Bayesian info crit = 1116.51351
    R-squared          = 0.93905197
[[Variables]]
    amplitude:  1939.42461 +/- 45.8617460 (2.36%) (init = 200)
    center:     2388.77841 +/- 0.02992332 (0.00%) (init = 2390)
    sigma:      1.09575685 +/- 0.02992198 (2.73%) (init = 1)
    fwhm:       2.58031015 +/- 0.07046088 (2.73%) == '2.3548200*sigma'
    height:     706.104198 +/- 16.6986338 (2.36%) == '0.3989423*amplitude/max(1e-15, sigma)'
[[Correlations]] (unreported correlations are < 0.100)
    C(amplitude, sigma) = +0.5773
[12]:
Text(0.5, 0, 'dldTimeSteps [step]')

Width of the photon peak from every quadrant and they offset in ps in respect to 0 quadrant

[13]:
plt.figure(figsize=(6,4))
sector_delays = np.zeros(8)
for i, item in enumerate(ph_peak):
    x=ph_peak['dldTimeSteps']
    y=item
    pars = Gauss_mod.make_params(amplitude=200, center=2390, sigma=1)
    out = Gauss_mod.fit(y, pars, x=x)
    Center = 2388.984276411258
    Diff = "{:.3f}".format(Center - out.values['center'])
    sector_delays[i] = (out.values['center'])
    FWHM = "{:.3f}".format(out.values['fwhm'])
    item.plot(label=f'S{i}={Diff}, FWHM = {FWHM}')
    plt.title(f'Run {run_number}, individual sectors, not aligned')
    plt.legend()
[14]:
sector_delays = sector_delays - np.mean(sector_delays)
sector_delays
[14]:
array([ 0.84143035,  0.63440337, -0.2350536 , -0.70526687,  0.31571148,
       -0.48552155, -0.14427478, -0.2214284 ])

sector alignment#

as usual, first, we jitter, but here we also align in time the 8 sectors of the dld. This is done by finding the time of the maximum of the signal in each sector, and then shifting the signal in each sector by the difference between the maximum time and the time of the maximum in each sector.

[15]:
sp_ph_peak.align_dld_sectors(sector_delays=sector_delays)
INFO - Aligning 8s sectors of dataframe
INFO - Dask DataFrame Structure:
               trainId pulseId electronId  dldPosX  dldPosY dldTimeSteps      bam timeStamp delayStage opticalDiode monochromatorPhotonEnergy   gmdBda sampleBias tofVoltage extractorVoltage extractorCurrent cryoTemperature sampleTemperature dldTimeBinSize dldSectorID
npartitions=17
                uint32   int64      int64  float64  float64      float32  float32   float64    float32      float32                   float32  float32    float32    float32          float32          float32         float32           float32        float32        int8
                   ...     ...        ...      ...      ...          ...      ...       ...        ...          ...                       ...      ...        ...        ...              ...              ...             ...               ...            ...         ...
...                ...     ...        ...      ...      ...          ...      ...       ...        ...          ...                       ...      ...        ...        ...              ...              ...             ...               ...            ...         ...
                   ...     ...        ...      ...      ...          ...      ...       ...        ...          ...                       ...      ...        ...        ...              ...              ...             ...               ...            ...         ...
                   ...     ...        ...      ...      ...          ...      ...       ...        ...          ...                       ...      ...        ...        ...              ...              ...             ...               ...            ...         ...
Dask Name: assign, 16 graph layers
[16]:
sp_ph_peak.dataframe[["dldTimeSteps", "dldSectorID"]].head()
[16]:
dldTimeSteps dldSectorID
0 2391.074463 3
1 2390.388184 0
2 4276.004883 7
3 4276.660645 6
4 4421.423340 7

Width of the photon peak after sector alignment#

Now we can repeat the fit procedure for combined and sector-separated photon peaks to see the effect of sector alignment

[17]:
axes = ['dldSectorID', 'dldTimeSteps','dldPosX','dldPosY']
ranges = [[0,8], [2360,2460], [435,885], [445,895]]
bins = [8,700,225,225]
res_ph_peak_align = sp_ph_peak.compute(bins=bins, axes=axes, ranges=ranges)

ph_peak_align = res_ph_peak_align.sel(dldTimeSteps=slice(2380,2400)).sum(('dldPosX','dldPosY'))

fig,ax = plt.subplots(1,2,figsize=(6,3.25), layout='tight')
ph_peak_align.sum('dldSectorID').plot(ax=ax[0])
for i, item in enumerate(ph_peak_align):
    item.plot(ax=ax[1], label=f'S{i}')
    plt.legend()
[18]:
Gauss_mod = GaussianModel()

x=ph_peak_align['dldTimeSteps']
y=ph_peak_align.sum('dldSectorID')

pars = Gauss_mod.make_params(amplitude=200, center=2390, sigma=1)
out = Gauss_mod.fit(y, pars, x=x)

print(out.fit_report())
plt.figure(figsize=(6,4))
plt.plot(x,y, 'rx')
plt.plot(x,out.best_fit, "b", label="FWHM = {:.3f}".format(out.values['fwhm']))
plt.title(f'Run {run_number}, full photon peak, sectors aligned')
plt.legend(loc="best")
plt.xlabel("dldTimeSteps [step]")
plt.show()
[[Model]]
    Model(gaussian)
[[Fit Statistics]]
    # fitting method   = leastsq
    # function evals   = 49
    # data points      = 141
    # variables        = 3
    chi-square         = 155431.599
    reduced chi-square = 1126.31593
    Akaike info crit   = 993.733361
    Bayesian info crit = 1002.57964
    R-squared          = 0.97271223
[[Variables]]
    amplitude:  1950.51777 +/- 30.4190303 (1.56%) (init = 200)
    center:     2388.97948 +/- 0.01947815 (0.00%) (init = 2390)
    sigma:      1.08151832 +/- 0.01947726 (1.80%) (init = 1)
    fwhm:       2.54678098 +/- 0.04586543 (1.80%) == '2.3548200*sigma'
    height:     719.492242 +/- 11.2216435 (1.56%) == '0.3989423*amplitude/max(1e-15, sigma)'
[[Correlations]] (unreported correlations are < 0.100)
    C(amplitude, sigma) = +0.5773

As we can see from the result of the last fit, after sector alignment, we have improved the photon peak width by 0.058 steps.

The same check can be done for every single sector to see/check that all sectors were properly corrected in time by their difference from the 0 sector.

[19]:
plt.figure(figsize=(6,4))
for i, item in enumerate(ph_peak_align):
    x=ph_peak_align['dldTimeSteps']
    y=item
    pars = Gauss_mod.make_params(amplitude=800.0, center=2390, sigma=1)
    out = Gauss_mod.fit(y, pars, x=x)
    Center = 2388.984276411258
    Diff = "{:.3f}".format(Center - out.values['center'])
    FWHM = "{:.3f}".format(out.values['fwhm'])
    item.plot(label=f'S{i}={Diff}, FWHM = {FWHM} step')
    plt.title(f'Run {run_number}, individual sectors, aligned')
    plt.legend()
plt.show()

Now we can make an ns conversion and do a fitting procedure again to see, e.g. DLD time resolution.

[20]:
sp_ph_peak.append_tof_ns_axis()
INFO - Adding time-of-flight column in nanoseconds to dataframe.
INFO - Dask DataFrame Structure:
               trainId pulseId electronId  dldPosX  dldPosY dldTimeSteps      bam timeStamp delayStage opticalDiode monochromatorPhotonEnergy   gmdBda sampleBias tofVoltage extractorVoltage extractorCurrent cryoTemperature sampleTemperature dldTimeBinSize dldSectorID  dldTime
npartitions=17
                uint32   int64      int64  float64  float64      float32  float32   float64    float32      float32                   float32  float32    float32    float32          float32          float32         float32           float32        float32        int8  float64
                   ...     ...        ...      ...      ...          ...      ...       ...        ...          ...                       ...      ...        ...        ...              ...              ...             ...               ...            ...         ...      ...
...                ...     ...        ...      ...      ...          ...      ...       ...        ...          ...                       ...      ...        ...        ...              ...              ...             ...               ...            ...         ...      ...
                   ...     ...        ...      ...      ...          ...      ...       ...        ...          ...                       ...      ...        ...        ...              ...              ...             ...               ...            ...         ...      ...
                   ...     ...        ...      ...      ...          ...      ...       ...        ...          ...                       ...      ...        ...        ...              ...              ...             ...               ...            ...         ...      ...
Dask Name: assign, 22 graph layers
[21]:
sp_ph_peak.dataframe[["dldTimeSteps", "dldTime", "dldSectorID"]].head()
[21]:
dldTimeSteps dldTime dldSectorID
0 2390.302979 393.465517 3
1 2389.917969 393.402141 0
2 4276.472168 703.946046 7
3 4277.381836 704.095786 6
4 4421.681152 727.848760 7
[22]:
axes = ['dldSectorID', 'dldTime']
ranges = [[0,8], [390,397]]
bins = [8,350]
res_ph_peak_ns = sp_ph_peak.compute(bins=bins, axes=axes, ranges=ranges)

plt.figure()
res_ph_peak_ns.sum(('dldSectorID')).plot()
plt.show()
[23]:
Gauss_mod = GaussianModel()

x=res_ph_peak_ns['dldTime']
y=res_ph_peak_ns.sum('dldSectorID')

pars = Gauss_mod.make_params(amplitude=340.0, center=393.2, sigma=0.19)
# pars = Gauss_mod.guess(y, x=x)
out = Gauss_mod.fit(y, pars, x=x)

print(out.fit_report())
plt.figure()
plt.plot(x,y, 'rx')
plt.plot(x,out.best_fit, "b", label="FWHM = {:.3f} ns".format(out.values['fwhm']))
#plt.title(f'Run {runs}, full photon peak')
plt.legend(loc="best")
plt.xlabel("dldTime [ns]")
plt.title(f'Run {run_number}, all sectors aligned')
plt.show()
[[Model]]
    Model(gaussian)
[[Fit Statistics]]
    # fitting method   = leastsq
    # function evals   = 25
    # data points      = 350
    # variables        = 3
    chi-square         = 142167.863
    reduced chi-square = 409.705656
    Akaike info crit   = 2108.39072
    Bayesian info crit = 2119.96451
    R-squared          = 0.97421364
[[Variables]]
    amplitude:  273.025296 +/- 2.77706424 (1.02%) (init = 340)
    center:     393.246191 +/- 0.00207909 (0.00%) (init = 393.2)
    sigma:      0.17699854 +/- 0.00207899 (1.17%) (init = 0.19)
    fwhm:       0.41679971 +/- 0.00489566 (1.17%) == '2.3548200*sigma'
    height:     615.379867 +/- 6.25982720 (1.02%) == '0.3989423*amplitude/max(1e-15, sigma)'
[[Correlations]] (unreported correlations are < 0.100)
    C(amplitude, sigma) = +0.5773
[ ]: