Live notebook

You can run this notebook in a live session Binder or view it on Github.

Geometrical EBSD simulations#

In this tutorial, we will inspect and visualize the results from EBSD indexing by plotting Kikuchi lines and zone axes onto an EBSD signal. We consider this a geometrical EBSD simulation, since it is only positions of Kikuchi lines and zone axes that are computed. These simulations are based on the work by Aimo Winkelmann in the supplementary material to [Britton et al., 2016].

These simulations can be helpful when checking whether indexing results are correct and for interpreting them.

Let’s import the necessary libraries

[1]:
# Exchange inline for notebook or qt5 (from pyqt) for interactive plotting
%matplotlib inline

import matplotlib.pyplot as plt
import numpy as np

from diffpy.structure import Atom, Lattice, Structure
from diffsims.crystallography import ReciprocalLatticeVector
import hyperspy.api as hs
import kikuchipy as kp
from orix.crystal_map import Phase
from orix.quaternion import Rotation


# Plotting parameters
plt.rcParams.update(
    {"figure.figsize": (10, 10), "font.size": 20, "lines.markersize": 10}
)

We’ll inspect the indexing results of a small nickel EBSD dataset of 3 x 3 patterns

[2]:
s = (
    kp.data.nickel_ebsd_small()
)  # Use kp.load("data.h5") to load your own data
s
[2]:
<EBSD, title: patterns My awes0m4 ..., dimensions: (3, 3|60, 60)>

Let’s enhance the Kikuchi bands by removing the static and dynamic backgrounds

