First notebook

This tutorial is for users who can read basic Python and run notebook cells. It introduces the xarray concepts that the workflow uses.

Download this notebook and run it in Jupyter.

Before you start, complete the installation verification. Run this notebook in the same ERLabPy environment.

You will follow one analysis path from simulated angle-resolved data to momentum space and dispersion plots. The fixed simulation makes each result reproducible. You can compare your output with the output on this page.

Analysis path

  1. Create a three-dimensional angle-resolved dataset.

  2. Inspect its xarray dimensions, coordinates, and metadata.

  3. Crop the dataset by coordinate values.

  4. Select a constant energy surface and average an energy interval.

  5. Plot constant energy surfaces and energy–angle cuts.

  6. Check the momentum conversion parameters and set the normal emission position.

  7. Convert the angle-resolved data to momentum space and plot constant energy surfaces and energy–momentum cuts.

  8. Overlay the Brillouin zone and extract K–M–K′ and Γ–M–K–Γ cuts.

Imports

Import NumPy, xarray, Matplotlib, and ERLabPy.

import matplotlib.pyplot as plt
import numpy as np
import xarray as xr

import erlab.analysis as era
import erlab.plotting as eplt
from erlab.io.exampledata import generate_data_angles

Example ARPES data

erlab.io.exampledata.generate_data_angles() creates a simulated three-dimensional xarray.DataArray. It contains intensity as a function of alpha, beta, and eV that mimics a typical angle-resolved photoemission spectroscopy (ARPES) experiment.

example_map = generate_data_angles(assign_attributes=True, seed=1)
example_map
<xarray.DataArray (alpha: 500, beta: 60, eV: 500)> Size: 120MB
127.7 125.3 132.3 127.3 127.8 ... 0.02784 0.0004322 0.000864 0.05651 0.2788
Coordinates:
  * alpha    (alpha) float64 4kB -15.0 -14.94 -14.88 -14.82 ... 14.88 14.94 15.0
  * beta     (beta) float64 480B -15.0 -14.49 -13.98 -13.47 ... 13.98 14.49 15.0
  * eV       (eV) float64 4kB -0.45 -0.4489 -0.4477 ... 0.1177 0.1189 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

Labeled data and metadata

A xarray.DataArray stores one array together with labels and metadata. These parts have different roles:

  • Dimensions name the array axes. Their order describes the stored layout.

  • Dimension coordinates give a physical value for each position on an axis.

  • Scalar coordinates record a fixed condition that applies to the complete array.

  • Attributes describe the array but do not take part in coordinate selection or alignment.

The DataArray display shows all four parts. You may access each part individually:

example_map.dims
('alpha', 'beta', 'eV')
example_map.shape
(500, 60, 500)
  • xarray.DataArray.coords contains the coordinate labels. Here, eV, beta, and alpha label dimensions. Coordinates such as hv, xi, and delta are scalars because they do not vary across this map.

example_map.coords
Coordinates:
  * alpha    (alpha) float64 4kB -15.0 -14.94 -14.88 -14.82 ... 14.88 14.94 15.0
  * beta     (beta) float64 480B -15.0 -14.49 -13.98 -13.47 ... 13.98 14.49 15.0
  * eV       (eV) float64 4kB -0.45 -0.4489 -0.4477 ... 0.1177 0.1189 0.12
    xi       float64 8B 0.0
    delta    float64 8B 0.0
    hv       float64 8B 50.0
  • xarray.DataArray.attrs contains descriptive metadata. ERLabPy accessors can read known attributes even though xarray does not use them for label-based selection.

example_map.attrs
{'configuration': 1, 'sample_temp': 20.0, 'sample_workfunction': 4.5}

The energy range is -0.45 to 0.12 eV. Both angle ranges are -15° to 15°.

The names and units follow ERLabPy’s ARPES data conventions:

Name

Role in this map

Physical meaning

eV

Dimension coordinate

Energy in eV. ERLabPy uses negative values for occupied states.

alpha

Dimension coordinate

Emission angle measured along the analyzer slit.

beta

Dimension coordinate

Polar mapping angle for this Type 1 configuration.

hv

Scalar coordinate

Photon energy in eV.

configuration

Attribute

Experimental geometry used for momentum conversion.

sample_workfunction

Attribute

Work function in eV used for momentum conversion.

The additional scalar coordinates describe fixed sample angles. sample_temp records the sample temperature.

