This page was generated from doc/tutorials/clustering_across_fundamental_region_boundaries.ipynb. Interactive online version: Binder badge.

Clustering across fundamental region boundaries#

In this tutorial we will perform density based clustering of crystal orientations with and without the application of crystal symmetry 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 artificial data#

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 = 65  # 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()

Orientation clustering#

Perform clustering without application of crystal symmetry#

Compute misorientations, i.e. distance between orientations

# Remove symmetry by setting it to point group 1 (identity operation)
ori_without_symmetry = Orientation(, 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_))
Labels: [0 1 2 3 4]

Perform clustering with application of crystal symmetry#

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"
print("Labels:", np.unique(dbscan.labels_))
Labels: [0 1 2]

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)

ani = animation.FuncAnimation(
    fig, animate, np.linspace(75, 360 + 74, 720), interval=25