# -*- coding: utf-8 -*-
"""
@author: Steinn Ymir Agustsson, Davide Curcio, Maciej Dendzik
"""
import sys, os
import numpy as np
[docs]def gen_sector_correction(prc, energies, eref, tofVoltage=None, sampleBias=None, monoEnergy=None):
"""
This function is helpful in generating the sector_correction list.
This takes into account the time shift caused by the bit stealing hack
plus is keeps track of the time shift due to detector misalignment by making sure
all values of energies are at eref.
Usage: use the function to create the sector_correction list and assign it to prc.SECTOR_CORRECTION
or paste it into the settings with no brackets
:param prc:
:param energies:
:param eref:
:return:
"""
if sampleBias is None:
sampleBias = np.nanmean(prc.dd['sampleBias'].values)
if monoEnergy is None:
monoEnergy = np.nanmean(prc.dd['monochromatorPhotonEnergy'].values)
if tofVoltage is None:
tofVoltage = np.nanmean(prc.dd['tofVoltage'].values)
# Here, the most basic sector_correction list is generated, where the bit stealing hack gets corrected
n_sectors=prc.dd['dldSectorId'].values.compute().max().astype(int)+1
sector_correction = np.floor(np.array(range(n_sectors))*np.power(2,prc.DLD_ID_BITS)/n_sectors)
# e.g. for 3 stolen bits, 8 detector ids (new s8 data), this will look like [0,1,2,3,4,5,6,7]
# e.g. for 1 stolen bit, 8 detector ids (old modified s8 data), this will look like [0,0,0,0,1,1,1,1]
# e.g. for 1 stolen bit, 2 detector ids (old s8 data), this will look like [0,1]
t_ref = energy2tof(eref, l=0.965, eoffset=-2.64-sampleBias+tofVoltage+monoEnergy)
times=[]
for ee in energies:
times.append((energy2tof(ee, l=0.965, eoffset=-2.64-47+30+monoEnergy)-t_ref)/prc.TOF_STEP_TO_NS)
sector_correction=sector_correction+np.array(times)
# here the sectors are shifter by the required time to align the energies
return sector_correction
# %% Energy calibration
""" The following functions convert between binding energy (Eb) in eV (negative convention)
and time of flight (ToF) in ns.
The formula used is based on the ToF for an electron with a kinetic energy Ek. Then the
binding energy Eb is given by
.. math::
-E_b = E_k + W - hv - V = \\frac{1}{2} mv^2 + W - hv - V
With W the work function, hv the photon energy, V the electrostatic potential applied to
the sample, v the velocity of the electrons in the drift tube, m the mass of the electron.
The velocity v in the drift tube can be calculated knowing the length (1m) and the flight
time in the drift tube. The measured ToF, however, has some offset due to clock start not
coinciding with entry in the drift section.
offs is supposed to include the time offset for when the electrons enter the drift section.
Its main mainly affects peak spacing, and there are several strategies for calibrating this
value:
1. By measuring the photon peak and correcting by some extractor voltage-dependent offset
2. Shifting the potential by 1V and imposing the same shift in the measured spectrum
3. Imposing some calibrated spacing between features in a spectrum
oo is supposed to include -W+hv+V. It mainly affects absolute position of the peaks, and
there are several strategies for calibrating this value:
1. By getting the correct values for W, hv, and V
2. It can be calibrated by imposing peak position
Parameters:
t (float) the ToF
e (float) the binding energy
"""
[docs]def tof2energy(t, toffset=None, eoffset=None, l=None):
""" Transform ToF to eV.
The functions (tof2energy and energy2tof) convert between binding energy (:math:`E_b`) in eV (negative convention)
and time of flight (ToF) in ns.
The formula used is based on the ToF for an electron with a kinetic energy :math:`E_k`. Then the
binding energy :math:`E_b` is given by
.. math::
-E_b = E_k + W - hv - V = \\frac{1}{2} mv^2 + W - hv - V
With W the work function, hv the photon energy, V the electrostatic potential applied to
the sample with respect to the drift section voltage, v the velocity of the electrons in the drift tube,
m the mass of the electron.
The velocity v in the drift tube can be calculated knowing the length (1m) and the flight
time in the drift tube. The measured ToF, however, has some offset due to clock start not
coinciding with entry in the drift section.
toffset is supposed to include the time offset for when the electrons enter the drift section.
Its main mainly affects peak spacing, and there are several strategies for calibrating this value,
1. By measuring the photon peak and correcting by some extractor voltage-dependent offset
2. Shifting the potential by 1V and imposing the same shift in the measured spectrum
3. Imposing some calibrated spacing between features in a spectrum
eoffset is supposed to include -W+hv+V. It mainly affects absolute position of the peaks, and
there are several strategies for calibrating this value,
1. By getting the correct values for W, hv, and V
2. It can be calibrated by imposing peak position
Parameters:
t: float
The time of flight
toffset: float
The time offset from thedld clock start to when the fastest photoelectrons reach the detector
eoffset: float
The energy offset given by W-hv-V
l: float
the effective length of the drift section
Return:
e: float
The binding energy
Authors:
Davide Curcio <davide.curcio@phys.au.dk>
"""
from configparser import ConfigParser
settings = ConfigParser()
if os.path.isfile(os.path.join(os.path.dirname(__file__), 'SETTINGS.ini')):
settings.read(os.path.join(os.path.dirname(__file__), 'SETTINGS.ini'))
else:
settings.read(
os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'SETTINGS.ini'))
if toffset is None:
toffset = float(settings['processor']['ET_CONV_T_OFFSET'])
if eoffset is None:
eoffset = float(settings['processor']['ET_CONV_E_OFFSET'])
if l is None:
l = float(settings['processor']['ET_CONV_L'])
e = 0.5 * 1e18 * 9.10938e-31 * l * l / (((t) - toffset) * ((t) - toffset)) / 1.602177e-19 - eoffset
return e
[docs]def energy2tof(e, toffset=None, eoffset=None, l=None):
""" Transform eV to time of flight (ToF).
The functions (tof2energy and energy2tof) convert between binding energy (:math:`E_b`) in eV (negative convention)
and time of flight (ToF) in ns.
The formula used is based on the ToF for an electron with a kinetic energy :math:`E_k`. Then the
binding energy :math:`E_b` is given by
.. math::
-E_b = E_k + W - hv - V = \\frac{1}{2} mv^2 + W - hv - V
With W the work function, hv the photon energy, V the electrostatic potential applied to
the sample, v the velocity of the electrons in the drift tube, m the mass of the electron.
The velocity v in the drift tube can be calculated knowing the length (1m) and the flight
time in the drift tube. The measured ToF, however, has some offset due to clock start not
coinciding with entry in the drift section.
offs is supposed to include the time offset for when the electrons enter the drift section.
Its main mainly affects peak spacing, and there are several strategies for calibrating this value,
1. By measuring the photon peak and correcting by some extractor voltage-dependent offset
2. Shifting the potential by 1V and imposing the same shift in the measured spectrum
3. Imposing some calibrated spacing between features in a spectrum
eoffset is supposed to include -W+hv+V. It mainly affects absolute position of the peaks, and
there are several strategies for calibrating this value,
1. By getting the correct values for W, hv, and V
2. It can be calibrated by imposing peak position
Parameters:
e: float
The binding energy
toffset: float
The time offset from thedld clock start to when the fastest photoelectrons reach the detector
eoffset: float
The energy offset given by W-hv-V
l: float
the effective length of the drift section
Return:
t: float
The time of flight
Authors:
Davide Curcio <davide.curcio@phys.au.dk>
"""
from configparser import ConfigParser
settings = ConfigParser()
if os.path.isfile(os.path.join(os.path.dirname(__file__), 'SETTINGS.ini')):
settings.read(os.path.join(os.path.dirname(__file__), 'SETTINGS.ini'))
else:
settings.read(
os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'SETTINGS.ini'))
if toffset is None:
toffset = float(settings['processor']['ET_CONV_T_OFFSET'])
if eoffset is None:
eoffset = float(settings['processor']['ET_CONV_E_OFFSET'])
if l is None:
l = float(settings['processor']['ET_CONV_L'])
t = l * np.sqrt(0.5 * 1e18 * 9.10938e-31 / 1.602177e-19 / (e + eoffset)) + toffset
return t
[docs]def e2t(e, toffset=None, eoffset=None, l=0.77):
""" Same as energy2tof, but different name for retrocompatibility
Parameters:
e: float
The binding energy
toffset: float
The time offset from thedld clock start to when the fastest photoelectrons reach the detector
eoffset: float
The energy offset given by W-hv-V
l: float
the effective length of the drift section
Return:
t: float
The time of flight
Authors:
Davide Curcio <davide.curcio@phys.au.dk>
"""
return energy2tof(e, toffset, eoffset, l)
[docs]def t2e(t, toffset=None, eoffset=None, l=0.77):
""" Same as tof2energy, but different name for retrocompatibility
Parameters:
t: float
The time of flight
toffset: float
The time offset from thedld clock start to when the fastest photoelectrons reach the detector
eoffset: float
The energy offset given by W-hv-V
l: float
the effective length of the drift section
Return:
e: float
The binding energy
Authors:
Davide Curcio <davide.curcio@phys.au.dk>
"""
return tof2energy(t, toffset, eoffset, l)
# %% Detector calibration
# ==================
# Methods by Mac! Built for the April 2018 beamtime, where a four quadrant detector was used.
# ==================
[docs]def shiftQuadrants(self, shiftQ1=0.231725, shiftQ2=-0.221625, shiftQ3=0.096575, shiftQ4=-0.106675, xCenter=1350,
yCenter=1440):
""" Apply corrections to the dataframe. (Maciej Dendzik)
Each quadrant of DLD is shifted in DLD time by shiftQn.
xCenter and yCenter are used to define the center of the division.
+-----------+-----------+
| Q2 | Q4 |
+-----------+-----------+
| Q1 | Q3 |
+-----------+-----------+
This picture is upside-down in ``plt.imshow`` because it starts from 0 in top right corner.
"""
# Q1
# daskdataframe.where(condition,value) keeps the data where condition is True
# and changes them to value otherwise.
cond = ((self.dd['dldPosX'] > xCenter) | (self.dd['dldPosY'] > yCenter))
self.dd['dldTime'] = self.dd['dldTime'].where(cond, self.dd['dldTime'] + shiftQ1)
cond = ((self.dd['dldPosX'] > xCenter) | (self.dd['dldPosY'] < yCenter))
self.dd['dldTime'] = self.dd['dldTime'].where(cond, self.dd['dldTime'] + shiftQ2)
cond = ((self.dd['dldPosX'] < xCenter) | (self.dd['dldPosY'] > yCenter))
self.dd['dldTime'] = self.dd['dldTime'].where(cond, self.dd['dldTime'] + shiftQ3)
cond = ((self.dd['dldPosX'] < xCenter) | (self.dd['dldPosY'] < yCenter))
self.dd['dldTime'] = self.dd['dldTime'].where(cond, self.dd['dldTime'] + shiftQ4)
[docs]def filterCircleDLDPos(self, xCenter=1334, yCenter=1426, radius=1250):
""" Apply corrections to the dataframe. (Maciej Dendzik)
Filters events with dldPosX and dldPosY within the radius from (xCenter,yCenter).
"""
self.dd = self.dd[
(((self.dd['dldPosX'] - xCenter) ** 2 + (self.dd['dldPosY'] - yCenter) ** 2) ** 0.5 <= radius)]
[docs]def correctOpticalPath(self, poly1=-0.00020578, poly2=4.6813e-7, xCenter=1334, yCenter=1426):
""" Apply corrections to the dataframe. (Maciej Dendzik)
Each DLD time is subtracted with a polynomial poly1*r + poly2*r^2,
where r=sqrt((posx-xCenter)^2+(posy-yCenter)^2)
This function makes corrections to the time of flight which take into account
the path difference between the center of the detector and the edges of the detector.
"""
# Q1
# daskdataframe.where(condition,value) keeps the data where condition is True
# and changes them to value otherwise.
self.dd['dldTime'] = self.dd['dldTime'] - \
(poly1 * ((self.dd['dldPosX'] - xCenter) ** 2 + (
self.dd['dldPosY'] - yCenter) ** 2) ** 0.5 + \
poly2 * ((self.dd['dldPosX'] - xCenter) ** 2 + (self.dd['dldPosY'] - yCenter) ** 2))