Coordinate selection and averaging

xarray.DataArray.sel() selects data by coordinate labels. xarray.DataArray.isel() instead selects integer array positions. This tutorial uses coordinate labels because their physical meaning remains stable after cropping or transposition.

A slice passed to sel crops a coordinate interval and keeps that dimension. It does not average the selected points.

analysis_map = example_map.sel(eV=slice(-0.4, 0.1))
analysis_map
<xarray.DataArray (alpha: 500, beta: 60, eV: 438)> Size: 105MB
101.1 93.94 87.38 87.24 90.83 89.71 ... 0.002556 0.002137 0.1377 0.5784 0.2489
Coordinates:
  * alpha    (alpha) float64 4kB -15.0 -14.94 -14.88 -14.82 ... 14.88 14.94 15.0
  * beta     (beta) float64 480B -15.0 -14.49 -13.98 -13.47 ... 13.98 14.49 15.0
  * eV       (eV) float64 4kB -0.3997 -0.3986 -0.3975 ... 0.09715 0.0983 0.09944
    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

The result still has the eV, beta, and alpha dimensions. Only the eV size and range are smaller. The full alpha and beta ranges remain. The original coordinate labels and metadata remain attached.

A scalar selection removes one dimension. The requested coordinate does not have to occur exactly on a measured grid. Pass method="nearest" to select the closest sampled value:

nearest_energy_map = analysis_map.sel(eV=-0.3, method="nearest")
nearest_energy_map
<xarray.DataArray (alpha: 500, beta: 60)> Size: 240kB
58.15 56.51 68.68 78.34 95.12 111.3 ... 104.7 91.98 67.72 70.48 68.62 60.84
Coordinates:
  * alpha    (alpha) float64 4kB -15.0 -14.94 -14.88 -14.82 ... 14.88 14.94 15.0
  * beta     (beta) float64 480B -15.0 -14.49 -13.98 -13.47 ... 13.98 14.49 15.0
    eV       float64 8B -0.3004
    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

The result has beta and alpha dimensions. Its scalar eV coordinate contains the sampled energy that xarray selected.

It is also common to average values over a small window across an axis to improve the signal-to-noise ratio. This can be done by xarray.DataArray.qsel().A _width argument gives the averaging width in the coordinate unit.

The first selection averages a 0.02 eV energy window centered at −0.3 eV. The second selects the beta value nearest to 5° without angular averaging. Each selected coordinate remains in the result as a scalar coordinate.

angle_constant_energy = analysis_map.qsel(eV=-0.3, eV_width=0.02)
angle_energy_cut = analysis_map.qsel(beta=5.0)

angle_constant_energy
<xarray.DataArray (alpha: 500, beta: 60)> Size: 240kB
58.81 62.92 69.03 77.52 93.05 107.8 ... 107.9 90.69 77.23 69.07 64.29 60.73
Coordinates:
  * alpha    (alpha) float64 4kB -15.0 -14.94 -14.88 -14.82 ... 14.88 14.94 15.0
  * beta     (beta) float64 480B -15.0 -14.49 -13.98 -13.47 ... 13.98 14.49 15.0
    eV       float64 8B -0.2998
    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

Before you display angle_energy_cut, predict its dimensions. Selecting one beta value removes that dimension. The result must keep eV and alpha, with the selected beta value stored as a scalar coordinate.

angle_energy_cut
<xarray.DataArray (alpha: 500, eV: 438)> Size: 2MB
34.57 38.43 36.15 32.34 31.92 ... 0.00213 2.461e-06 4.223e-07 0.0004314 0.02782
Coordinates:
  * alpha    (alpha) float64 4kB -15.0 -14.94 -14.88 -14.82 ... 14.88 14.94 15.0
  * eV       (eV) float64 4kB -0.3997 -0.3986 -0.3975 ... 0.09715 0.0983 0.09944
    beta     float64 8B 4.831
    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

Inspecting data interactively

It is useful to inspect the working data interactively to choose appropriate coordinates and averaging windows.

If Qt is installed, use xarray.DataArray.qshow() to inspect the working data in ImageTool from your local notebook:

analysis_map.qshow()

In the right-click menu of each panel, you can use the Copy selection code to copy the xarray.DataArray.qsel() call required to reproduce the selection in that panel from the original data.

Plotting two-dimensional data

Use Matplotlib to create the axes and ERLabPy to plot and annotate the data:

