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.

HOLZ Lines

Chapter 2: Diffraction


HOLZ Lines

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.

%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.11'
print('pyTEM version: ', pyTEMlib.__version__)
print('notebook version: ', __notebook_version__)
pyTEM version:  0.2026.1.0
notebook version:  2026.01.11

Define crystal

### Please choose another crystal like: Silicon, Aluminium, GaAs , ZnO
atoms = pyTEMlib.crystal_tools.structure_by_name('silicon')
atoms
Lattice(symbols='Si8', pbc=True, cell=[5.43088, 5.43088, 5.43088])

Plot the unit cell

from ase.visualize.plot import plot_atoms

plot_atoms(atoms, radii=0.3, rotation=('0x,4y,0z'))
<Axes: >
Loading...

Parameters for Diffraction Calculation

tags = {}

tags['acceleration_voltage_V'] = 99 *1000.0 #V
tags['convergence_angle_mrad'] = 0
tags['zone_hkl'] = np.array([2,2,1])  # incident neares zone axis: defines Laue Zones!!!!

tags['Sg_max'] = .01 # 1/Ang  maximum allowed excitation error ; This parameter is related to the thickness
tags['hkl_max'] = 36   # Highest evaluated Miller indices

Kinematic Scattering Calculation

diff_dict = pyTEMlib.diffraction_tools.get_bragg_reflections(atoms, tags, verbose=True)
Of the 724 possible reflection 723 are allowed.
Of those, there are 12 in ZOLZ  and 711 in HOLZ
Of the 0 forbidden reflection in ZOLZ  0 can be dynamically activated.

Plot Selected Area Electron Diffraction Pattern

#####################
# Plot ZOLZ SAED Pattern #
#####################

#We plot only the allowed diffraction spots
g = diff_dict['allowed']['g']
# we sort them by order of Laue zone
ZOLZ = diff_dict['allowed']['ZOLZ']
HOLZ = diff_dict['allowed']['HOLZ']

rotation = 0
x = g[:, 0] * np.cos(g[:, 1]+np.pi+rotation)*10
y = g[:, 0] * np.sin(g[:, 1]+np.pi+rotation)*10
slope = np.tan(g[:, 1]+rotation-np.pi/2)
# Plot
fig = plt.figure()
ax = fig.add_subplot(111)
# We plot the x,y axis only; the z -direction is set to zero - this is our projection
ax.scatter(x[ZOLZ], y[ZOLZ], c='red', s=20)
ax.scatter(x[HOLZ], y[HOLZ], c='green', s=20)

# zero spot plotting
ax.scatter(0,0, c='red', s=100)
ax.scatter(0,0, c='white', s=40)

ax.set_aspect('equal')
FOV = 16
plt.ylim(-FOV,FOV); plt.xlim(-FOV,FOV); plt.show()
Loading...
import pyTEMlib.animation  as animate

plt.figure()
animate.deficient_holz_line(exact_bragg=False, laue_zone=1)
animate.deficient_holz_line(exact_bragg='True', laue_zone=1, color='blue')
animate.deficient_holz_line(exact_bragg='True', laue_zone=1, color='red', shift=True)
Loading...

HOLZ Line Construction

Position of deficient HOLZ line

What is dθd\theta:

No such environment: eqnarray* at position 7: \begin{̲e̲q̲n̲a̲r̲r̲a̲y̲*̲}̲		
    d\theta …

\begin{eqnarray*}		
    d\theta &+&(90-\theta_B)+ \varphi =  90\\
    d\theta  &=&  \theta_B -\varphi\\
    &&\\
    \sin(\theta_B) &=&|\vec{g}/2|/ |\vec{K}_0| \\
    \tan(\varphi) &=& |\vec{g}_{HOLZ}|/|\vec{g}-\vec{g}_{HOLZ}|\\
    &&\\
    |\vec{g}_{deficient}| &=& 2\sin(  d\theta/ 2 )* |\vec{K}_0|\\
\end{eqnarray*}

For exact Bragg position in ZOLZ

dθ=θBd\theta = \theta_B then gdeficient=g/2\vec{g}_{deficient} = -\vec{g}/2

dθ=θφd\theta = \theta - \varphi

with:

sin(θ)=g/2K0\sin(\theta) =\frac{|\vec{g}|/2}{|\vec{K}_0| }

