This page was generated from doc/user_guide/geometrical_ebsd_simulations.ipynb. Interactive online version: Binder badge.

Geometrical EBSD simulations

This section details how to inspect and visualize the results from pattern matching or Hough indexing of cubic crystals by plotting Kikuchi bands and zone axes onto an EBSD signal. We consider this a geometrical EBSD simulation, since it’s only the Kikuchi band centres and zone axis positions that will be computed. These simulations are based on the work by Aimo Winkelmann in the supplementary material to [BJG+16].

Let’s import the necessary libraries and a small (3, 3) Nickel EBSD test data set

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

import tempfile
from diffsims.crystallography import ReciprocalLatticePoint
import hyperspy.api as hs
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from orix import crystal_map, quaternion
import kikuchipy as kp


s = kp.data.nickel_ebsd_small()  # Use kp.load("data.h5") to load your own data
s
WARNING:hyperspy.api:The ipywidgets GUI elements are not available, probably because the hyperspy_gui_ipywidgets package is not installed.
WARNING:hyperspy.api:The traitsui GUI elements are not available, probably because the hyperspy_gui_traitsui package is not installed.
[1]:
<EBSD, title: patterns My awes0m4 ..., dimensions: (3, 3|60, 60)>

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

[2]:
s.remove_static_background()
s.remove_dynamic_background()
Removing the static background:
[########################################] | 100% Completed |  0.1s
Removing the dynamic background:
[########################################] | 100% Completed |  0.1s
[3]:
_ = hs.plot.plot_images(
    s, axes_decor=None, label=None, colorbar=False, tight_layout=True
)
../_images/user_guide_geometrical_ebsd_simulations_5_0.png

To project Kikuchi bands and zone axis positions onto our detector, we need 1. a description of the crystal phase, in the geometrical case only the space group 2. the set of Kikuchi bands to consider, e.g. the {111}, {200}, {220}, and {311} crystal plane families 3. the crystal orientations with respect to the reference frame 4. the position of the detector with respect to the sample reference frame, 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\))

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

[4]:
phase = crystal_map.Phase(name="ni", space_group=225)
phase
[4]:
<name: ni. space group: Fm-3m. point group: m-3m. proper point group: 432. color: tab:blue>

We’ll set up the relevant Kikuchi bands (and the zone axes from these) in a diffsims.crystallography.ReciprocalLatticePoint instance

[5]:
rlp = ReciprocalLatticePoint(
    phase=phase, hkl=[[1, 1, 1], [2, 0, 0], [2, 2, 0], [3, 1, 1]]
)
rlp
[5]:
ReciprocalLatticePoint (4,)
Phase: ni (m-3m)
[[1 1 1]
 [2 0 0]
 [2 2 0]
 [3 1 1]]

We can get a new instance with the symmetrically equivalent planes in each plane family using ReciprocalLatticePoint.symmetrise()

[6]:
rlp2 = rlp.symmetrise()
rlp2
[6]:
ReciprocalLatticePoint (50,)
Phase: ni (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 know from pattern matching of these nine patterns, to about 7 500 dynamically simulated patterns of orientations uniformly distributed in the orientation space of the point group \(m\bar{3}m\), that they come from two grains with orientations of about \((\phi_1, \Phi, \phi_2) = (80^{\circ}, 34^{\circ}, -90^{\circ})\) and \((\phi_1, \Phi, \phi_2) = (115^{\circ}, 27^{\circ}, -95^{\circ})\). We store these orientations in an orix.quaternion.Rotation instance

[7]:
grain1 = [80, 34, -90]
grain2 = [115, 27, -95]
r = quaternion.Rotation.from_euler(np.deg2rad([
        [grain1, grain2, grain2],
        [grain1, grain2, grain2],
        [grain1, grain2, grain2]
]))
r
[7]:
Rotation (3, 3)
[[[ 0.9527 -0.0255 -0.2913  0.0833]
  [ 0.9576  0.0604 -0.2255 -0.1689]
  [ 0.9576  0.0604 -0.2255 -0.1689]]

 [[ 0.9527 -0.0255 -0.2913  0.0833]
  [ 0.9576  0.0604 -0.2255 -0.1689]
  [ 0.9576  0.0604 -0.2255 -0.1689]]

 [[ 0.9527 -0.0255 -0.2913  0.0833]
  [ 0.9576  0.0604 -0.2255 -0.1689]
  [ 0.9576  0.0604 -0.2255 -0.1689]]]

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 guide 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)