The dimension order determines the default plot axes. The surface has dimensions beta and alpha. The cut has dimensions eV and alpha. Use the same intensity color map for both panels. Mark the selected beta value on the constant energy surface and the averaged energy interval on the cut.

fig, axes = plt.subplots(
    1, 2, figsize=(6.4, 2.8), layout="compressed", width_ratios=(1.0, 1.35)
)

eplt.plot_array(
    angle_constant_energy.T, ax=axes[0], cmap="Greys", gamma=0.7, aspect="equal"
)
axes[0].axhline(angle_energy_cut.beta, color="tab:red", linestyle="--", linewidth=0.8)

eplt.plot_array(angle_energy_cut.T, ax=axes[1], cmap="Greys", gamma=0.7)
axes[1].axhspan(-0.31, -0.29, color="tab:red", alpha=0.15)
eplt.fermiline(ax=axes[1], color="0.35", linestyle="--", linewidth=0.8)

eplt.set_titles(axes, ["Constant energy surface", "Energy–angle cut"])
../../_images/91d09f40668cb670af8f8a8b03dc6782253a1b5b6534cf53e25146aaaf5c8b5d.svg

In order to plot alpha on the horizontal axis, the data are transposed with .T prior to plotting.

The left panel has alpha on the horizontal axis and beta on the vertical axis. Both axes use degrees, so an equal aspect ratio is meaningful. The right panel has alpha on the horizontal axis and eV on the vertical axis. Its gray dashed line marks eV=0.

The red dashed line on the constant energy surface marks the selected beta value. The red band on the cut marks the 0.02 eV averaging interval used for the constant energy surface. gamma applies a power-law normalization to the displayed colors without changing the DataArray values.

Constant energy surfaces

Use erlab.plotting.plot_slices() to compare constant energy surfaces at several energies. The function performs the requested xarray.DataArray.qsel() selections and arranges the panels. Here, each panel uses the same 0.02 eV averaging width. Each panel has independent color limits so that changes in the contours remain visible.

fig, axes = eplt.plot_slices(
    analysis_map,
    figsize=(6.4, 2.3),
    eV=[-0.4, -0.2, 0.0],
    eV_width=0.02,
    cmap="Greys",
    gamma=0.7,
    axis="image",
)
../../_images/b528d15871e3e5e10902f552b6def60ce25689e311ec4a9fd5c269dfc2e7bb66.svg

The automatic labels give the center of the energy window for each panel.

Momentum conversion parameters

Momentum conversion uses these parameters:

  • The experimental configuration defines the relation between the measured angles and kx and ky.

  • The photon energy and sample work function determine the photoelectron kinetic energy.

  • The normal emission position sets zero in-plane momentum.

The generated data already contains the configuration, photon energy, and work function. You inspected these values in the coordinate and attribute displays above.

For this simulation, normal emission is alpha=beta=delta=0.0. For experimental data, determine normal emission from the measurement geometry, a known symmetry point, or a separate calibration. An intensity maximum alone does not identify normal emission.

Use xarray.DataArray.kspace.set_normal() to store the normal emission position in the data. The method converts this position to the angular offsets for the selected configuration. xarray.DataArray.kspace.offsets shows the stored offsets.

analysis_map.kspace.set_normal(alpha=0.0, beta=0.0, delta=0.0)

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

xarray.DataArray.kspace.convert() uses the momentum conversion functions and interpolates the intensity onto a regular momentum grid.

Here, ERLabPy automatically selects the momentum bounds and grid spacing.

momentum_data = analysis_map.kspace.convert()
momentum_data
<xarray.DataArray (kx: 310, ky: 310, eV: 438)> Size: 337MB
nan nan nan nan nan nan nan nan nan nan ... nan nan nan nan nan nan nan nan nan
Coordinates:
  * kx       (kx) float64 2kB -0.8954 -0.8896 -0.8838 ... 0.8838 0.8896 0.8954
  * ky       (ky) float64 2kB -0.8954 -0.8896 -0.8838 ... 0.8838 0.8896 0.8954
  * eV       (eV) float64 4kB -0.3997 -0.3986 -0.3975 ... 0.09715 0.0983 0.09944
    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:         0.0
    xi_offset:            0.0
    beta_offset:          0.0

The result should have dimensions kx, ky, and eV.

Conversion to momentum space

Select the same energy and averaging width before and after momentum conversion. Plot the two constant energy surfaces together. This comparison shows how the angular coordinates map to a regular kx, ky grid.

