Momentum conversion

Momentum conversion in ERLabPy is exact with no small angle approximation, but is also very fast, thanks to the numba-accelerated trilinear interpolation in erlab.analysis.interpolate.

Nomenclature

Momentum conversion in ERLabPy follows the nomenclature from Ishida and Shin [2018].

All experimental geometry can be classified into two configurations, Type 1 and Type 2, based on the relative position of the rotation axis and the analyzer slit. These can be further divided into 4 configurations depending on the use of photoelectron deflectors (DA).

Definition of angles differ for each geometry, but in all cases, \(\delta\) is the azimuthal angle that indicates in-plane rotation, \(\alpha\) is the angle detected by the analyzer, and \(\beta\) is the angle along which mapping is performed.

For instance, imagine a typical Type 1 setup with a vertical slit that acquires maps by rotating about the z axis in the lab frame. In this case, the polar angle (rotation about z) is \(\beta\), and the tilt angle becomes \(\xi\).

The following table summarizes angle conventions for commonly encountered configurations.

Analyzer slit orientation

Mapping angle

Configuration

Polar

Tilt

Deflector

Azimuth

Analyzer

Vertical

Polar

1 (Type 1)

beta

xi

delta

alpha

Horizontal

Tilt

2 (Type 2)

xi

beta

Vertical

Deflector

3 (Type 1 + DA)

chi

xi

beta

Horizontal

4 (Type 2 + DA)

Note

Analyzers that can measure two-dimensional angular information simultaneously (e.g. time-of-flight analyzers) can be treated like hemispherical analyzers equipped with a deflector.

import matplotlib.pyplot as plt
import numpy as np

import erlab.plotting as eplt

Momentum conversion

Note

For momentum conversion to work properly, the data must follow the conventions listed here.

Setting parameters

Parameters that are needed for momentum conversion are:

  • The information about the experimental configuration

  • Work function of the system

  • The inner potential \(V_0\) (for photon energy dependent data)

  • Angle offsets

These parameters are all stored as data attributes. The kspace accessor provides various ways to access and modify these parameters.

Experimental configuration

The first step is to set the experimental configuration. Most of the time, this information is already recorded in the data file by the data loader plugin. If not, it can be set manually using xarray.DataArray.kspace.configuration:

data.kspace.configuration = erlab.constants.AxesConfiguration.Type1DA

This will set the configuration to Type 1 with a deflector. The configuration can also be set using numbers:

data.kspace.configuration = 3

Sometimes, the automatically determined configuration may be incorrect. For example, plugins for setups equipped with an electrostatic deflector will assign configuration 3 or 4 to the data, but the data may have been acquired without the deflector, in which case configuration 1 or 2 should be used. Also, some setups (e.g. ALS BL7) have variable slit orientation by allowing the analyzer to be rotated about the lens axis. As such, it is not always possible to determine the configuration from the data alone. In these cases, the configuration can be converted with xarray.DataArray.kspace.as_configuration() which takes a configuration number or an enum as an argument. For example, consider data taken at ALS BL7 with configuration 2 (horizontal slit, tilt map). By default, the loader will assign configuration 3 (vertical slit, DA map) to the data, which is incorrect. To convert the data to configuration 2, you can do:

data = data.kspace.as_configuration(2)

which returns a copy of the data with the configuration set to 2, and the coordinates renamed accordingly.

Note

The method assumes a typical ARPES setup with a vertical cryostat. For complex setups, the user should manually set the configuration attribute and rename the coordinates.

Work function of the system

The work function of the system can be set using xarray.DataArray.kspace.work_function:

data.kspace.work_function = 4.5

Inner potential \(V_0\)

The inner potential (for photon energy dependent data) can be set using xarray.DataArray.kspace.inner_potential:

data.kspace.inner_potential = 10.0

Angle offsets

The preferred way to set angle offsets is xarray.DataArray.kspace.set_normal(), which takes the data angles corresponding to normal emission and derives the required offsets for the current geometry.

For demonstration, let’s generate some example data, this time in angle coordinates.

from erlab.io.exampledata import generate_data_angles

