Converting ARPES Data to Momentum-Space#

Note: We will use Fermi edge corrected data for conversion. The function load_energy_corrected below handles this. You can see the document on Fermi edge corrections for more details.

Converting Volumetric Data#

PyARPES provides a consistent interface for converting ARPES data from angle to momentum space. This means that there is only a single function that provides an entrypoint for converting volumetric data: arpes.utilities.conversion.convert_to_kspace.

Using the PyARPES data plugins, you can be confident that your data will convert to momentum immediately after you load it, so long as it follows the PyARPES spectral model.

[1]:
import matplotlib.pyplot as plt
from lmfit.models import ConstantModel, QuadraticModel

import arpes

from arpes.fits.fit_models import AffineBroadenedFD
from arpes.io import example_data
from arpes.utilities.conversion import convert_to_kspace


def load_energy_corrected():
    fmap = example_data.map.spectrum
    cut = fmap.sum("theta", keep_attrs=True).sel(
        eV=slice(-0.2, 0.1),
        phi=slice(-0.25, 0.3),
    )
    params = AffineBroadenedFD().make_params(
        center=0,
        width=0.005,
        sigma=0.02,
        const_bkg=200000,
        lin_slope=0,
    )
    fit_results = cut.S.modelfit(
        "eV", AffineBroadenedFD() + ConstantModel(), params=params
    )

    edge = (
        fit_results.modelfit_results.F.p("center")
        .S.modelfit("phi", QuadraticModel())
        .modelfit_results.item()
        .eval(x=fmap.phi)
    )
    return fmap.G.shift_by(edge, shift_axis="eV", by_axis="phi")
Activating auto-logging. Current session state plus future input saved.
Filename       : logs/unnamed_2026-03-24_23-28-18.log
Mode           : backup
Output logging : False
Raw input log  : False
Timestamping   : False
State          : active
[2]:
energy_corrected = load_energy_corrected()
energy_corrected.attrs["energy_notation"] = "Binding"

A first conversion#

Now that we have our data loaded, let’s convert a Fermi surface to momentum.

[3]:
import numpy as np

fig, ax = plt.subplots()
ax = convert_to_kspace(
    energy_corrected.S.fat_sel(eV=0),  # just convert the Fermi surface
    kx=np.linspace(-2.5, 1.5, 400),  # along -2.5 <= kx < 1.5 (inv ang.)
    #  with 400 steps
    ky=np.linspace(-2, 2, 400),  # as above, with -2 <= ky < 2
).S.plot(ax=ax)
../_images/notebooks_converting-to-kspace_4_0.png

Coordinate Offsets#

PyARPES knows how to convert data from angle-to-momentum because data in PyARPES specifies both the experimental geometry and the angular offsets corresponding to normal emission.

The geometry is specified by the data loading code which you typically do not change, but the angular offsets may change depending on how samples are mounted, DAQ software calibration, or any number of other reasons.

The data sample above already has offsets set, which is why the \(\Gamma\) point correctly shows up at zero parallel momentum. We can also set offsets:

[4]:
# Let's look at what the assigned values were, for some reference
import pprint  # pretty printk

pprint.pprint(energy_corrected.S.offsets)

fig, ax = plt.subplots()

example_fs = energy_corrected.S.fat_sel(eV=0.0).copy(deep=True)
example_fs.S.apply_offsets(
    {
        "phi": 0.0,  # controls the offset along the analyzer center axis
        "theta": 0.0,
        "chi": 0.0,  # sample rotation, controls kx-ky plane orientation
        "alpha": 0.0,
        "psi": 0.0,
    }
)
ax = convert_to_kspace(
    example_fs,
    kx=np.linspace(-2.5, 1.5, 400),
    ky=np.linspace(-2, 2, 400),
).S.plot(ax=ax)
{'alpha': np.float64(0.0),
 'beta': np.float64(0.0),
 'chi': np.float64(-1.4851355),
 'phi': np.float64(0.05671),
 'psi': np.float64(0.0),
 'theta': np.float64(-0.01392)}
../_images/notebooks_converting-to-kspace_6_1.png

Best Practices for Offsets#

Maintaining Offsets#

Once you’ve found offsets, they tend to be the same for all scans on a given cleave or sample. It’s a good idea to keep your angular offsets in a local module or otherwise written down. I tend to put them into a local module next to notebooks so that I can run code like:

from local.constants import angle_offsets

my_scan.S.apply_offsets(angle_offsets["sample_1"])

You’ll likely have different ideas about what you find convenient.

Finding Offsets#