momentum_constant_energy = momentum_data.qsel(eV=-0.3, eV_width=0.02).T

fig, axes = plt.subplots(1, 2, figsize=(6.4, 3.0), layout="compressed")

eplt.plot_array(angle_constant_energy, ax=axes[0], aspect="equal")
eplt.plot_array(momentum_constant_energy, ax=axes[1], aspect="equal")
eplt.set_titles(axes, ["Angle coordinates", "Momentum coordinates"])
../../_images/1a7ef09b566df82a67774a5e62a8171cd253b5d22624b35c74764a25811ac62c.svg

The left panel has alpha and beta axes in degrees. The right panel has kx and ky axes in Å⁻¹. Momentum conversion changes the coordinates and interpolates the intensity.

Slices of multidimensional data

Use erlab.plotting.plot_slices() to make one summary figure from the converted data. The first row contains constant energy surfaces. The second row contains energy–momentum cuts at fixed ky. Each cut averages over a 0.04 Å⁻¹ window.

fig, axes = plt.subplots(
    2,
    3,
    layout="compressed",
    sharex=True,
    sharey="row",
)

eplt.plot_slices(
    [momentum_data],
    eV=[-0.4, -0.2, 0.0],
    eV_width=0.02,
    transpose=True,
    axes=axes[0],
    axis="image",
)
eplt.plot_slices(
    [momentum_data],
    ky=[0.0, 0.1, 0.3],
    ky_width=0.04,
    transpose=True,
    axes=axes[1],
)
eplt.clean_labels(axes)
../../_images/a15ed6de1503419dec7ec6dacfb986dce1f6accafb160888662744fa30844fdf.svg

The constant energy surfaces show the change in contour shape. The cuts show how the dispersion changes with ky. Each panel has independent color limits.

High-symmetry cuts

erlab.io.exampledata.generate_data_angles() uses a hexagonal tight-binding model with the default lattice constant \(a=6.97\) Å.

K–M–K′

Define the vertices of one Brillouin-zone edge. erlab.analysis.interpolate.slice_along_path() interpolates the converted data between these vertices. The K–M and M–K′ segments have equal length. Subtract \(2\pi/(3a)\) from the path coordinate to express momentum relative to M.

a = 6.97
kmk_vertices = {
    "kx": [0.0, np.pi / (np.sqrt(3) * a), 2 * np.pi / (np.sqrt(3) * a)],
    "ky": [4 * np.pi / (3 * a), np.pi / a, 2 * np.pi / (3 * a)],
}

kmk_cut = era.interpolate.slice_along_path(
    momentum_data,
    vertices=kmk_vertices,
    step_size=0.005,
)
kmk_m_position = 2 * np.pi / (3 * a)
kmk_cut = kmk_cut.assign_coords(path=kmk_cut.path - kmk_m_position)
kmk_cut
<xarray.DataArray (path: 119, eV: 438)> Size: 417kB
145.7 146.9 142.7 145.4 147.5 149.6 ... 0.05814 0.242 0.1525 0.06132 0.02317
Coordinates:
  * path     (path) float64 952B -0.3005 -0.2954 -0.2903 ... 0.2954 0.3005
    kx       (path) float64 952B 0.0 0.004411 0.008821 ... 0.5116 0.516 0.5205
    ky       (path) float64 952B 0.601 0.5984 0.5959 ... 0.3056 0.303 0.3005
  * eV       (eV) float64 4kB -0.3997 -0.3986 -0.3975 ... 0.09715 0.0983 0.09944
    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:         0.0
    xi_offset:            0.0
    beta_offset:          0.0
kmk_vertex_positions = [-kmk_m_position, 0.0, kmk_m_position]

fig, axes = plt.subplots(
    1,
    2,
    figsize=(6.4, 3.2),
    layout="compressed",
    gridspec_kw={"width_ratios": (1.0, 1.35)},
)

kmk_energy_map = momentum_data.qsel(eV=-0.2, eV_width=0.02)
eplt.plot_array(kmk_energy_map.T, ax=axes[0], aspect="equal")
eplt.plot_hex_bz(a=a, ax=axes[0], fill=False, edgecolor="0.35", linewidth=0.8)
axes[0].plot(
    kmk_vertices["kx"], kmk_vertices["ky"], color="tab:red", marker="o", markersize=3
)
axes[0].set_title(r"$E = E_F - 0.2$ eV")