sin(φ)=gHOLZg\sin(\varphi) = \frac{|\vec{g}_{\rm HOLZ}|}{|\vec{g}|}

Because dθd\theta is the same as dφd \varphi we can now calculate the deficient HOLZ lines

No such environment: eqnarray* at position 7: \begin{̲e̲q̲n̲a̲r̲r̲a̲y̲*̲}̲
				\vec{K}_{B…

\begin{eqnarray*}
				\vec{K}_{Bg} &=& ( (\vec{g}-\vec{g}_{HOLZ})/ |\vec{g}-\vec{g}_{HOLZ}|*|\vec{g}_{HOLZ}| \\
				&& + \vec{K}_{0}/|\vec{K}_{0}|*|\vec{g}-\vec{g}_{HOLZ}|)\\
				\vec{K}_{Bg} &=&(\vec{g}_{ZOLZ}/|\vec{g}_{ZOLZ}|*|\vec{g}_{HOLZ}|) \\
				&+&\vec{K}_{0}/|\vec{K}_{0}|*|\vec{g}-\vec{g}_{HOLZ}|)\\
				\vec{K}_{B} &=&	\vec{K}_{Bg}/|\vec{K}_{Bg}|*\sqrt{|\lambda^2-g^2/4|}\\
				\vec{K}_d &=& -(\vec{g}/2+\vec{K}_B)\\
				\vec{g}_{deficient} &=& \vec{K}_{0}-\vec{K}_{d}\\
\end{eqnarray*}

For exact Bragg position in ZOLZ KB=K0\vec{K}_B=\vec{K}_{0} then gdeficient=g/2\vec{g}_{deficient} = \vec{g}/2

This is our Kikuchi line equation

#Calculate angle between K0 and deficient cone vector
#For dynamic calculations K0 is replaced by Kg
K0 = diff_dict['K_0']
g_allowed = diff_dict['allowed']['g']
g_norm_allowed = g_allowed[:, 0]
dtheta = np.arcsin(g_norm_allowed/K0/2.)-np.arcsin(np.abs(g_allowed[:,2])/g_norm_allowed)

#calculate length of distance of deficient cone to K0 in ZOLZ plane

gd_length =2*np.sin((dtheta)/2 )*K0

#Calculate nearest point of HOLZ and Kikuchi lines

gd = g_allowed.copy()
gd[:,0] = -gd[:,0]*gd_length/g_norm_allowed
gd[:,1] = -gd[:,1]*gd_length/g_norm_allowed
gd[:,2] = 0.

###calculate and save line in Hough space coordinates (distance and theta)

slope = gd[:,0]/(gd[:,1]+1e-20)

distance = gd_length

theta = np.arctan(slope)

Now everything together in a single cell

We change the lattice parameter (Vegard’s law of alloys) by a few pm and observe the effect on the pattern.

# ----- Input -----------
unit_cell_change_pm = -1.0
# -----------------------

atoms = pyTEMlib.crystal_tools.structure_by_name('Silicon')
cell = atoms.cell.lengths()
atoms.set_cell(cell+unit_cell_change_pm/100, scale_atoms=True)
atoms

tags = {'crystal_name': 'silicon',
        'acceleration_voltage_V': 100.8*1000.0, #V
        'convergence_angle_mrad': 5.,
        'Sg_max': .03,   # 1/Ang  maximum allowed excitation error ; This parameter is related to the thickness
        'hkl_max': 9,   # Highest evaluated Miller indices
        'zone_hkl': np.array([1, 2, -2]),  
        'mistilt_alpha degree': 0.,  # -45#-35-0.28-1+2.42
        'mistilt_beta degree': 0.,
        'plot_FOV': .5}

diff_dict ={}
diff_dict = pyTEMlib.diffraction_tools.get_bragg_reflections(atoms, tags, verbose=True) 

diff_dict['output']=pyTEMlib.diffraction_tools.plot_holz_parameter()
diff_dict['output']['plot_reflections']=False
diff_dict['output']['plot_Kikuchi']=False
diff_dict['output']['linewidth_HOLZ'] = -1
diff_dict['output']['plot_HOLZ']=True


pyTEMlib.diffraction_tools.plot_diffraction_pattern(diff_dict)