There are a lot of different ways to do this. You can plot like above until you are happy. Often, you might want to use a Brillouin zone overlay to help you align symmetry points.

[5]:
from arpes.plotting.bz import overplot_standard
import arpes.utilities.bz_spec as bz_spec
from ase.lattice import HEX2D
from scipy.spatial.transform import Rotation

fig, ax = plt.subplots()
ax = convert_to_kspace(
    example_fs,
    kx=np.linspace(-2.5, 1.5, 400),
    ky=np.linspace(-2, 2, 400),
).S.plot(ax=ax)

ws2 = HEX2D(a=bz_spec.A_WS2)
r = Rotation.from_rotvec([0, 0, np.pi / 12])
# plot a graphene BZ over the data... obviously silly here
# to demonstrate parameters we'll rotate 30 degrees and plot several higher BZs.
bz_plotter = overplot_standard(cell=ws2.tocell(), repeat=([-2, 2, 1]), transforms=[r])
ax = bz_plotter(plt.gca())
ax.set_xlim([-2, 2])
ax.set_ylim([-2.5, 1.5])
[5]:
(-2.5, 1.5)
../_images/notebooks_converting-to-kspace_8_1.png

You can also use prior knowledge of the sample setup to find good offsets, adjust from other scans, or use an interactive conversion tool:

[6]:
# from arpes.plotting.qt_ktool import ktool

# ktool(example_fs, zone="graphene")

ktool.png

Converting the entire volume#

[7]:
import numpy as np

kcube = convert_to_kspace(
    energy_corrected,
    kx=np.linspace(-2.5, 1.5, 400),
    ky=np.linspace(-2, 2, 400),
)
[8]:
fig, ax = plt.subplots()
ax = kcube.sel(kx=slice(-0.02, 0.02)).mean("kx").S.plot(ax=ax)
../_images/notebooks_converting-to-kspace_14_0.png

Determining the Momentum Coordinates of Particular Points#

Frequently, you might need to perform some analysis in the vicinity of a particular point whose coordinates you know in angle-space, but your results need to be presented in momentum-space.

Unless you want to perform your analysis in angle-space and convert results to momentum, you need to know how to project points and coordinates forward from angle to momentum.

The volumetric transform is based off an interpolated inverse transform: the momentum coordinates are converted to angle for the interplation step, which may be contrary to your expectations if you’ve not worked with interpolations before. In PyARPES and most ARPES software, this inverse transform is small angle approximated in some circumstances. For this reason, PyARPES provides both an exact angle-to-momentum converter suitable for coordinates, and an exact inverse to the small angle approximated volumetric transform.

The latter is typically what you want when you are doing volumetric transforms, because it tells you exactly where a feature of interest will end up in momentum after you invoke convert_to_kspace.

Let’s see how it works.

First, let’s pick any point in angle space which we will use to represent our feature of interest:

[9]:
ax = energy_corrected.S.fat_sel(eV=0).S.plot()
plt.gca().scatter([-0.13], [-0.1], color="red")

# we will determine where this point goes
test_point = {
    "phi": -0.13,
    "theta": -0.1,
    "eV": 0,
}
../_images/notebooks_converting-to-kspace_16_0.png
[10]:
from arpes.analysis.forward_conversion import convert_coordinate_forward

k_test_point = convert_coordinate_forward(energy_corrected, test_point)
[11]:
ax = kcube.sel(eV=slice(-0.05, 0.05)).sum("eV").S.plot(ax=ax)
plt.gca().scatter([k_test_point["ky"]], [k_test_point["kx"]], color="red")
[11]:
<matplotlib.collections.PathCollection at 0x797085ab55e0>
../_images/notebooks_converting-to-kspace_18_1.png

Excellent, this enables all kinds of analysis which we frequently want to perform.

Exactracting a Momentum Cut Passing Through Known Angular Coordinates#

For instance, we can determine a momentum cut passing through our point of interest.

[12]:
fig, ax = plt.subplots()
ky_slice = (
    convert_to_kspace(
        energy_corrected,
        kx=np.linspace(k_test_point["kx"] - 0.02, k_test_point["kx"] + 0.02, 20),
        ky=np.linspace(-2, 2, 800),
    )
    .mean("kx")
    .S.plot(ax=ax)
)

plt.gca().scatter([k_test_point["ky"]], [0.0], color="red")
[12]:
<matplotlib.collections.PathCollection at 0x797085ae6b10>
../_images/notebooks_converting-to-kspace_20_1.png

This is common enough that there is a utility for it in PyARPES: convert_through_angular_point.

[13]:
from arpes.analysis.forward_conversion import convert_through_angular_point

