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


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)


From Matplotlib with StereographicPlot.scatter()

[4]:

fig, ax = plt.subplots(subplot_kw=dict(projection="stereographic"))
ax.scatter(v1, c=["r", "g", "b"])


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"])


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()


### 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))


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)


We can also remove the grid if desirable

[11]:

v4.scatter(hemisphere="both", grid=False)


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)


### 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)
)


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)


### 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")
)
ax.scatter(azimuth, polar, c=["C0", "C1", "C2"], s=200)
ax.set_labels("RD", "TD", None)
ax.show_hemisphere_label()


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
)


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

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)


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]:


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"]
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
)


### 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")


## 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)
# 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)