Multi-Image Transformation

This example notebook demonstrates how to load and transform a series of RHEED images taken during sequential azimuthal rotations.

The rotation starts at \(\alpha = 0^\circ\), where the electron beam is aligned along the \([11\bar{2}]\) direction, and ends at \(\alpha = 30^\circ\), corresponding to alignment along the \([1\bar{1}0]\) direction.

import matplotlib.pyplot as plt
import numpy as np
from pathlib import Path
import xarray as xr

import xrheed
from xrheed.plotting.overview import plot_images
from xrheed.preparation.filters import high_pass_filter
from xrheed.preparation import (
    find_horizontal_center,
    find_incident_angle,
    find_vertical_center,
)
🎉 xrheed v2.1.0 loaded!

Image Preprocessing and Correction

To begin, we load a series of images and apply the necessary corrections.
Due to minor manipulator inaccuracies, each image may exhibit slight variations in positional shifts along the \(S_x\) and \(S_y\) axes.

Typically, these variations change linearly during the rotation.

For instance, the shift along \(S_y\) could be prepared as a linear space between the values recorded for the first and last images.

The same approach applies to the azimuthal angle, α.

The example shown below is using a semiautomatic alignment where we use some of the images to get the center position and incident angle and later use those values to calculate linear components applied for all images.

image_dir = Path("example_data")
image_paths = list(sorted(image_dir.glob("Si_111_r3Ag_thA_phi*.raw")))

n_images = len(image_paths)

# Load data into a stack
arheed = xrheed.load_data(
    image_paths, 
    plugin="dsnp_arpes_raw", 
    stack_dim="frame", 
    stack_coords=np.arange(n_images)
)

# Set screen ROI
arheed.ri.screen_roi_width = 30
arheed.ri.screen_roi_height = 30

# Select some of images where transmission spots is visible and use them for guided alignment
selected_indices = [0, 1, 2, 3]
all_indices = np.arange(n_images)

center_x_samples = []
center_y_samples = []
incident_angle_samples = []

for idx in selected_indices:
    # Work on copy of an image - do not shift the original stack
    rheed = arheed[idx].copy()

    # Get ROI image
    roi_image = rheed.ri.get_roi_image()

    # Find center coordinates
    center_x = find_horizontal_center(roi_image)
    center_y = find_vertical_center(roi_image, center_x=center_x)

    # Update ROI center
    rheed.ri.set_center_manual(center_x, center_y)

    # Recompute ROI and incident angle
    roi_image = rheed.ri.get_roi_image()
    incident_angle = find_incident_angle(roi_image)
    rheed.ri.incident_angle = incident_angle

    # Store results
    center_x_samples.append(center_x)
    center_y_samples.append(center_y)
    incident_angle_samples.append(incident_angle)

    # Plot diagnostics
    rheed.ri.plot_image(
        show_center_lines=True,
        show_specular_spot=True,
        auto_levels=0.5
    )
    plt.title("")
    plt.show()
../_images/c652542326b98dccf67913fd7b7c7c8eb48e9c64432fb167f27d7918573d6461.png ../_images/b841af7b38267659bb67bdbcd0c13a456b52518205d7ed3e5d5ffe44e435b6c3.png ../_images/773d977ce704a52d390f291a8c8c2bf08b2fe3acc1f2a786bcb2085db74caae8.png ../_images/9d308798ad904eec77688d54da256c4ff44b2dac0221d1db4d0439e2d34e046d.png

Linear fits for interpolation across all images

slope, intercept = np.polyfit(selected_indices, center_x_samples, 1)
center_x_fit = all_indices * slope + intercept

slope, intercept = np.polyfit(selected_indices, center_y_samples, 1)
center_y_fit = all_indices * slope + intercept

slope, intercept = np.polyfit(selected_indices, incident_angle_samples, 1)
incident_angle_fit = all_indices * slope + intercept

arheed.ri.set_center_manual(center_x=center_x_fit, center_y=center_y_fit)

alpha_coords = np.linspace(0.0, 29.6, n_images)
beta_coords = incident_angle_fit

