import ase
import matplotlib.pyplot as plt
import numpy as np
from abtem.bloch import BlochWaves, StructureFactor

import abtem

abtem.config.set(
    {
        "precision": "float64",
        "device": "gpu",
    }
)
<abtem.core.config.set at 0x23bcdab9bd0>

Blochwave simulations#

Blochwave simulations, are implemented in abTEM, as an alternative to the multislice simulations, offering advantages under certain conditions. Blochwave allows for simulations of structures with arbitrary crystal rotations and arbitrary unit cells, which are difficult to handle in multislice due to the requirement of orthogonal and periodic unit cells. Blochwave is efficient for crystals described by small unit cells, but is scales as in both memory and time. Hence, becomes prohibitively expensive for large unit cells.

In this tutorial, we go through how to perform blochwave simulations as implemented in abTEM, and compare the simulation results to multislice.

Currently Under Construction

This section uses a new relatively untested feature.

Structure factor#

atoms = ase.build.bulk("Si", cubic=True)

abtem.show_atoms(atoms, legend=True, plane="yz")
(<Figure size 640x480 with 1 Axes>, <Axes: xlabel='y [Å]', ylabel='z [Å]'>)
../../_images/6890ef3049eee57bacbccfbb00cd21145c83b4d022a6c675a4eb8ef286398ef6.png
k_max = 12

structure_factor = StructureFactor(
    atoms,
    k_max=k_max,
    parametrization="lobato",
)
sg_max = 0.1

bloch_waves = BlochWaves(
    structure_factor=structure_factor,
    energy=200e3,
    sg_max=sg_max,
)
len(bloch_waves)
3573

The selected beams can be accessed through the hkl property.

bloch_waves.hkl
array([[ 0,  0,  0],
       [ 0,  1,  0],
       [ 0,  2,  0],
       ...,
       [-1, -3,  0],
       [-1, -2,  0],
       [-1, -1,  0]])
thicknesses = np.arange(0, 1086 + 0.1, 5.43)

diffraction_thickness_series = bloch_waves.calculate_diffraction_patterns(thicknesses)

diffraction_thickness_series
<abtem.measurements.IndexedDiffractionPatterns at 0x23bd3e78e50>
diffraction_thickness_series[::30].crop(k_max=4.5).block_direct().show(
    explode=True,
    scale=0.08,
    power=0.5,
    annotations=False,
    figsize=(14, 5),
    common_color_scale=True,
    cbar=True,
)
<abtem.visualize.visualizations.Visualization at 0x23bb0d06310>
../../_images/ec915f35100c4529432ba61191538a3cae3116158e31545dbfc40bd9e3fefd21.png
data_array = diffraction_thickness_series.to_data_array()
fig, axes = plt.subplots(1, 4, figsize=(13, 4))

for ax, hkl in zip(axes, ["0 0 0", "2 2 0", "4 0 0", "-21 1 -1"]):
    data_array.sel(hkl=hkl).plot(hue="hkl", ax=ax)

plt.tight_layout()
../../_images/7f04091915b17d28e99557997c323fa2bad4d3763241da48e93ad5a7cd7e6340.png

Rotations#

We can get diffraction intensities from other directions using the rotations method. Here, we rotate about \(z\) at \(4\) different angles between \(0\) and \(45 \ \mathrm{deg}\), for each \(z\) rotation we also rotate around \(x\) at \(5\) different angles between \(0\) and \(45 \ \mathrm{deg}\), i.e. resulting in \(16\) different simulations.

x = np.linspace(0, np.pi / 4, 4)
z = np.linspace(0, np.pi / 4, 4)

ensemble = bloch_waves.rotate("z", z, "x", x)

ensemble
<abtem.bloch.dynamical.BlochwaveEnsemble at 0x23c0b9cdbd0>

The previous operation produces a BlochwaveEmsenble object, we run the same calculate_diffraction_patterns method to create the tasks for running a Bloch wave simulation for each direction.

diffraction_patterns = ensemble.calculate_diffraction_patterns(thicknesses=1000)

