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

Clustering misorientations#

In this tutorial we will cluster Ti crystal misorientations using data obtained from a highly deformed specimen, using EBSD, as presented in [Johnstone et al., 2020]. The data can be downloaded to your local cache via the orix.data module.

Import orix classes and various dependencies

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

from matplotlib.colors import to_rgb
from matplotlib.lines import Line2D
import matplotlib.pyplot as plt
import numpy as np
from skimage.color import label2rgb
from sklearn.cluster import DBSCAN

from orix.plot import register_projections, IPFColorKeyTSL
from orix import data
from orix.quaternion import Misorientation, Rotation
from orix.vector import Vector3d

plt.rcParams.update({"font.size": 20, "figure.figsize": (10, 10)})

register_projections()

Import data#

Load Ti orientations with the point group symmetry 622 (D6). We have to explicitly allow downloading from an external source.

[2]:
ori = data.ti_orientations(allow_download=True)
ori
/home/docs/checkouts/readthedocs.org/user_builds/orix/conda/latest/lib/python3.14/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
[2]:
Orientation (193167,) 622
[[ 0.3027  0.0869 -0.5083  0.8015]
 [ 0.3088  0.0868 -0.5016  0.8034]
 [ 0.3057  0.0818 -0.4995  0.8065]
 ...
 [ 0.4925 -0.1633 -0.668   0.5334]
 [ 0.4946 -0.1592 -0.6696  0.5307]
 [ 0.4946 -0.1592 -0.6696  0.5307]]

Extract the symmetry

[3]:
sym = ori.symmetry

Reshape the orientation mapping data to the correct spatial dimension for the scan and select a subset of the orientations with a suitable size for this demonstration (bottom left corner)

[4]:
ori = ori.reshape(381, 507)
ori = ori[-100:, :200]

Plot orientation maps

[5]:
ckey = IPFColorKeyTSL(sym)

directions = [(1, 0, 0), (0, 1, 0)]
titles = ["X", "Y"]

fig, axes = plt.subplots(ncols=2, figsize=(15, 10))
for i, ax in enumerate(axes):
    ckey.direction = Vector3d(directions[i])
    ax.imshow(ckey.orientation2color(ori))
    ax.set_title(f"IPF-{titles[i]}")
    ax.axis("off")

# Add color key
ax_ipfkey = fig.add_axes(
    [0.932, 0.37, 0.1, 0.1],  # (Left, bottom, width, height)
    projection="ipf",
    symmetry=ori.symmetry.laue,
)
ax_ipfkey.plot_ipf_color_key()
ax_ipfkey.set_title("")
fig.subplots_adjust(wspace=0.01)
../_images/tutorials_clustering_misorientations_10_0.png

Map the orientations into the Rodrigues fundamental zone (find symmetrically equivalent orientations with the smallest angle of rotation) of 622

[6]:
ori = ori.reduce()

Compute misorientations \(m_{AB} = g_B \cdot g_A^{-1}\) (in the horizontal direction)

[7]:
mori_all = Misorientation(ori[:, :-1] * ~ori[:, 1:], symmetry=(sym, sym))

Keep only misorientations with a disorientation angle higher than 7\(^{\circ}\), assumed to represent grain boundaries

[8]:
boundary_mask = mori_all.angle > np.deg2rad(7)
mori = mori_all[boundary_mask]

Map the misorientations into the fundamental zone of 622-622

[9]:
mori = mori.reduce()

Compute distance matrix#

[10]:
D = mori.get_distance_matrix(lazy=False)

Clustering#

Apply mask to remove small misorientations associated with grain orientation spread

[11]:
small_mask = mori.angle < np.deg2rad(7)
D = D[~small_mask][:, ~small_mask]
mori = mori[~small_mask]

For parameter explanations of the DBSCAN algorithm (Density-Based Spatial Clustering for Applications with Noise), see the scikit-learn documentation.

[12]:
# Compute clusters
D = D.astype("float32")
dbscan = DBSCAN(eps=0.05, min_samples=10, metric="precomputed").fit(D)

unique_labels, all_cluster_sizes = np.unique(
    dbscan.labels_, return_counts=True
)
print("Labels:", unique_labels)

n_clusters = unique_labels.size - 1
print("Number of clusters:", n_clusters)
Labels: [-1  0  1  2  3]
Number of clusters: 4

Calculate the mean misorientation associated with each cluster

[13]:
unique_cluster_labels = unique_labels[1:]  # Drop "no-cluster" label
cluster_sizes = all_cluster_sizes[1:]

rc = Rotation.from_axes_angles((0, 0, 1), 15, degrees=True)

mori_mean = []
for label in unique_cluster_labels:
    # Rotate
    mori_i = rc * mori[dbscan.labels_ == label]

    # Map into the fundamental zone
    mori_i.symmetry = (sym, sym)
    mori_i = mori_i.reduce()

    # Get the cluster mean
    mori_i = mori_i.mean()

    # Rotate back and add to list
    cluster_mean_local = ~rc * mori_i
    mori_mean.append(cluster_mean_local)

cluster_means = Misorientation.stack(mori_mean).flatten()