dat = generate_data_angles(shape=(200, 60, 300), assign_attributes=True, seed=1).T
dat
<xarray.DataArray (eV: 300, beta: 60, alpha: 200)> Size: 29MB
129.6 115.6 96.64 76.55 65.6 56.97 ... 0.1651 0.004688 0.1373 0.5505 0.1394
Coordinates:
  * eV       (eV) float64 2kB -0.45 -0.4481 -0.4462 ... 0.1162 0.1181 0.12
  * beta     (beta) float64 480B -15.0 -14.49 -13.98 -13.47 ... 13.98 14.49 15.0
  * alpha    (alpha) float64 2kB -15.0 -14.85 -14.7 -14.55 ... 14.7 14.85 15.0
    xi       float64 8B 0.0
    delta    float64 8B 0.0
    hv       float64 8B 50.0
Attributes:
    configuration:        1
    sample_temp:          20.0
    sample_workfunction:  4.5

To view the currently stored angle offsets:

dat.kspace.offsets
delta0.0
xi0.0
beta0.0
normal alpha0.0
normal beta0.0

Since we haven’t set any offsets, they are all zero. Suppose the sample normal appears at alpha=1.5° and beta=-0.8° in the data. We can assign the angle offsets directly:

dat.kspace.set_normal(alpha=1.5, beta=-0.8)

This is usually the most intuitive way to work, because it uses the measured normal-emission position instead of making you solve the offset signs by hand.

If you also know the azimuthal offset, you can set it at the same time:

dat.kspace.set_normal(alpha=1.5, beta=-0.8, delta=30.0)

dat.kspace.offsets
delta30.0
xi-1.5
beta-0.8
normal alpha1.5
normal beta-0.8

If you need direct, dictionary-style control over offsets, see the advanced section on angle offsets and caveats below.

Note

Use ktool when you want to interactively adjust parameters and overlay Brillouin zones.

Converting to momentum space

Momentum conversion is done by the xarray.DataArray.kspace.convert() method after applying appropriate offsets. The bounds and resolution are automatically determined from the data if no input is provided. The method returns a new DataArray in momentum space.

dat.kspace.set_normal(alpha=0.0, beta=0.0, delta=30.0)
dat.kspace.work_function = 4.5

dat_kconv = dat.kspace.convert()
dat_kconv
<xarray.DataArray (kx: 418, ky: 414, eV: 300)> Size: 415MB
nan nan nan nan nan nan nan nan nan nan ... nan nan nan nan nan nan nan nan nan
Coordinates:
  * kx       (kx) float64 3kB -1.208 -1.202 -1.197 -1.191 ... 1.197 1.202 1.208
  * ky       (ky) float64 3kB -1.197 -1.191 -1.185 -1.18 ... 1.185 1.191 1.197
  * eV       (eV) float64 2kB -0.45 -0.4481 -0.4462 ... 0.1162 0.1181 0.12
    xi       float64 8B 0.0
    delta    float64 8B 0.0
    hv       float64 8B 50.0
Attributes:
    configuration:        1
    sample_temp:          20.0
    sample_workfunction:  4.5
    xi_offset:            0.0
    beta_offset:          0.0
    delta_offset:         30.0

Let us plot the original and converted data side by side.

fig, axs = plt.subplots(1, 2, layout="compressed")
eplt.plot_array(dat.qsel(eV=-0.3), ax=axs[0], aspect="equal")
eplt.plot_array(dat_kconv.qsel(eV=-0.3), ax=axs[1], aspect="equal")
<matplotlib.image.AxesImage at 0x7687767682d0>
../_images/5d94074651ae02feb9ef18fa0d4a87b71121bbbd623248cbc57eb26efed5f776.svg

We can see the effect of angle offsets on the conversion.

The step size and bounds of momentum coordinates can be set manually as well:

dat_kconv = dat.kspace.convert(
    bounds=dict(kx=(-0.5, 0.5), ky=(-0.5, 0.5)),
    resolution=dict(kx=0.01, ky=0.01),
)
dat_kconv
<xarray.DataArray (kx: 101, ky: 101, eV: 300)> Size: 24MB
450.3 430.5 410.3 412.9 403.1 380.8 ... 0.06322 0.01239 0.05766 0.09326 0.1791
Coordinates:
  * kx       (kx) float64 808B -0.5 -0.49 -0.48 -0.47 ... 0.47 0.48 0.49 0.5
  * ky       (ky) float64 808B -0.5 -0.49 -0.48 -0.47 ... 0.47 0.48 0.49 0.5
  * eV       (eV) float64 2kB -0.45 -0.4481 -0.4462 ... 0.1162 0.1181 0.12
    xi       float64 8B 0.0
    delta    float64 8B 0.0
    hv       float64 8B 50.0
Attributes:
    configuration:        1
    sample_temp:          20.0
    sample_workfunction:  4.5
    xi_offset:            0.0
    beta_offset:          0.0
    delta_offset:         30.0
