Check out the Hyperspy Workshop May 13-17, 2024 Online

Strain Mapping#

This tutorial demonstrates different routes to obtain strain maps from scanning electron diffraction data.

The code functionality is illustrated using synthetic data, which is first generated using pyxem. This synthetic data represents a simple cubic crystal that is distorted to a tetragonal stucture. The intention is for this to provide an easy to understand illustration of the code functionality rather than to model any physical system.

This functionaility has been checked to run in pyxem-0.15.0 (April 2023). Bugs are always possible, do not trust the code blindly, and if you experience any issues please report them here: pyxem/pyxem-demos#issues

Contents#

  1. Setting up & Creating Synthetic Data

  2. Image Affine Transform Based Mapping

  3. Vector Based Mapping

  1. Setting up & Creating Synthetic Data


Import pyxem and other required libraries

[ ]:
%matplotlib inline
import pyxem as pxm
import numpy as np
import hyperspy.api as hs
import diffpy.structure
from matplotlib import pyplot as plt
from pyxem.generators.indexation_generator import IndexationGenerator
from diffsims.generators.diffraction_generator import DiffractionGenerator
WARNING:silx.opencl.common:Unable to import pyOpenCl. Please install it from: https://pypi.org/project/pyopencl
[ ]:
hs.set_log_level('ERROR') # removes Warnings from hyperspy.

Define a structure for the creation of synthetic data. We will start by showing how changing the lattice parameter changes the diffraction spots.

[ ]:
latt = diffpy.structure.lattice.Lattice(3,3,3,90,90,90)
atom = diffpy.structure.atom.Atom(atype='Ni',xyz=[0,0,0],lattice=latt)
structure = diffpy.structure.Structure(atoms=[atom],lattice=latt)

latt = diffpy.structure.lattice.Lattice(3.2,3.2,3,90,90,90)
atom = diffpy.structure.atom.Atom(atype='Ni',xyz=[0,0,0],lattice=latt)
structure2 = diffpy.structure.Structure(atoms=[atom],lattice=latt)

Simulate an electron diffraction pattern. This just calculates the electron diffraction spots which should be visable based on the paramters given. We simulate a 300 keV beam here.

[ ]:
ediff = DiffractionGenerator(300.)
diffraction = ediff.calculate_ed_data(structure,
                                      reciprocal_radius=5.,
                                      max_excitation_error=0.025, shape_factor_width=5.0,
                                      with_direct_beam=False)
strained_diffraction = ediff.calculate_ed_data(structure2,
                                               reciprocal_radius=5.,
                                               max_excitation_error=0.025, shape_factor_width=5.0,
                                               with_direct_beam=False)

Check we have some spots. Notice that the material is strained in both x,y so the diffraction spots are all at smaller reciporical spacing.

[ ]:
%matplotlib inline
fig, ax = plt.subplots(1,1,)
diffraction.plot(ax=ax, label="Unstrained Material")
strained_diffraction.plot(ax=ax, label="Strained Material")
ax.legend()
<matplotlib.legend.Legend at 0x17d5fa2d0>
../../_images/tutorials_pyxem-demos_05_Simulate_Data_-_Strain_Mapping_15_1.png

We define a “detector” configuration here so that we can simulate the dataset.

[ ]:

diffraction.calibration = 1e-2 pattern = diffraction.get_diffraction_pattern(shape = (128,128), sigma = 0) strained_diffraction.calibration = 1e-2 pattern2 = strained_diffraction.get_diffraction_pattern(shape = (128,128), sigma = 0) pxm.signals.Diffraction2D([pattern, pattern2]).plot()
../../_images/tutorials_pyxem-demos_05_Simulate_Data_-_Strain_Mapping_17_0.png
../../_images/tutorials_pyxem-demos_05_Simulate_Data_-_Strain_Mapping_17_1.png

Define a distorted structure and simulate diffraction. Note that this structure is fairly unphysical but it does give us something to fit to.

[ ]:
from skimage.draw import disk
distortion_x = np.zeros((10, 10))
distortion_y = np.zeros((10, 10))

xx, yy = disk(center=(5, 5), radius=4, shape=(20, 20))

distortion_x[xx,yy]= np.abs(np.abs(xx-10)-5)
distortion_y[xx,yy]= np.abs(np.abs(yy-10)-5)