[8]:
detector = kp.detectors.EBSDDetector(
    shape=s.axes_manager.signal_shape[::-1],
    sample_tilt=70,
    pc=[0.421, 0.7794, 0.5049],
    convention="tsl"
)
detector
[8]:
EBSDDetector (60, 60), px_size 1 um, binning 1, tilt 0, azimuthal 0, pc (0.421, 0.221, 0.505)

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

Let’s create an EBSDSimulationGenerator instance

[9]:
sim_gen = kp.generators.EBSDSimulationGenerator(
    detector=detector,
    phase=phase,
    rotations=r
)
sim_gen
[9]:
EBSDSimulationGenerator (3, 3)
EBSDDetector (60, 60), px_size 1 um, binning 1, tilt 0, azimuthal 0, pc (0.421, 0.221, 0.505)
<name: ni. space group: Fm-3m. point group: m-3m. proper point group: 432. color: tab:blue>
Rotation (3, 3)

Now we’re ready to simulate geometrical EBSD patterns from the generator and the sets of crystal plane families

[10]:
sim = sim_gen.geometrical_simulation(reciprocal_lattice_point=rlp2)
sim
[10]:
GeometricalEBSDSimulation (3, 3)
EBSDDetector (60, 60), px_size 1 um, binning 1, tilt 0, azimuthal 0, pc (0.421, 0.221, 0.505)
<name: ni. space group: Fm-3m. point group: m-3m. proper point group: 432. color: tab:blue>
KikuchiBand (3, 3|27)
Rotation (3, 3)

We see that 27 of the 50 Kikuchi bands we’re visible on the detector in the nine patterns, stored in an instance of the kikuchipy.simulations.features.KikuchiBand class, which is a class inheriting from the ReciprocalLatticePoint

[11]:
sim.bands
[11]:
KikuchiBand (3, 3|27)
Phase: ni (m-3m)
[[ 1  1  1]
 [-1  1  1]
 [-1 -1  1]
 [ 1 -1  1]
 [ 0  2  0]
 [-2  0  0]
 [ 0 -2  0]
 [ 0  0  2]
 [-2  2  0]
 [-2 -2  0]
 [ 2 -2  0]
 [ 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]
 [ 1 -1  3]
 [ 1  1  3]
 [-1  1  3]
 [-1 -1  3]]
[12]:
sim.zone_axes.size
[12]:
91

We also see that there are 91 zone axes resulting from the 27 Kikuchi bands, stored in an instance of the kikuchipy.simulations.features.ZoneAxis class, also inheriting from ReciprocalLatticePoint.

We can now add these simulations as markers to be displayed on top of our experimental EBSD signal when plotting

[13]:
markers = sim.as_markers(pc=False)
s.add_marker(marker=markers, plot_marker=False, permanent=True)

The markers update with the pattern when navigating, thus helping us determine whether an indexing was successful, and in labeling the bands and zone axes in the pattern

[14]:
s.plot(navigator=None)
../_images/user_guide_geometrical_ebsd_simulations_27_0.png

Whether to plot only bands, zone axes, zone axes labels, projection center, or all of them, can be set in the GeometricalEBSDSimulation.as_markers() method. Their appearance on the pattern can also be controlled to some extent. The above method itself calls bands_as_markers(), pc_as_markers(), zone_axes_as_markers(), and zone_axes_labels_as_markers(). See their documentation for available modifications.

Let’s first remove the markers from the signal, and add only the bands and zone axes

[15]:
del s.metadata.Markers
s.add_marker(
    marker=sim.as_markers(
        pc=False,
        zone_axes_labels=False,
        bands_kwargs=dict(
            family_colors=["w", "magenta", "cyan", "lime"], linestyle="--",
        ),
        zone_axes_kwargs=dict(
            marker="s", size=150, facecolor="none", edgecolor="w",
        ),
    ),
    plot_marker=False,
    permanent=True,
)
[16]:
s.plot(navigator=None)
../_images/user_guide_geometrical_ebsd_simulations_30_0.png
[17]:
del s.metadata.Markers
s.add_marker(marker=markers, plot_marker=False, permanent=True)