fig, axs = plt.subplots(1, 2, layout="compressed")
eplt.plot_array(dat.qsel(eV=-0.3), ax=axs[0], aspect="equal")
eplt.plot_array(dat_kconv.qsel(eV=-0.3), ax=axs[1], aspect="equal")
<matplotlib.image.AxesImage at 0x768774a091d0>
../_images/be059a7282341e5625143302c50c48172a27c259fc4090f467979bd5c3f6b9bd.svg

The target momentum coordinates can also be set manually:

dat.kspace.convert(kx=np.linspace(-0.6, 0.6, 100))
<xarray.DataArray (kx: 100, ky: 414, eV: 300)> Size: 99MB
nan nan nan nan nan nan nan nan nan nan ... nan nan nan nan nan nan nan nan nan
Coordinates:
  * kx       (kx) float64 800B -0.6 -0.5879 -0.5758 ... 0.5758 0.5879 0.6
  * ky       (ky) float64 3kB -1.197 -1.191 -1.185 -1.18 ... 1.185 1.191 1.197
  * eV       (eV) float64 2kB -0.45 -0.4481 -0.4462 ... 0.1162 0.1181 0.12
    xi       float64 8B 0.0
    delta    float64 8B 0.0
    hv       float64 8B 50.0
Attributes:
    configuration:        1
    sample_temp:          20.0
    sample_workfunction:  4.5
    xi_offset:            0.0
    beta_offset:          0.0
    delta_offset:         30.0

Advanced angle offsets and caveats

If you would rather set the angle offsets manually, you can do so by editing xarray.DataArray.kspace.offsets directly. The offsets can be manipulated using dictionary-style access with keys corresponding to the offset angle names.

For example, if you already know the azimuthal and polar offsets, you can update them directly:

dat.kspace.offsets.update(delta=60.0, beta=30.0)
dat.kspace.offsets
delta60.0
xi0.0
beta30.0
normal alpha0.0
normal beta30.0

You can also replace the stored offset mapping in one shot:

dat.kspace.offsets = dict(delta=30.0)
dat.kspace.offsets
delta30.0
xi0.0
beta0.0
normal alpha0.0
normal beta0.0

These direct edits are useful, but they require you to keep the sign conventions and geometry straight. The relative-offset behavior below explains why xarray.DataArray.kspace.set_normal() is usually safer unless you fully understand the geometry and conventions.

Momentum conversion uses the angle coordinates in the data along with angle offsets. This is to ensure that scans with varying angles (e. g. photon-energy dependent scans with varying sample angle) are handled correctly. Hence, angle offsets refer to the relative angular displacement of the sample normal with respect to the angle coordinates in the data, rather than the absolute position of the sample normal. This means that angle offsets required to center the data in momentum space differ depending on the angle coordinates in the data.

To get a better idea, let’s start with an example. Consider a typical Type 1 setup (vertical manipulator, vertical slit). The sample is slightly tilted such that you have to move the tilt angle (\(\xi\)) to 3° in order to align the sample normal to be at zero analyzer angle (\(\alpha=0\)°).

from erlab.io.exampledata import generate_data_angles

dat = (
    generate_data_angles(shape=(200, 60, 300), assign_attributes=True, seed=1)
    .assign_coords(xi=3)
    .T
)
dat
<xarray.DataArray (eV: 300, beta: 60, alpha: 200)> Size: 29MB
129.6 115.6 96.64 76.55 65.6 56.97 ... 0.1651 0.004688 0.1373 0.5505 0.1394
Coordinates:
  * eV       (eV) float64 2kB -0.45 -0.4481 -0.4462 ... 0.1162 0.1181 0.12
  * beta     (beta) float64 480B -15.0 -14.49 -13.98 -13.47 ... 13.98 14.49 15.0
  * alpha    (alpha) float64 2kB -15.0 -14.85 -14.7 -14.55 ... 14.7 14.85 15.0
    xi       int64 8B 3
    delta    float64 8B 0.0
    hv       float64 8B 50.0
Attributes:
    configuration:        1
    sample_temp:          20.0
    sample_workfunction:  4.5

The generated data simulates a map acquired by rotating the polar angle (\(\beta\)) while keeping the tilt angle (\(\xi\)) fixed at 3°. A quick plot shows that the bands are nicely centered at \(\alpha=0\)° and \(\beta=0\)°.