fig, ax = plt.subplots()
ax = convert_through_angular_point(
    energy_corrected,
    test_point,
    {"ky": np.linspace(-1, 1, 400)},  # give the cut which has +/- 1 inv ang
    # in `kx` around our point of interest
    {"kx": np.linspace(-0.02, 0.02, 10)},  # take 20 milli inv ang. perpendicular
).S.plot(vmax=11000)  # set vmax for better comparison to above
../_images/notebooks_converting-to-kspace_22_0.png

What if we wanted to take a rotated cut through this point, rather than one aligned to the kx and ky axes above?

The simplest thing to do is to apply a sample rotation offset before performing the slice, so that the momentum axes are now aligned to the desirable directions.

We can use the rotation offset context manager for this.

[14]:
from arpes.config import use_tex

use_tex(enable=False)  # just to be safe, in case you don't have LaTeX installed

fig, ax = plt.subplots(3, 3, figsize=(14, 12))

for inc in range(9):
    with energy_corrected.S.with_rotation_offset(-inc * np.pi / 8):
        convert_through_angular_point(
            energy_corrected,
            test_point,
            {"ky": np.linspace(-1, 1, 400)},
            {"kx": np.linspace(-0.02, 0.02, 10)},
        ).S.plot(vmax=11000, ax=ax.ravel()[inc])
        ax.ravel()[inc].set_title(f"-{inc}pi/8")

plt.tight_layout()
../_images/notebooks_converting-to-kspace_24_0.png

Note that the first and last plots are merely reflections of one another, because a rotation by pi is equivalent to inverting the ky axis of the cut.

Getting an ARPES Cut Passing Through Two Angular Points#

Suppose we know the angular coordinates of two high symmetry points in our dataset and we wanted to get a high symmetry cut passing through both. We have just seen how to do this with one point.

PyARPES has convert_through_angular_pair for this purpose.

This type of conversion is very useful for presenting high symmetry directions in a band structure which came from a higher dimensional dataset like a map.

Let’s pick two points so we can be concrete.

[15]:
ax = energy_corrected.S.fat_sel(eV=0).S.plot()
plt.gca().scatter([0.055], [-0.013], color="red")
plt.gca().scatter([-0.09], [-0.18], color="white")


# we will get the cut through these two points
p1 = {
    "phi": 0.055,
    "theta": -0.013,
    "eV": 0,
}
p2 = {
    "phi": -0.09,
    "theta": -0.18,
    "eV": 0,
}
../_images/notebooks_converting-to-kspace_27_0.png
[16]:
from arpes.analysis.moire import angle_between_vectors

kp1 = convert_coordinate_forward(energy_corrected, p1)
kp2 = convert_coordinate_forward(energy_corrected, p2)


def to_vec(p):
    return np.array([p["kx"], p["ky"]])


print(kp1)
print(kp2)
print(to_vec(kp1))
print(to_vec(kp2))
delta = to_vec(kp2) - to_vec(kp1)
ang = np.arctan2(delta[1], delta[0])
print(ang)

with energy_corrected.S.with_rotation_offset(-ang):
    kp1 = convert_coordinate_forward(energy_corrected, p1)
    kp2 = convert_coordinate_forward(energy_corrected, p2)

    print(kp1, kp2)

    delta = to_vec(kp2) - to_vec(kp1)
    print(np.arctan2(delta[1], delta[0]))
{'kx': -0.027115300158778416, 'ky': -0.022266815310293564}
{'kx': 0.9003614742745181, 'ky': -0.7552690787473395}
[-0.0271153  -0.02226682]
[ 0.90036147 -0.75526908]
-0.6688097857188012
{'kx': -0.007721360764839022, 'ky': -0.03447721360764837} {'kx': 1.174384649167258, 'ky': -0.03447721360764837}
0.0
[17]:
from arpes.analysis.forward_conversion import convert_through_angular_pair

fig, ax = plt.subplots()

ax = convert_through_angular_pair(
    energy_corrected,
    p1,
    p2,
    {"kx": np.linspace(-0, 0, 400)},  # interpolate from p1 to p2 only
    {"ky": np.linspace(-0.02, 0.02, 10)},  # take 20 milli inv ang. perpendicular
).S.plot(vmax=11000, ax=ax)

# plotted only for legibility
plt.gca().scatter([-0.007], [0.0], color="red")
plt.gca().scatter([1.17], [0.0], color="white")
[17]:
<matplotlib.collections.PathCollection at 0x797085d380e0>
../_images/notebooks_converting-to-kspace_29_1.png

This interpolate only between p1 and p2. To interpolate further in either direction, wejust need to set the margin wider. Let’s interpolate one inverse angstrom past p1 away from p2.

