Analyzing Spot Diffraction Pattern¶
part of
a pycroscopy ecosystem package
Notebook by Gerd Duscher, 2025
Microscopy Facilities
Institute of Advanced Materials & Manufacturing
The University of Tennessee, Knoxville
Model based analysis and quantification of data acquired with transmission electron microscopes
Content¶
An introduction into diffraction_tools and how to use the functions in this package to index spot diffraction pattern of single crystal sample areas.
The scope of this notebook includes calculation and plotting of
allowed, forbidden and dynamically activated Bragg reflections,
Kikuchi- and HOLZ-lines.
The diffraction vectors are given in polar coordinates and internally in Å or 1/Å because that is the base unit of the ase (atomic simulation evironment) package. The package ase is the crystallography package used in pyTEMlib and allows easy conversion to MD and DFT calculations as well as abTEM image and diffraction simulations. Please note that we often plot in the for microscopy more convenient units of nm and 1/nm.
An explanation on the physcial background can be found in the Diffraction chapter of MSE672-Introduction to TEM
Install pyTEMlib¶
If you have not done so in the Introduction Notebook, please test and install pyTEMlib and other important packages with the code cell below.
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.1.0':
print('installing pyTEMlib')
!{sys.executable} -m pip install git+https://github.com/pycroscopy/pyTEMlib.git@main -q --upgrade
if 'google.colab' in sys.modules:
!{sys.executable} -m pip install numpy==1.24.4
print('done')done
Load the plotting and figure packages¶
Import the python packages that we will use:
Beside the basic numerical (numpy) and plotting (pylab of matplotlib) libraries,
we will use pyTEMlib - especially:
diffraction_tools library.
# import matplotlib and numpy
# use "inline" instead of "notebook" for non-interactive plots
%matplotlib widget
import matplotlib.pyplot as plt
import numpy as np
import sys
if 'google.colab' in sys.modules:
from google.colab import output
output.enable_custom_widget_manager()
# Import libraries from the pyTEMlib
%load_ext autoreload
%autoreload 2
sys.path.insert(0, '../../')
import pyTEMlib
__notebook_version__ = '2025.12.12'
print('pyTEM version: ', pyTEMlib.__version__)
print('notebook version: ', __notebook_version__)pyTEM version: 0.2025.12.1
notebook version: 2025.12.12
Define Crystal¶
Define a Crystal as an ase object.
That allows for easy access to things like reciprocal unit cells (atoms.cell.reciprocal())
With the provided crystal tools it is straight forward to change to ‘Gold’, ‘graphite’, ‘Pt’, or any other supported crystals. Adding structruees with POSCAR or cif files is also supported.
Plot Diffraction Pattern¶
For a minimum we need the zone_axis in Miller indices and the acceleration voltage fo the TEM. We pack those information in a dictionary and run get_bragg_reflections from diffraction_tools
Note:
We calculate the Bragg reflections in polar coordinates.
#Initialize the dictionary with all the input
atoms = pyTEMlib.crystal_tools.structure_by_name('Silicon')
print(atoms)
import ase.visualize
ase.visualize.view(atoms*2, viewer='x3d')
Lattice(symbols='Si8', pbc=True, cell=[5.43088, 5.43088, 5.43088])
# --------------- INPUT ------------------------
zone_hkl = np.array([1, 1, 0])
hkl_max = 35 # maximum allowed Miller index
sg_max = 0.03 # 1/Ang maximum allowed excitation error
acceleration_voltage = 200.0 * 1000.0 #V
rotation = np.radians(0) # rotation of diffraction pattern
# -------------------------------------------
tags = {'zone_hkl': zone_hkl,
'hkl_max': hkl_max,
'Sg_max': sg_max,
'acceleration_voltage': acceleration_voltage}
diff_dict ={}
diff_dict = pyTEMlib.diffraction_tools.get_bragg_reflections(atoms, tags, verbose=True)
# Simple Plot
ZOLZ = diff_dict['allowed']['ZOLZ']
HOLZ = diff_dict['allowed']['HOLZ']
r = diff_dict['allowed']['g'][:, 0]
phi = diff_dict['allowed']['g'][:, 1]
x = r *np.cos(phi+rotation)*10
y = r * np.sin(phi+rotation)*10
plt.figure()
plt.scatter(x[ZOLZ], y[ZOLZ], label='ZOLZ allowed', c='r')
plt.scatter(x[HOLZ], y[HOLZ], label="HOLZ allowed", c ='orange')
plt.axis('equal')
plt.xlabel('reciprocal distance (1/nm)');Of the 357910 tested reciprocal lattice points 2076 have an excitation error less than 0.30 1/nm
Of the 2076 possible reflection 404 are allowed.
Of those, there are 56 in ZOLZ and 348 in HOLZ
Of the 98 forbidden reflection in ZOLZ 20 can be dynamically activated.
Allowed, Forbidden and Dynamically-Activated Reflections¶
We can now plot allowed forbidden and dynamically activated reflections independently.
# ---- Input -----
rotation = np.radians(0)
# ----------------
ZOLZ = diff_dict['allowed']['ZOLZ']
HOLZ = diff_dict['allowed']['HOLZ']
r = diff_dict['allowed']['g'][:, 0]
phi = diff_dict['allowed']['g'][:, 1]
x = r *np.cos(phi+rotation)*10
y = r * np.sin(phi+rotation)*10
ZOLZ_forbidden = diff_dict['forbidden']['ZOLZ']
HOLZ_forbidden = diff_dict['forbidden']['HOLZ']
activated = (diff_dict['forbidden']['dynamically_activated'])
r_forbidden = diff_dict['forbidden']['g'][:, 0]
phi_forbidden = diff_dict['forbidden']['g'][:, 1]
x_forbidden = r_forbidden *np.cos(phi_forbidden+rotation)*10
y_forbidden = r_forbidden *np.sin(phi_forbidden+rotation)*10
x_dynamic = x_forbidden[ZOLZ_forbidden][activated]
y_dynamic = y_forbidden[ZOLZ_forbidden][activated]
plt.figure()
plt.scatter(x_forbidden, y_forbidden, label='forbidden', c='green', alpha = .3 )
plt.scatter(x[ZOLZ], y[ZOLZ], label='ZOLZ allowed', c='r')
plt.scatter(x[HOLZ], y[HOLZ], label="HOLZ allowed", c ='orange')
plt.scatter(x_dynamic, y_dynamic, label='activated')
plt.axis('equal')
plt.xlabel('reciprocal distance (1/nm)')
plt.legend();
Plotting in Polar Coordinates¶
Like for ring-diffraction pattern, it is illustrative to look at the diffraction pattern in polar coordinates.
You will notece that we internally use only polar cooredinates.
# ---- Input -----
rotation = np.radians(-10)
# ----------------
ZOLZ = diff_dict['allowed']['ZOLZ']
HOLZ = diff_dict['allowed']['HOLZ']
r = diff_dict['allowed']['g'][:, 0]
phi = diff_dict['allowed']['g'][:, 1]
plt.figure()
plt.scatter( np.degrees(phi)[ZOLZ], r[ZOLZ])
plt.xlabel('angle (degrees)');
Plot Kikuchi Pattern¶
With the knowledge that the slope we can plot the perpendicular bisector of the line from origin to the Bragg peaks which are the Kikuchi lines.
# ---- Input -----
rotation = np.radians(0)
size = 500 # mrad
# ----------------
ZOLZ = diff_dict['allowed']['ZOLZ']
r = diff_dict['allowed']['g'][ZOLZ, 0] * 10 # now in 1/nm
phi = diff_dict['allowed']['g'][ZOLZ, 1]
x = r *np.cos(phi+rotation)
y = r * np.sin(phi+rotation)
plt.figure()
plt.scatter(x,y, s=size, alpha = .6, c= 'r')
for i, ph in enumerate(phi-np.pi/2):
plt.axline( (x[i]/2,y[i]/2),slope=(np.tan(ph)))
plt.axis('equal')
plt.xlabel('reciprocal distance (1/nm)')Or with the help of a function and the HOLZ line equation for d_theta () in the zero order Laue zone (ZOLZ)
d_theta in units of length is stored in the 4th dimension of the g vector for convenience.
def plotting_coordinates(g, rotation=0, feature='spot'):
if feature == 'line':
# Note: d_theta in g{: 3] is negative so we need to rotate phi by 180 degree
x = g[:, 3] * np.cos(g[:, 1]+np.pi+rotation)*10
y = g[:, 3] * np.sin(g[:, 1]+np.pi+rotation)*10
return np.stack((x, y, np.tan(g[:, 1]+rotation-np.pi/2)), axis= 1)
x = g[:, 0] * np.cos(g[:, 1]+rotation)*10
y = g[:, 0] * np.sin(g[:, 1]+rotation)*10
return np.stack((x, y), axis= 1) ZOLZ = diff_dict['allowed']['ZOLZ']
xy = plotting_coordinates(diff_dict['allowed']['g'][ZOLZ], feature='spot')
kikuchi = plotting_coordinates(diff_dict['allowed']['g'], feature='line')
plt.figure()
plt.scatter(xy[:, 0], xy[:,1], color = 'r')
for line in kikuchi[ZOLZ]:
plt.axline( (line[0], line[1]),slope=line[2], linewidth = 2)
plt.axis('equal');Plot HOLZ Lines¶
Because, we calculated the for all reflections, we do not need to do anything else for HOLZ lines
ZOLZ = diff_dict['allowed']['ZOLZ']
FOLZ = diff_dict['allowed']['FOLZ']
SOLZ = diff_dict['allowed']['SOLZ']
xy = plotting_coordinates(diff_dict['allowed']['g'][ZOLZ], feature='spot')
kikuchi = plotting_coordinates(diff_dict['allowed']['g'], feature='line')
plt.figure()
plt.scatter(xy[:, 0], xy[:,1], color = 'r')
line = (kikuchi[ZOLZ])[0]
plt.axline( (line[0], line[1]), slope=line[2], linewidth=2, label='Kikuchi')
for line in kikuchi[ZOLZ]:
plt.axline( (line[0], line[1]), slope=line[2], linewidth=2)
line = (kikuchi[FOLZ])[0]
plt.axline( (line[0], line[1]), slope=line[2], color='g', alpha=0.5, label='FOLZ')
for line in kikuchi[FOLZ]:
plt.axline( (line[0], line[1]),slope=line[2], color='g', alpha=0.5)
line = (kikuchi[SOLZ])[0]
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha=0.2, label='SOLZ')
for line in kikuchi[SOLZ]:
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha = 0.2)
plt.axis('equal')
plt.legend();HOLZ lines in Higher Order Zone Axis¶
tags = {'acceleration_voltage_V': 99.2 * 1000.0, # V
'convergence_angle_mrad': 7.15, # mrad;
'zone_hkl': np.array([1, 2, -2]),
'mistilt': np.array([0, 0, 0]), # mistilt in degrees
'Sg_max': .03, # 1/A maximum allowed excitation error
'hkl_max': 9} # Highest evaluated Miller indices
diff_dict = {}
diff_dict = pyTEMlib.diffraction_tools.get_bragg_reflections(atoms, tags, verbose=True)
ZOLZ = diff_dict['allowed']['ZOLZ']
FOLZ = diff_dict['allowed']['FOLZ']
SOLZ = diff_dict['allowed']['SOLZ']
xy = plotting_coordinates(diff_dict['allowed']['g'][ZOLZ], feature='spot')
kikuchi = plotting_coordinates(diff_dict['allowed']['g'], feature='line')
plt.figure()
plt.scatter(xy[:, 0], xy[:,1], color = 'r')
line = (kikuchi[ZOLZ])[0]
plt.axline( (line[0], line[1]), slope=line[2], linewidth=2, label='Kikuchi')
for line in kikuchi[ZOLZ]:
plt.axline( (line[0], line[1]), slope=line[2], linewidth=2)
line = (kikuchi[FOLZ])[0]
plt.axline( (line[0], line[1]), slope=line[2], color='g', alpha=0.5, label='FOLZ')
for line in kikuchi[FOLZ]:
plt.axline( (line[0], line[1]),slope=line[2], color='g', alpha=0.5)
if SOLZ.sum()>0:
line = (kikuchi[SOLZ])[0]
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha=0.2, label='SOLZ')
for line in kikuchi[SOLZ]:
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha = 0.2)
plt.axis('equal')
plt.scatter(0,0)
plt.legend();Of the 6858 tested reciprocal lattice points 156 have an excitation error less than 0.30 1/nm
Of the 156 possible reflection 28 are allowed.
Of those, there are 4 in ZOLZ and 24 in HOLZ
Of the 46 forbidden reflection in ZOLZ 0 can be dynamically activated.
diff_dict['K_0']np.float64(27.021910916479413)image = plt.imread('./Zuo-HOLZ-experiment.jpg')
plt.figure()
extent = np.array([-2.7006, 2.8206, -2.15 , 3.01 ])
extent[:2] -= 0.125
extent[2:] -= 0.06
extent /=1.03
plt.imshow(image, extent=extent)
plt.axis('auto')
for line in kikuchi[FOLZ]:
plt.axline( (line[0], line[1]),slope=line[2], color='g', alpha=0.5)
if SOLZ.sum()>0:
line = (kikuchi[SOLZ])[0]
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha=0.2, label='SOLZ')
for line in kikuchi[SOLZ]:
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha = 0.2)
plt.scatter(0,0)
plt.xlim(-2.5,2)
plt.ylim(-2,2.5)
plt.axis('equal')
extent;Or with a few more lines (higher hkl_max)
tags = {'acceleration_voltage_V': 99.2 * 1000.0, # V
'convergence_angle_mrad': 7.15, # mrad;
'zone_hkl': np.array([1, 2, -2]),
'mistilt': np.array([0, 0, 0]), # mistilt in degrees
'Sg_max': .03, # 1/A maximum allowed excitation error
'hkl_max': 15} # Highest evaluated Miller indices
diff_dict = {}
diff_dict = pyTEMlib.diffraction_tools.get_bragg_reflections(atoms, tags, verbose=True)
zolz = diff_dict['allowed']['ZOLZ']
folz = diff_dict['allowed']['FOLZ']
solz = diff_dict['allowed']['SOLZ']
hholz = diff_dict['allowed']['HOLZ_plus']
xy = plotting_coordinates(diff_dict['allowed']['g'][zolz], feature='spot')
kikuchi = plotting_coordinates(diff_dict['allowed']['g'], feature='line')
plt.close('all')
plt.figure()
extent = np.array([-2.7006, 2.8206, -2.15 , 3.01 ])
extent[:2] -= 0.125
extent[2:] -= 0.06
extent /=1.03
plt.imshow(image, extent=extent)
plt.axis('auto')
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha=1, linewidth =2, label='Kikuchi')
for line in kikuchi[zolz]:
plt.axline( (line[0], line[1]),slope=line[2], color='b', alpha=1, linewidth =2)
if folz.sum()>0:
line = (kikuchi[folz])[0]
plt.axline( (line[0], line[1]), slope=line[2], color='r', alpha=0.5, label='FOLZ')
for line in kikuchi[folz]:
plt.axline( (line[0], line[1]), slope=line[2], color='r', alpha = 0.5)
if solz.sum()>0:
line = (kikuchi[solz])[0]
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha=0.5, label='SOLZ')
for line in kikuchi[solz]:
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha = 0.5)
if hholz.sum()>0:
line = (kikuchi[hholz])[0]
plt.axline( (line[0], line[1]), slope=line[2], color='g', alpha=0.2, label='HHOLZ')
for line in kikuchi[hholz]:
plt.axline( (line[0], line[1]), slope=line[2], color='g', alpha = 0.2)
plt.legend()
plt.axis('equal')
extentOf the 29790 tested reciprocal lattice points 421 have an excitation error less than 0.30 1/nm
Of the 421 possible reflection 79 are allowed.
Of those, there are 4 in ZOLZ and 75 in HOLZ
Of the 46 forbidden reflection in ZOLZ 0 can be dynamically activated.
array([-2.74330097, 2.61708738, -2.14563107, 2.86407767])Calculation and Plotting with pyTEMlib¶
The parameters controlling the plotting output are defined in the ‘output’ subdictionory.
Default values are applied.
tags = {'acceleration_voltage_V': 99.2 * 1000.0, # V
'convergence_angle_mrad': 7.15, # mrad;
'zone_hkl': np.array([1, 2, -2]),
'mistilt': np.array([0, 0, 0]), # mistilt in degrees
'Sg_max': .03, # 1/A maximum allowed excitation error
'hkl_max': 15} # Highest evaluated Miller indices
diff_dict = {}
diff_dict = pyTEMlib.diffraction_tools.get_bragg_reflections(atoms, tags, verbose=True)
diff_dict['output'] = {}
diff_dict['output']['plot_HOLZ'] = True
diff_dict['output']['plot_Kikuchi'] = True
fig = pyTEMlib.diffraction_tools.plot_diffraction_pattern(diff_dict, diffraction_pattern=None)Of the 29790 tested reciprocal lattice points 373 have an excitation error less than 0.30 1/nm
Of the 373 possible reflection 69 are allowed.
Of those, there are 4 in ZOLZ and 65 in HOLZ
Of the 38 forbidden reflection in ZOLZ 0 can be dynamically activated.
Conclusion¶
The scattering geometry provides all the tools to determine which reciprocal lattice points are possible and which of them are allowed.
The diffraction pattern is a projection onto the plane perpendicular to the zone axis. For an easy projection we tilt everything so that the x,y plane is our projection plane.
Determination of Bragg reflections in polar coordinates allows for easy rotation in plane and calculation of Kikuchi and HOLZ lines.
plt.close('all')