Skip to content

Commit

Permalink
Merge pull request #306 from harripj/add_Symmetry_scatter
Browse files Browse the repository at this point in the history
Add Symmetry plot method
  • Loading branch information
hakonanes committed Mar 29, 2022
2 parents 0d97520 + 546ceb5 commit 40e4807
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 4 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Expand Up @@ -12,6 +12,9 @@ Unreleased

Added
-----
- Point group `Symmetry` elements can now be viewed under the stereographic projection
using `Symmetry.plot()`. The notebook point_groups.ipynb has been added to the
documentation.
- Add `reproject` argument to `Vector3d.scatter()` which reprojects vectors located on
the hidden hemisphere to the visible hemisphere.
- `Rotation` objects can now be checked for equality. Equality is determined by
Expand Down
8 changes: 4 additions & 4 deletions doc/conf.py
Expand Up @@ -44,11 +44,11 @@
intersphinx_mapping = {
"dask": ("https://docs.dask.org/en/latest", None),
"diffpy.structure": ("https://www.diffpy.org/diffpy.structure", None),
"h5py": ("http://docs.h5py.org/en/stable/", None),
"matplotlib": ("https://matplotlib.org", None),
"numpy": ("https://docs.scipy.org/doc/numpy", None),
"h5py": ("https://docs.h5py.org/en/stable", None),
"matplotlib": ("https://matplotlib.org/stable", None),
"numpy": ("https://numpy.org/doc/stable", None),
"python": ("https://docs.python.org/3", None),
"scipy": ("https://docs.scipy.org/doc/scipy/reference", None),
"scipy": ("https://docs.scipy.org/doc/scipy", None),
}

# Add any paths that contain templates here, relative to this directory.
Expand Down
1 change: 1 addition & 0 deletions doc/index.rst
Expand Up @@ -23,6 +23,7 @@ Work using orix
clustering_across_fundamental_region_boundaries.ipynb
clustering_orientations.ipynb
uniform_sampling_of_orientation_space.ipynb
point_groups.ipynb

