This notebook is part of the orix documentation https://orix.readthedocs.io. Links to the documentation won’t work from the notebook.
This tutorial demonstrates density based clustering of crystal orientations with and without the application of crystal symmetry using simulated data, as presented in Johnstone et al. (2020).
Import orix classes and various dependencies
# exchange inline for notebook (or qt5 from pyqt) for interactive plotting
%matplotlib inline
# Import core external
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import DBSCAN
# Colorisation & Animation
from skimage.color import label2rgb
from matplotlib.colors import to_rgb
import matplotlib.animation as animation
# Import orix classes
from orix.quaternion import Orientation, OrientationRegion, Rotation
from orix.quaternion.symmetry import C1, Oh
Generate three random von Mises distributions of orientations as model clusters and set the Oh ($m\bar{3}m$) point group symmetry
n_orientations = 50
alpha = 50 # Lower value gives "looser" distribution
# Cluster 1
cluster1 = Rotation.random_vonmises(n_orientations, alpha=alpha)
# Cluster 2
centre2 = Rotation.from_axes_angles((1, 0, 0), np.pi / 4)
cluster2 = Rotation.random_vonmises(n_orientations, alpha=alpha, reference=centre2)
# Cluster 3
centre3 = Rotation.from_axes_angles((1, 1, 0), np.pi / 3)
cluster3 = Rotation.random_vonmises(n_orientations, alpha=alpha, reference=centre3)
# Stack and map into the Oh fundamental zone
ori = Orientation.stack([cluster1, cluster2, cluster3]).flatten()
ori.symmetry = Oh
ori = ori.map_into_symmetry_reduced_zone()
Compute misorientations, i.e. distance between orientations
# Remove symmetry by setting it to point group 1 (identity operation)
ori_without_symmetry = Orientation(ori.data, symmetry=C1)
# Misorientations
mori1 = (~ori_without_symmetry).outer(ori_without_symmetry)
# Misorientation angles
D1 = mori1.angle
Perform clustering
dbscan_naive = DBSCAN(eps=0.3, min_samples=10, metric="precomputed").fit(D1)
print("Labels:", np.unique(dbscan_naive.labels_))
Compute misorientations, i.e. distance between orientations, with symmetry
mori2 = (~ori).outer(ori)
mori2.symmetry = Oh
mori2 = mori2.map_into_symmetry_reduced_zone()
D2 = mori2.angle
Perform clustering
dbscan = DBSCAN(
eps=np.deg2rad(17), min_samples=20, metric="precomputed"
).fit(D2.astype(np.float32))
print("Labels:", np.unique(dbscan.labels_))
This should have shown that without symmetry there are 6 clusters, whereas with symmetry there are 3.
Assign colours to each cluster
color_names = [to_rgb(f"C{i}") for i in range(6)] # ['C0', 'C1', ...]
colors_naive = label2rgb(dbscan_naive.labels_, colors=color_names, bg_label=-1)
colors = label2rgb(dbscan.labels_, colors=color_names, bg_label=-1)
Plot orientation clusters with Matplotlib and (Mis)orientation.scatter()
# Set symmetry to "trick" the scatter plot to use the Oh fundamental zone
ori_without_symmetry.symmetry = ori.symmetry
# Create figure with a height/width ratio of 1/2
fig = plt.figure(figsize=(12, 6))
# Add the fundamental zones with clusters to the existing figure
ori_without_symmetry.scatter(figure=fig, position=(1, 2, 1), c=colors_naive)
ori.scatter(figure=fig, position=122, c=colors)
Generate an animation of the plot (assuming an interactive Matplotlib backend is used)
def animate(angle):
fig.axes[0].view_init(15, angle)
fig.axes[1].view_init(15, angle)
plt.draw()
ani = animation.FuncAnimation(fig, animate, np.linspace(75, 360+74, 720), interval=25)