Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions examples/plotting/orientation_projections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#
# Copyright 2018-2025 the orix developers
#
# This file is part of orix.
#
# orix is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# orix is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with orix. If not, see <http://www.gnu.org/licenses/>.
#
r"""
=======================
Orientation projections
=======================

This example demonstrates the four different projections used by ORX to
project Orientations (which are non-Euclidean) into either 2D or 3D
orthogonal space. This is done by subclassing matplotlib's Axes and
Axes3D classes.

There are four options for plotting projections. The first and only 2D
option is :class:`~orix.plot.InversePoleFigurePlot`, which is calculated
as :math:`(X, Y) = ((v_x/(1-v_z)),(v_y/(1-v_z)))`. This is computationally
efficient and translates well to print publication, but loses orientation
information perpendicular to whatever axis is being plotted, similar to a 2D
X-ray of a skeleton.

The next three are 3D axis-angle projections, sometimes also called
Neo-Eulerian projections. Here, the fact that any rotation can be described
by a twist math:`\omega` around an axis :math:`\hat{\mathbf{n}}`. The
math:`(X,Y,Z)` coordinates of the orientation's projection is the
math:`(V_x,V_y,V_z)` coordinates of a unit vector describing
math:`\hat{\mathbf{n}}`, scaled by a function of :math:`\omega`.
The scaling options are:

* :class:`~orix.plot.AxAnglePlot`:
math:`\omega * \hat{\mathbf{n}}`, A linear projection
* :class:`~orix.plot.RodriguesPlot`
math:`tan(\omega/2) * \hat{\mathbf{n}}`, A Rectilinear projection,
where orientations sharing a common rotation axis linearly align
* :class:`~orix.plot.HomochoricPlot`
math:`(0.75*(\omega-sin(\omega)))^{1/3} * \hat{\mathbf{n}}`, An
equal-volume projection, where a cube placed anywhere inside takes
up an identical solid angle in orientation space.

Note this list is not exhaustive and the descriptions are simplified.
For a deeper dive into the advantages and disadganvages of these projections
as well as enlightening comparisions of their warping of orientation space,
refer to the following open access publication:


.. _On three-dimensional misorientation spaces: https://royalsocietypublishing.org/doi/10.1098/rspa.2017.0274

(doi link: https://doi.org/10.1098/rspa.2017.0274)

"""

import matplotlib.pyplot as plt
import numpy as np

from orix.plot import IPFColorKeyTSL, register_projections
from orix.quaternion import Orientation, OrientationRegion
from orix.quaternion.symmetry import D3

plt.close("all")
register_projections() # Register our custom Matplotlib projections
np.random.seed(2319) # Create reproducible random data

n = 30
ori = Orientation.random(n, symmetry=D3)
# create orientation-dependent colormap for more informative plots.
color_key = IPFColorKeyTSL(D3)
clrs = color_key.orientation2color(ori)

############################################################################
# Orientation plots can be made in one of two ways. The first and simplest
# is via Orientation.scatter().
fig = plt.figure(figsize=(12, 3), layout="constrained")
ori.scatter(c=clrs, position=(1, 4, 1), projection="axangle", figure=fig)
fig.axes[0].set_title("Axis-Angle Projection")
ori.scatter(c=clrs, position=(1, 4, 2), projection="rodrigues", figure=fig)
fig.axes[1].set_title("Rodrigues Projection")
ori.scatter(c=clrs, position=(1, 4, 3), projection="homochoric", figure=fig)
fig.axes[2].set_title("Homochoric Projection")
# TODO: Following line does not plot properly due to the logic in
# Orientation.scatter
# ori.scatter(c=clrs, position=(1, 4, 4), projection="ipf", figure=fig)
plt.tight_layout()


# This can also be used to create standalone figures
ori.scatter(c=clrs, projection="ipf")
plt.tight_layout()


############################################################################
# The second method is by setting the projections when defining the
# matplotlib axes. This can require more tinkering since the plots are not
# auto-formatted like above, but it allows for more customization as well
# as the plotting of multiple datasets on a single plot

fig = plt.figure(figsize=(12, 4), layout="constrained")
ax_ax = fig.add_subplot(1, 4, 1, projection="axangle")
ax_rod = fig.add_subplot(1, 4, 2, projection="rodrigues")
ax_hom = fig.add_subplot(1, 4, 3, projection="homochoric")
ax_ipf = fig.add_subplot(1, 4, 4, projection="ipf", symmetry=D3)

ax_ipf.scatter(ori, c=clrs)

ax_ax.set_title("Axis-Angle Projection")
ax_rod.set_title("Rodrigues Projection")
ax_hom.set_title("Homochoric Projection")
ax_ipf.set_title("Inverse Pole Figure Projection \n\n")