dat.qsel(eV=-0.3).qplot(aspect="equal")
<matplotlib.image.AxesImage at 0x7687749cfc50>
../_images/009de36a3aae560b52309897772f0d73411ea42131036d569f133afece780c49.svg

If we convert this data with zero angle offsets, we get:

dat.kspace.convert().qsel(eV=-0.3).qplot(aspect="equal")
<matplotlib.image.AxesImage at 0x76877412bb10>
../_images/9fb0b0397767c34a786fea9accd0379da9ca001e28fecf26951e9338810b5ab1.svg

As you can see, the bands are off-centered in momentum space in the \(k_x\) direction because the tilt of the sample was not compensated with the correct angle offset.

To fix this, we need to set the tilt angle offset to 3° before conversion, which centers the bands correctly:

dat.kspace.offsets = dict(xi=3.0)
dat.kspace.convert().qsel(eV=-0.3).qplot(aspect="equal")
<matplotlib.image.AxesImage at 0x768775c8d590>
../_images/e171b99c2e7bbc02c8ca1253b8dfca4ac71f8fc2efdba6d196f24005d9d684c2.svg

Converting coordinates only

Sometimes, we need to obtain the converted coordinates in momentum space without modifying the data grid.

This can be done using xarray.DataArray.kspace.convert_coords() which adds momentum coordinates to the DataArray.

The code below demonstrates a possible use case where we convert the coordinates of a cut to momentum space and overlay the location of the cut on the converted constant energy map.

First, we select a cut from the original data along constant beta.

cut = dat.qsel(beta=-10)
cut
<xarray.DataArray (eV: 300, alpha: 200)> Size: 480kB
171.5 221.9 379.4 661.3 1.126e+03 ... 8.253e-07 0.000433 0.02783 0.112 0.03038
Coordinates:
  * eV       (eV) float64 2kB -0.45 -0.4481 -0.4462 ... 0.1162 0.1181 0.12
  * alpha    (alpha) float64 2kB -15.0 -14.85 -14.7 -14.55 ... 14.7 14.85 15.0
    beta     float64 8B -9.915
    xi       int64 8B 3
    delta    float64 8B 0.0
    hv       float64 8B 50.0
Attributes:
    configuration:        1
    sample_temp:          20.0
    sample_workfunction:  4.5
    xi_offset:            3.0
cut = cut.kspace.convert_coords()
cut
<xarray.DataArray (eV: 300, alpha: 200)> Size: 480kB
171.5 221.9 379.4 661.3 1.126e+03 ... 8.253e-07 0.000433 0.02783 0.112 0.03038
Coordinates:
  * eV       (eV) float64 2kB -0.45 -0.4481 -0.4462 ... 0.1162 0.1181 0.12
  * alpha    (alpha) float64 2kB -15.0 -14.85 -14.7 -14.55 ... 14.7 14.85 15.0
    kx       (eV, alpha) float64 480kB 0.89 0.8812 0.8725 ... -0.8868 -0.8956
    ky       (eV, alpha) float64 480kB 0.5719 0.5723 0.5727 ... 0.5759 0.5755
    beta     float64 8B -9.915
    xi       int64 8B 3
    delta    float64 8B 0.0
    hv       float64 8B 50.0
Attributes:
    configuration:        1
    sample_temp:          20.0
    sample_workfunction:  4.5
    xi_offset:            3.0

We can see that coordinate conversion adds momentum coordinates kx and ky, but does not affect any existing coordinates. Now, let’s annotate the cut location on the constant energy map.

fig, ax = plt.subplots()

dat.kspace.convert().qsel(eV=-0.3).qplot(ax=ax, aspect="equal")

mdc = cut.qsel(eV=-0.3)
ax.plot(mdc.ky, mdc.kx, color="r")
[<matplotlib.lines.Line2D at 0x768775d01090>]
../_images/95d111bf209621f33a8e31481c64839c50ad64589d5a498e30e8b54e54e639db.svg

\(k_z\)-dependent data

Converting \(k_z\)-dependent data can be done in the exact same way by choosing an appropriate value for the inner potential \(V_0\). Let’s generate some example data that resembles photon energy dependent cuts.

from erlab.io.exampledata import generate_hvdep_cuts