[18]:
from arpes.analysis.forward_conversion import convert_through_angular_pair

fig, ax = plt.subplots()

ax = convert_through_angular_pair(
    energy_corrected,
    p1,
    p2,
    {"kx": np.linspace(-1, 0, 400)},  # interpolate 1 inv ang. further left
    {"ky": np.linspace(-0.02, 0.02, 10)},  # take 20 milli inv ang. perpendicular
).S.plot(vmax=11000, ax=ax)

# plotted only for legibility
plt.gca().scatter([-0.007], [0.0], color="red")
plt.gca().scatter([1.17], [0.0], color="white")
[18]:
<matplotlib.collections.PathCollection at 0x7970859bb410>
../_images/notebooks_converting-to-kspace_31_1.png

API for Other Types of Conversions#

There’s only one API in PyARPES for coordinate conversion. The entire geometry is specified on the data by convention. So you typically do not need to do any work to choose the appropriate conversion routine.

Extra dimensions#

You can convert datasets with extra dimensions, which just act as passthroughs: each slice is converted as you would expect.

We can see this on our temperature dependence data.

[19]:
from arpes.io import example_data
from arpes.utilities.conversion import convert_to_kspace

temp_dep = example_data.temperature_dependence.spectrum

# We will Let PyARPES infer an appropriate kp range
ktemp_dep = convert_to_kspace(temp_dep)
ktemp_dep.dims
[19]:
('eV', 'kp', 'temperature')
[20]:
fig, ax = plt.subplots()
ax = ktemp_dep.isel(temperature=0).S.plot(ax=ax)
../_images/notebooks_converting-to-kspace_34_0.png

Photon Energy Scans#

This also operates the same way. The only caveat is that you may need to assign the inner potential (on .attrs) in order to get good periodicity in kz.

[21]:
import numpy as np
from arpes.io import example_data
from arpes.utilities.conversion import convert_to_kspace

hv_scan = example_data.photon_energy.spectrum

kz_data = convert_to_kspace(
    hv_scan.S.fat_sel(eV=0),
    kp=np.linspace(-2, 2, 500),
    kz=np.linspace(3.5, 5.2, 400),
)

kz_data.T.S.plot(vmax=800)
plt.gca().set_title("")
[21]:
Text(0.5, 1.0, '')
../_images/notebooks_converting-to-kspace_36_1.png

Inner potential#

We can also set the inner potential to a different value to see its effect.

[22]:
hv_fs = example_data.photon_energy.spectrum.S.fat_sel(eV=0.0)

fig, axes = plt.subplots(1, 3, figsize=(16, 4))

hv_fs.attrs["inner_potential"] = 0.0
kz_data = convert_to_kspace(
    hv_fs,
    kp=np.linspace(-2, 2, 500),
    kz=np.linspace(3.2, 5, 400),
).T.S.plot(vmax=700, ax=axes[0])

hv_fs.attrs["inner_potential"] = 10.0
kz_data = convert_to_kspace(
    hv_fs,
    kp=np.linspace(-2, 2, 500),
    kz=np.linspace(3.2 + 0.4, 5 + 0.4, 400),
).T.S.plot(vmax=700, ax=axes[1])

hv_fs.attrs["inner_potential"] = 100.0
kz_data = convert_to_kspace(
    hv_fs,
    kp=np.linspace(-2, 2, 500),
    kz=np.linspace(3.2 + 2.4, 5 + 2.4, 400),
).T.S.plot(vmax=700, ax=axes[2])

axes[0].set_title("Inner potential: 0 eV")
axes[1].set_title("Inner potential: 10 eV")
axes[2].set_title("Inner potential: 100 eV")
[22]:
Text(0.5, 1.0, 'Inner potential: 100 eV')
../_images/notebooks_converting-to-kspace_38_1.png

From the above, we can see that an inner potential for this test sample of above 10eV is probably too large. 10eV is possibly too large as well, but does not look very distorted.

Absolute \(k_z\) values depend on the value of the inner potential \(V_0\), so it’s a good idea to consider also the periodicity in kz when setting the inner potential.

Exercises#

  1. If you look at the PyARPES spectral model, some angles may appear to have the same effect. Under what conditions does increasing the phi_offset and decreasing the theta_offset result in the same momentum space data?

  2. Play around with finding good coordinate offsets and applying them.

  3. What is the meaning of the original chi_offset on example_data.map. What assumptions does this default behavior make?

  4. Use each of convert_through_angular_point and convert_through_angular_pair to make high symmetry selections through the kx-ky scan above.