Transformations

The erlab.analysis.transform module provides transformations for xarray.DataArray objects. Typical use cases include rotating 2D maps, compensating for shifts, and symmetrizing spectra.

import matplotlib.pyplot as plt

import erlab.analysis as era
import erlab.plotting as eplt
from erlab.io.exampledata import generate_data, generate_hvdep_cuts

Rotate maps and volumes

The erlab.analysis.transform.rotate() function rotates data in the plane defined by two dimensions. Any remaining dimensions are preserved, so a 3D volume is rotated slice-by-slice (for example, rotating the \(k_x\)-\(k_y\) plane for every energy). Coordinates along the rotated axes must be evenly spaced.

Below we create a synthetic \(k_x\)-\(k_y\)-\(E\) volume and rotate it.

# Simulated kx-ky-eV volume (graphene-like tight-binding model).
volume = generate_data(
    shape=(160, 160, 180),
    bandshift=-0.2,
    seed=1,
).transpose("ky", "kx", "eV")


fig, axs = eplt.plot_slices(
    [volume],
    eV=[0.0, -0.2, -0.4],
    axis="image",
    figsize=(6.4, 3.0),
    cmap="Greys",
    gamma=0.5,
)
../_images/b2f65bdfc67b169d7a0443e8862393cb59773ed719d978a546b85d00cfb399ff.svg
rotated = era.transform.rotate(
    volume,
    angle=25.0,
    axes=("ky", "kx"),
    center={"ky": 0.0, "kx": 0.0},
    reshape=True,
)

fig, axs = eplt.plot_slices(
    [rotated],
    eV=[0.0, -0.2, -0.4],
    axis="image",
    figsize=(6.4, 2.5),
    cmap="Greys",
    gamma=0.5,
)
../_images/2fac49293a94a767d2c27ed28b5c5a09b74e460c263703deae36da1e5a80b86b.svg

GUI equivalent

Rotation can also be performed in ImageTool, which also provides guidelines for tuning the center and angle of the rotation interactively.

Shift spectra to correct drift

The erlab.analysis.transform.shift() function shifts data along a single dimension. If you supply a DataArray for the shift values, the shift can vary across any other dimensions, and it will broadcast across the remaining ones.

Here we generate a synthetic \(h\nu\)-dependent stack with an energy drift across hν:

# Photon-energy dependent cuts: alpha x eV x hv.
spectra = generate_hvdep_cuts(
    shape=(9, 250, 300),
    Erange=(-0.25, 0.08),
    hvrange=(60.0, 100.0),
    bandshift=-0.2,
    hv_shift=(-0.012, 0.008),
    noise=False,
)

alpha = spectra["alpha"].values
hv = spectra["hv"].values

fig, axs = plt.subplots(1, 3, figsize=(9.6, 3.0))
spectra.isel(hv=0).T.qplot(ax=axs[0], cmap="Greys", gamma=0.5)
spectra.isel(hv=-1).T.qplot(ax=axs[1], cmap="Greys", gamma=0.5)
spectra.qsel(alpha=0.0).qplot(ax=axs[2], cmap="Greys", gamma=0.5)
eplt.set_titles(
    axs,
    [
        rf"$hν = {hv[0]:.1f}$ eV",
        rf"$hν = {hv[-1]:.1f}$ eV",
        "constant angle ($α = 0.0°$)",
    ],
)
../_images/e5bf5239ed9dd54689b78abf65161e9fd92759242060098540403e0c3214e3e7.svg

You can see that the Fermi level shifts as a function of photon energy.

First, we extract the shift values using a fit to a broadened step function:

fit_result = spectra.qsel(alpha=0.0).xlm.modelfit(
    "eV",
    model=era.fit.models.StepEdgeModel(),
    guess=True,
)

shift = fit_result.modelfit_coefficients.sel(param="center")

fig, ax = plt.subplots()
spectra.qsel(alpha=0.0).qplot(ax=ax, cmap="Greys", gamma=0.5)
ax.plot(fit_result.hv, shift, "ro-")
ax.set_xlabel("$hν$ (eV)")
ax.set_ylabel("Extracted $E_F$ (eV)")
Text(0, 0.5, 'Extracted $E_F$ (eV)')
../_images/7a6e307cc91948bc29b33b1a1560c22dbf4a9c677d022ec62b677f2ac4214474.svg

By supplying the shift values to erlab.analysis.transform.shift(), we can correct for this drift and align the spectra.

aligned = era.transform.shift(spectra, shift=-shift, along="eV")

fig, axs = plt.subplots(1, 3, figsize=(9.6, 3.0))
aligned.isel(hv=0).T.qplot(ax=axs[0], cmap="Greys", gamma=0.5)
aligned.isel(hv=-1).T.qplot(ax=axs[1], cmap="Greys", gamma=0.5)
aligned.qsel(alpha=0.0).qplot(ax=axs[2], cmap="Greys", gamma=0.5)
eplt.set_titles(
    axs,
    [
        rf"$hν = {hv[0]:.1f}$ eV",
        rf"$hν = {hv[-1]:.1f}$ eV",
        "constant angle ($α = 0.0°$)",
    ],
)
../_images/a04bfdcb016452f20a6cdb693ce54a011d858b78223a36376471775e72a8eb83.svg

