diff --git a/.zenodo.json b/.zenodo.json
index de384312..604945ea 100644
--- a/.zenodo.json
+++ b/.zenodo.json
@@ -31,6 +31,10 @@
"name": "Magnus Nord",
"orcid": "0000-0001-7981-5293",
"affiliation": "Norwegian University of Science and Technology"
+ },
+ {
+ "name": "Zachary Varley",
+ "affiliation": "Carnegie Mellon University"
}
]
-}
\ No newline at end of file
+}
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 1af0a279..12a864d1 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -19,6 +19,8 @@ Unreleased
Added
-----
+Fast PCA Dictionary Indexing
+
Changed
-------
diff --git a/doc/tutorials/pattern_matching_fast.ipynb b/doc/tutorials/pattern_matching_fast.ipynb
new file mode 100644
index 00000000..2bffe829
--- /dev/null
+++ b/doc/tutorials/pattern_matching_fast.ipynb
@@ -0,0 +1,329 @@
+{
+ "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 matching them to simulated patterns of known phases and orientations, see e.g. Chen et al. (2015), Nolze et al. (2016), Foden et al. (2019).\n",
+ "\n",
+ "In this tutorial, we will perform *dictionary indexing* (DI) using a small Ni EBSD data set and a dynamically simulated Ni master pattern from EMsoft, both of low resolution and found in the [kikuchipy.data](../reference/generated/kikuchipy.data.rst) module.\n",
+ "The pattern dictionary is generated from a uniform grid of orientations with a fixed projection center (PC) followng Singh and De Graef (2016).\n",
+ "The true orientation is likely to fall in between grid points, which means there is always a lower angular accuracy associated with DI.\n",
+ "We can improve upon each solution by letting the orientation deviate from the grid points.\n",
+ "We do this by maximizing the similarity between experimental and simulated patterns using numerical optimization algorithms.\n",
+ "This is here called *orientation refinement*.\n",
+ "We could instead keep the orientations fixed and let the PC parameters deviate from their fixed values used in the dictionary, here called *projection center refinement*.\n",
+ "Finally, we can also refine both at the same time, here called *orientation and projection center refinement*.\n",
+ "The need for orientation or orientation and PC refinement is discussed by e.g. Singh et al. (2017), Winkelmann et al. (2020), and Pang et al. (2020).\n",
+ "\n",
+ "The term *pattern matching* is here used for the combined approach of DI followed by refinement.\n",
+ "\n",
+ "Before we can generate a dictionary of simulated patterns, we need a master pattern containing all possible scattering vectors for a candidate phase.\n",
+ "This can be simulated using EMsoft (Callahan and De Graef (2013) and Jackson et al. (2014)) and then read into kikuchipy.\n",
+ "\n",
+ "First, we import libraries"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2023-05-13T12:26:10.880617050Z",
+ "start_time": "2023-05-13T12:26:09.630506890Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# Exchange inline for notebook or qt5 (from pyqt) for interactive plotting\n",
+ "%matplotlib inline\n",
+ "\n",
+ "import tempfile\n",
+ "\n",
+ "import matplotlib.pyplot as plt\n",
+ "import numpy as np\n",
+ "\n",
+ "import hyperspy.api as hs\n",
+ "import kikuchipy as kp\n",
+ "from orix import sampling, plot, io\n",
+ "from orix.vector import Vector3d\n",
+ "\n",
+ "\n",
+ "plt.rcParams.update({\"figure.facecolor\": \"w\", \"font.size\": 15})"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Load the small experimental nickel test data"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "tags": [
+ "nbval-ignore-output"
+ ],
+ "ExecuteTime": {
+ "end_time": "2023-05-13T12:26:18.263046637Z",
+ "start_time": "2023-05-13T12:26:10.882308954Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "2023-05-13 08:26:12,186 - numexpr.utils - INFO - Note: NumExpr detected 16 cores but \"NUMEXPR_MAX_THREADS\" not set, so enforcing safe limit of 8.\n",
+ "2023-05-13 08:26:12,187 - numexpr.utils - INFO - NumExpr defaulting to 8 threads.\n",
+ "2023-05-13 08:26:12,535 - numba.cuda.cudadrv.driver - INFO - init\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[########################################] | 100% Completed | 100.95 ms\n",
+ "[########################################] | 100% Completed | 403.06 ms\n",
+ "Dictionary size: 100347\n",
+ "[########################################] | 100% Completed | 1.61 ss\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Use kp.load(\"data.h5\") to load your own data\n",
+ "s = kp.data.nickel_ebsd_large(allow_download=True) # External download\n",
+ "s.remove_static_background()\n",
+ "s.remove_dynamic_background()\n",
+ "# load master pattern\n",
+ "energy = 20\n",
+ "mp = kp.data.nickel_ebsd_master_pattern_small(\n",
+ " projection=\"lambert\", energy=energy\n",
+ ")\n",
+ "ni = mp.phase\n",
+ "rot = sampling.get_sample_fundamental(\n",
+ " method=\"cubochoric\", resolution=2.0, point_group=ni.point_group\n",
+ ")\n",
+ "print(\"Dictionary size: \", rot.shape[0])\n",
+ "det = kp.detectors.EBSDDetector(\n",
+ " shape=s.axes_manager.signal_shape[::-1],\n",
+ " pc=[0.4198, 0.2136, 0.5015],\n",
+ " sample_tilt=70,\n",
+ ")\n",
+ "sim = mp.get_patterns(\n",
+ " rotations=rot,\n",
+ " detector=det,\n",
+ " energy=energy,\n",
+ " dtype_out=np.float32,\n",
+ " compute=True,\n",
+ ")\n",
+ "signal_mask = ~kp.filters.Window(\"circular\", det.shape).astype(bool)\n",
+ "p = s.inav[0, 0].data"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Finally, let's use the [pca_dictionary_indexing()](../reference/generated/kikuchipy.signals.EBSD.pca_dictionary_indexing.rst) method to match the simulated patterns to our experimental patterns. Let's keep the best matching orientation alone. The results are returned as a [orix.crystal_map.CrystalMap](https://orix.readthedocs.io/en/stable/reference/generated/orix.crystal_map.CrystalMap.html)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2023-05-13T12:27:16.660506988Z",
+ "start_time": "2023-05-13T12:27:09.423828612Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "Experiment loop: 0%| | 0/2 [00:00, ?it/s]\u001B[A\n",
+ "Experiment loop: 50%|█████ | 1/2 [00:01<00:01, 1.35s/it]\u001B[A\n",
+ " \u001B[A"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " Indexing speed: 619.90476 patterns/s, \n"
+ ]
+ },
+ {
+ "data": {
+ "text/plain": "Phase Orientations Name Space group Point group Proper point group Color\n 0 4125 (100.0%) ni Fm-3m m-3m 432 tab:blue\nProperties: scores, simulation_indices\nScan unit: um"
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from kikuchipy.indexing.di_indexers import PCAIndexer\n",
+ "indexer = PCAIndexer(\n",
+ " datatype='float32',\n",
+ " zero_mean=True,\n",
+ " unit_var=False,\n",
+ " whiten=False,\n",
+ " n_components=500,\n",
+ " )\n",
+ "xmap = s.pca_dictionary_indexing(\n",
+ " dictionary=sim,\n",
+ " indexer=indexer,\n",
+ " keep_n=1,\n",
+ " n_experimental_per_iteration=4096,\n",
+ " navigation_mask= None,\n",
+ " signal_mask = signal_mask\n",
+ ")\n",
+ "xmap"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The results can be exported to an HDF5 file re-readable by orix, or an .ang file readable by MTEX and some commercial packages"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2023-05-13T12:27:19.409102819Z",
+ "start_time": "2023-05-13T12:27:19.368511426Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "temp_dir = tempfile.mkdtemp() + \"/\"\n",
+ "io.save(temp_dir + \"ni.h5\", xmap)\n",
+ "io.save(temp_dir + \"ni.ang\", xmap)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Validate indexing results\n",
+ "\n",
+ "With the [orix library](https://orix.readthedocs.io) we can plot inverse pole figures (IPFs), color orientations to produce orientation maps (also called IPF maps), and more.\n",
+ "This is useful to quickly validate our results before processing them further.\n",
+ "If we want to analyze the results in terms of reconstructed grains, textures from orientation density functions, or similar, we have to use other software, like MTEX.\n",
+ "\n",
+ "Let's generate an IPF color key and plot it"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {
+ "tags": [],
+ "ExecuteTime": {
+ "end_time": "2023-05-13T12:27:20.613613247Z",
+ "start_time": "2023-05-13T12:27:20.436274547Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/plain": "