Chapter 3: Imaging


3.10. Z-Contrast Imaging#

Download

Open In Colab

part of

MSE672: Introduction to Transmission Electron Microscopy

Spring 2025
by Gerd Duscher

Microscopy Facilities
Institute of Advanced Materials & Manufacturing
Materials Science & Engineering
The University of Tennessee, Knoxville

Background and methods to analysis and quantification of data acquired with transmission electron microscopes.

Note: This notebook needs abTEM installled

3.10.1. Load important packages#

In Colab the next code cell must be run first

3.10.1.1. Check Installed Packages#

import sys
import importlib.metadata
def test_package(package_name):
    """Test if package exists and returns version or -1"""
    try:
        version = importlib.metadata.version(package_name)
    except importlib.metadata.PackageNotFoundError:
        version = '-1'
    return version

if test_package('pyTEMlib') < '0.2024.2.3':
    print('installing pyTEMlib')
    !{sys.executable} -m pip install  --upgrade pyTEMlib -q
if test_package('abtem') < '1.0.4':
    print('installing abtem')
    !{sys.executable} -m pip install  --upgrade pyTEMlib -q

# ------------------------------
print('done')
done

3.10.1.2. Load Packages#

We will use

  • numpy and matplotlib

  • physical constants from scipy

  • The pyTEMlib kinematic scattering librarty is only used to determine the wavelength.

abTEM

please cite abTEM methods article:

J. Madsen & T. Susi, “The abTEM code: transmission electron microscopy from first principles”, Open Research Europe 1: 24 (2021)

# import matplotlib and numpy
#                       use "inline" instead of "notebook" for non-interactive plots
import sys
%matplotlib ipympl
if 'google.colab' in sys.modules:    
    from google.colab import output
    output.enable_custom_widget_manager()

import matplotlib.pyplot as plt
import numpy as np

# import atomic simulation environment
import ase
import ase.spacegroup
import ase.visualize

# import abintio-tem library
import abtem

__notebook__ = 'CH3_09-Z_Contrast'
__notebook_version__ = '2021_03_29'

3.10.2. Z-contrast imaging#

A Z-contrast image is acquired by scanning a convergent beam accross the sample and collecting signals with an annular (ring-like) detector. The detector sits in the convergent beam electron diffraction pattern plane and so it integrates over a ring-like part of the convergent beam diffraction (CBED) pattern.

More generally a scanning transmisison electron microscopy (STEM) image is still scanning the same probe but integrates over different portions of the CBED pattern. A bright field detector for instance integrates over the inner part of the CBED pattern and is disk-like.

3.10.3. Make Structure and Potential with Frozen Phonons#

As in the Dynamic Diffraction with Frozen Phonons part in the Thermal Diffuse Scattering notebook, we first define the potential of the slices.

Again we use the abtem and ase packages to do this

3.10.3.1. Defining the structure#

Here we make a SrTiO3 crystal again

# ------- Input -----------#
thickness =30  # in nm
number_of_layers = 2  # per unit cell
# -------------------------#


atom_pos = [(0.0, 0.0, 0.0), (0.5, 0.5, 0.5), (0.5, 0.5, 0.0)]
srtio3 = ase.spacegroup.crystal(['Sr','Ti','O'], atom_pos, spacegroup=221, cellpar=3.905, size=(10, 10, 60))

srtio3.center()
print(f"Simulation cell: {srtio3.cell}")
abtem.show_atoms(srtio3);
Simulation cell: Cell([39.05, 39.05, 234.29999999999998])

3.10.3.2. Make the potential#

with frozen phonon approximation

# ------ Input ------ #
number_of_frozen_phonon_runs = 12
# --------------------#
abtem.config.set({"device": "cpu", "fft": "fftw"})
frozen_phonons = abtem.FrozenPhonons(srtio3, number_of_frozen_phonon_runs, {'Sr' : .1, 'Ti' : .1, 'O' : .1}, seed=1)
tds_potential = abtem.Potential(frozen_phonons, gpts=512, slice_thickness=3.905/2, 
                                projection='infinite', parametrization='kirkland')
print(f"Real space sampling: {tds_potential.sampling} Angstrom ")
Real space sampling: (0.07626953125, 0.07626953125) Angstrom 

3.10.3.3. Make the probe#

The probe has to be on the same grid (matrix, pixels) as the potential, which is ensured with the grid.match function.

# ---- Input ----- #
convergence_angle = 20  # in mrad of half angle
acceleration_voltage = 200e3 # in V
defocus = 40  # in nm
C_s = .5 # in mm            conversion to ase and Angstrom    *1-6 * 1e10 = 1e4
# -----------------#


probe = abtem.Probe(energy=acceleration_voltage, semiangle_cutoff=convergence_angle, 
                    defocus=defocus, Cs=C_s*1e4)