hvdep = generate_hvdep_cuts(seed=1)
hvdep
<xarray.DataArray (alpha: 250, eV: 300, hv: 50)> Size: 30MB
26.74 24.21 23.19 23.06 24.27 ... 0.002132 4.158e-07 0.02825 6.355e-09 0.1394
Coordinates:
  * alpha    (alpha) float64 2kB -15.0 -14.88 -14.76 -14.64 ... 14.76 14.88 15.0
  * eV       (eV) float64 2kB -0.45 -0.4481 -0.4462 ... 0.1162 0.1181 0.12
  * hv       (hv) float64 400B 20.0 21.0 22.0 23.0 24.0 ... 66.0 67.0 68.0 69.0
    beta     (hv) float64 400B -8.87 -8.577 -8.31 -8.067 ... -4.29 -4.256 -4.222
    xi       float64 8B 0.0
    delta    float64 8B 0.0
Attributes:
    configuration:        1
    sample_temp:          20.0
    sample_workfunction:  4.5

In this simulated data, the cuts are not through the BZ center, so the beta angle also varies for each photon energy.

We can convert this data to momentum space like before, after setting the inner potential.

hvdep.kspace.inner_potential = 10.0
hvdep_kconv = hvdep.kspace.convert()
hvdep_kconv
<xarray.DataArray (kx: 637, kz: 63, eV: 300)> Size: 96MB
nan nan nan nan nan nan nan nan nan nan ... nan nan nan nan nan nan nan nan nan
Coordinates:
  * kx       (kx) float64 5kB -1.066 -1.063 -1.059 -1.056 ... 1.059 1.063 1.066
  * kz       (kz) float64 504B 2.495 2.525 2.556 2.587 ... 4.353 4.384 4.415
  * eV       (eV) float64 2kB -0.45 -0.4481 -0.4462 ... 0.1162 0.1181 0.12
    ky       (kx, kz, eV) float64 96MB 0.2668 0.2668 0.2669 ... nan nan nan
    xi       float64 8B 0.0
    delta    float64 8B 0.0
Attributes:
    configuration:        1
    sample_temp:          20.0
    sample_workfunction:  4.5
    inner_potential:      10.0
fig, axs = plt.subplots(1, 2, layout="constrained")
eplt.plot_array(hvdep.qsel(eV=-0.3).T, ax=axs[0])
eplt.plot_array(hvdep_kconv.qsel(eV=-0.3).T, ax=axs[1])
<matplotlib.image.AxesImage at 0x76874e394690>
../_images/610b5c26881fc29fb99e3025c7b576099720913fbe4bf0ee341126ba74cdab04.svg

Note

Since the generated example data is 2D-like, there is no visible periodicity in \(k_z\), so it is impossible to estimate \(V_0\). In practice, \(V_0\) must be chosen so that the periodicity in \(k_z\) matches the known periodicity of the lattice.

Annotating the photon energy

Each photon energy can be annotated on the converted data using xarray.DataArray.kspace.convert_coords() with the data before conversion as described above. However, this only works for photon energies that exist in the data.

The annotation can be done more easily by using xarray.DataArray.kspace.hv_to_kz() on the converted data. The method returns the \(k_z\) value for given photon energies based on the parameters stored in the data.

Here, we calculate the \(k_z\) values for three different photon energies and select a given binding energy.

kz_values = hvdep_kconv.kspace.hv_to_kz([30, 45, 60]).qsel(eV=-0.3)
kz_values
<xarray.DataArray 'kz' (hv: 3, kx: 637)> Size: 15kB
2.833 2.834 2.835 2.837 2.838 2.839 2.84 ... 3.991 3.99 3.989 3.988 3.987 3.987
Coordinates:
  * hv       (hv) int64 24B 30 45 60
  * kx       (kx) float64 5kB -1.066 -1.063 -1.059 -1.056 ... 1.059 1.063 1.066
    xi       float64 8B 0.0
    delta    float64 8B 0.0
    eV       float64 8B -0.2994

We can now plot the calculated \(k_z\) values on top of the converted data.

fig, ax = plt.subplots(layout="constrained")

hvdep_kconv.qsel(eV=-0.3).T.qplot(ax=ax, aspect="equal")

for i in range(len(kz_values.hv)):
    kz = kz_values.isel(hv=i)

    ax.plot(kz.kx, kz, label=rf"$h\nu = {kz.hv:d}$ eV")

ax.legend()
<matplotlib.legend.Legend at 0x76877661fcb0>
../_images/ceeba44310649b5fe2c64c83b87da51b54dcd36e69a3e10774f554dc69238095.svg

GUI equivalent

Use ktool when you want to discover offsets, work function, bounds, and resolution visually.

You can use Copy to clipboard in ktool to write the final data.kspace.offsets state and data.kspace.convert(...) call back into the notebook.