[3]:
s.remove_static_background()
s.remove_dynamic_background()
[########################################] | 100% Completed | 104.36 ms
[########################################] | 100% Completed | 102.07 ms
[4]:
_ = hs.plot.plot_images(
    s, axes_decor=None, label=None, colorbar=False, tight_layout=True
)
../_images/tutorials_geometrical_ebsd_simulations_7_0.png

To project Kikuchi lines and zone axes onto our detector, we need

  1. a description of the crystal phase

  2. the set of Kikuchi bands to consider, e.g. the sets of planes {111}, {200}, {220}, and {311}

  3. the crystal orientations with respect to the reference frame

  4. the position of the detector with respect to the sample, in the form of a sample-detector model which includes the sample and detector tilt and the projection center (shortes distance from the source point on the sample to the detector), given here as (PC:math:_x, PC\(_y\), PC\(_z\))

Prepare phase and reflector list#

We’ll store the crystal phase information in an orix.crystal_map.Phase instance

[5]:
phase = Phase(
    space_group=225,
    structure=Structure(
        atoms=[Atom("Ni", [0, 0, 0])],
        lattice=Lattice(3.52, 3.52, 3.52, 90, 90, 90),
    ),
)

print(phase)
print(phase.structure)
<name: . space group: Fm-3m. point group: m-3m. proper point group: 432. color: tab:blue>
lattice=Lattice(a=3.52, b=3.52, c=3.52, alpha=90, beta=90, gamma=90)
Ni   0.000000 0.000000 0.000000 1.0000

We’ll build up the reflector list using diffsims.crystallography.ReciprocalLatticeVector

[6]:
ref = ReciprocalLatticeVector(
    phase=phase, hkl=[[1, 1, 1], [2, 0, 0], [2, 2, 0], [3, 1, 1]]
)
ref
[6]:
ReciprocalLatticeVector (4,),  (m-3m)
[[1. 1. 1.]
 [2. 0. 0.]
 [2. 2. 0.]
 [3. 1. 1.]]

We’ll obtain the symmetrically equivalent vectors and plot each family of vectors in a distinct colour in the stereographic projection

[7]:
ref = ref.symmetrise().unique()
ref.size
[7]:
50
[8]:
ref.print_table()
 h k l      d     |F|_hkl   |F|^2   |F|^2_rel   Mult
 3 1 1    1.061     nan      nan       nan       24
 1 1 1    2.032     nan      nan       nan       8
 2 2 0    1.245     nan      nan       nan       12
 2 0 0    1.760     nan      nan       nan       6
[9]:
# Dictionary with {hkl} as key and indices into `ref` as values
hkl_sets = ref.get_hkl_sets()
hkl_sets
[9]:
defaultdict(tuple,
            {(2.0, 0.0, 0.0): (array([ 8,  9, 10, 11, 12, 13]),),
             (2.0,
              2.0,
              0.0): (array([14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]),),
             (1.0, 1.0, 1.0): (array([0, 1, 2, 3, 4, 5, 6, 7]),),
             (3.0,
              1.0,
              1.0): (array([26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
                     43, 44, 45, 46, 47, 48, 49]),)})
[10]:
hkl_colors = np.zeros((ref.size, 3))
for idx, color in zip(
    hkl_sets.values(),
    [
        [1, 0, 0],
        [0, 1, 0],
        [0, 0, 1],
        [0.75, 0, 0.75],
    ],  # Red, green, blue, magenta
):
    hkl_colors[idx] = color
[11]:
hkl_labels = []
for hkl in ref.hkl.round(0).astype(int):
    hkl_labels.append(str(hkl).replace("[", "(").replace("]", ")"))
[12]:
ref.scatter(c=hkl_colors, grid=True, ec="k", vector_labels=hkl_labels)
../_images/tutorials_geometrical_ebsd_simulations_18_0.png

We can also plot the plane traces, i.e. the Kikuchi lines, in both hemispheres (they are identical for Ni)

[13]:
ref.draw_circle(
    color=hkl_colors, hemisphere="both", figure_kwargs=dict(figsize=(15, 10))
)
../_images/tutorials_geometrical_ebsd_simulations_20_0.png

Specify rotations and detector-sample geometry#

We will index the nine patterns using PyEBSDIndex, by first estimating the projection center (PC) followed by Hough indexing. The initial estimate of the PC is based on previous experiments on the same microscope.

Note

kikuchipy cannot depend on PyEBSDIndex at the moment, as PyEBSDIndex does not support all the combinations of Python versions and operating systems that kikuchipy does. To install PyEBSDIndex, see their installation instructions.

PyEBSDIndex supports indexing face centered and body centered cubic (FCC and BCC) materials.

Alternatively, if PyEBSDIndex is not available, we can use orientations determined from a previous dictionary indexing (see the pattern matching tutorial): the nine patterns are from two grains with orientations given in Euler angles of about \((\phi_1, \Phi, \phi_2) = (258^{\circ}, 58^{\circ}, 1^{\circ})\) and \((\phi_1, \Phi, \phi_2) = (292^{\circ}, 62^{\circ}, 182^{\circ})\).

[14]:
from pyebsdindex import ebsd_index, pcopt
[15]:
sig_shape = s.axes_manager.signal_shape[::-1]
indexer = ebsd_index.EBSDIndexer(
    vendor="KIKUCHIPY", sampleTilt=70, camElev=0, patDim=sig_shape
)
[16]:
pc0 = (0.4, 0.2, 0.5)
pc = pcopt.optimize(s.data.reshape((-1,) + sig_shape), indexer, pc0)
print(pc)
[0.4229063  0.21784512 0.50005421]
[17]:
data, *_ = indexer.index_pats(s.data.reshape((-1,) + sig_shape), PC=pc)
rot = Rotation(data[-1]["quat"]).reshape(3, 3)
rot
[17]:
Rotation (3, 3)
[[[ 0.0826  0.4783 -0.0521 -0.8727]
  [ 0.2945  0.0334  0.7135  0.6349]
  [ 0.8379 -0.0915  0.5104 -0.1703]]

 [[ 0.5598 -0.2999  0.3754  0.6751]
  [ 0.2951  0.0315  0.7128  0.6354]
  [ 0.4731  0.2977  0.424  -0.7126]]

 [[ 0.0828  0.4768 -0.0547 -0.8734]
  [ 0.2943  0.0326  0.7139  0.6346]
  [ 0.294   0.0351  0.7147  0.6337]]]
[18]:
# Run if PyEBSDIndex is unavailable
# pc = (0.42, 0.22, 0.50)
# sig_shape = s.axes_manager.signal_shape[::-1]

# grain1 = np.deg2rad((258, 58, 1))
# grain2 = np.deg2rad((292, 62, 182))
# rot = Rotation.from_euler(
#    [[grain1, grain2, grain2], [grain1, grain2, grain2], [grain1, grain2, grain2]]
# )
# rot

We describe the sample-detector model in an kikuchipy.detectors.EBSDDetector instance. From Hough indexing we know the projection center to be, in the EDAX TSL convention (see the reference frame tutorial for the various conventions and more details on the use of the sample-detector model), \((x^{*}, y^{*}, z^{*}) = (0.421, 0.7794, 0.5049)\). The sample was tilted \(70^{\circ}\) about the microscope X direction towards the detector, and the detector normal was orthogonal to the optical axis (beam direction)

[19]:
detector = kp.detectors.EBSDDetector(sig_shape, sample_tilt=70, pc=pc)
detector
[19]:
EBSDDetector (60, 60), px_size 1 um, binning 1, tilt 0, azimuthal 0, pc (0.423, 0.218, 0.5)

Note that the projection center gets converted internally to the Bruker convention.

Create simulations on detector#

Now we’re ready to create geometrical simulations. We create simulations using the kikuchipy.simulations.KikuchiPatternSimulator, which takes the reflectors as input

[21]:
sim = simulator.on_detector(detector, rot)
Finding bands that are in some pattern:
[########################################] | 100% Completed | 103.28 ms
Finding zone axes that are in some pattern:
[########################################] | 100% Completed | 102.08 ms
Calculating detector coordinates for bands and zone axes:
[########################################] | 100% Completed | 103.66 ms

By passing the detector and crystal orientations to KikuchiPatternSimulator.on_detector(), we’ve obtained a kikuchipy.simulations.GeometricalKikuchiPatternSimulation, which stores the detector and gnomonic coordinates of the Kikuchi lines and zone axes for each crystal orientation

[22]:
[22]:
GeometricalKikuchiPatternSimulation (3, 3):
ReciprocalLatticeVector (50,),  (m-3m)
[[ 1.  1.  1.]
 [-1.  1.  1.]
 [-1. -1.  1.]
 [ 1. -1.  1.]
 [ 1. -1. -1.]
 [ 1.  1. -1.]
 [-1.  1. -1.]
 [-1. -1. -1.]
 [ 2.  0.  0.]
 [ 0.  2.  0.]
 [-2.  0.  0.]
 [ 0. -2.  0.]
 [ 0.  0.  2.]
 [ 0.  0. -2.]
 [ 2.  2.  0.]
 [-2.  2.  0.]
 [-2. -2.  0.]
 [ 2. -2.  0.]
 [ 0.  2.  2.]
 [-2.  0.  2.]
 [ 0. -2.  2.]
 [ 2.  0.  2.]
 [ 0.  2. -2.]
 [-2.  0. -2.]
 [ 0. -2. -2.]
 [ 2.  0. -2.]
 [ 3.  1.  1.]
 [-1.  3.  1.]
 [-3. -1.  1.]
 [ 1. -3.  1.]
 [ 1.  3.  1.]
 [-3.  1.  1.]
 [-1. -3.  1.]
 [ 3. -1.  1.]
 [ 3. -1. -1.]
 [ 1.  3. -1.]
 [-3.  1. -1.]
 [-1. -3. -1.]
 [-1.  3. -1.]
 [-3. -1. -1.]
 [ 1. -3. -1.]
 [ 3.  1. -1.]
 [ 1. -1.  3.]
 [ 1.  1.  3.]
 [-1.  1.  3.]
 [-1. -1.  3.]
 [-1. -1. -3.]
 [ 1. -1. -3.]
 [ 1.  1. -3.]
 [-1.  1. -3.]]

We see that not all 50 of the reflectors in the reflector list are present in some pattern.

Plot simulations#

These geometrical simulations can be plotted one-by-one by themselves

[23]:
sim.plot()
../_images/tutorials_geometrical_ebsd_simulations_35_0.png

Or, they can be plotted on top of patterns in three ways: passing a pattern to GeometricalKikuchiPatternSimulation.plot()

[24]:
sim.plot(index=(1, 2), pattern=s.inav[2, 1].data)
../_images/tutorials_geometrical_ebsd_simulations_37_0.png

Or, we can obtain collections of lines, zone axes and zone axes labels as Matplotlib objects via GeometricalKikuchiPatternSimulation.as_collections() and add them to an existing Matplotlib axis

[25]:
fig, ax = plt.subplots(ncols=3, nrows=3, figsize=(15, 15))

for idx in np.ndindex(s.axes_manager.navigation_shape[::-1]):
    ax[idx].imshow(s.data[idx], cmap="gray")
    ax[idx].axis("off")

    lines, zone_axes, zone_axes_labels = sim.as_collections(
        idx,
        zone_axes=True,
        zone_axes_labels=True,
        zone_axes_labels_kwargs=dict(fontsize=12),
    )
    ax[idx].add_collection(lines)
    ax[idx].add_collection(zone_axes)
    for label in zone_axes_labels:
        ax[idx].add_artist(label)

fig.tight_layout()
../_images/tutorials_geometrical_ebsd_simulations_39_0.png

Or, we can obtain the lines, zone axes, zone axes labels and PCs as HyperSpy markers via GeometricalKikuchiPatternSimulation.as_markers() and add them to a signal of the same navigation shape as the simulation instance. This enables navigating the patterns with the geometrical simulations

[26]:
markers = sim.as_markers()

# To delete previously added permanent markers, do
# del s.metadata.Markers

s.add_marker(markers, plot_marker=False, permanent=True)
[27]:
../_images/tutorials_geometrical_ebsd_simulations_42_0.png
../_images/tutorials_geometrical_ebsd_simulations_42_1.png