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 [3].

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 a 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

Angle offsets can be set using xarray.DataArray.kspace.offsets. 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:
  * alpha    (alpha) float64 2kB -15.0 -14.85 -14.7 -14.55 ... 14.7 14.85 15.0
  * beta     (beta) float64 480B -15.0 -14.49 -13.98 -13.47 ... 13.98 14.49 15.0
  * 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

To view the currently set angle offsets:

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

Since we haven’t set any offsets, they are all zero. Now, if we want to set the azimuthal angle to 60 degrees and the polar offset to 30 degrees, we can update the offsets as follows:

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

The offsets behave like a dictionary, and you can also reset or override all offsets at once using a dictionary:

dat.kspace.offsets = dict(delta=30.0)

dat.kspace.offsets
delta30.0
xi0.0
beta0.0

See xarray.DataArray.kspace.offsets for more ways to access and modify offsets.

Note

Offsets can be easily determined with the interactive momentum conversion tool.

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.offsets = dict(delta=30, xi=0.0, beta=0.0)
dat.kspace.work_function = 4.5

dat_kconv = dat.kspace.convert()
dat_kconv
<xarray.DataArray (eV: 300, kx: 418, ky: 414)> Size: 415MB
nan nan nan nan nan nan nan nan nan nan ... nan nan nan nan nan nan nan nan nan
Coordinates:
    xi       float64 8B 0.0
    delta    float64 8B 0.0
    hv       float64 8B 50.0
  * eV       (eV) float64 2kB -0.45 -0.4481 -0.4462 ... 0.1162 0.1181 0.12
  * 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
Attributes:
    configuration:        1
    sample_temp:          20.0
    sample_workfunction:  4.5
    delta_offset:         30.0
    xi_offset:            0.0
    beta_offset:          0.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 0x719959b2c0e0>
../_images/e9654250617582de0077c5a34fb204c5b8520392e15360cee31f3d132cb8a322.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 (eV: 300, kx: 101, ky: 101)> Size: 24MB
450.3 472.4 481.6 527.4 580.3 628.0 ... 0.1541 0.09057 0.01983 0.136 0.1791
Coordinates:
    xi       float64 8B 0.0
    delta    float64 8B 0.0
    hv       float64 8B 50.0
  * eV       (eV) float64 2kB -0.45 -0.4481 -0.4462 ... 0.1162 0.1181 0.12
  * 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
Attributes:
    configuration:        1
    sample_temp:          20.0
    sample_workfunction:  4.5
    delta_offset:         30.0
    xi_offset:            0.0
    beta_offset:          0.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 0x719959aa7500>
../_images/0f35d7061e4e79dfab95bee22a326bc529d6338d5c519855ff4970fb9cb7a478.svg

The target momentum coordinates can also be set manually:

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

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       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
    delta_offset:         30.0
    xi_offset:            0.0
    beta_offset:          0.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
    beta     float64 8B -9.915
    xi       float64 8B 0.0
    delta    float64 8B 0.0
    hv       float64 8B 50.0
    kx       (eV, alpha) float64 480kB 0.4848 0.477 0.4692 ... -1.056 -1.063
    ky       (eV, alpha) float64 480kB 0.9403 0.9363 0.9322 ... 0.05538 0.05063
Attributes:
    configuration:        1
    sample_temp:          20.0
    sample_workfunction:  4.5
    delta_offset:         30.0
    xi_offset:            0.0
    beta_offset:          0.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 0x71995a647440>]
../_images/8d03cd0462f26b03c353a9de3a02458f343521ba00825953c6a157e1ec9bda66.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
    beta     (hv) float64 400B -8.87 -8.577 -8.31 -8.067 ... -4.29 -4.256 -4.222
  * 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       (hv) float64 400B 20.0 21.0 22.0 23.0 24.0 ... 66.0 67.0 68.0 69.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:
    xi       float64 8B 0.0
    delta    float64 8B 0.0
  * kx       (kx) float64 5kB -1.066 -1.063 -1.059 -1.056 ... 1.059 1.063 1.066
    ky       float64 8B 0.3019
  * 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
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 0x71995336e0f0>
../_images/4d62682381d05f51669d9ad0f40b651129239c7cb9615772867f3f025709b300.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 (hv: 3, kx: 637)> Size: 15kB
2.831 2.832 2.833 2.834 2.836 2.837 2.838 ... 3.99 3.989 3.988 3.987 3.987 3.986
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
    ky       float64 8B 0.3019
    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 0x719959ad5f40>
../_images/e35e12b6741d60778da44eb129f44b78c1a55276e9da0f6069164c2a62ca0806.svg

Interactive conversion

For three dimensional momentum conversion like maps or photon energy scans, an interactive window can be opened where you can adjust the parameters and see the effect right away.

There are three ways to invoke the GUI. The first one is to call xarray.DataArray.kspace.interactive():

data.kspace.interactive()

The second option is to invoke the GUI with erlab.interactive.ktool(). This option is recommended because the name of the input data will be automatically detected and applied to the generated code that is copied to the clipboard.

import erlab.interactive as eri

eri.ktool(data)

The final option is to trigger the GUI from the ImageTool with the “Open in ktool” menu in the View menu. The button will be disabled if the data is not compatible with erlab.interactive.ktool().

The GUI is divided into two tabs.

KspaceTool 1 KspaceTool 1

The first tab is for setting momentum conversion parameters. The image is updated in real time as you change the parameters. Clicking the “Copy code” button will copy the code for conversion to the clipboard. The “Open in ImageTool” button performs a full three-dimensional conversion and opens the result in the ImageTool.

KspaceTool 2 KspaceTool 2

The second tab provides visualization options. You can overlay Brillouin zones and high symmetry points on the result, adjust colors, apply binning, and more. The “Add Circle ROI” button allows you to add a circular region of interest to the image, which can be edited by dragging or right-clicking on it.

You can pass some parameters to customize the GUI. For example, you can set the Brillouin zone size/orientation and the colormap like this:

data.kspace.interactive(
    avec=np.array([[-3.485, 6.03], [6.97, 0.0]]), rotate_bz=30.0, cmap="viridis"
)

See the documentation of erlab.interactive.ktool() for more information.