Module pyms.Probe
Functions for emulating electron optics of a TEM.
Expand source code
"""Functions for emulating electron optics of a TEM."""
import numpy as np
import copy
from .utils.numpy_utils import q_space_array
class aberration:
"""A class describing electron lens aberrations."""
def __init__(self, Krivanek, Haider, Description, amplitude, angle, n, m):
"""
Intialize the lens aberration object.
Parameters
----------
Krivanek : str
A string describing the aberration coefficient in Krivanek notation
(C_mn)
Haider : str
A string describing the aberration coefficient in Haider notation
(ie. A1, A2, B2)
Description : str
A string describing the colloqiual name of the aberration ie. 2-fold
astig.
amplitude : float
The amplitude of the aberration in Angstrom
angle : float
The angle of the aberration in radians
n : int
The principle aberration order
m : int
The rotational order of the aberration.
"""
self.Krivanek = Krivanek
self.Haider = Haider
self.Description = Description
self.amplitude = amplitude
self.m = m
self.n = n
if m > 0:
self.angle = angle
else:
self.angle = 0
def __str__(self):
"""Return a string describing the aberration."""
if self.m > 0:
return (
"{0:17s} ({1:2s}) -- {2:3s} = {3:9.2e} \u00E5 \u03B8 = "
+ "{4:4d}\u00B0 "
).format(
self.Description,
self.Haider,
self.Krivanek,
self.amplitude,
int(np.rad2deg(self.angle)),
)
else:
return " {0:17s} ({1:2s}) -- {2:3s} = {3:9.2e} \u00E5".format(
self.Description, self.Haider, self.Krivanek, self.amplitude
)
def depth_of_field(eV, alpha):
"""
Calculate the probe depth of field (z-resolution) for a probe.
Parameters
----------
eV : float
Probe accelerating voltage in electron volts
alpha : float
Probe forming semi-angle in mrad
Returns
-------
dof : float
The probe full-width-at-half-maximum (FWHM) depth of field
"""
return 1.77 / wavev(eV) / alpha / alpha * 1e6
def aberration_starter_pack():
"""Create the set of aberrations up to fifth order."""
aberrations = []
aberrations.append(aberration("C10", "C1", "Defocus ", 0.0, 0.0, 1, 0))
aberrations.append(aberration("C12", "A1", "2-Fold astig. ", 0.0, 0.0, 1, 2))
aberrations.append(aberration("C23", "A2", "3-Fold astig. ", 0.0, 0.0, 2, 3))
aberrations.append(aberration("C21", "B2", "Axial coma ", 0.0, 0.0, 2, 1))
aberrations.append(aberration("C30", "C3", "3rd order spher. ", 0.0, 0.0, 3, 0))
aberrations.append(aberration("C34", "A3", "4-Fold astig. ", 0.0, 0.0, 3, 4))
aberrations.append(aberration("C32", "S3", "Axial star aber. ", 0.0, 0.0, 3, 2))
aberrations.append(aberration("C45", "A4", "5-Fold astig. ", 0.0, 0.0, 4, 5))
aberrations.append(aberration("C43", "D4", "3-Lobe aberr. ", 0.0, 0.0, 4, 3))
aberrations.append(aberration("C41", "B4", "4th order coma ", 0.0, 0.0, 4, 1))
aberrations.append(aberration("C50", "C5", "5th order spher. ", 0.0, 0.0, 5, 0))
aberrations.append(aberration("C56", "A5", "6-Fold astig. ", 0.0, 0.0, 5, 6))
aberrations.append(aberration("C52", "S5", "5th order star ", 0.0, 0.0, 5, 2))
aberrations.append(aberration("C54", "R5", "5th order rosette", 0.0, 0.0, 5, 4))
return aberrations
def chi(q, qphi, lam, df=0.0, aberrations=[]):
r"""
Calculate the aberration function, chi.
Parameters
----------
q : float or array_like
Reciprocal space extent (Inverse angstroms).
qphi : float or array_like
Azimuth of grid in radians
lam : float
Wavelength of electron (Inverse angstroms).
df : float, optional
Defocus in Angstrom
aberrations : list, optional
A list containing a set of the class aberration, pass an empty list for
an unaberrated contrast transfer function.
Returns
-------
chi : float or array_like
The aberration function, will be the same shape as `q`. This is used to
calculate the probe wave function in reciprocal space.
"""
qlam = q * lam
chi_ = qlam ** 2 / 2 * df
for ab in aberrations:
chi_ += (
qlam ** (ab.n + 1)
* float(ab.amplitude)
/ (ab.n + 1)
* np.cos(ab.m * (qphi - float(ab.angle)))
)
return 2 * np.pi * chi_ / lam
def make_contrast_transfer_function(
pix_dim,
real_dim,
eV,
app,
optic_axis=[0, 0],
aperture_shift=[0, 0],
tilt_units="mrad",
df=0,
aberrations=[],
q=None,
app_units="mrad",
):
"""
Make an electron lens contrast transfer function.
Parameters
---------
pix_dim : (2,) int array_like
The pixel size of the grid
real_dim : (2,) float array_like
The size of the grid in Angstrom
eV : float
The energy of the probe electrons in eV
app : float or None
The aperture in units specified by app_units, pass `app` = None for
no aperture
optic_axis : (2,) array_like, optional
allows the user to specify a different optic axis in units specified by
`tilt_units`
aperture_shift : (2,) array_like, optional
Shift of the objective aperture relative to the center of the array
tilt_units : string
Units of the `optic_axis` or `aperture_shift` values, default is mrad
df : float
Probe defocus in A, a negative value indicate overfocus
aberrations : array_like of aberration objects
List containing instances of class aberration
q :
Precomputed reciprocal space array, allows the user to reduce
computation time somewhat
app_units : string
The units of `app` (A^-1 or mrad)
Returns
-------
ctf : array_like
The lens contrast transfer function in reciprocal space
"""
# Make reciprocal space array
if q is None:
q = q_space_array(pix_dim, real_dim[:2])
# Get electron wave number (inverse of wavelength)
k = wavev(eV)
# Convert tilts to units of inverse Angstrom
optic_axis_ = convert_tilt_angles(
optic_axis, tilt_units, real_dim, eV, invA_out=True
)
aperture_shift_ = convert_tilt_angles(
aperture_shift, tilt_units, real_dim, eV, invA_out=True
)
if app is None:
app_ = np.amax(np.abs(q))
else:
# Get aperture size in units of inverse Angstrom
app_ = convert_tilt_angles(app, app_units, real_dim, eV, invA_out=True)
# Initialize the array to contain the CTF
CTF = np.zeros(pix_dim, dtype=np.complex)
# Calculate the magnitude of the reciprocal lattice grid
# qarray1 accounts for a shift of the optic axis
qarray1 = np.sqrt(
np.square(q[0] - optic_axis_[0]) + np.square(q[1] - optic_axis_[1])
)
# qarray2 accounts for a shift of both the optic axis and the aperture
qarray2 = np.square(q[0] - optic_axis_[0] - aperture_shift_[0]) + np.square(
q[1] - optic_axis_[1] - aperture_shift_[1]
)
# Calculate azimuth of reciprocal space array in case it is required for
# aberrations
qphi = np.arctan2(q[0] - optic_axis_[0], q[1] - optic_axis_[1])
# Only calculate CTF for region within the aperture
mask = qarray2 <= app_ ** 2
CTF[mask] = np.exp(-1j * chi(qarray1[mask], qphi[mask], 1.0 / k, df, aberrations))
return CTF
def focused_probe(
gridshape,
rsize,
eV,
app,
beam_tilt=[0, 0],
aperture_shift=[0, 0],
tilt_units="mrad",
df=0,
aberrations=[],
q=None,
app_units="mrad",
qspace=False,
):
"""
Make a focused electron probe wave function.
Parameters
---------
gridshape : (2,) array_like
The pixel size of the grid
rsize : (2,) array_like
The size of the grid in Angstrom
eV : float
The energy of the probe electrons in electron volts
app : float
The probe-forming apperture in units specified by app_units, pass None
if no probe forming aperture is to be used
beam_tilt : array_like, optional
Allows the user to simulate a (small < 50 mrad) beam tilt. To maintain
periodicity of the wave function at the boundaries this tilt is rounded
to the nearest pixel value.
aperture_shift : array_like, optional
Allows the user to simulate a (small < 50 mrad) aperture shift. To
maintain periodicity of the wave function at the boundaries this tilt
is rounded to the nearest pixel value.
tilt_units : string, optional
Units of beam tilt and aperture shift, can be 'mrad','pixels' or 'invA'
df : float, optional
Probe defocus in A, a negative value indicate overfocus
aberrations : list, optional
A list of of probe aberrations of class pyms.Probe.aberration, pass an
empty list for an un-aberrated probe
app_units : string, optional
The units of the aperture size ("invA", "pixels" or "mrad")
qspace : bool, optional
If True return the probe in reciprocal space
Returns
-------
probe : complex (Y,X) np.ndarray
The requested electron probe wave function
"""
probe = make_contrast_transfer_function(
gridshape,
rsize,
eV,
app,
beam_tilt,
aperture_shift,
tilt_units,
df,
aberrations,
q,
app_units,
)
# Normalize the STEM probe so that its sum-squared intensity is unity
probe *= np.sqrt(np.prod(gridshape)) / np.sqrt(np.sum(np.square(np.abs(probe))))
# Return real or diffraction space probe depending on user preference
if not qspace:
return np.fft.ifft2(probe)
return probe
def plane_wave_illumination(
gridshape, gridsize, eV, tilt=[0, 0], tilt_units="mrad", qspace=False
):
"""
Generate plane wave illumination for input to multislice.
The wave function will be normalized such that sum of intensity is unity in
real space.
Parameters
----------
gridshape : (2,) array_like
Pixel dimensions of the 2D grid
gridsize : (2,) array_like
Size of the grid in real space
eV : float
Probe energy in electron volts (irrelevant for untilted illumination)
tilt : array_like, optional
Allows the user to simulate a (small < 50 mrad) beam tilt, To maintain
periodicity of the wave function at the boundaries this tilt is rounded
to the nearest pixel value.
tilt_units : string, optional
Units of beam tilt, can be 'mrad','pixels' or 'invA'
qspace : bool, optional
Pass qspace = True to get the probe in momentum (q) space
Returns
------
illum : np.ndarray (Y,X)
"""
# Initialize array that contains wave function
illum = np.zeros(gridshape, dtype=np.complex)
# Convert tilt to units of pixels
tilt_ = convert_tilt_angles(tilt, tilt_units, gridsize, eV)
# Case of an untilted plane wave (phase is zero everywhere)
if tilt[0] == 0 and tilt[1] == 0:
illum[:, :] = 1 / np.sqrt(np.product(gridshape))
if qspace:
return np.fft.fft2(illum)
else:
return illum
# Set the value of wavefunction amplitude such that after inverse Fourier
# transform (and resulting division by the total number of pixels) the sum
# of intensity will be 1
illum[tilt_[0], tilt_[1]] = np.sqrt(np.product(gridshape))
# Return wave function in real space
if qspace:
return illum
else:
return np.fft.ifft2(illum)
def wavev(E):
"""
Evaluate the relativistically corrected wavenumber of an electron with energy E.
Energy E must be in electron-volts, see Eq. (2.5) in Kirkland's Advanced
Computing in electron microscopy
"""
# Planck's constant times speed of light in eV Angstrom
hc = 1.23984193e4
# Electron rest mass in eV
m0c2 = 5.109989461e5
return np.sqrt(E * (E + 2 * m0c2)) / hc
def relativistic_mass_correction(E):
"""
Evaluate the relativistic mass correction for electron with energy E in eV.
See Eq. (2.2) in Kirkland's Advanced Computing in electron microscopy.
"""
# Electron rest mass in eV
m0c2 = 5.109989461e5
return (m0c2 + E) / m0c2
def simulation_result_with_Cc(
func, Cc, deltaE, eV, args=[], kwargs={}, npoints=7, deltaEconv="1/e"
):
"""
Perform a simulation using function, taking into account chromatic aberration.
Pass in the function that simulates the multislice result, it is assumed
that defocus is a variable named 'df' somewhere in the keyword argument
list.
Parameters
----------
func : function
Function that simulates the result of interest, ie. pyms.HRTEM. The
defocus must be present in the keyword argument list as 'df'
Cc : float
Chromatic aberration coefficient in Angstroms
deltaE : float
Energy spread in electron volts using 1/e measure of spread (to convert
from FWHM divide by 1.655, and divide by sqrt(2) to convert from
standard deviation )
eV : float
(Mean) beam energy in electron volts
args : list, optional
Arguments for the method function used to propagate probes to the exit
surface
kwargs : Dict, optional
Keyword arguments for the method function used to propagate probes to
the exit surface
npoints : int,optional
Number of integration points in the Cc numerical integration
Returns
-------
average : dict or array_like
The simulation requested but averaged over the different defocus values
to account for chromatic aberration.
"""
# Check if a defocii has already been specified if not, assume that nominal
# (mean) defocus is 0
if "df" in kwargs.keys():
nominal_df = kwargs["df"]
else:
nominal_df = 0
# Get defocii to integrate over
defocii = Cc_integration_points(Cc, deltaE, eV, npoints, deltaEconv) + nominal_df
ndf = len(defocii)
# Initialise the average result to None
average = None
# Now integrate the function over those defocus values
for df in defocii:
kwargs["df"] = df
result = func(*args, **kwargs)
# Assume that every function will either return a numpy array
# (eg. pyms.HRTEM) or a list of numpy arrays (eg. pyms.STEM with both
# conventional and 4D-STEM options) so average these results
if isinstance(result, np.ndarray):
if average is None:
average = result / ndf
else:
average += result / ndf
elif isinstance(result, dict):
if average is None:
average = copy.deepcopy(result)
for key in average.keys():
if average[key] is not None:
average[key] /= ndf
else:
for key in average.keys():
if average[key] is not None:
average[key] += result[key] / ndf
else:
if average is None:
average = [x / ndf for x in result]
else:
average = [x / ndf + y for x, y in zip(result, average)]
return average
def Cc_integration_points(Cc, deltaE, eV, npoints=7, deltaEconv="1/e"):
"""
Calculate the defocus integration points for simulating chromatic aberration.
The integration points are selected by dividing the assumed gaussian defocus
spread into npoints regions of equal probability, then finding the mean
defocus in each of those regions.
Parameters
----------
Cc : float
Chromatic aberration coefficient in Angstroms
deltaE : float
Energy spread in electron volts using 1/e measure of spread (to convert
from FWHM divide by 1.655, and divide by sqrt(2) to convert from
standard deviation )
eV : float
(Mean) beam energy in electron volts
npoints : int,optional
Number of integration points in the Cc numerical integration
deltaEconv : float,optional
The convention for deltaE, the energy spread, acceptable inputs are '1/e'
the energy point that the probability density function drops to 1/e times
its maximum value, 'std' for standard deviation and 'FWHM' for the full
width at half maximum of the energy spread.
Returns
-------
defocii : (`npoints`,) array_like
The defocus integration points
"""
# Import the error function (integral of Gaussian) and inverse error function
# from scipy's special functions library
from scipy.special import erfinv, erf
# First divide the gaussian pdf into npoints different regions of equal
# "area"
partitions = erfinv(2 * (np.arange(npoints - 1) + 1) / npoints - 1)
# Now calculate the mean (expectation value) within each partition
x = np.zeros(npoints)
prefactor = 1 / (2 * np.sqrt(np.pi))
x[0] = -prefactor * np.exp(-partitions[0] ** 2) / (1 + erf(partitions[0])) * 2
x[1:-1] = (
prefactor
* (np.exp(-partitions[:-1] ** 2) - np.exp(-partitions[1:] ** 2))
/ (erf(partitions[1:]) - erf(partitions[:-1]))
* 2
)
x[-1] = prefactor * np.exp(-partitions[-1] ** 2) / (1 - erf(partitions[-1])) * 2
# Multiply by 1/e spread of defocus values
return x * Cc * convert_deltaE(deltaE, deltaEconv) / eV
def Cc_defocus_spread(df, Cc, deltaE, eV, deltaEconv):
"""
Calculate the defocus spread for chromatic aberration.
Evaluates the probability density function at defocus df for the defocus
spread of a chromatic aberration (Cc) for (1/e) energy spread deltaE and
beam energy eV in electron volts.
Parameters
----------
df : float or array_like
defocus or defocii at which to evaluate the probability density function
Cc : float
Chromatic aberration coefficient in Angstroms
deltaE : float
Energy spread in electron volts using the measure given by deltaEconv
(1/e measure of spread is default)
eV : float
(Mean) beam energy in electron volts
deltaEconv : string,optional
The convention for deltaE, the energy spread, acceptable inputs are '1/e'
the energy point that the probability density function drops to 1/e times
its maximum value, 'std' for standard deviation and 'FWHM' for the full
width at half maximum of the energy spread
Returns
-------
Cc_pdf : float or array_like
the probability density function, will be the same size and shape as `df`
"""
# Calculate defocus spread
df_spread = Cc * convert_deltaE(deltaE, deltaEconv) / eV
# Evaluate probability density function for given defocus df
return np.exp(-df * df / df_spread / df_spread) / np.sqrt(np.pi) / df_spread
def convert_deltaE(deltaE, deltaEconv):
"""
Convert the energy spread input to a 1/e spread.
Parameters
----------
deltaE : float
Energy spread in electron volts using the measure given by deltaEconv
deltaEconv : string
The convention for deltaE, the energy spread, acceptable inputs are '1/e'
the energy point that the probability density function drops to 1/e times
its maximum value, 'std' for standard deviation and 'FWHM' for the full
width at half maximum of the energy spread
"""
if deltaEconv == "1/e":
return deltaE
elif deltaEconv == "FWHM":
return deltaE / 2 / np.sqrt(np.log(2))
elif deltaEconv == "std":
return deltaE * np.sqrt(2)
else:
raise AttributeError(
"detlaEconv "
+ deltaEconv
+ " input not recognized, needs to be one of '1/e', 'FWHM' or 'std'"
)
def convert_tilt_angles(tilt, tilt_units, rsize, eV, invA_out=False):
"""
Convert tilt to pixel or inverse Angstroms units regardless of input units.
Input units can be mrad, pixels or inverse Angstrom
Parameters
----------
tilt : array_like
Tilt in units of mrad, pixels or inverse Angstrom
tilt_units : string
Units of specimen and beam tilt, can be 'mrad','pixels' or 'invA'
rsize : (2,) array_like
The size of the grid in Angstrom
eV : float
Probe energy in electron volts
invA_out : bool
Pass True if inverse Angstrom units are desired.
"""
# If units of the tilt are given in mrad, convert to inverse Angstrom
if tilt_units == "mrad":
k = wavev(eV)
tilt_ = np.asarray(tilt) * 1e-3 * k
else:
tilt_ = tilt
# If inverse Angstroms are requested our work here is done
if invA_out:
return tilt_
# Convert inverse Angstrom to pixel coordinates, this will be rounded
# to the nearest pixel
if tilt_units != "pixels":
tilt_ = np.round(tilt_ * rsize[:2]).astype(int)
return tilt_
Functions
def Cc_defocus_spread(df, Cc, deltaE, eV, deltaEconv)
-
Calculate the defocus spread for chromatic aberration.
Evaluates the probability density function at defocus df for the defocus spread of a chromatic aberration (Cc) for (1/e) energy spread deltaE and beam energy eV in electron volts.
Parameters
df
:float
orarray_like
- defocus or defocii at which to evaluate the probability density function
Cc
:float
- Chromatic aberration coefficient in Angstroms
deltaE
:float
- Energy spread in electron volts using the measure given by deltaEconv (1/e measure of spread is default)
eV
:float
- (Mean) beam energy in electron volts
deltaEconv
:string
,optional- The convention for deltaE, the energy spread, acceptable inputs are '1/e' the energy point that the probability density function drops to 1/e times its maximum value, 'std' for standard deviation and 'FWHM' for the full width at half maximum of the energy spread
Returns
Cc_pdf
:float
orarray_like
- the probability density function, will be the same size and shape as
df
Expand source code
def Cc_defocus_spread(df, Cc, deltaE, eV, deltaEconv): """ Calculate the defocus spread for chromatic aberration. Evaluates the probability density function at defocus df for the defocus spread of a chromatic aberration (Cc) for (1/e) energy spread deltaE and beam energy eV in electron volts. Parameters ---------- df : float or array_like defocus or defocii at which to evaluate the probability density function Cc : float Chromatic aberration coefficient in Angstroms deltaE : float Energy spread in electron volts using the measure given by deltaEconv (1/e measure of spread is default) eV : float (Mean) beam energy in electron volts deltaEconv : string,optional The convention for deltaE, the energy spread, acceptable inputs are '1/e' the energy point that the probability density function drops to 1/e times its maximum value, 'std' for standard deviation and 'FWHM' for the full width at half maximum of the energy spread Returns ------- Cc_pdf : float or array_like the probability density function, will be the same size and shape as `df` """ # Calculate defocus spread df_spread = Cc * convert_deltaE(deltaE, deltaEconv) / eV # Evaluate probability density function for given defocus df return np.exp(-df * df / df_spread / df_spread) / np.sqrt(np.pi) / df_spread
def Cc_integration_points(Cc, deltaE, eV, npoints=7, deltaEconv='1/e')
-
Calculate the defocus integration points for simulating chromatic aberration.
The integration points are selected by dividing the assumed gaussian defocus spread into npoints regions of equal probability, then finding the mean defocus in each of those regions.
Parameters
Cc
:float
- Chromatic aberration coefficient in Angstroms
deltaE
:float
- Energy spread in electron volts using 1/e measure of spread (to convert from FWHM divide by 1.655, and divide by sqrt(2) to convert from standard deviation )
eV
:float
- (Mean) beam energy in electron volts
npoints
:int
,optional- Number of integration points in the Cc numerical integration
deltaEconv
:float
,optional- The convention for deltaE, the energy spread, acceptable inputs are '1/e' the energy point that the probability density function drops to 1/e times its maximum value, 'std' for standard deviation and 'FWHM' for the full width at half maximum of the energy spread.
Returns
defocii
:(
npoints,) array_like
- The defocus integration points
Expand source code
def Cc_integration_points(Cc, deltaE, eV, npoints=7, deltaEconv="1/e"): """ Calculate the defocus integration points for simulating chromatic aberration. The integration points are selected by dividing the assumed gaussian defocus spread into npoints regions of equal probability, then finding the mean defocus in each of those regions. Parameters ---------- Cc : float Chromatic aberration coefficient in Angstroms deltaE : float Energy spread in electron volts using 1/e measure of spread (to convert from FWHM divide by 1.655, and divide by sqrt(2) to convert from standard deviation ) eV : float (Mean) beam energy in electron volts npoints : int,optional Number of integration points in the Cc numerical integration deltaEconv : float,optional The convention for deltaE, the energy spread, acceptable inputs are '1/e' the energy point that the probability density function drops to 1/e times its maximum value, 'std' for standard deviation and 'FWHM' for the full width at half maximum of the energy spread. Returns ------- defocii : (`npoints`,) array_like The defocus integration points """ # Import the error function (integral of Gaussian) and inverse error function # from scipy's special functions library from scipy.special import erfinv, erf # First divide the gaussian pdf into npoints different regions of equal # "area" partitions = erfinv(2 * (np.arange(npoints - 1) + 1) / npoints - 1) # Now calculate the mean (expectation value) within each partition x = np.zeros(npoints) prefactor = 1 / (2 * np.sqrt(np.pi)) x[0] = -prefactor * np.exp(-partitions[0] ** 2) / (1 + erf(partitions[0])) * 2 x[1:-1] = ( prefactor * (np.exp(-partitions[:-1] ** 2) - np.exp(-partitions[1:] ** 2)) / (erf(partitions[1:]) - erf(partitions[:-1])) * 2 ) x[-1] = prefactor * np.exp(-partitions[-1] ** 2) / (1 - erf(partitions[-1])) * 2 # Multiply by 1/e spread of defocus values return x * Cc * convert_deltaE(deltaE, deltaEconv) / eV
def aberration_starter_pack()
-
Create the set of aberrations up to fifth order.
Expand source code
def aberration_starter_pack(): """Create the set of aberrations up to fifth order.""" aberrations = [] aberrations.append(aberration("C10", "C1", "Defocus ", 0.0, 0.0, 1, 0)) aberrations.append(aberration("C12", "A1", "2-Fold astig. ", 0.0, 0.0, 1, 2)) aberrations.append(aberration("C23", "A2", "3-Fold astig. ", 0.0, 0.0, 2, 3)) aberrations.append(aberration("C21", "B2", "Axial coma ", 0.0, 0.0, 2, 1)) aberrations.append(aberration("C30", "C3", "3rd order spher. ", 0.0, 0.0, 3, 0)) aberrations.append(aberration("C34", "A3", "4-Fold astig. ", 0.0, 0.0, 3, 4)) aberrations.append(aberration("C32", "S3", "Axial star aber. ", 0.0, 0.0, 3, 2)) aberrations.append(aberration("C45", "A4", "5-Fold astig. ", 0.0, 0.0, 4, 5)) aberrations.append(aberration("C43", "D4", "3-Lobe aberr. ", 0.0, 0.0, 4, 3)) aberrations.append(aberration("C41", "B4", "4th order coma ", 0.0, 0.0, 4, 1)) aberrations.append(aberration("C50", "C5", "5th order spher. ", 0.0, 0.0, 5, 0)) aberrations.append(aberration("C56", "A5", "6-Fold astig. ", 0.0, 0.0, 5, 6)) aberrations.append(aberration("C52", "S5", "5th order star ", 0.0, 0.0, 5, 2)) aberrations.append(aberration("C54", "R5", "5th order rosette", 0.0, 0.0, 5, 4)) return aberrations
def chi(q, qphi, lam, df=0.0, aberrations=[])
-
Calculate the aberration function, chi.
Parameters
q
:float
orarray_like
- Reciprocal space extent (Inverse angstroms).
qphi
:float
orarray_like
- Azimuth of grid in radians
lam
:float
- Wavelength of electron (Inverse angstroms).
df
:float
, optional- Defocus in Angstrom
aberrations
:list
, optional- A list containing a set of the class aberration, pass an empty list for an unaberrated contrast transfer function.
Returns
chi
:float
orarray_like
- The aberration function, will be the same shape as
q
. This is used to calculate the probe wave function in reciprocal space.
Expand source code
def chi(q, qphi, lam, df=0.0, aberrations=[]): r""" Calculate the aberration function, chi. Parameters ---------- q : float or array_like Reciprocal space extent (Inverse angstroms). qphi : float or array_like Azimuth of grid in radians lam : float Wavelength of electron (Inverse angstroms). df : float, optional Defocus in Angstrom aberrations : list, optional A list containing a set of the class aberration, pass an empty list for an unaberrated contrast transfer function. Returns ------- chi : float or array_like The aberration function, will be the same shape as `q`. This is used to calculate the probe wave function in reciprocal space. """ qlam = q * lam chi_ = qlam ** 2 / 2 * df for ab in aberrations: chi_ += ( qlam ** (ab.n + 1) * float(ab.amplitude) / (ab.n + 1) * np.cos(ab.m * (qphi - float(ab.angle))) ) return 2 * np.pi * chi_ / lam
def convert_deltaE(deltaE, deltaEconv)
-
Convert the energy spread input to a 1/e spread.
Parameters
deltaE
:float
- Energy spread in electron volts using the measure given by deltaEconv
deltaEconv
:string
- The convention for deltaE, the energy spread, acceptable inputs are '1/e' the energy point that the probability density function drops to 1/e times its maximum value, 'std' for standard deviation and 'FWHM' for the full width at half maximum of the energy spread
Expand source code
def convert_deltaE(deltaE, deltaEconv): """ Convert the energy spread input to a 1/e spread. Parameters ---------- deltaE : float Energy spread in electron volts using the measure given by deltaEconv deltaEconv : string The convention for deltaE, the energy spread, acceptable inputs are '1/e' the energy point that the probability density function drops to 1/e times its maximum value, 'std' for standard deviation and 'FWHM' for the full width at half maximum of the energy spread """ if deltaEconv == "1/e": return deltaE elif deltaEconv == "FWHM": return deltaE / 2 / np.sqrt(np.log(2)) elif deltaEconv == "std": return deltaE * np.sqrt(2) else: raise AttributeError( "detlaEconv " + deltaEconv + " input not recognized, needs to be one of '1/e', 'FWHM' or 'std'" )
def convert_tilt_angles(tilt, tilt_units, rsize, eV, invA_out=False)
-
Convert tilt to pixel or inverse Angstroms units regardless of input units.
Input units can be mrad, pixels or inverse Angstrom
Parameters
tilt
:array_like
- Tilt in units of mrad, pixels or inverse Angstrom
tilt_units
:string
- Units of specimen and beam tilt, can be 'mrad','pixels' or 'invA'
rsize
:(2,) array_like
- The size of the grid in Angstrom
eV
:float
- Probe energy in electron volts
invA_out
:bool
- Pass True if inverse Angstrom units are desired.
Expand source code
def convert_tilt_angles(tilt, tilt_units, rsize, eV, invA_out=False): """ Convert tilt to pixel or inverse Angstroms units regardless of input units. Input units can be mrad, pixels or inverse Angstrom Parameters ---------- tilt : array_like Tilt in units of mrad, pixels or inverse Angstrom tilt_units : string Units of specimen and beam tilt, can be 'mrad','pixels' or 'invA' rsize : (2,) array_like The size of the grid in Angstrom eV : float Probe energy in electron volts invA_out : bool Pass True if inverse Angstrom units are desired. """ # If units of the tilt are given in mrad, convert to inverse Angstrom if tilt_units == "mrad": k = wavev(eV) tilt_ = np.asarray(tilt) * 1e-3 * k else: tilt_ = tilt # If inverse Angstroms are requested our work here is done if invA_out: return tilt_ # Convert inverse Angstrom to pixel coordinates, this will be rounded # to the nearest pixel if tilt_units != "pixels": tilt_ = np.round(tilt_ * rsize[:2]).astype(int) return tilt_
def depth_of_field(eV, alpha)
-
Calculate the probe depth of field (z-resolution) for a probe.
Parameters
eV
:float
- Probe accelerating voltage in electron volts
alpha
:float
- Probe forming semi-angle in mrad
Returns
dof
:float
- The probe full-width-at-half-maximum (FWHM) depth of field
Expand source code
def depth_of_field(eV, alpha): """ Calculate the probe depth of field (z-resolution) for a probe. Parameters ---------- eV : float Probe accelerating voltage in electron volts alpha : float Probe forming semi-angle in mrad Returns ------- dof : float The probe full-width-at-half-maximum (FWHM) depth of field """ return 1.77 / wavev(eV) / alpha / alpha * 1e6
def focused_probe(gridshape, rsize, eV, app, beam_tilt=[0, 0], aperture_shift=[0, 0], tilt_units='mrad', df=0, aberrations=[], q=None, app_units='mrad', qspace=False)
-
Make a focused electron probe wave function.
Parameters
gridshape
:(2,) array_like
- The pixel size of the grid
rsize
:(2,) array_like
- The size of the grid in Angstrom
eV
:float
- The energy of the probe electrons in electron volts
app
:float
- The probe-forming apperture in units specified by app_units, pass None if no probe forming aperture is to be used
beam_tilt
:array_like
, optional- Allows the user to simulate a (small < 50 mrad) beam tilt. To maintain periodicity of the wave function at the boundaries this tilt is rounded to the nearest pixel value.
aperture_shift
:array_like
, optional- Allows the user to simulate a (small < 50 mrad) aperture shift. To maintain periodicity of the wave function at the boundaries this tilt is rounded to the nearest pixel value.
tilt_units
:string
, optional- Units of beam tilt and aperture shift, can be 'mrad','pixels' or 'invA'
df
:float
, optional- Probe defocus in A, a negative value indicate overfocus
aberrations
:list
, optional- A list of of probe aberrations of class pyms.Probe.aberration, pass an empty list for an un-aberrated probe
app_units
:string
, optional- The units of the aperture size ("invA", "pixels" or "mrad")
qspace
:bool
, optional- If True return the probe in reciprocal space
Returns
probe
:complex (Y,X) np.ndarray
- The requested electron probe wave function
Expand source code
def focused_probe( gridshape, rsize, eV, app, beam_tilt=[0, 0], aperture_shift=[0, 0], tilt_units="mrad", df=0, aberrations=[], q=None, app_units="mrad", qspace=False, ): """ Make a focused electron probe wave function. Parameters --------- gridshape : (2,) array_like The pixel size of the grid rsize : (2,) array_like The size of the grid in Angstrom eV : float The energy of the probe electrons in electron volts app : float The probe-forming apperture in units specified by app_units, pass None if no probe forming aperture is to be used beam_tilt : array_like, optional Allows the user to simulate a (small < 50 mrad) beam tilt. To maintain periodicity of the wave function at the boundaries this tilt is rounded to the nearest pixel value. aperture_shift : array_like, optional Allows the user to simulate a (small < 50 mrad) aperture shift. To maintain periodicity of the wave function at the boundaries this tilt is rounded to the nearest pixel value. tilt_units : string, optional Units of beam tilt and aperture shift, can be 'mrad','pixels' or 'invA' df : float, optional Probe defocus in A, a negative value indicate overfocus aberrations : list, optional A list of of probe aberrations of class pyms.Probe.aberration, pass an empty list for an un-aberrated probe app_units : string, optional The units of the aperture size ("invA", "pixels" or "mrad") qspace : bool, optional If True return the probe in reciprocal space Returns ------- probe : complex (Y,X) np.ndarray The requested electron probe wave function """ probe = make_contrast_transfer_function( gridshape, rsize, eV, app, beam_tilt, aperture_shift, tilt_units, df, aberrations, q, app_units, ) # Normalize the STEM probe so that its sum-squared intensity is unity probe *= np.sqrt(np.prod(gridshape)) / np.sqrt(np.sum(np.square(np.abs(probe)))) # Return real or diffraction space probe depending on user preference if not qspace: return np.fft.ifft2(probe) return probe
def make_contrast_transfer_function(pix_dim, real_dim, eV, app, optic_axis=[0, 0], aperture_shift=[0, 0], tilt_units='mrad', df=0, aberrations=[], q=None, app_units='mrad')
-
Make an electron lens contrast transfer function.
Parameters
pix_dim
:(2,) int array_like
- The pixel size of the grid
real_dim
:(2,) float array_like
- The size of the grid in Angstrom
eV
:float
- The energy of the probe electrons in eV
app
:float
orNone
- The aperture in units specified by app_units, pass
app
= None for no aperture optic_axis
:(2,) array_like
, optional- allows the user to specify a different optic axis in units specified by
tilt_units
aperture_shift
:(2,) array_like
, optional- Shift of the objective aperture relative to the center of the array
tilt_units
:string
- Units of the
optic_axis
oraperture_shift
values, default is mrad df
:float
- Probe defocus in A, a negative value indicate overfocus
aberrations
:array_like
ofaberration objects
- List containing instances of class aberration
- q :
- Precomputed reciprocal space array, allows the user to reduce
- computation time somewhat
app_units
:string
- The units of
app
(A^-1 or mrad)
Returns
ctf
:array_like
- The lens contrast transfer function in reciprocal space
Expand source code
def make_contrast_transfer_function( pix_dim, real_dim, eV, app, optic_axis=[0, 0], aperture_shift=[0, 0], tilt_units="mrad", df=0, aberrations=[], q=None, app_units="mrad", ): """ Make an electron lens contrast transfer function. Parameters --------- pix_dim : (2,) int array_like The pixel size of the grid real_dim : (2,) float array_like The size of the grid in Angstrom eV : float The energy of the probe electrons in eV app : float or None The aperture in units specified by app_units, pass `app` = None for no aperture optic_axis : (2,) array_like, optional allows the user to specify a different optic axis in units specified by `tilt_units` aperture_shift : (2,) array_like, optional Shift of the objective aperture relative to the center of the array tilt_units : string Units of the `optic_axis` or `aperture_shift` values, default is mrad df : float Probe defocus in A, a negative value indicate overfocus aberrations : array_like of aberration objects List containing instances of class aberration q : Precomputed reciprocal space array, allows the user to reduce computation time somewhat app_units : string The units of `app` (A^-1 or mrad) Returns ------- ctf : array_like The lens contrast transfer function in reciprocal space """ # Make reciprocal space array if q is None: q = q_space_array(pix_dim, real_dim[:2]) # Get electron wave number (inverse of wavelength) k = wavev(eV) # Convert tilts to units of inverse Angstrom optic_axis_ = convert_tilt_angles( optic_axis, tilt_units, real_dim, eV, invA_out=True ) aperture_shift_ = convert_tilt_angles( aperture_shift, tilt_units, real_dim, eV, invA_out=True ) if app is None: app_ = np.amax(np.abs(q)) else: # Get aperture size in units of inverse Angstrom app_ = convert_tilt_angles(app, app_units, real_dim, eV, invA_out=True) # Initialize the array to contain the CTF CTF = np.zeros(pix_dim, dtype=np.complex) # Calculate the magnitude of the reciprocal lattice grid # qarray1 accounts for a shift of the optic axis qarray1 = np.sqrt( np.square(q[0] - optic_axis_[0]) + np.square(q[1] - optic_axis_[1]) ) # qarray2 accounts for a shift of both the optic axis and the aperture qarray2 = np.square(q[0] - optic_axis_[0] - aperture_shift_[0]) + np.square( q[1] - optic_axis_[1] - aperture_shift_[1] ) # Calculate azimuth of reciprocal space array in case it is required for # aberrations qphi = np.arctan2(q[0] - optic_axis_[0], q[1] - optic_axis_[1]) # Only calculate CTF for region within the aperture mask = qarray2 <= app_ ** 2 CTF[mask] = np.exp(-1j * chi(qarray1[mask], qphi[mask], 1.0 / k, df, aberrations)) return CTF
def plane_wave_illumination(gridshape, gridsize, eV, tilt=[0, 0], tilt_units='mrad', qspace=False)
-
Generate plane wave illumination for input to multislice.
The wave function will be normalized such that sum of intensity is unity in real space.
Parameters
gridshape
:(2,) array_like
- Pixel dimensions of the 2D grid
gridsize
:(2,) array_like
- Size of the grid in real space
eV
:float
- Probe energy in electron volts (irrelevant for untilted illumination)
tilt
:array_like
, optional- Allows the user to simulate a (small < 50 mrad) beam tilt, To maintain periodicity of the wave function at the boundaries this tilt is rounded to the nearest pixel value.
tilt_units
:string
, optional- Units of beam tilt, can be 'mrad','pixels' or 'invA'
qspace
:bool
, optional- Pass qspace = True to get the probe in momentum (q) space
Returns
illum
:np.ndarray (Y,X)
Expand source code
def plane_wave_illumination( gridshape, gridsize, eV, tilt=[0, 0], tilt_units="mrad", qspace=False ): """ Generate plane wave illumination for input to multislice. The wave function will be normalized such that sum of intensity is unity in real space. Parameters ---------- gridshape : (2,) array_like Pixel dimensions of the 2D grid gridsize : (2,) array_like Size of the grid in real space eV : float Probe energy in electron volts (irrelevant for untilted illumination) tilt : array_like, optional Allows the user to simulate a (small < 50 mrad) beam tilt, To maintain periodicity of the wave function at the boundaries this tilt is rounded to the nearest pixel value. tilt_units : string, optional Units of beam tilt, can be 'mrad','pixels' or 'invA' qspace : bool, optional Pass qspace = True to get the probe in momentum (q) space Returns ------ illum : np.ndarray (Y,X) """ # Initialize array that contains wave function illum = np.zeros(gridshape, dtype=np.complex) # Convert tilt to units of pixels tilt_ = convert_tilt_angles(tilt, tilt_units, gridsize, eV) # Case of an untilted plane wave (phase is zero everywhere) if tilt[0] == 0 and tilt[1] == 0: illum[:, :] = 1 / np.sqrt(np.product(gridshape)) if qspace: return np.fft.fft2(illum) else: return illum # Set the value of wavefunction amplitude such that after inverse Fourier # transform (and resulting division by the total number of pixels) the sum # of intensity will be 1 illum[tilt_[0], tilt_[1]] = np.sqrt(np.product(gridshape)) # Return wave function in real space if qspace: return illum else: return np.fft.ifft2(illum)
def relativistic_mass_correction(E)
-
Evaluate the relativistic mass correction for electron with energy E in eV.
See Eq. (2.2) in Kirkland's Advanced Computing in electron microscopy.
Expand source code
def relativistic_mass_correction(E): """ Evaluate the relativistic mass correction for electron with energy E in eV. See Eq. (2.2) in Kirkland's Advanced Computing in electron microscopy. """ # Electron rest mass in eV m0c2 = 5.109989461e5 return (m0c2 + E) / m0c2
def simulation_result_with_Cc(func, Cc, deltaE, eV, args=[], kwargs={}, npoints=7, deltaEconv='1/e')
-
Perform a simulation using function, taking into account chromatic aberration.
Pass in the function that simulates the multislice result, it is assumed that defocus is a variable named 'df' somewhere in the keyword argument list.
Parameters
func
:function
- Function that simulates the result of interest, ie. pyms.HRTEM. The defocus must be present in the keyword argument list as 'df'
Cc
:float
- Chromatic aberration coefficient in Angstroms
deltaE
:float
- Energy spread in electron volts using 1/e measure of spread (to convert from FWHM divide by 1.655, and divide by sqrt(2) to convert from standard deviation )
eV
:float
- (Mean) beam energy in electron volts
args
:list
, optional- Arguments for the method function used to propagate probes to the exit surface
kwargs
:Dict
, optional- Keyword arguments for the method function used to propagate probes to the exit surface
npoints
:int
,optional- Number of integration points in the Cc numerical integration
Returns
average
:dict
orarray_like
- The simulation requested but averaged over the different defocus values to account for chromatic aberration.
Expand source code
def simulation_result_with_Cc( func, Cc, deltaE, eV, args=[], kwargs={}, npoints=7, deltaEconv="1/e" ): """ Perform a simulation using function, taking into account chromatic aberration. Pass in the function that simulates the multislice result, it is assumed that defocus is a variable named 'df' somewhere in the keyword argument list. Parameters ---------- func : function Function that simulates the result of interest, ie. pyms.HRTEM. The defocus must be present in the keyword argument list as 'df' Cc : float Chromatic aberration coefficient in Angstroms deltaE : float Energy spread in electron volts using 1/e measure of spread (to convert from FWHM divide by 1.655, and divide by sqrt(2) to convert from standard deviation ) eV : float (Mean) beam energy in electron volts args : list, optional Arguments for the method function used to propagate probes to the exit surface kwargs : Dict, optional Keyword arguments for the method function used to propagate probes to the exit surface npoints : int,optional Number of integration points in the Cc numerical integration Returns ------- average : dict or array_like The simulation requested but averaged over the different defocus values to account for chromatic aberration. """ # Check if a defocii has already been specified if not, assume that nominal # (mean) defocus is 0 if "df" in kwargs.keys(): nominal_df = kwargs["df"] else: nominal_df = 0 # Get defocii to integrate over defocii = Cc_integration_points(Cc, deltaE, eV, npoints, deltaEconv) + nominal_df ndf = len(defocii) # Initialise the average result to None average = None # Now integrate the function over those defocus values for df in defocii: kwargs["df"] = df result = func(*args, **kwargs) # Assume that every function will either return a numpy array # (eg. pyms.HRTEM) or a list of numpy arrays (eg. pyms.STEM with both # conventional and 4D-STEM options) so average these results if isinstance(result, np.ndarray): if average is None: average = result / ndf else: average += result / ndf elif isinstance(result, dict): if average is None: average = copy.deepcopy(result) for key in average.keys(): if average[key] is not None: average[key] /= ndf else: for key in average.keys(): if average[key] is not None: average[key] += result[key] / ndf else: if average is None: average = [x / ndf for x in result] else: average = [x / ndf + y for x, y in zip(result, average)] return average
def wavev(E)
-
Evaluate the relativistically corrected wavenumber of an electron with energy E.
Energy E must be in electron-volts, see Eq. (2.5) in Kirkland's Advanced Computing in electron microscopy
Expand source code
def wavev(E): """ Evaluate the relativistically corrected wavenumber of an electron with energy E. Energy E must be in electron-volts, see Eq. (2.5) in Kirkland's Advanced Computing in electron microscopy """ # Planck's constant times speed of light in eV Angstrom hc = 1.23984193e4 # Electron rest mass in eV m0c2 = 5.109989461e5 return np.sqrt(E * (E + 2 * m0c2)) / hc
Classes
class aberration (Krivanek, Haider, Description, amplitude, angle, n, m)
-
A class describing electron lens aberrations.
Intialize the lens aberration object.
Parameters
Krivanek
:str
- A string describing the aberration coefficient in Krivanek notation (C_mn)
Haider
:str
- A string describing the aberration coefficient in Haider notation (ie. A1, A2, B2)
Description
:str
- A string describing the colloqiual name of the aberration ie. 2-fold astig.
amplitude
:float
- The amplitude of the aberration in Angstrom
angle
:float
- The angle of the aberration in radians
n
:int
- The principle aberration order
m
:int
- The rotational order of the aberration.
Expand source code
class aberration: """A class describing electron lens aberrations.""" def __init__(self, Krivanek, Haider, Description, amplitude, angle, n, m): """ Intialize the lens aberration object. Parameters ---------- Krivanek : str A string describing the aberration coefficient in Krivanek notation (C_mn) Haider : str A string describing the aberration coefficient in Haider notation (ie. A1, A2, B2) Description : str A string describing the colloqiual name of the aberration ie. 2-fold astig. amplitude : float The amplitude of the aberration in Angstrom angle : float The angle of the aberration in radians n : int The principle aberration order m : int The rotational order of the aberration. """ self.Krivanek = Krivanek self.Haider = Haider self.Description = Description self.amplitude = amplitude self.m = m self.n = n if m > 0: self.angle = angle else: self.angle = 0 def __str__(self): """Return a string describing the aberration.""" if self.m > 0: return ( "{0:17s} ({1:2s}) -- {2:3s} = {3:9.2e} \u00E5 \u03B8 = " + "{4:4d}\u00B0 " ).format( self.Description, self.Haider, self.Krivanek, self.amplitude, int(np.rad2deg(self.angle)), ) else: return " {0:17s} ({1:2s}) -- {2:3s} = {3:9.2e} \u00E5".format( self.Description, self.Haider, self.Krivanek, self.amplitude )