Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Z-Contrast Imaging

Chapter 3: Imaging


Z-Contrast Imaging

Download

Open In Colab

part of

MSE672: Introduction to Transmission Electron Microscopy

Spring 2026
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

Load important packages

In Colab the next code cell must be run first

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.2026.1.0':
    print('installing pyTEMlib')
    !{sys.executable} -m pip install  --upgrade pyTEMlib -q
if test_package('abtem') < '1.0.8':
    print('installing abtem')
    !{sys.executable} -m pip install  --upgrade pyTEMlib -q

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

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'

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.

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

Defining the structure

Here we make a SrTiO3_3 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])
Loading...

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 

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);
Loading...
Loading...
Loading...

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)
Loading...
<abtem.visualize.visualizations.Visualization at 0x230650a4a50>
Loading...

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.

Definition of a detector

The software only retains the radial integrated intensity of the detector

detector = abtem.FlexibleAnnularDetector()

Scanning the Probe

Define scanning area

The scanning area can be small, but the structure in real space (width and height) is important because it determines the pixel size in reciprocal space.

We calculate a diffraction pattern for each point of the scanned area.

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

fig, ax = abtem.show_atoms(srtio3)

grid_scan.add_to_plot(ax)
Loading...

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()
Loading...
<abtem.measurements.PolarMeasurements at 0x23000a0b650>

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, 100)

Plot images

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

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

“”### 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);
Loading...

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,
);
Loading...

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,
);
Loading...

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

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.

References
  1. Madsen, J., & Susi, T. (2021). The abTEM code: transmission electron microscopy from first principles. Open Research Europe, 1, 24. 10.12688/openreseurope.13015.1