Chapter 2: Diffraction
Lattice Determination with HOLZ¶
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
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 pyTEMlib -q --upgrade
print('done')done
Import numerical and plotting python packages¶
Import the python packages that we will use:
Beside the basic numerical (numpy) and plotting (pylab of matplotlib) libraries,
and some libraries from the book
kinematic scattering library.
file_tools library
%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 pyTEMlib
import pyTEMlib
__notebook_version__ = '2026.01.12'
print('pyTEM version: ', pyTEMlib.__version__)
print('notebook version: ', __notebook_version__)pyTEM version: 0.2026.1.0
notebook version: 2026.01.12
Define crystal¶
### Please choose another crystal like: Silicon, Aluminium, GaAs , ZnO
atoms = pyTEMlib.crystal_tools.structure_by_name('silicon')
Plot the unit cell¶
## Just to be sure the crystal structure is right
from ase.visualize.plot import plot_atoms
plot_atoms(atoms, radii=0.3, rotation=('0x,4y,0z'))<Axes: >Load Diffraction Pattern¶
image = plt.imread("images/Zuo-HOLZ-experiment.jpg")
plt.figure()
plt.imshow(image);Parameters for Diffraction Calculation¶
Please note that we are using a rather small number of reflections: the maximum number of hkl is 1
tags = {'acceleration_voltage': 99.2 * 1000.0, # V
'convergence_angle': 9.15, # mrad;
'zone_hkl': np.array([1, 2, -2]),
'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 = pyTEMlib.diffraction_tools.plotting_coordinates(diff_dict['allowed']['g'][ZOLZ], feature='spot')
kikuchi = pyTEMlib.diffraction_tools.plotting_coordinates(diff_dict['allowed']['g'], rotation=np.pi/2, feature='HOLZ')
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 158 possible reflection 158 are allowed.
Of those, there are 50 in ZOLZ and 108 in HOLZ
Of the 0 forbidden reflection in ZOLZ 0 can be dynamically activated.
Initial Overlay¶
tags = {'acceleration_voltage_V': 99.2 * 1000.0, # V
'convergence_angle_mrad': 7.15, # mrad;
'zone_hkl': np.array([1, 2, -2]),
'Sg_max': .03, # 1/A maximum allowed excitation error
'hkl_max': 9} # Highest evaluated Miller indicesPlotting with mistilt included and objective stigmation compensated¶
# -----Input----
O_stig = 1.07
# --------------
tags = {}
tags['acceleration_voltage'] = 99.2*1000.0 #V
tags['zone_hkl'] = np.array([-2,2,1]) # incident neares zone axis: defines Laue Zones!!!!
tags['mistilt'] = np.array([-0.05,-0.05,-0.03]) # mistilt in degrees
tags['Sg_max'] = .02 # 1/nm maximum allowed excitation error ; This parameter is related to the thickness
tags['hkl_max'] = 9 # Highest evaluated Miller indices
tags['convergence_angle'] = 5 # mrad
tags = {'acceleration_voltage': 99.2 * 1000.0, # V
'convergence_angle': 7.15, # mrad;
'zone_hkl': np.array([1, 2, -2]),
'Sg_max': .03, # 1/A maximum allowed excitation error
'hkl_max': 7} # Highest evaluated Miller indices
tagsD ={}
tagsD = pyTEMlib.diffraction_tools.get_bragg_reflections(atoms, tags, verbose=True)
ZOLZ = tagsD['allowed']['ZOLZ']
HOLZ = tagsD['allowed']['HOLZ']
xy = pyTEMlib.diffraction_tools.plotting_coordinates(tagsD['allowed']['g'][ZOLZ], rotation=np.pi/2, feature='spot')
holz = pyTEMlib.diffraction_tools.plotting_coordinates(tagsD['allowed']['g'], rotation=np.pi/2, feature='HOLZ')
# we sort them by order of Laue zone
extent = np.array([-2.7006, 2.8206, -2.15 , 3.01 ])
extent[:2] -= 0.125
extent[2:] -= 0.06
extent /=1.03
plt.figure()#
plt.imshow(image, extent=extent)
if HOLZ.sum()>0:
line = (holz[HOLZ])[0]
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha=0.2, label='SOLZ')
for line in holz[HOLZ]:
plt.axline( (line[0], line[1]), slope=line[2], color='r', alpha = .4, linewidth=3)
zero_disk = plt.Circle((0, 0), tags['convergence_angle'],
color='r', fill=False)
plt.scatter([0],[0],c='red')
plt.gca().add_artist(zero_disk)
s = 2
plt.gca().set_xlim(-s,s)
plt.gca().set_ylim(-s,s);Of the 105 possible reflection 105 are allowed.
Of those, there are 50 in ZOLZ and 55 in HOLZ
Of the 0 forbidden reflection in ZOLZ 0 can be dynamically activated.
# -----Input----
Sg_max = .02 # 1/nm maximum allowed excitation error ; This parameter is related to the thickness
maximum_hkl = 7 # Highest evaluated Miller indices
# --------------
O_stig = 1.07
tags['acceleration_voltage'] = 99.2*1000.0 #V
tags['zone_hkl'] = np.array([1,2,-2]) # incident neares zone axis: defines Laue Zones!!!!
tags['mistilt_alpha'] = -0.00 # mistilt in radians
tags['mistilt_beta'] = -.000# -0.07
tags['Sg_max'] = Sg_max # 1/nm maximum allowed excitation error ; This parameter is related to the thickness
tags['hkl_max'] = maximum_hkl # Highest evaluated Miller indices
tags['convergence_angle_A-1'] = 19
tagsD ={}
tagsD = pyTEMlib.diffraction_tools.get_bragg_reflections(atoms, tags, verbose=True)
tagsD['convergence_angle_A-1'] = 1.9
tagsD['plot image FOV'] = .516
tagsD['plot shift x'] = 0.006#-0.01
tagsD['plot shift y'] = .043#-.03
# we sort them by order of Laue zone
ZOLZ = tagsD['allowed']['ZOLZ']
HOLZ = tagsD['allowed']['HOLZ']
xy = pyTEMlib.diffraction_tools.plotting_coordinates(tagsD['allowed']['g'][ZOLZ], rotation=np.pi/2, feature='spot')
holz = pyTEMlib.diffraction_tools.plotting_coordinates(tagsD['allowed']['g'], rotation=np.pi/2, feature='HOLZ')
kikuchi = pyTEMlib.diffraction_tools.plotting_coordinates(tagsD['Kikuchi']['g'], rotation=np.pi/2, feature='Kikuchi')
l = -tagsD['plot image FOV']/2*O_stig + tagsD['plot shift x']
r = tagsD['plot image FOV']/2*O_stig + tagsD['plot shift x']
t = -tagsD['plot image FOV']/2+tagsD['plot shift y']
b = tagsD['plot image FOV']/2+tagsD['plot shift y']
plt.figure()
plt.imshow(image, extent=np.array([r,l,t,b])*10)
line = (kikuchi)[0]
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha=0.2, label='Kikuchi')
for line in kikuchi:
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha = .4, linewidth=3)
if HOLZ.sum()>0:
line = (holz[HOLZ])[0]
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha=0.2, label='HOLZ')
for line in holz[HOLZ]:
plt.axline( (line[0], line[1]), slope=line[2], color='r', alpha = .2, linewidth=3)
zero_disk = plt.Circle((tagsD['plot shift x']*2.1, tagsD['plot shift y']*0.7), tagsD['convergence_angle_A-1'],
color='r', fill=False)
plt.scatter([0],[0],c='red')
plt.gca().add_artist(zero_disk)
s = 2.2
plt.gca().set_xlim(-s,s)
plt.gca().set_ylim(-s,s);
Of the 66 possible reflection 66 are allowed.
Of those, there are 32 in ZOLZ and 34 in HOLZ
Of the 0 forbidden reflection in ZOLZ 0 can be dynamically activated.
Plotting more HOLZ lines with intensity¶
In the image above, all major lines are reproduced.
On the bottom of the HOLZ plot, however, the faint lines are not there.
Increase the maximum hkl in the simulation to see how well we did
Influence of unit cell deformation¶
Introduce a bit of distortion in the unit cell and see what happens
# -----Input----
atoms = pyTEMlib.crystal_tools.structure_by_name('silicon')
atoms.cell[0,0] += 0.00
atoms.cell[1,1] +=0.00
atoms.cell[2,1] += .004
# --------------
tags['acceleration_voltage'] = 99.2*1000.0 #V
tags['zone_hkl'] = np.array([1,2,-2]) # incident neares zone axis: defines Laue Zones!!!!
tags['mistilt_alpha'] = -0.00 # mistilt in radians
tags['mistilt_beta'] = -.000# -0.07
tags['Sg_max'] = Sg_max # 1/nm maximum allowed excitation error ; This parameter is related to the thickness
tags['hkl_max'] = maximum_hkl # Highest evaluated Miller indices
tags['convergence_angle_A-1'] = 19
tagsD ={}
tagsD = pyTEMlib.diffraction_tools.get_bragg_reflections(atoms, tags, verbose=True)
tagsD['convergence_angle_A-1'] = 1.9
tagsD['plot image FOV'] = .516
tagsD['plot shift x'] = 0.006#-0.01
tagsD['plot shift y'] = .043#-.03
# we sort them by order of Laue zone
ZOLZ = tagsD['allowed']['ZOLZ']
HOLZ = tagsD['allowed']['HOLZ']
xy = pyTEMlib.diffraction_tools.plotting_coordinates(tagsD['allowed']['g'][ZOLZ], rotation=np.pi/2, feature='spot')
holz = pyTEMlib.diffraction_tools.plotting_coordinates(tagsD['allowed']['g'], rotation=np.pi/2, feature='HOLZ')
kikuchi = pyTEMlib.diffraction_tools.plotting_coordinates(tagsD['Kikuchi']['g'], rotation=np.pi/2, feature='Kikuchi')
l = -tagsD['plot image FOV']/2*O_stig + tagsD['plot shift x']
r = tagsD['plot image FOV']/2*O_stig + tagsD['plot shift x']
t = -tagsD['plot image FOV']/2+tagsD['plot shift y']
b = tagsD['plot image FOV']/2+tagsD['plot shift y']
plt.figure()
plt.imshow(image, extent=np.array([r,l,t,b])*10)
line = (kikuchi)[0]
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha=0.2, label='Kikuchi')
for line in kikuchi:
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha = .4, linewidth=3)
if HOLZ.sum()>0:
line = (holz[HOLZ])[0]
plt.axline( (line[0], line[1]), slope=line[2], color='b', alpha=0.2, label='HOLZ')
for line in holz[HOLZ]:
plt.axline( (line[0], line[1]), slope=line[2], color='r', alpha = .2, linewidth=3)
zero_disk = plt.Circle((tagsD['plot shift x']*2.1, tagsD['plot shift y']*0.7), tagsD['convergence_angle_A-1'],
color='r', fill=False)
plt.scatter([0],[0],c='red')
plt.gca().add_artist(zero_disk)
s = 2.2
plt.gca().set_xlim(-s,s)
plt.gca().set_ylim(-s,s);
Of the 67 possible reflection 67 are allowed.
Of those, there are 33 in ZOLZ and 34 in HOLZ
Of the 0 forbidden reflection in ZOLZ 0 can be dynamically activated.
Conclusion¶
Due to the high angles involved in the scattering but measured at low angles, HOLZ lines provide a very sensitive measurement of experimental and materials parameter.
Navigation¶
Back: HOLZ Lines
Chapter 2: Diffraction
List of Content: Front