plt.gca().set_xlim(-2,2)
plt.gca().set_ylim(-2,2)
Of the 156 possible reflection 156 are allowed.
Of those, there are 50 in ZOLZ  and 106 in HOLZ
Of the 0 forbidden reflection in ZOLZ  0 can be dynamically activated.
(-2.0, 2.0)
Loading...
def calculate_holz(dif, tags):
    """ Calculate HOLZ lines (of allowed reflections)"""
    # dif = atoms.info['diffraction']
    intensities = dif['allowed']['intensities']
    k_0 = dif['K_0']
    g_norm_allowed = dif['allowed']['g'][:, 0]

    # Dynamic Correction
    # Equation Spence+Zuo 3.86a
    gamma_1 = - 1./(2.*k_0) * (intensities / (2.*k_0*dif['allowed']['excitation_error'])).sum()

    # Equation Spence+Zuo 3.84
    kg = k_0 - k_0*gamma_1/(dif['allowed']['g'][:, 2]+1e-15)
    kg[dif['allowed']['ZOLZ']] = k_0

    # Calculate angle between K0 and deficient cone vector
    # For dynamic calculations K0 is replaced by kg
    kg[:] = k_0
    d_theta = np.arcsin(g_norm_allowed/kg/2.) - (np.arcsin(np.abs(dif['allowed']['g'][:, 2])
                                                           /g_norm_allowed))

    # calculate length of distance of deficient cone to K0 in ZOLZ plane
    gd_length = 2*np.sin(d_theta/2) * k_0

    # Calculate nearest point of HOLZ and Kikuchi lines
    g_closest = dif['allowed']['g'].copy()
    g_closest = g_closest*(gd_length/np.linalg.norm(g_closest, axis=1))[:, np.newaxis]

    g_closest[:, 2] = 0.

    # calculate and save line in Hough space coordinates (distance and theta)
    slope = g_closest[:, 0]/(g_closest[:, 1]+1e-10)
    distance = gd_length
    theta = dif['allowed']['g'][:, 1]

    dif['HOLZ'] = {}
    dif['HOLZ']['slope'] = slope

    return distance, theta
r, phi = calculate_holz(diff_dict, tags)
x = r * np.cos(phi)*10
y = r * np.sin(phi)*10

slope = np.tan(phi+np.pi/2)
plt.figure()
for i in range(len(x)):
    plt.axline((x[i], y[i]), slope=slope[i], c='r')
plt.gca().set_aspect('equal')
Loading...

Now for graphite and low acceleration voltages.

Change the acceleration voltage and see what happens.

Tip: 60.49keV is especially interesting

### Please choose another crystal like: Silicon, Aluminium, GaAs , ZnO

# ----- Input -----------
crystal_name = 'graphite'
acceleration_voltage =  61.49 * 1000.0
# -----------------------

atoms = pyTEMlib.crystal_tools.structure_by_name(crystal_name)

tags = {'crystal_name': crystal_name,
        'acceleration_voltage': acceleration_voltage, #V
        'convergence_angle': 7.,
        'Sg_max': .03,   # 1/Ang  maximum allowed excitation error ; This parameter is related to the thickness
        'hkl_max': 9,   # Highest evaluated Miller indices
        'zone_hkl': np.array([0, 0, 1]),  
        'mistilt_alpha': .0,  # -45#-35-0.28-1+2.42
        'mistilt_beta': 0.,
        'plot_FOV': .5}

diff_dict ={}
diff_dict = pyTEMlib.diffraction_tools.get_bragg_reflections(atoms, tags, verbose=True) 

diff_dict['output']=pyTEMlib.diffraction_tools.plot_holz_parameter()
diff_dict['output']['plot_reflections']=True
diff_dict['output']['plot_Kikuchi']=False
diff_dict['output']['linewidth_HOLZ'] = 2
diff_dict['output']['plot_HOLZ']=True

pyTEMlib.diffraction_tools.plot_diffraction_pattern(diff_dict)

plt.title(crystal_name + ': ' + str(tags['zone_hkl']))
plt.gca().set_xlim(-1.7, 1.7)
plt.gca().set_ylim(-1.7, 1.7)
plt.gca().set_aspect('equal')
Of the 138 possible reflection 118 are allowed.
Of those, there are 18 in ZOLZ  and 100 in HOLZ
Of the 0 forbidden reflection in ZOLZ  0 can be dynamically activated.
Loading...