Hide code cell source
%config InlineBackend.rc = {"figure.dpi": 72, 'figure.figsize': (6.0, 4.0)}
%matplotlib inline

import abtem
import ase
import matplotlib.pyplot as plt
import numpy as np
from IPython.display import HTML

abtem.config.set({"local_diagnostics.progress_bar": True});

Visualizations#

Visualizing the results is an integral part of understanding the output of a scattering simulation. The abTEM visualization module wraps the matplotlib library to help produce typical visualizations needed when simulating transmission electron microscopy. In this tutorial, we cover how to customize the standard abTEM outputs to obtain publication-ready visualizations. We note that not every customization is possible within our framework; it may sometimes be more practical to create your own visualization from scratch.

See also

This tutorial only covers static visualizations. The abTEM visualization module also features animated and interactive visualizations, see the respective examples for more information.

Currently Under Construction

This section uses a new relatively untested feature.

Customizing an image ensemble#

The abTEM visualization module is based on the concept of ensembles of similar measurements. Given an object with one or more ensemble axes, abTEM can create an “exploded” plot when you set explode=True. The exploded plot is a 1D or 2D grid of images or line plots, where each subplot represents a member of the ensemble.

As an example, we create HRTEM images for silicon \(\{100\}\) with a single ensemble dimension representing different values of defocus.

Hide code cell source
atoms_cell = ase.build.bulk("Si", cubic=True)

atoms = atoms_cell * (1, 1, 100)

potential = abtem.Potential(atoms, sampling=0.05)

wave = abtem.PlaneWave(energy=100e3)

defocus = np.linspace(-80, 80, 5)

