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

Stereographic projection

The stereographic projection maps a sphere onto a plane and preserves angles at which curves meet. In orix, the projection is used to project unit Vector3d objects onto the equatorial plane represented in spherical coordinates. These are the azimuth angle \(\phi\), in the range \([0^{\circ}, 360^{\circ}]\), and the polar angle \(\theta\), in the range \([0^{\circ}, 90^{\circ}]\) on the upper hemisphere and \([90^{\circ}, 180^{\circ}]\) on the lower hemisphere. The projection is implemented in StereographicProjection, together with the InverseStereographicProjection. These are used in the StereographicPlot, which extends Matplotlib’s projections framework for plotting of Vector3d objects.

The projection can be used “from Matplotlib”, meaning that Vector3d objects or spherical coordinates (\(\phi\), \(\theta\)) are passed to Matplotlib functions. While this is the most customizable way of plotting vectors, Vector3d.scatter() and Vector3d.draw_circle() methods are also provided for quick and easy plotting.

[1]:
# Exchange "inline" for:
# "qt5" for interactive plotting from the pyqt package
# "notebook" for inline interactive plotting when running on Binder
%matplotlib inline

import tempfile
import numpy as np
import matplotlib.pyplot as plt
from orix import vector


# We'll want our plots to look a bit larger than the default size
new_params = {
    "figure.figsize": (10, 5),
    "lines.markersize": 10,
    "font.size": 20,
    "axes.grid": False,
}
plt.rcParams.update(new_params)

Plot vectors

Plot three vectors on the upper hemisphere with Vector3d.scatter()

[2]:
v1 = vector.Vector3d([[1, 0, 0], [0, 1, 0], [0, 0, 1]])
v1.scatter()
_images/stereographic_projection_5_0.png

The spherical coordinates can be viewed while moving the cursor over the equatorial plane when plotting interactively (with qt5, notebook, or similar backends).

We can return the figure to save it or continue building upon it by passing return_figure=True

[3]:
temp_dir = tempfile.mkdtemp() + "/"  # Write to a temporary directory
vector_file = temp_dir + "vectors.png"
fig0 = v1.scatter(c=["r", "g", "b"], return_figure=True)
fig0.savefig(vector_file, bbox_inches="tight", pad_inches=0)
_images/stereographic_projection_7_0.png

From Matplotlib with StereographicPlot.scatter()

[4]:
fig, ax = plt.subplots(subplot_kw=dict(projection="stereographic"))
ax.scatter(v1, c=["r", "g", "b"])
_images/stereographic_projection_9_0.png

As usual with Matplotlib, the figure axes can be obtained from the returned fig1 above

[5]:
fig0.axes[0].name
[5]:
'stereographic'

Let’s turn on the azimuth and polar grid by updating the Matplotlib preferences

[6]:
plt.rcParams["axes.grid"] = True

Upper and/or lower hemisphere

We can plot vectors impinging on the upper hemisphere and/or the lower hemisphere by passing “upper”, “lower”, or “both” to the hemisphere parameter in Vector3d.scatter()

[7]:
v2 = vector.Vector3d([[1, 1, 2], [1, 1, -1]])

fig_kwargs = dict(figsize=(5, 5))
labels = ["x", "y", None]
v2.scatter(
    axes_labels=labels, show_hemisphere_label=True, figure_kwargs=fig_kwargs
)  # "upper" default
v2.scatter(
    hemisphere="lower",
    axes_labels=labels,
    show_hemisphere_label=True,
    figure_kwargs=fig_kwargs
)
v2.scatter(hemisphere="both", axes_labels=labels, c=["C0", "C1"])
_images/stereographic_projection_15_0.png
_images/stereographic_projection_15_1.png
_images/stereographic_projection_15_2.png

From Matplotlib by setting the StereographicPlot.hemisphere attribute. Remember to set the hemisphere before calling scatter()

[8]:
fig, ax = plt.subplots(ncols=2, subplot_kw=dict(projection="stereographic"))

ax[0].scatter(v2, c="C0")  # blue
ax[0].show_hemisphere_label(color="C0")  # Pass keyword arguments to text()
ax[0].set_labels(zlabel=None)

ax[1].hemisphere = "lower"  # /"south", or "upper"/"north"
ax[1].scatter(v2, c="C1")  # orange
ax[1].show_hemisphere_label()
ax[1].set_labels("RD", "TD", "ND", size=15)  # Pass keyword arguments to text()
_images/stereographic_projection_17_0.png

Control grid