probe.grid.match(tds_potential)
print(f"defocus = {probe.aberrations.defocus} Å")
print(f"FWHM = {probe.profiles().width().compute()} Å")
defocus = 40 Å
FWHM = 0.6864258050918579 Å
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
probe.show(ax=ax1)
probe.profiles().show(ax=ax2);
[########################################] | 100% Completed | 208.23 ms
[########################################] | 100% Completed | 312.02 ms

3.10.4. Detectors#

The detectors are definded by their angles and ususally in mrad.

The detector integrate radially over the CBED pattern for each pixel.

detector = abtem.PixelatedDetector(max_angle='limit')
probe.grid.match(tds_potential)
tds_cbed = probe.multislice(tds_potential)
tds_cbed.mean(0).show(power=0.25)
[########################################] | 100% Completed | 34.44 ss
<abtem.visualize.visualizations.Visualization at 0x2b82e97e9c0>

We see that the HAADF detector is dominated by the features of thermal diffuse scattering.

While that part is not terribly important in the bright field image.

Please note:

The detectors have to be well aligned on the optical axis or the simulation here is not valid.

3.10.4.1. Definition of a detector#

The software only retains the radial integrated intensity of the detector

detector = abtem.FlexibleAnnularDetector()

3.10.5. Scanning the Probe#

3.10.5.1. Define scanning area#

grid_scan = abtem.GridScan(
    start=(0, 0),
    end=[1/8, 1/8],
    sampling=probe.aperture.nyquist_sampling,
    fractional=True,
    potential=tds_potential,
)

fig, ax = abtem.show_atoms(srtio3)

grid_scan.add_to_plot(ax)

3.10.5.2. Now we are scanning#

The results are going to be stored to file.

This takes about 20 min on my laptop

flexible_measurement = probe.scan(tds_potential, scan=grid_scan, detectors=detector)

flexible_measurement.compute()
[########################################] | 100% Completed | 4hr 16m
<abtem.measurements.PolarMeasurements at 0x169a80356a0>

3.10.5.3. Integrate measurements#

The measurements are integrated to obtain the bright field, medium-angle annular dark field and high-angle annular dark field signals.

bf_measurement = flexible_measurement.integrate_radial(0, probe.semiangle_cutoff)
maadf_measurement = flexible_measurement.integrate_radial(30, 50)
# maadf_measurement = flexible_measurement.integrate_radial(probe.semiangle_cutoff-10, probe.semiangle_cutoff+10)
haadf_measurement = flexible_measurement.integrate_radial(70, 200)
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[93], line 4
      2 maadf_measurement = flexible_measurement.integrate_radial(30, 50)
      3 # maadf_measurement = flexible_measurement.integrate_radial(probe.semiangle_cutoff-10, probe.semiangle_cutoff+10)
----> 4 haadf_measurement = flexible_measurement.integrate_radial(70, 200)

File ~\AppData\Local\anaconda3\Lib\site-packages\abtem\measurements.py:3439, in PolarMeasurements.integrate_radial(self, inner, outer)
   3418 def integrate_radial(
   3419     self, inner: float, outer: float
   3420 ) -> Images | RealSpaceLineProfiles:
   3421     """
   3422     Create images by integrating the polar measurements over an annulus defined by an inner and outer integration
   3423     angle.
   (...)
   3437         Integrated line profiles (returned if there is only one scan axis).
   3438     """
-> 3439     return self.integrate(radial_limits=(inner, outer))

File ~\AppData\Local\anaconda3\Lib\site-packages\abtem\measurements.py:3487, in PolarMeasurements.integrate(self, radial_limits, azimuthal_limits, detector_regions)
   3484     radial_slice = slice(inner_index, outer_index)
   3486     if outer_index > self.shape[-2]:
-> 3487         raise RuntimeError("Integration limit exceeded.")
   3489 if azimuthal_limits is None:
   3490     azimuthal_slice = slice(None)

RuntimeError: Integration limit exceeded.

3.10.5.4. Plot images#

measurements = abtem.stack(
    [bf_measurement, maadf_measurement, haadf_measurement], ("BF", "MAADF", "HAADF")
)

measurements.show(
    explode=True,
    figsize=(14, 5),
    cbar=True,
);

“”### Plotting result We make the images a bit bigger by tiling

haadf_image = haadf_measurement.tile((4, 4))
haadf_image = haadf_image.interpolate(.04)

bf_image = bf_measurement.tile((4, 4))
bf_image = bf_image.interpolate(.04)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(8,4), sharex=True, sharey=True)

haadf_image.show(ax=ax1, cmap = 'viridis', cbar=True)

im2 = bf_image.show(ax=ax2, cmap = 'viridis', cbar=True);

We can simulate partial spatial coherence by applying a gaussian filter. The standard deviation of the filter is 0.3 Å, the approximate size of the electron source.

interpolated_measurements = measurements.interpolate(0.05)

filtered_measurements = interpolated_measurements.gaussian_filter(0.3)
filtered_measurements.show(
    explode=True,
    figsize=(14, 5),
    cbar=True,
);

3.10.5.5. Noise#

We simulate a finite electron dose by applying poisson noise

# -------- Input -------------
dose_per_area = 1e7   # electrons/angstrom^2
# ----------------------------

noisy_measurements = filtered_measurements.poisson_noise(dose_per_area=dose_per_area)

noisy_measurements.show(
    explode=True,
    figsize=(14, 5),
    cbar=True,
);

3.10.6. Experimental Consideration#

  • We need an as small a probe as possible.

    • This will depend on the instrument especially everything before the sample!

    • This will depend on the defocus.

    • This will depend on the aperture, which will depend on the instrument and the largest coherent area.

    • This will depend on the aberrations and how well you corrected them

  • We need to be tilted in the relevant zone axis

  • We need a relatively thin specimen location

3.10.7. Summary#

For a quantitative image simulation we need to do dynamic scattering theory.

The dynamic scattering theory is done within the multislice algorithm that treats each slice like a weak phase object.

Thermal defuse scattering needs to be included into the multislice calculations for a good simulation

The thermal diffuse scattering can be approximated by the frozen phonon approximation but it is computationally intensive.