.. toctree::
:hidden:
Expand Down
127 changes: 127 additions & 0 deletions doc/point_groups.ipynb
@@ -0,0 +1,127 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"nbsphinx": "hidden"
},
"source": [
"This notebook is part of the orix documentation https://orix.rtfd.io. Links to the documentation won’t work from the notebook."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Visualizing point groups\n",
"\n",
"Point group symmetry operations are shown here under the stereographic projection.\n",
"\n",
"Vectors located on the upper (`z >= 0`) hemisphere are displayed as points (`o`), whereas vectors on the lower hemisphere are reprojected onto the upper hemisphere and shown as crosses (`+`) by default. For more information about plot formatting and visualization, see [Vector3d.scatter()](reference.rst#orix.vector.Vector3d.scatter).\n",
"\n",
"More explanation of these figures is provided at http://xrayweb.chem.ou.edu/notes/symmetry.html#point."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For example, the `O (432)` point group:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"tags": [
"nbsphinx-thumbnail"
]
},
"outputs": [],
"source": [
"%matplotlib inline\n",
"\n",
"from orix.quaternion import symmetry\n",
"\n",
"symmetry.O.plot()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The stereographic projection of all point groups is shown below:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from matplotlib import pyplot as plt\n",
"import numpy as np\n",
"\n",
"from orix import plot\n",
"from orix.quaternion import Rotation\n",
"from orix.vector import Vector3d\n",
"\n",
"schoenflies = ['C1', 'Ci', 'C2x', 'C2y', 'C2z', 'Csx', 'Csy', 'Csz', 'C2h', 'D2', 'C2v', 'D2h', 'C4', 'S4', 'C4h', 'D4', 'C4v', 'D2d', 'D4h', 'C3', 'S6', 'D3x', 'D3y', 'D3', 'C3v', 'D3d', 'C6', 'C3h', 'C6h', 'D6', 'C6v', 'D3h', 'D6h', 'T', 'Th', 'O', 'Td', 'Oh']\n",
"\n",
"assert len(symmetry._groups) == len(schoenflies)\n",
"\n",
"schoenflies = [s for s in schoenflies if not (s.endswith('x') or s.endswith('y'))]\n",
"\n",
"assert len(schoenflies) == 32\n",
"\n",
"orientation = Rotation.from_axes_angles((-1, 8, 1), np.deg2rad(65))\n",
"\n",
"fig, ax = plt.subplots(nrows=8, ncols=4, figsize=(10, 20), subplot_kw=dict(projection='stereographic'))\n",
"ax = ax.ravel()\n",
"\n",
"for i, s in enumerate(schoenflies):\n",
" sym = getattr(symmetry, s)\n",
"\n",
" ori_sym = sym.outer(orientation)\n",
" v = (ori_sym * Vector3d.zvector())\n",
" \n",
" # reflection in the projection plane (x-y) is performed internally in\n",
" # Symmetry.plot() or when using the `reproject=True` argument for\n",
" # Vector3d.scatter()\n",
" v_reproject = Vector3d(v.data.copy())\n",
" v_reproject.z *= -1\n",
" \n",
" # the Symmetry marker formatting for vectors on the upper and lower hemisphere\n",
" # can be set using `kwargs` and `reproject_scatter_kwargs`, respectively, for\n",
" # Symmetry.plot() \n",
" \n",
" # vectors on the upper hemisphere are shown as open circles\n",
" ax[i].scatter(v, marker='o', fc='None', ec='k', s=150)\n",
" # vectors on the lower hemisphere are reprojected onto the upper hemisphere and\n",
" # shown as crosses\n",
" ax[i].scatter(v_reproject, marker='+', ec='C0', s=150)\n",
"\n",
" ax[i].set_title(f'${s}$ $({sym.name})$')\n",
" ax[i].set_labels('a', 'b', None)\n",
"\n",
"fig.tight_layout()"
]
}
],
"metadata": {
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.7"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
1 change: 1 addition & 0 deletions doc/reference.rst
Expand Up @@ -417,6 +417,7 @@ Symmetry
.. currentmodule:: orix.quaternion.Symmetry
.. autosummary::
from_generators
plot
.. autoclass:: orix.quaternion.Symmetry
:show-inheritance:
:members:
Expand Down
69 changes: 69 additions & 0 deletions orix/quaternion/symmetry.py
Expand Up @@ -451,6 +451,75 @@ def fundamental_zone(self):
sr = SphericalRegion(n.unique())
return sr

def plot(
self,
orientation=None,
reproject_scatter_kwargs=None,
**kwargs,
):
"""Stereographic projection of symmetry operations.
The upper hemisphere of the stereographic projection is shown.
Vectors on the lower hemisphere are shown after reprojection
onto the upper hemisphere.
Parameters
----------
orientation : Orientation, optional
The symmetry operations are applied to this orientation
before plotting. The default value uses an orientation
optimized to show symmetry elements.
reproject_scatter_kwargs: dict, optional
Dictionary of keyword arguments for the reprojected scatter
points which is passed to
:meth:`~orix.plot.StereographicPlot.scatter`, which passes
these on to :meth:`matplotlib.axes.Axes.scatter`. The
default marker style for reprojected vectors is "+". Values
used for vector(s) on the visible hemisphere are used unless
another value is passed here.
kwargs : dict, optional
Keyword arguments passed to
:meth:`~orix.plot.StereographicPlot.scatter`, which passes
these on to :meth:`matplotlib.axes.Axes.scatter`.
Returns
-------
fig : matplotlib.figure.Figure
The created figure, returned if `return_figure` is supplied
as a keyword argument and is True.
"""
if orientation is None:
# orientation chosen to mimic stereographic projections as
# shown: http://xrayweb.chem.ou.edu/notes/symmetry.html
orientation = Rotation.from_axes_angles((-1, 8, 1), np.deg2rad(65))
if not isinstance(orientation, Rotation):
raise TypeError("Orientation must be a Rotation instance.")
orientation = self.outer(orientation)

kwargs.setdefault("return_figure", False)
return_figure = kwargs.pop("return_figure")

if reproject_scatter_kwargs is None:
reproject_scatter_kwargs = {}
reproject_scatter_kwargs.setdefault("marker", "+")
reproject_scatter_kwargs.setdefault("label", "lower")

v = orientation * Vector3d.zvector()

figure = v.scatter(
return_figure=True,
axes_labels=("a", "b", None),
label="upper",
reproject=True,
reproject_scatter_kwargs=reproject_scatter_kwargs,
**kwargs,
)
# add symmetry name to figure title
figure.suptitle(f"${self.name}$")

if return_figure:
return figure


# Triclinic
C1 = Symmetry((1, 0, 0, 0))
Expand Down
28 changes: 28 additions & 0 deletions orix/tests/quaternion/test_symmetry.py
Expand Up @@ -19,6 +19,7 @@
from copy import deepcopy

from diffpy.structure.spacegroups import GetSpaceGroup
from matplotlib import pyplot as plt
import numpy as np
import pytest

Expand Down Expand Up @@ -406,6 +407,33 @@ def test_hash_persistence():
assert all(h1a == h2a for h1a, h2a in zip(h1, h2))


@pytest.mark.parametrize("symmetry", [C1, C4, Oh])
def test_symmetry_plot(symmetry):
figure = symmetry.plot(return_figure=True)
assert isinstance(figure, plt.Figure)
assert len(figure.axes) == 1
ax = figure.axes[0]
num = 1 if symmetry.is_proper else 2
assert len(ax.collections) == num
c0 = ax.collections[0]
assert len(c0.get_offsets()) == np.count_nonzero(~symmetry.improper)
assert c0.get_label().lower() == "upper"
if num > 1:
c1 = ax.collections[1]
assert len(c1.get_offsets()) == np.count_nonzero(symmetry.improper)
assert c1.get_label().lower() == "lower"
assert len(ax.texts) == 2
assert ax.texts[0].get_text() == "a"
assert ax.texts[1].get_text() == "b"
plt.close("all")


@pytest.mark.parametrize("symmetry", [C1, C4, Oh])
def test_symmetry_plot_raises(symmetry):
with pytest.raises(TypeError, match="Orientation must be a Rotation instance"):
_ = symmetry.plot(return_figure=True, orientation="test")


class TestFundamentalSectorFromSymmetry:
"""Test the normals, vertices and centers of the fundamental sector
for all 32 crystallographic point groups.
Expand Down

0 comments on commit 40e4807

Please sign in to comment.