Whether to show the grid or not can be set globally via Matplotlib rcParams as we did above, or controlled via the parameters grid, True/False, and grid_resolution, a tuple with (azimuth, polar) resolution in degrees, to Vector3d.scatter(). Default grid resolution is \(10^{\circ}\) for both grids.

[9]:
v3 = vector.Vector3d([[0, 0, 1], [1, 0, 1], [1, 1, 1]])
v3.scatter(grid_resolution=(30, 15))
_images/stereographic_projection_19_0.png

These can also be set after the figure is created by returning the figure

[10]:
v4 = vector.Vector3d(np.append(v3.data, -v3.data, axis=0))

fig4 = v4.scatter(hemisphere="both", return_figure=True)
ax0, ax1 = fig4.axes
ax0.azimuth_grid(45)
ax1.polar_grid(5)
_images/stereographic_projection_21_0.png

We can also remove the grid if desirable

[11]:
v4.scatter(hemisphere="both", grid=False)
_images/stereographic_projection_23_0.png

From Matplotlib, the polar and azimuth grid resolution can be set either upon axis initialization or after the axis is created using StereographicPlot.polar_grid() and StereographicPlot.azimuth_grid()

[12]:
subplot_kw = dict(
    projection="stereographic", polar_resolution=10, azimuth_resolution=10
)
fig, ax = plt.subplots(ncols=3, figsize=(15, 20), subplot_kw=subplot_kw)

ax[0].scatter(v4)
ax[0].show_hemisphere_label()

ax[1].hemisphere = "lower"
ax[1].show_hemisphere_label()
ax[1].scatter(v4)
ax[1].polar_grid(15)
ax[1].azimuth_grid(30)

ax[2].scatter(v4)
ax[2].grid(False)
_images/stereographic_projection_25_0.png

Annotate vectors

Vectors can be annotated by passing a list of strings to the vector_labels parameter in Vector3d.scatter()

[13]:
v4.scatter(
    hemisphere="both",
    vector_labels=[str(vi).replace(" ", "") for vi in v4.data],
    text_kwargs=dict(size=15)
)
_images/stereographic_projection_27_0.png

From Matplotlib, by looping over the vectors and adding text markers using StereographicPlot.text()

[14]:
fig, ax = plt.subplots(ncols=2, subplot_kw=dict(projection="stereographic"))

format_vector = lambda v: str(v.data[0]).replace(" ", "")

ax[0].scatter(v4)
ax[0].show_hemisphere_label()
for vi in v4:
    ax[0].text(vi, s=format_vector(vi), size=15)

ax[1].hemisphere = "lower"
ax[1].scatter(v4)
ax[1].show_hemisphere_label()
for vi in v4[:2]:
    ax[1].text(vi, s=format_vector(vi), size=15)
for vi in v4[2:]:
    ax[1].text(vi, s=format_vector(vi), size=15)
_images/stereographic_projection_29_0.png

Pass spherical coordinates

We can pass azimuth and polar angles instead of passing vectors. This only works in the “from Matplotlib” way

[15]:
fig, ax = plt.subplots(
    figsize=(5, 5), subplot_kw=dict(projection="stereographic")
)
azimuth = np.deg2rad([0, 60, 180])
polar = np.deg2rad([0, 60, 60])
ax.scatter(azimuth, polar, c=["C0", "C1", "C2"], s=200)
ax.set_labels("RD", "TD", None)
ax.show_hemisphere_label()
_images/stereographic_projection_31_0.png

Here, we also passed None to StereographicPlot.set_labels() so that the Z axis label is not shown.

Producing the exact same plot with Vector3d.scatter()

[16]:
vector.Vector3d.from_polar(azimuth=azimuth, polar=polar).scatter(
    axes_labels=["RD", "TD", None],
    show_hemisphere_label=True,
    figure_kwargs=dict(figsize=(5, 5)),
    c=["C0", "C1", "C2"],
    s=200
)
_images/stereographic_projection_33_0.png

Draw great and small circles

We can draw the trace of a plane perpendicular to a vector using Vector3d.draw_circle()

[17]:
v6 = vector.Vector3d.from_polar(
    azimuth=np.deg2rad([0, 60, 180]), polar=np.deg2rad([0, 60, 60]),
)

colors = ["C0", "C1", "C2"]
fig6 = v6.scatter(
    c=colors,
    s=200,
    axes_labels=["RD", "TD", None],
    show_hemisphere_label=True,
    return_figure=True
)
v6.draw_circle(color=colors, linewidth=2, figure=fig6)
_images/stereographic_projection_35_0.png