fundamental_zone = OrientationRegion.from_symmetry(ori.symmetry)
for ax in [ax_ax, ax_rod, ax_hom]:
ax.plot_wireframe(fundamental_zone)
ax.set_proj_type = "ortho"
ax.axis("off")
ax.scatter(ori, c=clrs)

plt.tight_layout()
8 changes: 7 additions & 1 deletion orix/plot/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ from ._util.formatting import format_labels
from .crystal_map_plot import CrystalMapPlot
from .direction_color_keys import DirectionColorKeyTSL
from .orientation_color_keys import EulerColorKey, IPFColorKeyTSL
from .rotation_plot import AxAnglePlot, RodriguesPlot, RotationPlot
from .rotation_plot import (
AxAnglePlot,
HomochoricPlot,
RodriguesPlot,
RotationPlot,
)
from .stereographic_plot import StereographicPlot

# Must be imported below StereographicPlot since it imports it
Expand All @@ -35,6 +40,7 @@ __all__ = [
"CrystalMapPlot",
"DirectionColorKeyTSL",
"EulerColorKey",
"HomochoricPlot",
"InversePoleFigurePlot",
"IPFColorKeyTSL",
"RodriguesPlot",
Expand Down
4 changes: 3 additions & 1 deletion orix/plot/_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import matplotlib.projections as mprojections

from .crystal_map_plot import CrystalMapPlot
from .rotation_plot import AxAnglePlot, RodriguesPlot
from .rotation_plot import AxAnglePlot, HomochoricPlot, RodriguesPlot
from .stereographic_plot import StereographicPlot

# Inverse pole figure plot class must be imported below stereographic
Expand All @@ -43,6 +43,7 @@ def register_projections() -> None:
:class:`~orix.plot.InversePoleFigurePlot`
:class:`~orix.plot.AxAnglePlot`
:class:`~orix.plot.RodriguesPlot`
:class:`~orix.plot.HomochoricPlot`
:class:`~orix.plot.StereographicPlot`

Examples
Expand All @@ -63,6 +64,7 @@ def register_projections() -> None:
projections = [
AxAnglePlot,
CrystalMapPlot,
HomochoricPlot,
InversePoleFigurePlot,
RodriguesPlot,
StereographicPlot,
Expand Down
32 changes: 20 additions & 12 deletions orix/plot/rotation_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# along with orix. If not, see <http://www.gnu.org/licenses/>.
#

from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Literal

from matplotlib import projections
import matplotlib.collections as mcollections
Expand All @@ -29,7 +29,7 @@
from mpl_toolkits.mplot3d.art3d import Line3DCollection
import numpy as np

from orix.vector.neo_euler import AxAngle, Rodrigues
from orix.vector.neo_euler import AxAngle, Homochoric, Rodrigues

if TYPE_CHECKING: # pragma: no cover
from orix.quaternion.misorientation import Misorientation
Expand Down Expand Up @@ -154,29 +154,37 @@ def _correct_aspect_ratio(


class RodriguesPlot(RotationPlot):
"""Plot rotations in Rodrigues-Frank space."""
"""Plot rotations in Rodrigues-Frank (rectilinear) axis-angle space."""

name = "rodrigues"
transformation_class = Rodrigues


class AxAnglePlot(RotationPlot):
"""Plot rotations in a axis-angle space."""
"""Plot rotations in a linearly scalled axis-angle space."""

name = "axangle"
transformation_class = AxAngle


class HomochoricPlot(RotationPlot):
"""Plot rotations in homochoric (equi-volume) axis-angle space."""

name = "homochoric"
transformation_class = Homochoric


projections.register_projection(RodriguesPlot)
projections.register_projection(AxAnglePlot)
projections.register_projection(HomochoricPlot)


def _setup_rotation_plot(
figure: mfigure.Figure | None = None,
projection: str = "axangle",
projection: Literal["axangle", "rodrigues", "homochoric"] = "axangle",
position: int | tuple | SubplotSpec | None = (1, 1, 1),
figure_kwargs: dict | None = None,
) -> tuple[mfigure.Figure, AxAnglePlot | RodriguesPlot]:
) -> tuple[mfigure.Figure, AxAnglePlot | RodriguesPlot | HomochoricPlot]:
"""Return a figure and rotation plot axis of the correct type.

This is a convenience method used in e.g.
Expand All @@ -185,13 +193,13 @@ def _setup_rotation_plot(
Parameters
----------
figure
If given, a new plot axis :class:`orix.plot.AxAnglePlot` or
:class:`orix.plot.RodriguesPlot` is added to the figure in
the position specified by `position`. If not given, a new
figure is created.
If given, a new plot axis :class:`orix.plot.AxAnglePlot`,
:class:`orix.plot.RodriguesPlot`, or `orix.plot.HomochoricPlot`
is added to the figure in the position specified by `position`.
If not given, a new figure is created.
projection
Which orientation space to plot orientations in, either
"axangle" (default) or "rodrigues".
"axangle" (default), "rodrigues", or 'homochoric'.
position
Where to add the new plot axis. 121 or (1, 2, 1) places it
in the first of two positions in a grid of 1 row and 2
Expand All @@ -206,7 +214,7 @@ def _setup_rotation_plot(
figure
Figure with the added plot axis.
ax
The axis-angle or Rodrigues plot axis.
The plot axis.
"""
if figure is None:
if figure_kwargs is None:
Expand Down
41 changes: 26 additions & 15 deletions orix/quaternion/misorientation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from __future__ import annotations

from itertools import product as iproduct
from typing import Any
from typing import Any, Literal
import warnings

import dask.array as da
Expand All @@ -34,7 +34,11 @@
from orix._utils.deprecation import deprecated
from orix.quaternion.orientation_region import OrientationRegion
from orix.quaternion.rotation import Rotation
from orix.quaternion.symmetry import C1, Symmetry, _get_unique_symmetry_elements
from orix.quaternion.symmetry import (
C1,
Symmetry,
_get_unique_symmetry_elements,
)
from orix.vector.miller import Miller


Expand Down Expand Up @@ -206,7 +210,10 @@ def from_align_vectors(
out = list(out)

try:
out[0].symmetry = (initial.phase.point_group, other.phase.point_group)
out[0].symmetry = (
initial.phase.point_group,
other.phase.point_group,
)
except (AttributeError, ValueError):
pass

Expand Down Expand Up @@ -416,7 +423,7 @@ def reduce(self, verbose: bool = False) -> Misorientation:

def scatter(
self,
projection: str = "axangle",
projection: Literal["axangle", "rodrigues", "homochoric"] = "axangle",
figure: mfigure.Figure | None = None,
position: int | tuple[int, int, int] | SubplotSpec = (1, 1, 1),
return_figure: bool = False,
Expand All @@ -425,18 +432,23 @@ def scatter(
figure_kwargs: dict | None = None,
**kwargs,
) -> mfigure.Figure | None:
"""Plot misorientations in axis-angle space or the Rodrigues
fundamental zone.
"""Plot misorientations in 3D Euclidean space using a
Neo-Eulerian projection.

Parameters
----------
projection
Which axis-angle projection to use for plotting into
Euclidean space. The options are "axangle" (default)
for a linear scaling, "homochoric" for an equal-volume
scaling, or "rodrigues" for an rectilinear scaling.

Which misorientation space to plot misorientations in,
either ``"axangle"`` (default) or ``"rodrigues"``.
either "axangle" (default), "homochoric", or "rodrigues".
figure
If given, a new plot axis :class:`~orix.plot.AxAnglePlot` or
:class:`~orix.plot.RodriguesPlot` is added to the figure in
the position specified by ``position``. If not given, a new
If given, a new plot axis with the projection specified
by ``projection`` is added to the figure in the position
specified by "position" If not given, a new
figure is created.
position
Where to add the new plot axis. 121 or (1, 2, 1) places it
Expand All @@ -447,8 +459,7 @@ def scatter(
Whether to return the figure. Default is ``False``.
wireframe_kwargs
Keyword arguments passed to
:meth:`orix.plot.AxAnglePlot.plot_wireframe` or
:meth:`orix.plot.RodriguesPlot.plot_wireframe`.
:meth:`orix.plot.AxAnglePlot.plot_wireframe`.
size
If not given, all misorientations are plotted. If given, a
random sample of this ``size`` of the misorientations is
Expand All @@ -457,9 +468,8 @@ def scatter(
Dictionary of keyword arguments passed to
:func:`matplotlib.pyplot.figure` if ``figure`` is not given.
**kwargs
Keyword arguments passed to
:meth:`orix.plot.AxAnglePlot.scatter` or
:meth:`orix.plot.RodriguesPlot.scatter`.
Keyword arguments passed to the orix plotting class set by
'position'.

Returns
-------
Expand All @@ -470,6 +480,7 @@ def scatter(
--------
orix.plot.AxAnglePlot
orix.plot.RodriguesPlot
orix.plot.HomochoricPlot
"""
from orix.plot.rotation_plot import _setup_rotation_plot

Expand Down
Loading
Loading