arheed = arheed.assign_coords(
    alpha=("frame", alpha_coords),
    beta=("frame", beta_coords),
)

# Set screen ROI
arheed.ri.screen_roi_width = 30
arheed.ri.screen_roi_height = 40

# Optionally use high pass filter (could be applied to the whole stack)
arheed = high_pass_filter(arheed, sigma=2.0, threshold=0.7)

Prepare a Lattice Object for the Expected Reconstruction

To verify the alignment, we create a lattice object representing the expected (√3 × √3) R30° reconstruction.

This lattice will later be used by the Ewald object for spot calculation and overlay.

from xrheed.kinematics import Lattice

# Si(111)-(1x1)
si_111_1x1 = Lattice.from_surface_hex(a=3.84, label="(1x1)")

# Si(111)-(r3xr3)R30
si_111_r3 = Lattice.from_surface_hex(a=3.84 * np.sqrt(3), label="(√3 x √3)")
si_111_r3.rotate(30)

Generate Ewald Object and Overlay Expected Spot Positions

We generate an Ewald objects and use it to overlay the expected spot positions on each image.

This allows for visual verification of the alignment and consistency across the dataset.

Note: The Ewald object could take a stack of images. In such case a stack_index property could be used to select a particular image from a stack.

from xrheed.kinematics import Ewald

arheed.ri.screen_roi_width = 60
arheed.ri.screen_roi_height = 60

ew = Ewald(si_111_r3, arheed)

fig, axs = plt.subplots(n_images, 1, sharex=True, figsize=(4.0, 14))

for idx in range(n_images):
    ax = axs[idx]
    # Select the image from the stack
    ew.stack_index = idx

    ew.plot(
        ax=ax, show_image=True,
        vmin=3, vmax=35,
        marker="o", facecolor="none", edgecolor="c", s=50,
    )
    ax.set_title("")
    if idx < n_images - 1:
        ax.set_xlabel("")

plt.tight_layout()
plt.show()
../_images/acbaf0afab096a29e5a8bc62f1cec168c5b94db80750c785609029a8cf21ef51.png

Transform Image Stack to kx–ky Space

If the alignment appears satisfactory, we proceed by transforming the whole image stack into the kx–ky space.

This step uses the arguments rotate=True and point_symmetry=True.

  • The rotate=True option applies a rotation around the image center by the azimuthal angle α.

  • The point_symmetry=True option adds a mirrored image rotated by 180°, which is valid for most crystallographic structures.

from xrheed.conversion import transform_stack_to_kxky

kxky_stack = transform_stack_to_kxky(arheed, point_symmetry=True, rotate=True)

Compute the Mean Along the Azimuthal Dimension

Calculate the mean of the dataset for all frames.
This operation reduces the data to a single representative image, averaging over all angular orientations.

kxky_stack_mean = kxky_stack.mean(dim="frame")

Prepare the Final Plot

Finally, we generate an image that displays the mean intensity across all frames, along with overlays of the (1×1) and (√3 × √3) R30° reciprocal lattices for comparison.

As observed, the diffraction spots are elongated along several directions.
This elongation is a characteristic feature of RHEED, resulting from the finite domain size, which affects the shape of the diffraction spots.

Nonetheless, in this representation, it is straightforward to identify specific surface reconstructions, their symmetries, and mutual orientations.

fig, ax = plt.subplots(1, 1, figsize=(5, 5))

kxky_stack_mean.plot(ax=ax, cmap="gray", add_colorbar=False, vmin=2, vmax=20)

si_111_r3.plot_reciprocal(ax=ax, facecolor="none", edgecolor="c", s=150)
si_111_1x1.plot_reciprocal(ax=ax, facecolor="none", edgecolor="m", s=50)

ax.set_aspect(1)
ax.set_facecolor("black")
ax.set_xlim(-2.5, 2.5)
ax.set_ylim(-2.5, 2.5)
ax.legend(loc="upper right")
ax.set_title("")

plt.show()
../_images/7561f34e5594969cf4bc74cd7e9d247bbea7f670ad5f55b92078700f466fbcf8.png