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.

Lattice Determination with HOLZ

Chapter 2: Diffraction


Lattice Determination with HOLZ

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

Load relevant python packages

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 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: >
Loading...

Load Diffraction Pattern

image = plt.imread("images/Zuo-HOLZ-experiment.jpg")
plt.figure()
plt.imshow(image);
Loading...

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

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 indices

Plotting 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.
Loading...
# -----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.
Loading...

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

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.