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¶
Create a three-dimensional angle-resolved dataset.
Inspect its xarray dimensions, coordinates, and metadata.
Crop the dataset by coordinate values.
Select a constant energy surface and average an energy interval.
Plot constant energy surfaces and energy–angle cuts.
Check the momentum conversion parameters and set the normal emission position.
Convert the angle-resolved data to momentum space and plot constant energy surfaces and energy–momentum cuts.
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.5Labeled 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:
xarray.DataArray.dimsgives the dimension names in order.
example_map.dims
('alpha', 'beta', 'eV')
xarray.DataArray.shapegives the number of points along each dimension.
example_map.shape
(500, 60, 500)
xarray.DataArray.coordscontains the coordinate labels. Here,eV,beta, andalphalabel dimensions. Coordinates such ashv,xi, anddeltaare 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.attrscontains 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 |
|---|---|---|
|
Dimension coordinate |
Energy in eV. ERLabPy uses negative values for occupied states. |
|
Dimension coordinate |
Emission angle measured along the analyzer slit. |
|
Dimension coordinate |
Polar mapping angle for this Type 1 configuration. |
|
Scalar coordinate |
Photon energy in eV. |
|
Attribute |
Experimental geometry used for momentum conversion. |
|
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.5The 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.5The 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.5Before 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.5Inspecting 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:
matplotlib.pyplot.subplots()creates the figure and its axes.erlab.plotting.plot_array()draws a two-dimensional DataArray.erlab.plotting.fermiline()marks the Fermi level ateV=0.erlab.plotting.set_titles()applies titles to several axes.
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"])
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",
)
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
kxandky.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
| delta | 0.0 |
|---|---|
| xi | 0.0 |
| beta | 0.0 |
| normal alpha | 0.0 |
| normal beta | 0.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.0The 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"])
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)
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.0kmk_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}$)')
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.0The 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, '')
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.