# Map into the fundamental zone
cluster_means.symmetry = (sym, sym)
cluster_means = cluster_means.reduce()
cluster_means
[13]:
Misorientation (4,) 622, 622
[[ 0.8467  0.2664  0.4606  0.0037]
 [-0.7858 -0.3094 -0.5355 -0.0044]
 [-0.9515 -0.3075 -0.0015 -0.007 ]
 [ 0.8656  0.4338  0.2501  0.0055]]

Inspect misorientations in the axis-angle representation

[14]:
cluster_means.axis
[14]:
Vector3d (4,)
[[0.5006 0.8656 0.007 ]
 [0.5003 0.8658 0.0072]
 [0.9997 0.0048 0.0228]
 [0.8663 0.4994 0.0109]]
[15]:
np.rad2deg(cluster_means.angle)
[15]:
array([64.29349881, 76.40971082, 35.82903611, 60.10115054])

Define reference misorientations associated with twinning orientation relationships

[16]:
# From Krakow et al.
twin_theory = Rotation.from_axes_angles(
    axes=[
        (1, 0, 0),  # sigma7a
        (1, 0, 0),  # sigma11a
        (2, 1, 0),  # sigma11b
        (1, 0, 0),  # sigma13a
        (2, 1, 0),  # sigma13b
    ],
    angles=[64.40, 34.96, 85.03, 76.89, 57.22],
    degrees=True,
)

Calculate difference, defined as minimum rotation angle, between measured and theoretical values. This procedure accounts for the edges of the fundamental zone.

[17]:
mori2 = twin_theory.outer(cluster_means)
sym_ops = sym.outer(sym).unique()
mori2_equiv = (
    sym.outer(twin_theory).outer(sym_ops).outer(cluster_means).outer(sym)
)
D2 = mori2_equiv.angle.min(axis=(0, 2, 4))
[18]:
[18]:
array([[ 0.44196598, 12.01907556, 28.59166542, 31.00602591],
       [29.33528603, 41.45127188,  1.18969475, 34.15185331],
       [37.89471722, 35.23189781, 54.83306878, 25.25351347],
       [12.60503699,  0.70196626, 41.07796156, 37.24075935],
       [27.53333357, 34.58980591, 29.35942936,  4.44471339]])

We see that the first, second, third, and fourth clusters are within \(4.5^{\circ}\) of \(\Sigma7\)a, \(\Sigma13\)a, \(\Sigma11\)a, and \(\Sigma13\)b, respectively.

Visualisation#

Associate colours with clusters for plotting

[19]:
colors = [to_rgb(f"C{i}") for i in range(cluster_means.size)]
labels_rgb = label2rgb(dbscan.labels_, colors=colors, bg_label=-1)

Inspect misorientation axes of clusters in an inverse pole figure

[20]:
cluster_sizes = all_cluster_sizes[1:]
cluster_sizes_scaled = 1000 * cluster_sizes / cluster_sizes.max()

fig = plt.figure(figsize=(6, 6))
ax = fig.add_subplot(projection="ipf", symmetry=sym)
ax.scatter(
    cluster_means.axis, c=colors, s=cluster_sizes_scaled, alpha=0.5, ec="k"
)
../_images/tutorials_clustering_misorientations_39_0.png

Plot a top view of the misorientation clusters within the fundamental zone for the (D6, D6) bicrystal symmetry

[21]:
wireframe_kwargs = dict(
    color="black", linewidth=0.5, alpha=0.1, rcount=361, ccount=361
)
fig = mori.scatter(
    projection="axangle",
    wireframe_kwargs=wireframe_kwargs,
    c=labels_rgb.reshape(-1, 3),
    s=4,
    alpha=0.5,
    return_figure=True,
)
ax = fig.axes[0]
ax.view_init(elev=90, azim=-60)

handle_kwds = dict(marker="o", color="none", markersize=10)
handles = []
for i in range(n_clusters):
    line = Line2D(
        [0], [0], label=i + 1, markerfacecolor=colors[i], **handle_kwds
    )
    handles.append(line)
ax.legend(
    handles=handles,
    loc="upper left",
    numpoints=1,
    labelspacing=0.15,
    columnspacing=0.15,
    handletextpad=0.05,
);
../_images/tutorials_clustering_misorientations_41_0.png

Plot side view of misorientation clusters in the fundamental zone for the (D6, D6) bicrystal symmetry

[22]:
fig2 = mori.scatter(
    return_figure=True,
    wireframe_kwargs=wireframe_kwargs,
    c=labels_rgb.reshape(-1, 3),
    s=4,
)
ax2 = fig2.axes[0]
ax2.view_init(elev=0, azim=-60)
../_images/tutorials_clustering_misorientations_43_0.png

Generate map of boundaries colored according to cluster membership

[23]:
mapping = np.ones(mori_all.shape + (3,))
new_mask = (
    np.where(boundary_mask)[0][~small_mask],
    np.where(boundary_mask)[1][~small_mask],
)
mapping[new_mask] = labels_rgb

Plot map of boundaries colored according to cluster membership

[24]:
fig3, ax3 = plt.subplots(figsize=(15, 10))
ax3.imshow(mapping)
ax3.set(xticks=[], yticks=[]);
../_images/tutorials_clustering_misorientations_47_0.png