orix icon indicating copy to clipboard operation
orix copied to clipboard

`angle_with()` giving weird results

Open anderscmathisen opened this issue 2 years ago • 5 comments

I am trying to calculate some angles between Orientations by using the angle_with() function and getting some weird numbers.

Below is a code showing what I mean.

from orix.quaternion import Orientation
from orix.quaternion.symmetry import C6v
import orix.plot
import matplotlib.pyplot as plt
import numpy as np

plt.rcParams.update({
    "axes.grid": True,
})


ori1 = Orientation([[0.2283, 0.6892, -0.13, 0.6752]], symmetry=C6v)
ori2 = Orientation([[0.7028, -0.2065, 0.6714, 0.1126]], symmetry=C6v)

print(np.rad2deg(ori1.angle_with(ori2)))

fig = plt.figure()
ax = fig.add_subplot(projection="ipf", symmetry=C6v)
ax.scatter(ori1)
ax.scatter(ori2)

Output: [178.32181777]

In the scatterplot IPF (z-direction), these orientations are, as the image below shows, very close ~5° and I don't understand why angle_with() is giving me such a high angle.

PS: I am running v.0.10.0, but v.0.11.0 gives the same

output

anderscmathisen avatar Feb 15 '23 13:02 anderscmathisen

Thank you for bringing this up, @anderscmathisen.

I got the same misorientation angle with MTEX v5.8.2:

>> cs = crystalSymmetry('C6v');
%% Inverted, because MTEX defines orientations as crystal -> sample
>> o1 = orientation([0.2283 -0.6892 0.13 -0.6752], cs);
>> o2 = orientation([0.7028 0.2065 -0.6714 -0.1126], cs);
>> angle(o1, o2) / degree

ans =

  178.3219

What you've plotted above is where the sample Z direction points in the two crystals. The points are located about 5 degrees apart because they have been projected into the fundamental sector of 6mm:

from orix.quaternion import Orientation, symmetry
from orix.vector import Vector3d


pg = symmetry.C6v
ori1 = Orientation([0.2283, 0.6892, -0.13, 0.6752], symmetry=pg)
ori2 = Orientation([0.7028, -0.2065, 0.6714, 0.1126], symmetry=pg)

# Rotate sample Z into crystals
v1 = ori1 * Vector3d.zvector()
v2 = ori2 * Vector3d.zvector()

v1.angle_with(v2, degrees=True)  # Outputs: array([55.55565557])

fig = v1.scatter(return_figure=True, c="tab:blue")
v2.scatter(figure=fig, c="tab:orange")
v1.in_fundamental_sector(pg).scatter(figure=fig, c="tab:green")
fig.axes[0].plot(pg.fundamental_sector.edges, color="k")

test

hakonanes avatar Feb 15 '23 16:02 hakonanes

Forgot to ask, but what misorientation (angle) do you expect?

hakonanes avatar Feb 16 '23 12:02 hakonanes

Hmm, yes I suspected it might have something to do with a projection, however, I thought that angle_with(), which according to docs calculates the smallest symmetry reduced angle, should give me the angle in the projected symmetry reduced space, and not over the entire stereographic projection.

The orientations I am calculating angles between are from a TEM tilt series where the sample was tilted 5° between capturing SPED data. (orientations found by template matching using pyxem). And so I was expecting to get about that angle. But the angle_with() function is perhaps not the right one to use in this situation?

anderscmathisen avatar Feb 16 '23 17:02 anderscmathisen

Orientation.angle_with() calculates the smallest misorientation angle between orientations as they are defined. The largest misorientation angle possible for two crystals with the 6mm point group symmetry is 180 degrees, so an angle of 178 degrees is plausible. If you had imposed a mirror plane across the c axis, giving you the centrosymmetric point group 6/mmm (D6h), the misorientation angle between your two orientations is about 4 degrees. I believe the sudden jump to almost 180 degree misorientation from a tilt of 5 degrees is a case of pseudo-symmetry, i.e. two orientations with a misorientation of about 180 degrees about a or b looking near identical, and the indexing algorithm not being able to distinguish between the two.

I think I understand what you want: Miller.angle_with(..., use_symmetry=True). If you wrap the two vectors v1,2 above in Miller, you can get the symmetry reduced angle between where the sample Z direction points in the two crystals. Continuing on the example above:

from orix.crystal_map import Phase
from orix.vector import Miller


phase_6mm = Phase(point_group=symmetry.C6v)
m1 = Miller(uvw=(ori1 * Vector3d.zvector()).data, phase=phase_6mm)
m2 = Miller(uvw=(ori2 * Vector3d.zvector()).data, phase=phase_6mm)

m1.angle_with(m2, degrees=True)  # Output: array([55.55565557])
m1.angle_with(m2, use_symmetry=True, degrees=True)  # Output: array([3.16904388])

hakonanes avatar Feb 16 '23 17:02 hakonanes

Hey, late game addition, but i think this is touching on a bigger orix problem.

First off, symmetry aside, those orientations describe aligning two different poles. the first is a rotation that aligns the [0001], the second aligns the [000-1]. These are symmetrically equivalent, but it's still worth pointing out because it explains your massive angle difference, sans symmetry reduction.

Secondly, I believe the correct way to do this operation would be to find the misorientation between o1 and o2, then find the angle of it's symmetry reduced representation. VERY verbosely, this would be:

# find the misorientation between them
misori = Rotation(ori1*~ori2)
# calculate all 12*12 = 144 equivalent representations
equivalent_misoris = C6v.outer(misori).outer(C6v)
# the smallest one is the one you want
smallest = equivalent_misoris.flatten()[np.argmin(equivalent_misoris.flatten().angle)]
# and it's angular value is what you were originally asking for
print(smallest.angle)

This gives 3.58 degrees, which is also what EMsoft and MTEX give, and lines up with your picture. It is also the method described in "Texture and Anisotropy" by Kocks and Tome, and "Crystallographic Texture and Group Representations" by C.S. Mann

However, ORIX's current Orientation.angle_with operation strips off symmetry by using the dot product as a shortcut, which works great for vectors and rotations, but not symmetry-aware calculations.

Additionally, doing this the more orix-like way would be

np.rad2deg((ori1*~ori2).angle())

but ORIX only allows orientations going lab2crystal, and not crystal2lab (in fact it will overwrite the left-right symmetry operators if you try), so this operation still gives an incorrect value as well.

So, short term for @anderscmathisen , you can get what you want by manually doing the symmetry reduction:

all_m = ori1.symmetry.outer(ori1*~ori2).outer(ori2.symmetry).flatten()
m_min = all_m[np.argmin(all_m)]

but long term, @hakonanes @CSSFrancis , @harripj , I think it's worth reconsidering how ORIX handles rotation classes and their operations.

argerlt avatar Apr 18 '25 21:04 argerlt