Now, the Fermi levels are aligned across all photon energies. The energy coordinates remain unchanged; only the data values have been shifted.

If the shift values are very large, the resulting spectra may contain large regions of NaNs where no data is available after the shift. In such cases, using shift_coords=True will also shift the coordinate values to include all data points after the shift. An example of this is shown below:

# Expand coordinates to retain the full shifted range.
aligned_full = era.transform.shift(
    spectra,
    shift=-shift,
    along="eV",
    shift_coords=True,
)

fig, axs = plt.subplots(1, 3, figsize=(9.6, 3.0))
aligned_full.isel(hv=0).T.qplot(ax=axs[0], cmap="Greys", gamma=0.5)
aligned_full.isel(hv=-1).T.qplot(ax=axs[1], cmap="Greys", gamma=0.5)
aligned_full.qsel(alpha=0.0).qplot(ax=axs[2], cmap="Greys", gamma=0.5)
eplt.set_titles(
    axs,
    [
        rf"$hν = {hv[0]:.1f}$ eV",
        rf"$hν = {hv[-1]:.1f}$ eV",
        "constant angle ($α = 0.0°$)",
    ],
)
../_images/6848ff5ecd7a2860099c34c8525e1d814e7307f797c8b083b771eec3fd4fd829.svg

Symmetrization

Symmetrize about a center

The erlab.analysis.transform.symmetrize() function reflects data about a center and adds (or subtracts) it to produce symmetrized or antisymmetrized outputs. Symmetrization is applied along a single dimension while all other dimensions are preserved.

We start from a \(k_x\)-\(E\) cut. To mimic matrix-element asymmetry, we apply a gentle \(k_x\)-dependent weighting before symmetrizing about \(k_x = 0\).

Symmetrization also broadcasts over any remaining dimensions, so you can apply it to higher-dimensional data without manually looping over slices.

cut = generate_data(seed=3, bandshift=-0.2).qsel(ky=0.3).T

# Apply a weak kx-dependent weight to emulate matrix-element effects.
kx_weight = 0.1 + (cut.kx - cut.kx.min()) / (cut.kx.max() - cut.kx.min())
cut = cut * kx_weight

sym = era.transform.symmetrize(cut, dim="kx", center=0.0)
antisym = era.transform.symmetrize(cut, dim="kx", center=0.0, subtract=True)

fig, axs = eplt.plot_slices(
    [cut, sym, antisym],
    order="F",
    figsize=(9, 3),
    cmap=["Greys", "Greys", "bwr"],
    gamma=0.5,
    norm=[None, None, eplt.CenteredInversePowerNorm(1.0)],
)
eplt.set_titles(axs, ["Original", "Symmetrized", "Antisymmetrized"])
../_images/195894f5d270dfc0191995e7abc092437dbef595b25fcbcecd26f3a5b7ca55d8.svg

For additional options (such as returning only half of the symmetrized data), see the API reference at erlab.analysis.transform.

Rotational n-fold symmetrization

Use erlab.analysis.transform.symmetrize_nfold() when a map should be averaged over repeated in-plane rotations. For example, a 4-fold symmetrization of a \(k_x\)-\(k_y\) map would average over the original map and maps rotated by 90°, 180°, and 270°.

Let’s try it out with a \(k_x\)-\(k_y\)-\(E\) volume, sliced in momentum to include only a single K pocket.

volume_partial = volume.where((volume.kx < -0.25) * (volume.ky < 0.15), drop=True)

fig, axs = eplt.plot_slices(
    [volume_partial],
    eV=[0.0, -0.2, -0.4],
    axis="image",
    figsize=(6.4, 3.0),
    cmap="Greys",
    gamma=0.5,
)
../_images/5f1a602b56b93a3269b358a77d229602d020a9dac3f20de2d7b3b624d9c908b1.svg

We can symmetrize the pocket with respect to 6-fold rotations about the Brillouin zone center.

The reshape=True option ensures that the output coordinates are reshaped to include the full symmetrized region.

volume_partial_sym = era.transform.symmetrize_nfold(
    volume_partial, 6, axes=("kx", "ky"), center={"kx": 0.0, "ky": 0.0}, reshape=True
)

fig, axs = eplt.plot_slices(
    [volume_partial_sym],
    eV=[0.0, -0.2, -0.4],
    axis="image",
    figsize=(6.4, 3.0),
    cmap="Greys",
    gamma=0.5,
)
../_images/99441281644fb3d2ff7adbaf092a655a72b52c234f4065d1d99c5fc8a09f22fd.svg

GUI equivalent

Symmetrization can also be performed in ImageTool.