measurements = (
    wave.multislice(potential)
    .apply_ctf(defocus=defocus, Cs=10e6, semiangle_cutoff=20, focal_spread=30)
    .intensity()
    .tile((3, 3))
    .compute()
    .poisson_noise(dose_per_area=2e3, seed=100)
)
[########################################] | 100% Completed | 2.87 ss

We create a visualization using the .show method and set explode=True to create a grid plot. We set common_color_scale=True to show all the images on a single color scale; if this is set to False each image is scaled independently. We further set cbar=True to included a colorbar and cmap="gray" to show the images using a grayscale colormap (see the matplotlib documentation for a full list).

Note how each subplot is automatically labeled based on the defocus ensemble axis.

visualization = measurements.show(
    explode=True,
    common_color_scale=True,
    figsize=(14, 5),
    cbar=True,
    cmap="gray",
)
../../_images/79dbe5d04181e4aaa6292fb5d087675eedbf8ec626c7dacae58742eff5acdb6c.png

The .show method produces a MeasurementVisualization object, which wraps a matplotlib figure. Its specific subtype will depend on the type of measurement used to create the visualization – in this case, we obtain a MeasurementVisualization2D object.

object.__repr__(visualization.get_figure())
'<matplotlib.figure.Figure object at 0x7f074ba5cb10>'

The plots are laid out using the AxesGrid class.

visualization.axes
<abtem.visualize.axes_grid.AxesGrid at 0x7f07baa98810>

We can convert the AxesGrid to a numpy array of matplotlib Axes as below. The numpy array is always 2D, hence in this case it is a (3,1) array, where the first dimension indexes columns.

np.array(visualization.axes)
array([[<Axes: xlabel='$x$ [$\\mathrm{\\AA}$]', ylabel='$y$ [$\\mathrm{\\AA}$]'>],
       [<Axes: xlabel='$x$ [$\\mathrm{\\AA}$]', ylabel='$y$ [$\\mathrm{\\AA}$]'>],
       [<Axes: xlabel='$x$ [$\\mathrm{\\AA}$]', ylabel='$y$ [$\\mathrm{\\AA}$]'>],
       [<Axes: xlabel='$x$ [$\\mathrm{\\AA}$]', ylabel='$y$ [$\\mathrm{\\AA}$]'>],
       [<Axes: xlabel='$x$ [$\\mathrm{\\AA}$]', ylabel='$y$ [$\\mathrm{\\AA}$]'>]],
      dtype=object)

Each Axes can be adjusted using the methods from matplotlib, but the abTEM MeasurementVisualization implements some methods to assist in converting the standard figure to something more apporpriate for a publication.

We can adjust the vertical and horisontal spacing betwen images using the .set_axes_padding method; in our case we only need to adjust the horizontal spacing. The width of the colorbar can be adjusted using the .set_cbar_size method, and finally we can adjust the left and right padding of the colorbar using the .set_cbar_padding method.

visualization.axes.set_sizes(padding = .1)
#visualization.set_cbar_size(0.075)
#visualization.set_cbar_padding((0.05, 0.0))

visualization.get_figure()
../../_images/79dbe5d04181e4aaa6292fb5d087675eedbf8ec626c7dacae58742eff5acdb6c.png

We can change the units of the titles using the .set_column_titles method, the units of the \(x\) and \(y\)-axis can also be changed by using the set_x_units or set_y_units methods.

visualization.set_column_titles(units="nm")
visualization.set_xunits("nm")
visualization.set_yunits("nm")

visualization.fig
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[18], line 1
----> 1 visualization.set_column_titles(units="nm")
      2 visualization.set_xunits("nm")
      3 visualization.set_yunits("nm")

TypeError: Visualization.set_column_titles() missing 1 required positional argument: 'titles'

We can also simply remove the titles by setting them to an empty string.

visualization.set_column_titles(titles="")

visualization.fig
../../_images/b399af965e4f360fb4dab173d824b4674c9735149bd58533aa676b4b001aefdf.png

The axes ticks and labels can be removed using the axis_off method, and we can add a scalebar (based on the matplotlib object AnchoredSizeBar) to preserve a description of the scale.

visualization.axis_off(spines=False)
visualization.set_scalebars(size=3, color="w")

visualization.fig
../../_images/0931b313b30f81a4271181e85263d19ed02fe409dd2039857a534d76a4871d99.png

The scalebars placement within each panel can be controlled via the loc keyword (see matplotlib documentation), and within the ensamble with the panel_loc keyword (with ‘all’ showing the scale bar in each plot).

visualization.axis_off(spines=False)
visualization.set_scalebars(size=3, color="w", loc="lower left", panel_loc="all")

visualization.fig
../../_images/abd22cdc1ca43894b269e849ccc3a093104b2d3c9637d974885d46cc89d5807a.png

Finally, we can label each image using the same metadata used for the standard titles to add annotations.

visualization.set_panel_labels(
    labels="metadata", frameon=False, prop={"fontsize": 14, "color": "w"}
)

visualization.fig
../../_images/e2c7c79a3471b2e440f9f4b437fe25b624ba6b737a8151a74a6d704fd2587953.png

2D ensemble of images#

In the next example, we demonstrate a 2D grid of graphene images for different values of defocus and spherical aberrations.

Hide code cell source
defocus = np.linspace(-50, 50, 7)
Cs = np.linspace(0, 400000, 5)
focal_spread = 30

atoms = ase.build.graphene(vacuum=2)

exit_wave = abtem.PlaneWave(energy=80e3, sampling=0.05).multislice(atoms)

images = (
    exit_wave.apply_ctf(defocus=defocus, Cs=Cs, focal_spread=focal_spread)
    .intensity()
    .poisson_noise(5e5)
    .compute()
)
[########################################] | 100% Completed | 517.82 ms

We create a 2D grid of images on the same color scale by setting explode=True and common_color_scale=True. Note the automatic labeling of the rows and columns.

visualization = images.show(
    explode=True, figsize=(10, 10), common_color_scale=True, cmap="gray"
)
../../_images/e5416874e2cc6a6ea0cc20f95bf08cf63035472c9a83ac6a8ba0790ec52acea0.png

We can remove the spacing between the images using the set_axes_padding method. The axes desciptions can be removed using the axis_off method, and we can instead add a scalebar using the set_scalebar method.

visualization.set_axes_padding([0.0, 0.0])
visualization.axis_off(spines=False)

visualization.set_scalebars(size=1, color="w")

We can further change the units displayed in the titles using the set_row_titles and set_column_titles methods.

visualization.set_row_titles(units="um")
visualization.set_column_titles(units="nm")

visualization.fig
../../_images/fe9d577e19eecfb602ec0504f2873c6c8ec413d9df0a58cc9ebeb93ba3570f8c.png

Diffraction spots#

The diffraction intensities calculated using a multislice simulation are concentrated in single pixels, which does not naturally make for nice visualizations. Using the indexing algorithm, we can get the positions and intensities of the diffraction spots, which can be more helpfully visualized as disks. In the example below, we create a typical textbook image of electron diffraction patterns for SiO2 zeolite, where each diffraction spot is a white disk on a black background.

Hide code cell source
atoms_cell = ase.io.read("data/SiO2_zeolite.cif")

atoms = atoms_cell * (1, 1, 10)

potential = abtem.Potential(atoms, sampling=0.05)

wave = abtem.PlaneWave(energy=200e3)

wave.grid.match(potential)

measurements = wave.multislice(potential).compute()

spots = measurements.diffraction_patterns().index_diffraction_spots(
    cell=atoms_cell.cell, threshold=1e-4
)
[########################################] | 100% Completed | 3.52 ss

We create the visualization cropping the maximum scattering angle to \(10 \ \mathrm{mrad}\), blocking the direct beam, and setting the colormap to solid white.

visualization = (
    spots.crop(10)
    .block_direct()
    .show(
        figsize=(8, 8),
        scale=0.1,
        cmap="w",
        display=False,
    )
)

We can access our matplotlib Axes and apply the method for setting the background color as below.

visualization.axes[0, 0].set_facecolor("k")

To avoid too much visual clutter, we can set a threshold for which diffraction spots are labelled with their Miller indices to an intensity of \(0.0003\) (the full intensity including the direct beam should sum to \(1.0\)).

visualization.axis_off(spines=False)

visualization.set_miller_index_annotations(0.0003, size=10, color="w")

visualization.fig
../../_images/7d860801477947a46c2ed772d5b56bc5e8a157c265450cc029c0f96379721c23.png

Domain coloring#

For complex entities, phase and amplitude can be displayed simultaneously using a technique called domain coloring.

Hide code cell source
probe = abtem.Probe(
    semiangle_cutoff=30,
    defocus=60,
    astigmatism=np.linspace(0, 70, 3),
    astigmatism_angle=np.pi / 4,
    gpts=256,
    extent=10,
    energy=200e3,
)
probes = probe.build().complex_images().compute()
[########################################] | 100% Completed | 310.94 ms

By default, we map the phase to colors using the “HSLuv” color mapping; however, the more saturated “hsv”/”hsl” mapping is also available by setting cmap=hsv.

visualization = probes.show(
    explode=True, figsize=(12, 6), cbar=True, common_color_scale=True, cmap="hsv"
)
../../_images/d74f74d81f5116eb9b5dd2c34e5bb97c8e39628fd104eb4366a2ae7c4e50af40.png