{ "cells": [ { "cell_type": "markdown", "metadata": { "nbsphinx": "hidden" }, "source": [ "This notebook is part of the `kikuchipy` documentation https://kikuchipy.org.\n", "Links to the documentation won't work from the notebook." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Pattern matching\n", "\n", "Crystal orientations can be determined from experimental EBSD patterns by\n", "matching them to a dictionary of simulated patterns of known orientations\n", "Chen et al. (2015),\n", "Nolze et al. (2016), \n", "Foden et al. (2019).\n", "\n", "Here, we will demonstrate pattern matching using a Ni data set of 4125 EBSD\n", "patterns and a dynamically simulated master pattern from EMsoft, both of low\n", "resolution and found in the [kikuchipy.data](reference.rst#data) module.\n", "\n", "
\n", "\n", "Note\n", "\n", "The generated pattern dictionary is discrete, but no refinement of the best\n", "matching orientation is provided. The need for the latter is discussed in e.g.\n", "Singh et al. (2017).\n", "\n", "
\n", "\n", "Before we can generate a dictionary of\n", "simulated patterns, we need a master pattern containing all possible scattering\n", "vectors for a candidate phase. This can simulated done using EMsoft\n", "Callahan and De Graef (2013)\n", "Jackson et al. (2014), and then read\n", "into kikuchipy." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First, we import libraries and load the small experimental Nickel test data." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# exchange inline for qt5 for interactive plotting from the pyqt package\n", "%matplotlib inline\n", "\n", "import tempfile\n", "import matplotlib.pyplot as plt\n", "plt.rcParams[\"font.size\"] = 15\n", "import hyperspy.api as hs\n", "import numpy as np\n", "from orix import sampling, plot, io\n", "import kikuchipy as kp\n", "\n", "\n", "s = kp.data.nickel_ebsd_large() # Use kp.load(\"data.h5\") to load your own data\n", "s" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To obtain a good match, we must increase the signal-to-noise ratio. In this\n", "pattern matching analysis, the Kikuchi bands are considered the signal, and the\n", "angle-dependent backscatter intensity, along with unwanted detector effects,\n", "are considered to be noise. See the\n", "[pattern processing guide](pattern_processing.rst) for further details." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "s.remove_static_background()\n", "s.remove_dynamic_background()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we load a dynamically simulated Nickel master pattern generated with\n", "EMsoft, in the northern hemisphere projection of the square Lambert projection\n", "for an accelerating voltage of 20 keV." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "mp = kp.data.nickel_ebsd_master_pattern_small(projection=\"lambert\", energy=20)\n", "mp" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "mp.plot()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The Nickel phase information, specifically the crystal symmetry, asymmetric atom\n", "positions, and crystal lattice, is conveniently stored in an\n", "[orix.crystal_map.Phase](https://orix.readthedocs.io/en/stable/reference.html#orix.crystal_map.phase_list.Phase)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ni = mp.phase\n", "ni" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ni.structure # Element, x, y, z, site occupation" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ni.structure.lattice # nm and degrees" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we don't know anything about the possible crystal (unit cell) orientations in\n", "our sample, the safest thing to do is to generate a dictionary of orientations\n", "uniformly distributed in a candidate phase's orientation space. To achieve this,\n", "we sample the Rodrigues Fundamental Zone of the proper point group *432* with a\n", "4$^{\\circ}$ characteristic distance between orientations (we can either pass\n", "in the proper point group, or the space group, which is a subgroup of the proper\n", "point group) using\n", "[orix.sampling.get_sample_fundamental()](https://orix.readthedocs.io/en/stable/reference.html#orix.sampling.sample_generators.get_sample_fundamental)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "r = sampling.get_sample_fundamental(\n", " resolution=4, space_group=ni.space_group.number\n", ")\n", "r" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This sampling resulted in 14423 crystal orientations.\n", "\n", "
\n", "\n", "Note\n", "\n", "A characteristic distance of 4$^{\\circ}$ results in a course sampling of\n", "orientation space; a shorter distance should be used for real experimental work.\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we have our master pattern and crystal orientations, we need to\n", "describe the EBSD detector's position with respect to the sample (interaction\n", "volume). This ensures that projecting parts of the master pattern onto our\n", "detector yields dynamically simulated patterns resembling our experimental ones.\n", "See the [reference frames](reference_frames.rst) user guide and the\n", "[EBSDDetector](reference.rst#ebsddetector) class for further details." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "detector = kp.detectors.EBSDDetector(\n", " shape=s.axes_manager.signal_shape[::-1],\n", " pc=[0.421, 0.7794, 0.5049],\n", " sample_tilt=70,\n", " convention=\"tsl\",\n", ")\n", "detector" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's double check the projection/pattern center (PC) position on the detector\n", "using\n", "[plot()](reference.rst#kikuchipy.detectors.ebsd_detector.EBSDDetector.plot)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "detector.plot(coordinates=\"gnomonic\", pattern=s.inav[0, 0].data)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we're ready to generate our dictionary of simulated patterns by projecting\n", "parts of the master pattern onto our detector for all sampled orientations,\n", "using the\n", "[get_patterns()](reference.rst#kikuchipy.signals.ebsdmasterpattern.get_patterns)\n", "method. The method assumes the crystal orientations are represented with respect\n", "to the EDAX TSL sample reference frame RD-TD-ND." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sim = mp.get_patterns(\n", " rotations=r,\n", " detector=detector,\n", " energy=20,\n", " dtype_out=s.data.dtype,\n", " compute=True\n", ")\n", "sim" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's inspect the three first of the 14423 simulated patterns." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "#sim.plot() # Plot the patterns with a navigator for easy inspection\n", "fig, ax = plt.subplots(ncols=3, figsize=(18, 6))\n", "for i in range(3):\n", " ax[i].imshow(sim.inav[i].data, cmap=\"gray\")\n", " euler = np.rad2deg(sim.xmap[i].rotations.to_euler())[0]\n", " ax[i].set_title(\n", " f\"($\\phi_1, \\Phi, \\phi_2)$ = {np.array_str(euler, precision=1)}\"\n", " )\n", " ax[i].axis(\"off\")\n", "fig.tight_layout()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Finally, let's use the\n", "[match_patterns()](reference.rst#kikuchipy.signals.EBSD.match_patterns) method\n", "to match the simulated patterns to our nine experimental patterns, using the\n", "[zero-mean normalized cross correlation (NCC)](reference.rst#kikuchipy.indexing.similarity_metrics.ncc)\n", "coefficient $r$\n", "Gonzalez & Woods (2017), which is\n", "the default similarity metric. Let's keep the 10 best matching orientations. A\n", "number of 4125 * 14423 comparisons is quite small, which we can do in memory all\n", "at once. However, in cases where the number of comparisons are too big for our\n", "memory to handle, we can slice our simulated pattern data into $n$ slices. To\n", "demonstrate this, we use 10 slices here. The results are returned as a\n", "[orix.crystal_map.CrystalMap](https://orix.readthedocs.io/en/latest/reference.html#crystalmap)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "xmap = s.match_patterns(sim, n_slices=10, keep_n=10)\n", "xmap" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The results can be exported to an HDF5 file re-readable by orix." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "temp_dir = tempfile.mkdtemp()\n", "xmap_file = temp_dir + \"ni.h5\"\n", "io.save(xmap_file, xmap)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's inspect our matching results by plotting a map of the highest $r$\n", "(stored in the `scores` property)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots(subplot_kw=dict(projection=\"plot_map\"))\n", "ax.plot_map(xmap, xmap.scores[:, 0], scalebar=False)\n", "ax.add_colorbar(label=r\"$r$\")\n", "_ = ax.axis(\"off\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can use the crystal map property `simulation_indices` to get the best\n", "matching simulated patterns from the dictionary of simulated patterns." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "best_patterns = sim.data[xmap.simulation_indices[:, 0]].reshape(s.data.shape)\n", "s_best = kp.signals.EBSD(best_patterns)\n", "s_best" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The simplest way to visually compare the experimental and best matching\n", "simulated patterns are to\n", "[plot them in the same navigator](visualizing_patterns.ipynb#plot-multiple-signals).\n", "Here, we use the highest $r$ as a navigator. When using an interactive backend\n", "like `Qt5Agg`, we can then move the red square around to look at the patterns in\n", "each point." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ncc_navigator = hs.signals.Signal2D(xmap.get_map_data(xmap.scores[:, 0]))\n", "hs.plot.plot_signals([s, s_best], navigator=hs.signals.Signal2D(ncc_navigator))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's also plot the best matches for patterns from two grains" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "nbsphinx-thumbnail": { "tooltip": "Crystal orientation determination by comparing experimental EBSD patterns to a dictionary of simulated patterns obtained from a master pattern" }, "tags": [ "nbsphinx-thumbnail" ] }, "outputs": [], "source": [ "grain1 = (0, 0)\n", "grain2 = (30, 10)\n", "fig, ax = plt.subplots(ncols=2, nrows=2, figsize=(10, 10))\n", "ax[0, 0].imshow(s.inav[grain1].data, cmap=\"gray\")\n", "ax[0, 0].axis(\"off\")\n", "ax[0, 1].imshow(s_best.inav[grain1].data, cmap=\"gray\")\n", "ax[0, 1].axis(\"off\")\n", "ax[1, 0].imshow(s.inav[grain2].data, cmap=\"gray\")\n", "ax[1, 0].axis(\"off\")\n", "ax[1, 1].imshow(s_best.inav[grain2].data, cmap=\"gray\")\n", "ax[1, 1].axis(\"off\")\n", "fig.tight_layout(h_pad=0.5, w_pad=1)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "nbsphinx": "hidden" }, "outputs": [], "source": [ "import os\n", "os.rmdir(temp_dir)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.6" } }, "nbformat": 4, "nbformat_minor": 4 }