Let’s also add the vector perpendicular to the last two vectors and its great circle. We can add to the previous figure by passing the returned figure into the figure parameter in Vector3d.scatter() and Vector3d.draw_circle()

[18]:
v7 = v6[1].cross(v6[2])
v7.scatter(c="xkcd:pink", marker="p", s=250, figure=fig6)
v7.draw_circle(color="xkcd:pink", linestyle="--", linewidth=3, figure=fig6)

# We must redraw the figure when plotting in non-interactive mode
fig6
[18]:
_images/stereographic_projection_37_0.png

From Matplotlib using StereograhicPlot.draw_circle(), also showing a (not perpendicular, but at a \(45^{\circ}\) angle in this case)

[19]:
fig, ax = plt.subplots(
    figsize=(5, 5), subplot_kw=dict(projection="stereographic")
)
colors = ["C0", "C1", "C2"]
azimuth = np.deg2rad([0, 60, 180])
polar = np.deg2rad([0, 60, 60])
ax.scatter(azimuth, polar, c=colors, s=200)
ax.set_labels("RD", "TD", None)
ax.show_hemisphere_label()
ax.draw_circle(azimuth, polar, color=colors, linewidth=2)

# Let's also add the vector perpendicular to the last two vectors and its
# great circle
v6 = vector.Vector3d.from_polar(azimuth=azimuth[1:], polar=polar[1:])
v7 = v6[0].cross(v6[1])
ax.scatter(v7, c="xkcd:pink", marker="p", s=250)
ax.draw_circle(
    v7,
    color="xkcd:pink",
    linestyle="--",
    linewidth=3,
    opening_angle=0.25 * np.pi
)
_images/stereographic_projection_39_0.png

Create a Wulff net

We can create a Wulff net by drawing great and small circles

[20]:
n = int(90 / 2)  # Degree / net resolution
steps = 500
kwargs = dict(linewidth=0.25, color="k")

polar = np.linspace(0, 0.5 * np.pi, num=n)
v_right = vector.Vector3d.from_polar(azimuth=np.zeros(n), polar=polar)
v_left = vector.Vector3d.from_polar(azimuth=np.ones(n) * np.pi, polar=polar)
v010 = vector.Vector3d.zero(shape=(n,))
v010.y = 1
v010_opposite = -v010

fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(projection="stereographic"))
ax.grid(False)
ax.draw_circle(v_right, steps=steps, **kwargs)
ax.draw_circle(v_left, steps=steps, **kwargs)
ax.draw_circle(v010, opening_angle=polar, steps=steps, **kwargs)
ax.draw_circle(v010_opposite, opening_angle=polar, steps=steps, **kwargs)
for label, azimuth in zip(["B", "M''", "A", "M'"], np.array([0, 0.5, 1, 1.5]) * np. pi):
    ax.text(azimuth, 0.5 * np.pi, s=label, c="C1")
_images/stereographic_projection_41_0.png

Experimental functionality

Plot symmetry elements of point groups with proper rotations only, using StereographicPlot.symmetry_marker()

[21]:
fig, ax = plt.subplots(ncols=2, subplot_kw=dict(projection="stereographic"))

marker_size = 500
ax[0].grid(False)
ax[0].set_title("432", pad=20)
# 4-fold (outer markers will be clipped a bit...)
v4fold = vector.Vector3d(
    [[0, 0, 1], [1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0]]
)
ax[0].symmetry_marker(v4fold, fold=4, c="C4", s=marker_size)
ax[0].draw_circle(v4fold, color="C4")
# 3-fold
v3fold = vector.Vector3d([[1, 1, 1], [1, -1, 1], [-1, -1, 1], [-1, 1, 1]])
ax[0].symmetry_marker(v3fold, fold=3, c="C3", s=marker_size)
ax[0].draw_circle(v3fold, color="C3")
# 2-fold
v2fold = vector.Vector3d([
    [1, 0, 1],
    [0, 1, 1],
    [-1, 0, 1],
    [0, -1, 1],
    [1, 1, 0],
    [-1, -1, 0],
    [-1, 1, 0],
    [1, -1, 0],
])
ax[0].symmetry_marker(v2fold, fold=2, c="C2", s=marker_size)
ax[0].draw_circle(v2fold, color="C2")

ax[1].grid(False)
ax[1].set_title("222", pad=20)
# 2-fold
v2fold = vector.Vector3d([
    [0, 0, 1], [1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0]
])
ax[1].symmetry_marker(v2fold, fold=2, c="C2", s=800)
ax[1].draw_circle(v2fold, color="C2")
_images/stereographic_projection_43_0.png