eplt.plot_array(kmk_cut.T, ax=axes[1])
eplt.fermiline(ax=axes[1], linestyle="--", linewidth=0.8)
eplt.mark_points(kmk_vertex_positions, ["K", "M", "K′"], y=0.12, ax=axes[1])
axes[1].set_xlabel(r"$k - \mathrm{M}$ (Å$^{-1}$)")
Text(0.5, 0, '$k - \\mathrm{M}$ (Å$^{-1}$)')
../../_images/b2d73abed8bc534b333c288144d4db74e2d1d60e38438c39bb0a5d9c9174d174.svg

The cut coordinate is zero at M. The left panel confirms that the selected line follows the Brillouin-zone edge.

Γ–M–K–Γ

Define a multi-segment Γ–M–K–Γ path. erlab.analysis.interpolate.slice_along_path() interpolates the converted data at evenly spaced points along each segment. The result keeps eV and replaces the kx and ky dimensions with one cumulative path dimension.

high_symmetry_vertices = {
    "kx": [0.0, 2 * np.pi / (np.sqrt(3) * a), 2 * np.pi / (np.sqrt(3) * a), 0.0],
    "ky": [0.0, 0.0, 2 * np.pi / (3 * a), 0.0],
}

high_symmetry_cut = era.interpolate.slice_along_path(
    momentum_data,
    vertices=high_symmetry_vertices,
    step_size=0.005,
)
high_symmetry_cut
<xarray.DataArray (path: 282, eV: 438)> Size: 988kB
29.65 30.41 29.12 27.62 27.52 29.33 ... 0.02204 0.003089 0.03942 0.1582 0.0939
Coordinates:
  * path     (path) float64 2kB 0.0 0.005053 0.01011 ... 1.412 1.417 1.422
    kx       (path) float64 2kB 0.0 0.005053 0.01011 ... 0.008747 0.004374 0.0
    ky       (path) float64 2kB 0.0 0.0 0.0 0.0 ... 0.00505 0.002525 0.0
  * eV       (eV) float64 4kB -0.3997 -0.3986 -0.3975 ... 0.09715 0.0983 0.09944
    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:         0.0
    xi_offset:            0.0
    beta_offset:          0.0

The figure shows the selected path on a constant energy surface and uses the path vertices as tick labels.

path_vertices = np.column_stack(
    [high_symmetry_vertices["kx"], high_symmetry_vertices["ky"]]
)
segment_lengths = np.linalg.norm(np.diff(path_vertices, axis=0), axis=1)
path_vertex_positions = np.concatenate(([0.0], np.cumsum(segment_lengths)))
path_energy_map = momentum_data.qsel(eV=-0.2, eV_width=0.02)

fig, axes = plt.subplots(
    1,
    2,
    figsize=(6.4, 3.2),
    layout="compressed",
    gridspec_kw={"width_ratios": (1.0, 1.35)},
)

eplt.plot_array(path_energy_map.T, ax=axes[0], aspect="equal")
eplt.plot_hex_bz(a=a, ax=axes[0], fill=False, edgecolor="0.35", linewidth=0.8)
axes[0].plot(
    high_symmetry_vertices["kx"],
    high_symmetry_vertices["ky"],
    color="tab:red",
    marker="o",
    markersize=3,
)
axes[0].set_title(r"$E = E_F - 0.2$ eV")

eplt.plot_array(high_symmetry_cut.T, ax=axes[1])
eplt.fermiline(ax=axes[1], linestyle="--", linewidth=0.8)
for position in path_vertex_positions[1:-1]:
    axes[1].axvline(position, color="w", linewidth=0.5)
axes[1].set_xticks(path_vertex_positions, labels=["Γ", "M", "K", "Γ"])
axes[1].set_xlabel("")
Text(0.5, 0, '')
../../_images/ba53cc15f9d0e19b215dbf121ad366fc91862b12c5268a6bf18c3f79a9f4c9cb.svg

The hexagon and the red path use the lattice constant of the simulated band. The right panel shows the Γ–M–K–Γ cut.

Next steps

You started with simulated ARPES data and learned how to conduct basic analysis.

To try this out on your own data, use Data loading and saving, Data inspection and selection, Plotting gallery, and Momentum conversion. Complete function and parameter descriptions are in Reference.

To learn more about the concepts and design choices of this library, read Explanation, ARPES data conventions, and Momentum conversion.