We can write single EBSD patterns with the markers on top to file

[18]:
fig = s._plot.signal_plot.figure
bbox = matplotlib.transforms.Bbox.from_extents(
    np.array(fig.axes[0].bbox.extents) / 72  # The denominator may vary
)
[19]:
nav_x, nav_y = s.axes_manager.indices
temp_dir = tempfile.mkdtemp() + "/"
fname = temp_dir + f"geosim_y{nav_y}_x{nav_x}.png"
s._plot.signal_plot.figure.savefig(fname, bbox_inches=bbox, dpi=150)
[20]:
s.axes_manager.indices = (2, 1)
s.plot(navigator=None, colorbar=False, axes_off=True, title="")
../_images/user_guide_geometrical_ebsd_simulations_35_0.png
[21]:
nav_x, nav_y = s.axes_manager.indices
fname = temp_dir + f"geosim_y{nav_y}_x{nav_x}.png"
s._plot.signal_plot.figure.savefig(fname, bbox_inches=bbox, dpi=150)

The coordinates of these bands and zone axes are available as class attributes. For the bands, we can e.g. extract the plane trace coordinates (y0, x0, y1, x1) in either gnomonic or detector coordinates (taking into account the detector size in pixels) for all bands or per navigation position

[22]:
sim.bands[0, 0].plane_trace_coordinates[:10]  # Gnomonic
[22]:
array([[-1.86962815,  1.3839786 , -0.44997371,  1.33513632],
       [-0.19287074, -1.71126251, -1.91331823,  0.87724933],
       [ 1.81954214, -0.52126096,  0.62229589, -1.85101939],
       [-0.8909565 ,  1.52423106,  1.70416615, -1.17247834],
       [-0.37289001,  0.25549816, -1.88651499,  1.90596602],
       [ 1.84727965, -1.92275022, -0.53436284,  0.03189612],
       [        nan,         nan,         nan,         nan],
       [        nan,         nan,         nan,         nan],
       [ 0.91928302, -1.2811585 , -1.68905432,  1.43409158],
       [ 1.61490871, -1.45501536,  1.04405729, -1.25734485]])
[23]:
sim.bands_detector_coordinates[0, 0, :10]  # Detector
[23]:
array([[-30.85553999,  26.41971173,  66.06647692, -26.75710942],
       [ 19.09355422,  70.01142803, -26.13796996, -13.11706816],
       [ 79.0415227 ,  -5.52223447,   9.31110502,  68.15560163],
       [ -1.70179228, -37.75017597,  70.24447135,  47.94247445],
       [ 13.73094208,  69.21298364,  32.45006018, -43.76161226],
       [ 79.86779834,  28.93358806, -32.43799853,  12.06524342],
       [         nan,          nan,          nan,          nan],
       [         nan,          nan,          nan,          nan],
       [ 52.22361382,  63.33080802, -13.32555877, -29.70489743],
       [ 72.94567699, -18.08612708, -18.50459793,  50.47057156]])

The NaN values signify that that particular band is not visible on the detector in that position. The crystal plane normal of each band, pointing from the source point to the detector, is also available

[24]:
sim.bands[1, 1].hkl_detector[:10]
[24]:
Vector3d (10,)
[[ 1.4237 -0.7112  0.6836]
 [ 0.8314  1.1793  0.9581]
 [-1.0399  0.5474  1.2724]
 [-0.4477 -1.3431  0.9979]
 [ 1.8714  0.6319 -0.3142]
 [-0.5923  1.8905  0.2745]
 [-1.8714 -0.6319  0.3142]
 [ 0.3837 -0.1638  1.956 ]
 [ 1.2791  2.5224 -0.0397]
 [-2.4636  1.2586  0.5887]]

And where the vector hits the detector, in either detector or gnomonic coordinates