diffraction_patterns.array
Array Chunk
Bytes 3.94 MiB 252.21 kiB
Shape (4, 4, 32283) (1, 1, 32283)
Dask graph 16 chunks in 5 graph layers
Data type float64 numpy.ndarray
32283 4 4
diffraction_patterns.compute()
[########################################] | 100% Completed | 46.23 s
<abtem.measurements.IndexedDiffractionPatterns at 0x23c09c01cd0>
diffraction_patterns.shape
(4, 4, 32283)
diffraction_patterns = diffraction_patterns.remove_low_intensity(1e-6)
diffraction_patterns.crop(k_max=4).block_direct().show(
    explode=True,
    scale=0.03,
    power=0.5,
    annotation_kwargs={"threshold": 1},
    figsize=(14, 14),
)
<abtem.visualize.visualizations.Visualization at 0x23bd3ebc890>
../../_images/1d36dbe6b4d33e1d353e9fe51e81abda0eb0c7b1c1faf9ada2b588b2b14511a6.png

Arbitrary rotations may be given as a sequence of euler angles, for example, to rotate about a vector pointing along \((-1, 1, 0)\), we can rotate by \(\pi/4\) around \(z\), apply the desired rotations around \(x\), and then rotate back by \(-\pi/4\) around \(z\).

z = np.tile([np.pi / 4], (4,))
x = np.linspace(0, np.pi / 2, 4)

rotations = np.array([z, x, -z]).T
ensemble = bloch_waves.rotate("zxz", rotations)

diffraction_patterns = ensemble.calculate_diffraction_patterns(1000).compute()
[########################################] | 100% Completed | 9.69 ss
diffraction_patterns.crop(k_max=4).block_direct().show(
    explode=True,
    scale=0.03,
    power=0.5,
    annotation_kwargs={"threshold": 1},
    figsize=(14, 14),
)
<abtem.visualize.visualizations.Visualization at 0x23c7bc0f310>
../../_images/219915c1a57aa8737d79a1e6fc6d237e968552895242fc548d5388b5d8d44b73.png

Comparison to multislice#

k_max = 12

structure_factor = StructureFactor(
    atoms, k_max=k_max, parametrization="lobato", centering="F", thermal_sigma=0.078
)
potential_sf = structure_factor.get_projected_potential(slice_thickness=0.5)

potential_sf.sampling
(0.04082706766917293, 0.04082706766917293)
parametrization = abtem.parametrizations.LobatoParametrization(
    sigmas={"Si": 0.078 * np.sqrt(3)}
)

potential = (
    abtem.Potential(
        atoms,
        sampling=potential_sf.sampling,
        slice_thickness=0.1,
        projection="finite",
        parametrization=parametrization,
    )
    .build()
    .compute(progress_bar=False)
)
stack = abtem.stack(
    (potential.project(), potential_sf.project()), ("Direct", "From structure factor")
).compute()

stack.show(explode=True, common_color_scale=True, cbar=True);
[########################################] | 100% Completed | 205.39 ms
../../_images/9f1d723188916b1499beada9286dcc7ef428b1d2af97f8d12f3aa172f785fe3a.png
pw = abtem.PlaneWave(energy=200e3)

nz = int(1086 / potential.thickness)

repeated_potential = abtem.CrystalPotential(
    potential, repetitions=[1, 1, nz], exit_planes=potential.num_slices
)

exit_waves_ms = pw.multislice(potential=repeated_potential).compute()
[########################################] | 100% Completed | 14.37 s
bloch_waves = BlochWaves(
    structure_factor=structure_factor, energy=200e3, sg_max=sg_max, paraxial=True
)

len(bloch_waves)
901
exit_waves_bw = bloch_waves.calculate_exit_wave(
    repeated_potential.exit_thicknesses, gpts=exit_waves_ms.gpts
)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[26], line 1
----> 1 exit_waves_bw = bloch_waves.calculate_exit_wave(
      2     repeated_potential.exit_thicknesses, gpts=exit_waves_ms.gpts
      3 )

TypeError: BlochWaves.calculate_exit_wave() missing 1 required positional argument: 'extent'
exit_waves_bw.shape
exit_waves_ms.shape
stack = abtem.stack(
    [exit_waves_bw, exit_waves_ms],
    axis_metadata={"label": "algorithm", "values": ["MS", "BW"]},
    axis=1,
)
stack.shape
stack[1::30].show(explode=True, figsize=(12, 4))
stacked_diffraction = (
    stack.to_cpu().diffraction_patterns().index_diffraction_spots(cell=atoms)
)

The difference below is mostly do to unconverged bloch wave.

data_array = stacked_diffraction.to_data_array()

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

for ax, hkl in zip(axes, ["0 0 0", "2 2 0", "4 0 0", "-21 1 -1"]):
    data_array.sel(hkl=hkl).plot(hue="algorithm", ax=ax)

plt.tight_layout()

Rotating multislice simulations with periodic boundaries#

rotated_atoms = ase.build.bulk("Si", cubic=True)

angle = np.arctan(1 / 2)

rotated_atoms.rotate("x", angle * 180 / np.pi, rotate_cell=True)

rotated_atoms = abtem.orthogonalize_cell(rotated_atoms)

abtem.show_atoms(rotated_atoms, legend=True, plane="xy")
parametrization = abtem.parametrizations.LobatoParametrization(
    sigmas={"Si": 0.078 * np.sqrt(3)}
)

potential = abtem.Potential(
    atoms,
    sampling=0.05,
    slice_thickness=0.1,
    projection="finite",
    parametrization=parametrization,
)