fig, axs = plt.subplots(1, 2)
axs[0].imshow(distortion_x)
axs[1].imshow(distortion_y)
<matplotlib.image.AxesImage at 0x17d8c7dd0>
../../_images/tutorials_pyxem-demos_05_Simulate_Data_-_Strain_Mapping_19_1.png
[ ]:
data = np.empty((10,10,128,128))

for ind in np.ndindex((10,10)):
    dx = distortion_x[ind]*.1
    dy = distortion_y[ind]*.1
    latt = diffpy.structure.lattice.Lattice(3+dx,3+dy,3,90,90,90)
    atom = diffpy.structure.atom.Atom(atype='Ni',xyz=[0,0,0],lattice=latt)
    structure = diffpy.structure.Structure(atoms=[atom],lattice=latt)
    diffraction = ediff.calculate_ed_data(structure, reciprocal_radius=5.,
                                          max_excitation_error=0.025,
                                          with_direct_beam=True)
    diffraction.calibration = 1e-2
    data[ind] = diffraction.get_diffraction_pattern((128,128),sigma=4)

dp = pxm.signals.ElectronDiffraction2D(data)

dp = dp.shift_diffraction(7,8) #shifting diffraction patterns to simulate real_effects

dp = dp+np.random.random((10,10,128,128))*.1
[########################################] | 100% Completed | 107.51 ms
[ ]:
%matplotlib inline
dp.plot()
../../_images/tutorials_pyxem-demos_05_Simulate_Data_-_Strain_Mapping_21_0.png
../../_images/tutorials_pyxem-demos_05_Simulate_Data_-_Strain_Mapping_21_1.png

Calibrating the Diffraction Pattern#

It is important that we keep track of the center of the diffraction pattern as well as the calibration. Lets consider the diffraction pattern above.

[ ]:
dp.set_diffraction_calibration(1e-2) # known from simulation
dp.beam_energy = 300 #kEv
dp.unit = "k_A^-1"
[ ]:
shifts, aligned = dp.center_direct_beam(method="blur",half_square_width=20,sigma=5, return_shifts=True, inplace=False)
[########################################] | 100% Completed | 106.20 ms
[########################################] | 100% Completed | 110.89 ms
[ ]:
aligned.plot()
aligned.axes_manager

< Axes manager, axes: (10, 10|128, 128) >

Navigation axis name size index offset scale units
10 0 0.0 1.0
10 0 0.0 1.0
Signal axis name size offset scale units
kx 128 -0.64 0.01 k_A^-1
ky 128 -0.64 0.01 k_A^-1
../../_images/tutorials_pyxem-demos_05_Simulate_Data_-_Strain_Mapping_25_1.png
../../_images/tutorials_pyxem-demos_05_Simulate_Data_-_Strain_Mapping_25_2.png

Template Matching#

A common way to find spots in a diffraction pattern is through template matching. Template matching involves creating a template of one of the diffraction spots and convolving it with the entire dataset. Usually this is done using a flat disk or a summed image of the zero beam. Often times reducing the noise in the dataset is useful as it

There are a couple of ways to acomplish this is pyxem.

The first is to use the template_match_disk function which uses the normalized_cross_correlation to find shere some diffraction spot is. We can think of this as the template being slid across the image and multiplied at every point. The result is that objects similar to the template are very visable in the filtered dataset.

There are multiple ways to do this:

  1. Use the vaccum probe as a template and convolve the vaccum probe with the image

  2. Use the

[ ]:
matched = aligned.template_match_disk(disk_r=5) # the disk_r should be equal to the size of the diffraction spots.
matched.plot()
[########################################] | 100% Completed | 310.78 ms
../../_images/tutorials_pyxem-demos_05_Simulate_Data_-_Strain_Mapping_27_1.png
../../_images/tutorials_pyxem-demos_05_Simulate_Data_-_Strain_Mapping_27_2.png

Peak Finding#

We can then find the position of the peaks using the hyperspy.Signal2D.find_peaks() method. There is an interactive form of this method as well which makes defining the proper settings a little bit easier.

[ ]:
%matplotlib notebook
matched.find_peaks()
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
Cell In[14], line 2
      1 get_ipython().run_line_magic('matplotlib', 'notebook')
----> 2 matched.find_peaks()

File ~/mambaforge/envs/pyxem-demos/lib/python3.11/site-packages/hyperspy/_signals/signal2d.py:1023, in Signal2D.find_peaks(self, method, interactive, current_index, show_progressbar, parallel, max_workers, display, toolkit, **kwargs)
   1020     peaks = BaseSignal(np.empty(self.axes_manager.navigation_shape),
   1021                        axes=axes_dict)
   1022     pf2D = PeaksFinder2D(self, method=method, peaks=peaks, **kwargs)
-> 1023     pf2D.gui(display=display, toolkit=toolkit)
   1024 elif current_index:
   1025     peaks = method_func(self.__call__(), **kwargs)

File ~/mambaforge/envs/pyxem-demos/lib/python3.11/site-packages/hyperspy/ui_registry.py:162, in get_partial_gui.<locals>.pg(self, display, toolkit, **kwargs)
    161 def pg(self, display=True, toolkit=None, **kwargs):
--> 162     return get_gui(self, toolkey=toolkey, display=display,
    163                    toolkit=toolkit, **kwargs)

File ~/mambaforge/envs/pyxem-demos/lib/python3.11/site-packages/hyperspy/ui_registry.py:69, in get_gui(self, toolkey, display, toolkit, **kwargs)
     67 def get_gui(self, toolkey, display=True, toolkit=None, **kwargs):
     68     if not TOOLKIT_REGISTRY:
---> 69         raise ImportError(
     70             "No toolkit registered. Install hyperspy_gui_ipywidgets or "
     71             "hyperspy_gui_traitsui GUI elements."
     72         )
     73     from hyperspy.defaults_parser import preferences
     74     if isinstance(toolkit, str):

ImportError: No toolkit registered. Install hyperspy_gui_ipywidgets or hyperspy_gui_traitsui GUI elements.
[ ]:
peaks = matched.find_peaks(threshold=1.0, distance=5, interactive=False)

Converting Peaks to Diffraction Vectors#

We then need to take the peaks identified and transform them into diffraction Vectors. We can construct the signal using the following code. Note that the center of the diffraction pattern and the calibration need to be passed once again. We can take these from the diffraction signal.

[ ]:
center = [a.offset/a.scale*-1 for a in matched.axes_manager.signal_axes] # should be the center of the pattern now...

calibration = matched.axes_manager.signal_axes[0].scale

vectors = pxm.signals.DiffractionVectors.from_peaks(peaks,center = center, calibration=calibration)

Filter the Diffraction Vectors#

We want to only return the diffraction vectors which are within some distance of a basis set of diffraction vectors. This basis set can be just be a single set of diffraction spots but ideally more than 2 spots will be used. This allows for the least squares fitting to also return a residual. In many cases this is prefered as it allows outliers to be identified as points with high residuals. Note that in some cases, using too many spots can also be probalematic as low intensity spots are harder to fit. In those cases a weight parameter is also allowed to help refine the fitting.

Defining the basis set of vectors can be done in multiple different ways.

  1. Define a basis from a known basis set of vectors. Using the known lattice parameter for the material it is possible to define the proper basis set for the material.

  2. Define the basis from an unstrained part of the material.

In most cases the secound way is easier and prefered over the first. It is less dependent on the calibration and things like elliptical distortion in the diffraction pattern will show up as strains in the first case and a very high quality of experiment is necessary.

[ ]:
# Get the basis vectors from an unstrained part of the material.
basis = vectors.inav[1:2,1:2]
basis.filter_magnitude(min_magnitude =0.1, max_magnitude=1)# remove the zero_beam
basis_array = basis.data[0,0]
[ ]:
filtered_vectors = vectors.filter_basis(basis_array, distance =.2, inplace= False)
[ ]:
from pyxem.generators.displacement_gradient_tensor_generator import get_DisplacementGradientMap
[ ]:
strain_map = get_DisplacementGradientMap(filtered_vectors, basis_array)
[ ]:
maps = strain_map.get_strain_maps()
[ ]:
import matplotlib.pyplot as plt
f= plt.figure(figsize=(7,7))
hs.plot.plot_images(maps,per_row=2,fig=f,
                    label=["e11","e22", "e12", "theta"],tight_layout=True)
plt.show()