[25]:
sim.bands[0, 0].x_detector
[25]:
array([ 0.74374406,  1.03166579, -0.93794972, -1.22587145,  1.96961551,
        0.28792173, -1.96961551, -0.19420566,  2.25753723, -1.68169378,
       -2.25753723,  1.77540985,  0.09371607, -2.16382116, -0.48212738,
        0.45582233,  3.00128129, -0.65002799, -3.19548695,  2.71335957,
        1.31958752, -2.90756522, -1.51379317, -1.4200771 ,  0.5495384 ,
        0.83746013, -1.13215538])
[26]:
sim.bands[0, 0].x_gnomonic
[26]:
array([ 0.95284409,  0.81041393, -0.81262043, -1.852415  , 16.58171874,
        0.58466103, 16.58171874, -0.10037608,  3.69336327, -4.50039545,
        3.69336327,  0.86455112,  0.03861015, -1.19153285, -0.3342719 ,
        1.58220857,  2.15641313, -0.39474877, -5.88501573,  3.01707649,
        0.74744255, -2.80803112, -8.94095047, -0.54690929,  0.20238351,
        0.26107065, -0.36651089])

The same information is available for the zone axes

[27]:
sim.zone_axes_label_detector_coordinates[0, 0][20:30]  # Detector
[27]:
array([[18.15541142,  6.66929505],
       [        nan,         nan],
       [29.1906983 ,  8.58678901],
       [49.44397197,  5.76573284],
       [        nan,         nan],
       [        nan,         nan],
       [        nan,         nan],
       [        nan,         nan],
       [        nan,         nan],
       [10.97072449, 11.12462908]])
[28]:
sim.zone_axes[0, 1].uvw_detector[:10]
[28]:
Vector3d (10,)
[[-3.5439  4.1762  0.0225]
 [-3.1602  4.0124  1.9785]
 [-2.0326  4.2464  2.7994]
 [-0.1613  4.8783  2.4851]
 [ 0.1988  5.44   -0.606 ]
 [ 0.5826  5.2762  1.35  ]
 [-1.9283  3.3831  1.6841]
 [-0.4407  4.1788 -0.5861]
 [-0.057   4.015   1.3699]
 [-3.3117  1.7241  2.8391]]
[29]:
sim.zone_axes[0, 0].y_detector[:10]
[29]:
array([5.35259241, 4.88471474, 4.4876    , 4.16124817, 4.69988877,
       4.2320111 , 3.76307848, 3.90460433, 3.43672666, 2.89703112])
[30]:
sim.zone_axes[0, 0].y_gnomonic[:10]
[30]:
array([36.92063784,  2.3486962 ,  1.44456664,  1.29018168, 12.28599581,
        1.82625189,  1.98797814, 50.76220008,  1.70836955,  1.16099717])

With this information, it should be straight forward to go around kikuchipy when plotting and only use matplotlib

[31]:
nav_idx = (2, 1)[::-1]

fig, ax = plt.subplots(figsize=(5, 5))
ax.imshow(s.inav[nav_idx], cmap="gray")

print(sim.bands_detector_coordinates.shape)
for i in np.ndindex(sim.bands_detector_coordinates.shape[2]):
    sim_slice = nav_idx + (i,)
    coords = sim.bands_detector_coordinates[sim_slice][0]
    y0, x0, y1, x1 = coords
    ax.axline((y0, x0), (y1, x1), linestyle="--", color="w")

print(sim.zone_axes_detector_coordinates.shape)
for j in np.ndindex(sim.zone_axes_detector_coordinates.shape[2]):
    sim_slice = nav_idx + (j,)
    coords = sim.zone_axes_detector_coordinates[sim_slice][0]
    x, y = coords
    ax.scatter(x=x, y=y, zorder=5, s=50)

_ = ax.axis((0, 59, 59, 0))
(3, 3, 27, 4)
(3, 3, 91, 2)
../_images/user_guide_geometrical_ebsd_simulations_51_1.png
[32]:
# Remove files written to disk in this user guide
import os
for file in ["geosim_y0_x0.png", "geosim_y1_x2.png"]:
    os.remove(temp_dir + file)
os.rmdir(temp_dir)