diff --git a/doc/tutorials/clustering_misorientations.ipynb b/doc/tutorials/clustering_misorientations.ipynb
index 404f4d9d..b571f074 100644
--- a/doc/tutorials/clustering_misorientations.ipynb
+++ b/doc/tutorials/clustering_misorientations.ipynb
@@ -43,7 +43,6 @@
"from orix.quaternion.symmetry import D6\n",
"from orix.vector import Vector3d\n",
"\n",
- "\n",
"plt.rcParams.update({\"font.size\": 20, \"figure.figsize\": (10, 10)})\n",
"\n",
"register_projections()"
diff --git a/doc/tutorials/clustering_orientations.ipynb b/doc/tutorials/clustering_orientations.ipynb
index 5b15d014..1552bd56 100644
--- a/doc/tutorials/clustering_orientations.ipynb
+++ b/doc/tutorials/clustering_orientations.ipynb
@@ -46,7 +46,6 @@
"from orix.quaternion.symmetry import D6\n",
"from orix.vector import AxAngle, Vector3d\n",
"\n",
- "\n",
"plt.rcParams.update(\n",
" {\"font.size\": 20, \"figure.figsize\": (10, 10), \"figure.facecolor\": \"w\"}\n",
")"
diff --git a/doc/tutorials/crystal_directions.ipynb b/doc/tutorials/crystal_directions.ipynb
index 1e9953b1..77a5cb28 100644
--- a/doc/tutorials/crystal_directions.ipynb
+++ b/doc/tutorials/crystal_directions.ipynb
@@ -42,7 +42,6 @@
"from orix.quaternion import Orientation, Rotation, symmetry\n",
"from orix.vector import Miller, Vector3d\n",
"\n",
- "\n",
"plt.rcParams.update(\n",
" {\n",
" \"figure.figsize\": (7, 7),\n",
diff --git a/doc/tutorials/crystal_map.ipynb b/doc/tutorials/crystal_map.ipynb
index 65d0a67e..3f0787e4 100644
--- a/doc/tutorials/crystal_map.ipynb
+++ b/doc/tutorials/crystal_map.ipynb
@@ -44,7 +44,6 @@
"from orix.quaternion import Orientation, Rotation, symmetry\n",
"from orix.vector import Vector3d\n",
"\n",
- "\n",
"plt.rcParams.update({\"figure.figsize\": (7, 7), \"font.size\": 15})\n",
"tempdir = tempfile.mkdtemp() + \"/\""
]
diff --git a/doc/tutorials/inverse_pole_figures.ipynb b/doc/tutorials/inverse_pole_figures.ipynb
index 439fd1fb..7f2ed29b 100644
--- a/doc/tutorials/inverse_pole_figures.ipynb
+++ b/doc/tutorials/inverse_pole_figures.ipynb
@@ -61,7 +61,6 @@
"from orix.quaternion import Orientation, symmetry\n",
"from orix.vector import Vector3d\n",
"\n",
- "\n",
"# We'll want our plots to look a bit larger than the default size\n",
"new_params = {\n",
" \"figure.facecolor\": \"w\",\n",
diff --git a/doc/tutorials/point_groups.ipynb b/doc/tutorials/point_groups.ipynb
index fbdabebb..9d5583f5 100644
--- a/doc/tutorials/point_groups.ipynb
+++ b/doc/tutorials/point_groups.ipynb
@@ -37,7 +37,6 @@
"from orix.quaternion import Rotation, symmetry\n",
"from orix.vector import Vector3d\n",
"\n",
- "\n",
"plt.rcParams.update({\"font.size\": 15})"
]
},
diff --git a/doc/tutorials/s2_sampling.ipynb b/doc/tutorials/s2_sampling.ipynb
index 36d5a0cc..a1fb90eb 100644
--- a/doc/tutorials/s2_sampling.ipynb
+++ b/doc/tutorials/s2_sampling.ipynb
@@ -38,7 +38,6 @@
"from orix.quaternion import symmetry\n",
"from orix.sampling import sample_S2_methods, sample_S2\n",
"\n",
- "\n",
"plt.rcParams.update({\"font.size\": 20, \"lines.markersize\": 2})"
]
},
diff --git a/doc/tutorials/stereographic_projection.ipynb b/doc/tutorials/stereographic_projection.ipynb
index 418ab5e7..c2047cf9 100644
--- a/doc/tutorials/stereographic_projection.ipynb
+++ b/doc/tutorials/stereographic_projection.ipynb
@@ -47,7 +47,6 @@
"from orix import plot # Register orix' projections with Matplotlib\n",
"from orix.vector import Vector3d\n",
"\n",
- "\n",
"# We'll want our plots to look a bit larger than the default size\n",
"plt.rcParams.update(\n",
" {\n",
diff --git a/doc/tutorials/uniform_sampling_of_orientation_space.ipynb b/doc/tutorials/uniform_sampling_of_orientation_space.ipynb
index 38408963..507047c2 100644
--- a/doc/tutorials/uniform_sampling_of_orientation_space.ipynb
+++ b/doc/tutorials/uniform_sampling_of_orientation_space.ipynb
@@ -80,7 +80,6 @@
"from orix.sampling import get_sample_fundamental\n",
"from orix.vector import Vector3d\n",
"\n",
- "\n",
"plt.rcParams.update(\n",
" {\n",
" \"axes.grid\": True,\n",
diff --git a/examples/stereographic_projection/plot_symmetry_advanced.py b/examples/stereographic_projection/plot_symmetry_advanced.py
new file mode 100644
index 00000000..7f2ad238
--- /dev/null
+++ b/examples/stereographic_projection/plot_symmetry_advanced.py
@@ -0,0 +1,66 @@
+#
+# Copyright 2018-2026 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 .
+#
+
+r"""
+===============================
+Altering Crystal Symmetry Plots
+===============================
+
+This example demonstrates some advanced methods for altering
+stereographic plots of symmetries. For a more general example, see
+:ref:`sphx_glr_plot_symmetry_operations.py`. For examples of how to make
+symmetry objects, see :ref:`create_symmetry.py`
+"""
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+import orix.plot as opl
+import orix.quaternion as oqu
+import orix.vector as ove
+
+# Set a random seed for reproducability
+np.random.seed = 897897
+# Register our custom Matplotlib projections
+opl.register_projections()
+# Use the PointGroup Class to create a Symmetry from a common shorthand name
+pg_Oh = oqu.symmetry.PointGroups.get("m-3m")
+
+########################################################################################
+# Typically, plots can be made using the built-in plot function. By default this
+# will create a matplotlib Figure and Axis object, plot a single asymmetric
+# vector, and add the Point Group name as the title
+pg_Oh.plot()
+
+########################################################################################
+# Alternatively though, by creating a figure beforehand and passing the Axis
+# in, it's possible to modify this plot as desired, for example by adding
+# additional plots, changing the title, or modifying elements.
+
+v = ove.Vector3d.random(10)
+v_symm = pg_Oh.outer(v).flatten()
+fig, ax = plt.subplots(1, 1, subplot_kw={"projection": "stereographic"})
+pg_Oh.plot(ax=ax, show_name=False)
+ax.set_title("my cool custom title")
+ax.scatter(v_symm)
+
+for i in range(len(ax.lines)):
+ ax.lines[i].set_c([0, 1, 1])
+
+plt.show()
diff --git a/examples/stereographic_projection/plot_symmetry_operations.py b/examples/stereographic_projection/plot_symmetry_operations.py
index c9bb0019..e065cec5 100644
--- a/examples/stereographic_projection/plot_symmetry_operations.py
+++ b/examples/stereographic_projection/plot_symmetry_operations.py
@@ -21,53 +21,55 @@
Plot symmetry operations
========================
-This example shows how to draw proper symmetry operations :math:`s`
-(no reflections or inversions).
+This example shows how stereographic projections with symmetry operation
+markers can be automatically generated for the 32 crystallographic point
+groups.
+
+The ordering used here follows the one given in section 9.2 of "Structures
+of Materials" (DeGraef et.al, 2nd edition, 2012). This ordering starts with
+the 5 cyclic groups (C1, C2, C3, C4, and C6), followed by the 4 dihedral
+groups (D2, D3, D4, and D6). Next are the same groups combined with inversion
+centers (Ci, Cs, C3i, S4, and C3h), perpendicular mirror planes (C2h, C4h,
+and C6h), vertical mirror planes (C2v, C3v, C4v, and C6v), and diagonal
+mirror planes (D3d, D2d, and D3h). Next are groups formed from permutations
+of cyclic and dihedral groups (D2h, D4h, and D6h), and finally the groups
+with 3-fold rotations around the 111 axes (T, O, Th, Td, and Oh).
+
+The plots themselves as well as their labels follow the standards given
+in Table 10.2.2 of the "International Tables for Crystallography, Volume
+A" (ITC). Both the nomenclature and marker styles thus differ slightly from
+many textbooks, including "Structure of Materials", as there are arbitrary
+convention choices in ITC regarding both Schoenflies notation and marker
+style.
+
+Orix uses Schoenflies Notation (left label above each plot) for the default
+symmetry group names since they are short and always begin with a letter,
+but both Schoenflies and Hermann-Mauguin (right label above each plot) names
+can be used to look up symmetry groups using `PointGroups.get()`
"""
import matplotlib.pyplot as plt
-from orix.plot import register_projections
-from orix.vector import Vector3d
+import orix.plot as opl
+import orix.quaternion as oqu
+import orix.vector as ove
-register_projections() # Register our custom Matplotlib projections
+opl.register_projections() # Register our custom Matplotlib projections
-marker_size = 200
-fig, (ax0, ax1) = plt.subplots(
- ncols=2,
- subplot_kw={"projection": "stereographic"},
- layout="tight",
-)
+# create a list of the 32 crystallographic point groups
+point_groups = oqu.symmetry.PointGroups.get_set("procedural")
+
+# show the table of symmetry information
+print(point_groups)
-ax0.set_title("432", pad=20)
-# 4-fold (outer markers will be clipped a bit...)
-v4fold = Vector3d([[0, 0, 1], [1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0]])
-ax0.symmetry_marker(v4fold, fold=4, c="C4", s=marker_size)
-ax0.draw_circle(v4fold, color="C4")
-# 3-fold
-v3fold = Vector3d([[1, 1, 1], [1, -1, 1], [-1, -1, 1], [-1, 1, 1]])
-ax0.symmetry_marker(v3fold, fold=3, c="C3", s=marker_size)
-ax0.draw_circle(v3fold, color="C3")
-# 2-fold
-# fmt: off
-v2fold = 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],
- ]
+# prepare the plots
+fig, ax = plt.subplots(
+ 4, 8, subplot_kw={"projection": "stereographic"}, figsize=[14, 10]
)
-# fmt: on
-ax0.symmetry_marker(v2fold, fold=2, c="C2", s=marker_size)
-ax0.draw_circle(v2fold, color="C2")
+ax = ax.flatten()
-ax1.set_title("222", pad=20)
-# 2-fold
-v2fold = Vector3d([[0, 0, 1], [1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0]])
-ax1.symmetry_marker(v2fold, fold=2, c="C2", s=2 * marker_size)
-ax1.draw_circle(v2fold, color="C2")
+# create a vector to mirror over axes
+v = ove.Vector3d.from_polar(65, 80, degrees=True)
+# Iterate through the 32 Point groups
+for i, pg in enumerate(point_groups):
+ pg.plot(asymmetric_vector=v, ax=ax[i])
diff --git a/orix/crystal_map/_phase.py b/orix/crystal_map/_phase.py
index 7ac984ed..2d8fa7af 100644
--- a/orix/crystal_map/_phase.py
+++ b/orix/crystal_map/_phase.py
@@ -33,13 +33,7 @@
import numpy as np
from orix.plot._util.color import get_matplotlib_color
-from orix.quaternion.symmetry import (
- _EDAX_POINT_GROUP_ALIASES,
- VALID_SYSTEMS,
- Symmetry,
- _groups,
- get_point_group,
-)
+from orix.quaternion.symmetry import VALID_SYSTEMS, PointGroups, Symmetry
from orix.vector.miller import Miller
from orix.vector.vector3d import Vector3d
@@ -225,7 +219,7 @@ def point_group(self) -> Symmetry | None:
Point group.
"""
if self.space_group is not None:
- return get_point_group(self.space_group.number)
+ return PointGroups.from_space_group(self.space_group.number)
else:
return self._point_group
@@ -235,14 +229,7 @@ def point_group(self, value: int | str | Symmetry | None) -> None:
if isinstance(value, int):
value = str(value)
if isinstance(value, str):
- for key, aliases in _EDAX_POINT_GROUP_ALIASES.items():
- if value in aliases:
- value = key
- break
- for point_group in _groups:
- if value == point_group.name:
- value = point_group
- break
+ value = PointGroups.get(value)
if not isinstance(value, Symmetry) and value is not None:
raise ValueError(
f"{value!r} must be of type {Symmetry}, the name of a valid point group"
@@ -486,7 +473,13 @@ def new_structure_matrix_from_alignment(
def default_lattice(system: VALID_SYSTEMS) -> Lattice:
- if system in ["triclinic", "monoclinic", "orthorhombic", "tetragonal", "cubic"]:
+ if system in [
+ "triclinic",
+ "monoclinic",
+ "orthorhombic",
+ "tetragonal",
+ "cubic",
+ ]:
lat = Lattice()
elif system in ["trigonal", "hexagonal"]:
lat = Lattice(1, 1, 1, 90, 90, 120)
diff --git a/orix/plot/_symmetry_marker.py b/orix/plot/_symmetry_marker.py
deleted file mode 100644
index f9b9af7e..00000000
--- a/orix/plot/_symmetry_marker.py
+++ /dev/null
@@ -1,127 +0,0 @@
-#
-# 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 .
-#
-
-"""Symmetry element markers to plot in stereographic representations of
-crystallographic point groups.
-
-Meant to be used indirectly in
-:func:`~orix.plot.StereographicPlot.symmetry_marker`.
-"""
-
-from typing import Generator
-
-import matplotlib.path as mpath
-import matplotlib.transforms as mtransforms
-import numpy as np
-
-from orix.vector.vector3d import Vector3d
-
-
-class SymmetryMarker:
- """Symmetry marker for use in plotting.
-
- Parameters
- ----------
- v
- size
- """
-
- fold = None
- _marker = None
-
- def __init__(self, v: Vector3d | np.ndarray | list | tuple, size: int = 1) -> None:
- self._vector = Vector3d(v)
- self._size = size
-
- @property
- def angle_deg(self) -> np.ndarray:
- """Position in degrees."""
- return np.rad2deg(self._vector.azimuth) + 90
-
- @property
- def size(self) -> np.ndarray:
- """Multiplicity of each symmetry marker."""
- return np.ones(self.n) * self._size
-
- @property
- def n(self) -> int:
- """Number of symmetry markers."""
- return self._vector.size
-
- def __iter__(self) -> Generator:
- for v, marker, size in zip(self._vector, self._marker, self.size):
- yield v, marker, size
-
-
-class TwoFoldMarker(SymmetryMarker):
- """Two-fold symmetry marker."""
-
- fold = 2
-
- @property
- def size(self) -> np.ndarray:
- """Multiplicity of each symmetry marker."""
- # Assuming maximum polar angle is 90 degrees
- radial = np.tan(self._vector.polar / 2)
- radial = np.where(radial == 0, 1, radial)
- return self._size / np.sqrt(radial)
-
- @property
- def _marker(self) -> list[mpath.Path]:
- # Make an ellipse path (https://matplotlib.org/stable/api/path_api.html)
- circle = mpath.Path.circle()
- verts = np.copy(circle.vertices) # Paths considered immutable
- verts[:, 0] *= 2
- ellipse = mpath.Path(verts, circle.codes)
-
- # Set up rotations of ellipse
- azimuth = self._vector.azimuth
- trans = [mtransforms.Affine2D().rotate(a + (np.pi / 2)) for a in azimuth]
-
- return [ellipse.deepcopy().transformed(i) for i in trans]
-
-
-class ThreeFoldMarker(SymmetryMarker):
- """Three-fold symmetry marker."""
-
- fold = 3
-
- @property
- def _marker(self) -> list[tuple[int, int, float]]:
- return [(3, 0, angle) for angle in self.angle_deg]
-
-
-class FourFoldMarker(SymmetryMarker):
- """Four-fold symmetry marker."""
-
- fold = 4
-
- @property
- def _marker(self) -> list[str]:
- return ["D"] * self.n
-
-
-class SixFoldMarker(SymmetryMarker):
- """Six-fold symmetry marker."""
-
- fold = 6
-
- @property
- def _marker(self) -> list[str]:
- return ["h"] * self.n
diff --git a/orix/plot/stereographic_plot.py b/orix/plot/stereographic_plot.py
index 19891dcf..d1b3d870 100644
--- a/orix/plot/stereographic_plot.py
+++ b/orix/plot/stereographic_plot.py
@@ -22,8 +22,8 @@
"""
from collections.abc import Iterable
-from copy import deepcopy
-from typing import Any, Literal, overload
+from copy import copy, deepcopy
+from typing import Any, List, Literal, Optional, Tuple, Union, overload
import matplotlib as mpl
from matplotlib import rcParams
@@ -34,20 +34,14 @@
import matplotlib.path as mpath
import matplotlib.projections as mprojections
import matplotlib.pyplot as plt
+import matplotlib.transforms as mtransforms
import numpy as np
-# fmt: off
-# isort: off
from orix.measure import pole_density_function
-from orix.plot._symmetry_marker import (
- TwoFoldMarker,
- ThreeFoldMarker,
- FourFoldMarker,
- SixFoldMarker,
+from orix.projections import (
+ InverseStereographicProjection,
+ StereographicProjection,
)
-# isort: on
-# fmt: on
-from orix.projections import InverseStereographicProjection, StereographicProjection
from orix.vector import FundamentalSector, Vector3d
from orix.vector.fundamental_sector import _closed_edges_in_hemisphere
@@ -479,7 +473,10 @@ def draw_circle(
self._hemisphere = other_hemisphere[self._hemisphere]
for i, circle in enumerate(circles):
self.plot(
- circle.azimuth, circle.polar, color=c[i], **reproject_plot_kwargs
+ circle.azimuth,
+ circle.polar,
+ color=c[i],
+ **reproject_plot_kwargs,
)
self._hemisphere = other_hemisphere[self._hemisphere]
@@ -553,7 +550,10 @@ def restrict_to_sector(
)
self.add_patch(patch)
self.set_clip_path(patch)
- labels = [self._get_label("azimuth_grid"), self._get_label("polar_grid")]
+ labels = [
+ self._get_label("azimuth_grid"),
+ self._get_label("polar_grid"),
+ ]
for c in self.collections:
if c.get_label() in labels:
c.set_clip_path(patch)
@@ -704,7 +704,10 @@ def wulff_net(
lat_resolution, lat_resolution_major, linewidth_ratio
)
self._longitudinal_grid(
- long_resolution, long_resolution_major, linewidth_ratio, wulff_net_cap
+ long_resolution,
+ long_resolution_major,
+ linewidth_ratio,
+ wulff_net_cap,
)
self._wulff_net_grid = True
elif show_grid in [None, False] and self._wulff_net_grid is True:
@@ -717,7 +720,9 @@ def wulff_net(
self._remove_collection(label)
self._wulff_net_grid = False
- def symmetry_marker(self, v: Vector3d, fold: int, **kwargs) -> None:
+ def symmetry_marker(
+ self, v: Vector3d, folds: int, modifier="none", **kwargs
+ ) -> None:
"""Plot 2-, 3- 4- or 6-fold symmetry marker(s).
Parameters
@@ -725,26 +730,30 @@ def symmetry_marker(self, v: Vector3d, fold: int, **kwargs) -> None:
v
Position of the marker(s) to plot.
fold
- Which symmetry element to plot, can be either 2, 3, 4 or 6.
+ Which symmetry element to plot, can be either 1, 2, 3, 4 or 6.
**kwargs
Keyword arguments passed to :meth:`scatter`.
"""
- if fold not in [2, 3, 4, 6]:
- raise ValueError("Can only plot 2-, 3-, 4- or 6-fold elements.")
-
- marker_classes = {
- "2": TwoFoldMarker,
- "3": ThreeFoldMarker,
- "4": FourFoldMarker,
- "6": SixFoldMarker,
- }
- marker = marker_classes[str(fold)](v, size=kwargs.pop("s", 1))
+ marker = _SymmetryMarker(
+ v, size=kwargs.pop("s", 1), folds=folds, modifier=modifier
+ )
new_kwargs = dict(zorder=ZORDER["symmetry_marker"], clip_on=False)
for k, v in new_kwargs.items():
kwargs.setdefault(k, v)
for vec, marker, marker_size in marker:
+ if folds == 1:
+ marker_size = marker_size / 2
+ # It is not (currently) possible to make two-tone markers using custom-
+ # defined Path objects in matplotlib. Instead, for inversion and
+ # rotoinversion markers, a background white dot is plotted first, whereas
+ # the top markers themselves have empty interiors.
+ if modifier != "none":
+ bg_kwargs = copy(kwargs)
+ if "color" in bg_kwargs:
+ _ = bg_kwargs.pop("color")
+ self.scatter(vec, color="w", s=marker_size * 0.2, **bg_kwargs)
self.scatter(vec, marker=marker, s=marker_size, **kwargs)
# TODO: Find a way to control padding, so that markers aren't
@@ -987,7 +996,9 @@ def res2llonglines(res, cap, rotation_pole):
if self._long_resolution_major > 0:
lc_major = mcollections.LineCollection(
res2llonglines(
- self._long_resolution_major, self._wulff_net_cap, rotation_pole
+ self._long_resolution_major,
+ self._wulff_net_cap,
+ rotation_pole,
),
label=self._get_label("longitudinal_grid_major"),
**kwargs_major,
@@ -1013,7 +1024,9 @@ def _get_label(name: str) -> str:
@overload
def _get_collection(
- self, label: str, collections: maxes.Axes.ArtistList[mcollections.Collection]
+ self,
+ label: str,
+ collections: maxes.Axes.ArtistList[mcollections.Collection],
) -> mcollections.Collection: ... # pragma: no cover
@overload
@@ -1346,6 +1359,148 @@ def _order_in_hemisphere(polar: np.ndarray, pole: int) -> np.ndarray | None:
return order
+class _SymmetryMarker:
+ """A class for creating Symmetry element markers. Intended for making
+ stereographic plots of the crystallographic point groups.
+
+ Intended to be used indirectly in
+ :func:`~orix.plot.StereographicPlot.symmetry_marker`.
+
+ Parameters
+ ----------
+ v
+ Vector(s) giving marker positions in the stereographic plot.
+ size
+ Value(s) passed to matplotlib to determine relative marker
+ size.
+ folds
+ The rotational symmetry (typically 1, 2, 3, 4, or 6) that
+ determines the symmetry marker's shape
+ (circle, elliplse, triangle, square, or hex).
+ modifier
+ Determines what alterations, if any, should be added to
+ the marker. None (the default) or "rotation" will add
+ nothing. "rotoinversion" will add a white rotoinversion
+ symbol inside the marker, which for even-fold rotations is a
+ polygon with half as many corners as the marker, and for an
+ odd-fold rotation is a white dot. "inversion" will add an
+ inversion symbol, which is a white dot. The default is None.
+
+ """
+
+ def __init__(
+ self,
+ v: Vector3d | np.ndarray | list | tuple,
+ size: int = 1,
+ folds: Literal[1, 2, 3, 4, 6] = 2,
+ modifier: Literal[None, "rotation", "rotoinversion", "inversion"] = None,
+ ):
+ fold_opt = [1, 2, 3, 4, 6]
+ if folds not in fold_opt:
+ # NOTE: If anyone is interested insupoorting 5-fold, 7-fold,etc. rotations
+ # in the future, be aware you will also need to modify the affine rotation
+ # applied at the end of the self._marker property based on your axis
+ # choices, or the markers will not properly align.
+ raise ValueError(
+ f"Folds must be one of {', '.join(map(str, fold_opt))}, not {folds}"
+ )
+ mod_opt = [None, "none", "rotation", "rotoinversion", "inversion"]
+ if modifier not in mod_opt:
+ raise ValueError(
+ f"Modifier must be one of {', '.join(map(str, mod_opt))},"
+ + "not {modifier}"
+ )
+ self._vector = Vector3d(v)
+ self._size = size
+ self._folds = folds
+ self._inner_shape = modifier
+
+ @property
+ def angle_deg(self) -> np.ndarray:
+ """Position in degrees."""
+ return np.rad2deg(self._vector.azimuth) + 90
+
+ @property
+ def multiplicity(self) -> np.ndarray:
+ """Multiplicity of each symmetry marker, ie, how many symmetrically
+ equivalent markers will be plotted."""
+ return np.ones(self.n) * self._size
+
+ @property
+ def n(self) -> int:
+ """Number of symmetry markers."""
+ return self._vector.size
+
+ def __iter__(self) -> [Vector3d, mpath.Path, np.float64]:
+ """Dunder function for iterating through multiple markers defined within a
+ single _SymmetryMarker Class.
+
+ For example, if a _SymmetryMarker is created with 4 vertices, this allows for
+ iteration over those vertices in a 'for' loop."""
+ for v, marker, size in zip(self._vector, self._marker, self.multiplicity):
+ yield v, marker, size
+
+ @property
+ def _marker(self) -> mpath.Path:
+ """Returns a matplotlib Path object that describes the symmetry marker's
+ shape"""
+ azimuth = self._vector.azimuth
+ # pre-define inner cirle (reused)
+ inner_circle = mpath.Path.circle((0, 0), 0.5)
+ i_vert = np.copy(inner_circle.vertices)
+ i_code = inner_circle.codes
+ # pre-define 2-fold ellipse (reused)
+ e_vert = np.copy(i_vert * 2)
+ e_code = np.copy(i_code)
+ e_vert[:, 1] = e_vert[:, 1] * ((1 - e_vert[:, 0] ** 2) ** 0.5) * 0.35
+ if self._folds == 1:
+ # return either a normal circle, or a cirle with a dot in the
+ # center if this also an inversion center
+ circle = mpath.Path.circle((0, 0), 0.75)
+ vert = np.copy(circle.vertices)
+ code = circle.codes
+ if self._inner_shape == "inversion":
+ verts = np.concatenate([vert, i_vert[::-1]])
+ codes = np.concatenate([code, i_code])
+ marker = mpath.Path(verts, codes)
+ else:
+ marker = mpath.Path(vert, code)
+ return [marker.deepcopy() for ang in azimuth]
+ if self._folds == 2:
+ # use the ellipse defined above
+ marker = mpath.Path(e_vert, e_code)
+ else:
+ # if it's not 2-fold, just use a default polygon
+ marker = mpath.Path.unit_regular_polygon(self._folds)
+ if self._inner_shape == "inversion":
+ # add the inner circle
+ verts = np.concatenate([marker.vertices, i_vert[::-1]])
+ codes = np.concatenate([marker.codes, i_code])
+ marker = mpath.Path(verts, codes)
+ elif self._inner_shape == "rotoinversion":
+ # add an inner shape with half the folds
+ vert = np.copy(marker.vertices)
+ code = np.copy(marker.codes)
+ if self._folds == 4:
+ inner = mpath.Path(e_vert, e_code)
+ else:
+ inner = mpath.Path.unit_regular_polygon(int(self._folds / 2))
+ i_vert = np.copy(inner.vertices[::-1])
+ i_code = np.copy(inner.codes)
+ verts = np.concatenate([vert, i_vert])
+ codes = np.concatenate([code, i_code])
+ marker = mpath.Path(verts, codes)
+ # rotate each marker to align with local symmetry lines
+ # icons are not, by default, properly aligned. Let's fix that.
+ if self._folds == 3:
+ azimuth -= np.pi / 2
+ azimuth[self._vector.polar > 1e-6] -= np.pi / 2
+
+ trans = [mtransforms.Affine2D().rotate(a + (np.pi / 2)) for a in azimuth]
+
+ return [marker.deepcopy().transformed(i) for i in trans]
+
+
def _get_kwargs_for_wulff_net_grids(
linewidth_ratio: float,
) -> tuple[dict[str, Any], dict[str, Any]]:
@@ -1383,7 +1538,9 @@ def _get_color_from_dict(d: dict[str, Any]) -> COLORLIKE:
return c
-def _update_kwargs_for_fundamental_sector_patch(d: dict[str, Any]) -> dict[str, Any]:
+def _update_kwargs_for_fundamental_sector_patch(
+ d: dict[str, Any],
+) -> dict[str, Any]:
for k, v in [("facecolor", "none"), ("edgecolor", "k"), ("linewidth", 1)]:
d.setdefault(k, v)
return d
diff --git a/orix/quaternion/symmetry.py b/orix/quaternion/symmetry.py
index 6bad5242..b56d8fdd 100644
--- a/orix/quaternion/symmetry.py
+++ b/orix/quaternion/symmetry.py
@@ -19,12 +19,13 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Literal
+from typing import TYPE_CHECKING, Generator, Literal, Union
from diffpy.structure.spacegroups import GetSpaceGroup
-import matplotlib.figure as mfigure
+import matplotlib.pyplot as plt
import numpy as np
+from orix._utils.deprecation import deprecated, deprecated_argument
from orix.quaternion.rotation import Rotation
from orix.vector.vector3d import Vector3d
@@ -69,6 +70,7 @@ class Symmetry(Rotation):
"""
name = ""
+ _schoenflies = ""
# -------------------------- Properties -------------------------- #
@@ -85,7 +87,8 @@ def is_proper(self) -> bool:
@property
def subgroups(self) -> list[Symmetry]:
"""Return the list groups that are subgroups of this group."""
- return [g for g in _groups if g._tuples <= self._tuples]
+ groups = _point_groups_dictionary["permutations"]
+ return [g for g in groups if g._tuples <= self._tuples]
@property
def proper_subgroups(self) -> list[Symmetry]:
@@ -149,10 +152,12 @@ def euler_fundamental_region(self) -> tuple[int, int, int]:
"211": (360, 90, 360), # Monoclinic
"121": (360, 90, 360),
"112": (360, 180, 180),
+ "2": (360, 180, 180),
"222": (360, 90, 180), # Orthorhombic
"4": (360, 180, 90), # Tetragonal
"422": (360, 90, 90),
"3": (360, 180, 120), # Trigonal
+ "321": (360, 90, 120),
"312": (360, 90, 120),
"32": (360, 90, 120),
"6": (360, 180, 60), # Hexagonal
@@ -226,7 +231,8 @@ def fundamental_sector(self) -> "FundamentalSector":
if self.size > 1 + n.size:
angle = 2 * np.pi * (1 + n.size) / self.size
new_v = Vector3d.from_polar(
- azimuth=[np.pi / 2, angle - np.pi / 2], polar=[np.pi / 2, np.pi / 2]
+ azimuth=[np.pi / 2, angle - np.pi / 2],
+ polar=[np.pi / 2, np.pi / 2],
)
n = Vector3d(np.vstack([n.data, new_v.data]))
@@ -287,9 +293,9 @@ def _primary_axis_order(self) -> int | None:
name = self.proper_subgroup.name
if name in ["1", "211", "121"]:
return 1
- elif name in ["112", "222", "23"]:
+ elif name in ["112", "222", "23", "2"]:
return 2
- elif name in ["3", "312", "32"]:
+ elif name in ["3", "312", "321", "32"]:
return 3
elif name in ["4", "422", "432"]:
return 4
@@ -330,14 +336,14 @@ def symmetry_axis(v: Vector3d, n: int) -> Rotation:
if name in ["1", "211", "121"]:
# All proper operations
rot = self[~self.improper]
- elif name in ["112", "3", "4", "6"]:
+ elif name in ["2", "112", "3", "4", "6"]:
# Identity
rot = self[0]
- elif name in ["222", "422", "622", "32"]:
+ elif name in ["222", "422", "622", "32", "321"]:
# Two-fold rotation about a-axis perpendicular to c-axis
rot = symmetry_axis(-vx, 2)
elif name == "312":
- # Mirror plane perpendicular to c-axis?
+ # Mirror plane perpendicular to c-axis
rot = symmetry_axis(-mirror, 2)
elif name in ["23", "432"]:
# Three-fold rotation about [111]
@@ -417,6 +423,8 @@ def from_generators(cls, *generators: Rotation) -> Symmetry:
# --------------------- Other public methods --------------------- #
def get_axis_orders(self) -> dict[Vector3d, int]:
+ """Return a dictionary of every rotation axis and it's order
+ (ie, folds)"""
s = self[self.angle > 0]
if s.size == 0:
return {}
@@ -426,6 +434,8 @@ def get_axis_orders(self) -> dict[Vector3d, int]:
}
def get_highest_order_axis(self) -> tuple[Vector3d, np.ndarray]:
+ """Return the highest order rotational axis and it's order
+ (ie, folds)"""
axis_orders = self.get_axis_orders()
if len(axis_orders) == 0:
return Vector3d.zvector(), np.inf
@@ -469,82 +479,374 @@ def fundamental_zone(self) -> Vector3d:
sr = SphericalRegion(n.unique())
return sr
+ @deprecated_argument(name="orientation", since="1.5", removal="1.6")
+ @deprecated_argument(name="reproject_scatter_kwargs", since="1.5", removal="1.6")
def plot(
self,
+ asymmetric_vector: Vector3d | None = None,
orientation: "Orientation | None" = None,
+ show_name: bool = True,
+ ax: plt.Axes | None = None,
+ return_figure: bool = True,
+ marker_dict: dict = {},
reproject_scatter_kwargs: dict | None = None,
- **kwargs,
- ) -> mfigure.Figure | None:
- """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.
+ marker_size: float = 150.0,
+ mirror_width: float = 2.0,
+ asymmetric_vector_dict: dict = {},
+ asymmetric_vector_size: float = 50.0,
+ ) -> plt.Figure | None:
+ """Creates a stereographic projection of symmetry operations
+ in the group. Can also plot symmetrically equivalent variations
+ of orientations or vectors to demonstrate the effect of
+ symmetry operations.
Parameters
----------
- orientation
- The symmetry operations are applied to this orientation
- before plotting. The default value uses an orientation
- optimized to show symmetry elements.
- reproject_scatter_kwargs
- 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
- Keyword arguments passed to
- :meth:`~orix.plot.StereographicPlot.scatter`, which passes
- these on to :meth:`matplotlib.axes.Axes.scatter`.
+ asymmetric_vector
+ A marker will be added at the stereographic projection of
+ this vector, along with all it's symmetrically equivalent
+ rotations. By default, no vector will be plotted, and only
+ rotation and mirror markers will be added to the plot.
+ show_name
+ If True, add both the Schoenflies and Hermann-Mauguin names
+ of the point group to the title.
+ ax
+ The matplotlib.Axis object into which to add the
+ stereographic plot. If None is passed, a new figure and
+ axis will be generated.
+ return_figure
+ If True, return the figure containing the plotting axis.
+ marker_dict
+ A dictionary of arguments to modify how the symmetry markers
+ are generated. The following options are the overwritable
+ defaults:
+ 1: 'black' <-- 1-fold marker color
+ 2: 'green' <-- 2-fold marker color
+ 3: 'red' <-- 3-fold marker color
+ 4: 'purple' <-- 4-fold marker color
+ 6: 'magenta' <-- 6-fold marker color
+ 'm': 'blue' <-- 1-fold marker color
+ marker_size
+ The size of the rotational makers to be added to the plot.
+ This is equivalent to the argument "s" in matplotlib.scatter
+ mirror_width
+ The width of the line used to draw the mirror planes. This is
+ equivalent to the argument "linewidth" in matplotlib.plot
+ asymetric_vector_dict
+ A dictionary of arguments to modify the asymetric_vector
+ markers. The following options are the overwritable defaults:
+ 'upper_color': 'black' < -- Upper hemisphere marker color
+ 'lower_color': 'grey' < -- Lower hemisphere marker color
+ 'upper_marker': '+' < -- Upper hemisphere marker shape
+ 'lower_marker': 'o' < -- Lower hemisphere marker shape
+ asymmetric_vector_size
+ Size of the markers used to plot the asymetric vector markers.
+ Default is 50.
Returns
-------
fig
The created figure, returned if ``return_figure=True`` is
passed as a keyword argument.
+
+ Notes
+ -----
+
+ This function was designed to produce figures matching those in
+ International Tables for Crystallography Volume A, Table 10.2.2.
+ ITC includes certain design decisions not adhered to by other
+ sources and textbooks, such as excluding inversion markers from
+ axes in the same plane as the plot, or the rotational
+ orientation of the rotoinversion markers.
"""
- 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=[r"$e_1$", r"$e_2$", None],
- label="upper",
- reproject=True,
- reproject_scatter_kwargs=reproject_scatter_kwargs,
- **kwargs,
- )
- # add symmetry name to figure title
- figure.suptitle(f"${self.name}$")
+ # depreciated input arguments. remove after 0.16
+ if orientation is not None: # pragma: no cover
+ # 'orientation' was replaced with "asymmetric_vector", so if that
+ # input is not None, ignore 'orientation'.
+ if asymmetric_vector is None:
+ asymmetric_vector = orientation.axis
+ if reproject_scatter_kwargs is not None: # pragma: no cover
+ marker_dict.update(reproject_scatter_kwargs)
+
+ # import orix.plot so matplotlib knows what the stereographic projection is.
+ import orix.plot
+
+ # dictionary of default colors for the symmetry markers.
+ colors = {
+ 1: "black",
+ 2: "green",
+ 3: "red",
+ 4: "purple",
+ 6: "magenta",
+ "m": "blue",
+ }
+ # after resetting defaults, update color choices passed in via color_dict
+ colors.update(marker_dict)
+ # if the user did not pass in an axis, generate one
+ if ax is None:
+ fig, ax = plt.subplots(subplot_kw={"projection": "stereographic"})
+ else:
+ fig = ax.get_figure()
+
+ # add a default title if requested
+ if show_name:
+ ax.set_title(self._schoenflies + " ( " + self.name + " )")
+
+ # determine the symnmetry elements and plot them.
+ elements = self._get_symmetry_elements()
+ for v, m, t, f in zip(*elements):
+ # plot each symmetrically equivalent mirror plane only once
+ if m:
+ for mv in (self * v).unique():
+ m_circ = mv.get_circle()
+ ax.plot(m_circ, color=colors["m"], linewidth=mirror_width)
+ # plot each symmetrically equivalent rotation element only once
+ c = colors[f]
+ if f > 1:
+ for sv in (self * v).unique():
+ # ITC doesn't plot inversion or rotoinversion markers for
+ # symmetry elements with axes perpendicular to the out-of plane
+ # direction, as the information is redundant.
+ z_ang = np.abs(sv.angle_with(Vector3d.zvector()))
+ if np.abs(z_ang - (np.pi / 2)) < 1e-4:
+ ax.symmetry_marker(
+ sv, folds=f, s=marker_size, color=c, modifier=None
+ )
+ else:
+ ax.symmetry_marker(
+ sv, folds=f, s=marker_size, color=c, modifier=t
+ )
+ # if this is the primary axis and there is no rotation but an inversion
+ # (ie, this is symmetry.Ci, the `-1` PG), add the appropriate marker.
+ elif f == 1 and np.abs(v.angle_with(Vector3d.zvector())) < 1e-4:
+ if t != "inversion":
+ continue
+ for sv in (self * v).unique():
+ ax.symmetry_marker(sv, folds=f, s=marker_size, color=c, modifier=t)
+
+ # plot asymmetric markers if requested.
+ if asymmetric_vector is not None:
+ v_symm = self.outer(asymmetric_vector).flatten()
+ vdict = {
+ "upper_color": "black",
+ "lower_color": "grey",
+ "upper_marker": "+",
+ "lower_marker": "o",
+ }
+ vdict.update(asymmetric_vector_dict)
+ mask = v_symm.z >= 0
+ ax.scatter(
+ -1 * v_symm[~mask],
+ marker=vdict["lower_marker"],
+ c=vdict["lower_color"],
+ )
+ ax.scatter(
+ v_symm[mask],
+ marker=vdict["upper_marker"],
+ c=vdict["upper_color"],
+ )
+ # return the figure if requested
if return_figure:
- return figure
+ return fig
+
+ # ------------------------ private functions ------------------------- #
+
+ def _get_symmetry_elements(self) -> (Vector3d, bool, str, int):
+ """Return all the crystallographically unique axes and their
+ associated symmetry elements (mirrors, rotations,
+ rotoinversions, etc).
+
+ Returns
+ -------
+ axes
+ Vector(s) that are parallel to an axis of rotation or
+ perpendicular to a mirror plane, or both.
+ is_mirror
+ Whether each axis is perpendicular to a mirror plane.
+ s_type
+ The type of rotational symmetry assoicated with the axis.
+ Options are "rotation", "rotoinversion", or "inversion".
+ folds
+ The order of the rotationinal symmetry, either 1,2,3,4, or
+ 6. 1 indicates an axis with no rotational symmetry, meaning
+ it has a mirror associated with it.
+
+ Notes
+ -------
+ This function does not return ALL the axes and angles,
+ (that function would be `Symmetry.to_axes_angles`), nor does it
+ return the minimum generating elements. Instead, it returns all
+ the primary axes plus information about the rotations,
+ inversions, and/or mirrors associated with each.
+
+ The algorithm works by finding the crystallographically unique
+ rotation axes in a symmetry group, and for each one, searching
+ every symmetry operator sharing that axis. Based on each
+ subset, it determines if a mirror and/or inversion is present,
+ and if there is a rotation or rotoinversion. Based on the
+ choice of rotation or rotoinversion, the order of the
+ symmetry element (ie, the number of 'folds') is also found.
+ """
+ # grab the absolute value of the angular component as a quick way
+ # to find mirrors, inversion, and identity elements
+ abs_angle = np.abs(self.angle)
+ # create True/False arrays to help sort out which elements are what.
+ # proper elements
+ is_proper = ~self.improper
+ # mirror planes
+ is_mirror = (np.abs(abs_angle - np.pi) < 1e-4) * self.improper
+ # rotations (both proper and improper)
+ is_rotation = abs_angle > 1e-4
+ # rotoinversions
+ is_rotoinversion = is_rotation * self.improper * ~is_mirror
+ # the inversion symmetry
+ is_inversion = (~is_rotation) * self.improper
+ if np.sum(is_inversion) > 0:
+ has_inversion = True
+ else:
+ has_inversion = False
+ # Find the symmetrically unique axes, and record which axes correspond
+ # to each unique representation.
+ unique_axes, unique_idx = (self.axis.in_fundamental_sector(self)).unique(
+ return_inverse=True
+ )
+
+ # iterate through each unique axis and determine the associated
+ # symmetry elements.
+ elements = []
+ for i, axis in enumerate(unique_axes):
+ # mask out just axes elements in the fundamental sector to avoid repeats
+ is_axis = unique_idx == i
+ # check for mirrors
+ m_flag = np.any(is_mirror * is_axis)
+ # set 'folds' and 's_type' to illegal parameters. This way, if an
+ # edge case appears where the following if/then search does not
+ # overwrite their values, it will be obvious in the final results.
+ folds = 0
+ s_type = "empty"
+ # check to see if there are only proper rotations
+ if np.all(is_proper[is_axis]):
+ # This might just be the identity.
+ if not np.any(is_rotation * is_axis):
+ elements.append(
+ (axis, m_flag, None, 1),
+ )
+ continue
+ min_ang = np.abs(self[is_rotation * is_axis].angle).min()
+ folds = np.around(2 * np.pi / min_ang).astype(int)
+ elements.append(
+ (axis, m_flag, "rotation", folds),
+ )
+ continue
+ # Check if there is a rotation with an inversion
+ elif has_inversion:
+ # this might just be the 1-fold inversion center
+ if not np.any(is_rotation * is_axis):
+ elements.append(
+ (axis, m_flag, "inversion", 1),
+ )
+ continue
+ min_ang = np.abs(self[is_rotation * is_axis].angle).min()
+ folds = np.around(2 * np.pi / min_ang).astype(int)
+ elements.append(
+ (axis, m_flag, "inversion", folds),
+ )
+ continue
+ # the only other important option is a rotoinversion
+ elif np.any(is_rotoinversion[is_axis]):
+ min_ang = np.abs(self[is_rotoinversion * is_axis].angle).min()
+ folds = np.around(2 * np.pi / min_ang).astype(int)
+ elements.append(
+ (axis, m_flag, "rotoinversion", folds),
+ )
+ continue
+ # if it it not a rotational symmetry of any type, it's a mirror
+ else:
+ elements.append(
+ (axis, m_flag, None, 1),
+ )
+ # Finally, 3-fold rotations around the 111 create <110> mirrors
+ # not on the primary axes. These we can add by hand.
+ if np.any(np.abs(Vector3d([1, 1, 1]).angle_with(self.axis)) < 1e-4):
+ v = Vector3d([0, 1, 1]).in_fundamental_sector(self)
+ elements.append((v, True, None, 1))
+ # split the list of lists into 4 variables.
+ axes = [x[0] for x in elements]
+ is_mirror = [x[1] for x in elements]
+ s_type = [x[2] for x in elements]
+ folds = [x[3] for x in elements]
+ return axes, is_mirror, s_type, folds
+
+
+# ---------------- Proceedural definitions of Point Groups ---------------- #
+# NOTE: ORIX uses Schoenflies symbols to define point groups. This is partly
+# because the notation is short and always starts with a letter (ie, they
+# make convenient python variables), and partly because it helps limit
+# accidental misinterpretation of Hermann-Mauguin symbols as space group
+# numbers. For example. "222" could be interpreted as SG#222 == Pn-3n, or
+# as PG'222'== D3. there are similar examples with 2, 3, 4, 32, etc.
+
+# Additionally, there are 43 crystallographically valid Schonflies group
+# notations, but only 32 unique ones, meaning certain point groups have
+# redundant representations in Schonflies notation(S4==C4i, Ci==S2, S6==C3i,
+# and C2==D1, for example). The International Tables for Crystallography (ITC),
+# Volume A, Section 12.1 defines the 32 standard representations, but a few of
+# the commonly used redundant ones are given below for convenience.
+
+# Finally, while there are 32 Point groups, ITC names several additional
+# projections for the non-centrosymmetric groups (ie, using x and/or y as the
+# rotation axis instead of z). These are included below as well, following
+# the ITC naming convention (for example, a 2-fold cyclic rotation around
+# the x axis instead of the z axis is called C2x.)
+
+# For more details on how point groups can be generated, the following three
+# resources lay out three different but equally valid approaches:
+# 1)"Structure of Materials", De Graef et al, Section 9.2
+# 2)"International Tables for Crystallography: Volume A" Section 12.1
+# 3)"Crystallogrpahic Texture and Group Representations", Chi-Sing Man,Ch2
+
+# ---------------- Proceedural definitions of Point Groups ---------------- #
+# NOTE: ORIX uses Schoenflies symbols to define point groups. This is partly
+# because the notation is short and always starts with a letter (ie, they
+# make convenient python variables), and partly because it helps limit
+# accidental misinterpretation of Hermann-Mauguin symbols as space group
+# numbers. For example. "222" could be interpreted as SG#222 == Pn-3n, or
+# as PG'222'== D3. there are similar examples with 2, 3, 4, 32, etc.
+
+# Additionally, there are 43 crystallographically valid Schonflies group
+# notations, but only 32 unique ones, meaning certain point groups have
+# redundant representations in Schonflies notation(S4==C4i, Ci==S2, S6==C3i,
+# and C2==D1, for example). The International Tables for Crystallography,
+# Volume A, Section 12.1 defines the 32 standard representations, but a few of
+# the commonly used redundant ones are given below for convenience.
+
+# Finally, while there are 32 Point groups, ITC names several additional
+# projections for the non-centrosymmetric groups (ie, using x and/or y as the
+# rotation axis instead of z). These are included below as well, following
+# the ITC naming convention (for example, a 2-fold cyclic rotation around
+# the x axis instead of the z axis is called C2x.)
+
+# For more details on how point groups can be generated, the following three
+# resources lay out three different but equally valid approaches:
+# 1)"Structure of Materials", De Graef et al, Section 9.2
+# 2)"International Tables for Crystallography: Volume A" Section 12.1
+# 3)"Crystallogrpahic Texture and Group Representations", Chi-Sing Man,Ch2
# Triclinic
C1 = Symmetry((1, 0, 0, 0))
C1.name = "1"
-Ci = Symmetry([(1, 0, 0, 0), (1, 0, 0, 0)])
+C1._schoenflies = "C1"
+Ci = Symmetry([(1, 0, 0, 0), (-1, 0, 0, 0)])
Ci.improper = [0, 1]
Ci.name = "-1"
+Ci._schoenflies = "Ci"
+# include redundant point group S2 == Ci
+S2 = Symmetry([(1, 0, 0, 0), (-1, 0, 0, 0)])
+S2.improper = [0, 1]
+S2.name = "-1"
+S2._schoenflies = "S2"
# Special generators
_mirror_xy = Symmetry([(1, 0, 0, 0), (0, 0.75**0.5, -(0.75**0.5), 0)])
@@ -554,37 +856,53 @@ def plot(
# 2-fold rotations
C2x = Symmetry([(1, 0, 0, 0), (0, 1, 0, 0)])
C2x.name = "211"
+C2x._schoenflies = "C2x"
C2y = Symmetry([(1, 0, 0, 0), (0, 0, 1, 0)])
C2y.name = "121"
+C2y._schoenflies = "C2y"
C2z = Symmetry([(1, 0, 0, 0), (0, 0, 0, 1)])
C2z.name = "112"
+C2z._schoenflies = "C2z"
C2 = Symmetry(C2z)
C2.name = "2"
+C2._schoenflies = "C2"
+# included redundant point group D1 == C2
+D1 = Symmetry(C2z)
+D1.name = "2"
+D1._schoenflies = "D1"
# Mirrors
Csx = Symmetry([(1, 0, 0, 0), (0, 1, 0, 0)])
Csx.improper = [0, 1]
Csx.name = "m11"
+Csx._schoenflies = "Csx"
Csy = Symmetry([(1, 0, 0, 0), (0, 0, 1, 0)])
Csy.improper = [0, 1]
Csy.name = "1m1"
+Csy._schoenflies = "Csy"
Csz = Symmetry([(1, 0, 0, 0), (0, 0, 0, 1)])
Csz.improper = [0, 1]
Csz.name = "11m"
+Csz._schoenflies = "Csz"
Cs = Symmetry(Csz)
Cs.name = "m"
+Cs._schoenflies = "Cs"
# Monoclinic
C2h = Symmetry.from_generators(C2, Cs)
C2h.name = "2/m"
+C2h._schoenflies = "C2h"
# Orthorhombic
D2 = Symmetry.from_generators(C2z, C2x, C2y)
D2.name = "222"
+D2._schoenflies = "D2"
C2v = Symmetry.from_generators(C2z, Csx)
C2v.name = "mm2"
+C2v._schoenflies = "C2v"
D2h = Symmetry.from_generators(Csz, Csx, Csy)
D2h.name = "mmm"
+D2h._schoenflies = "D2h"
# 4-fold rotations
C4x = Symmetry(
@@ -592,7 +910,7 @@ def plot(
(1, 0, 0, 0),
(0.5**0.5, 0.5**0.5, 0, 0),
(0, 1, 0, 0),
- (-(0.5**0.5), 0.5**0.5, 0, 0),
+ ((0.5**0.5), -(0.5**0.5), 0, 0),
]
)
C4y = Symmetry(
@@ -600,7 +918,7 @@ def plot(
(1, 0, 0, 0),
(0.5**0.5, 0, 0.5**0.5, 0),
(0, 0, 1, 0),
- (-(0.5**0.5), 0, 0.5**0.5, 0),
+ ((0.5**0.5), -0, 0.5**0.5, 0),
]
)
C4z = Symmetry(
@@ -608,144 +926,366 @@ def plot(
(1, 0, 0, 0),
(0.5**0.5, 0, 0, 0.5**0.5),
(0, 0, 0, 1),
- (-(0.5**0.5), 0, 0, 0.5**0.5),
+ ((0.5**0.5), 0, 0, -(0.5**0.5)),
]
)
C4 = Symmetry(C4z)
C4.name = "4"
+C4._schoenflies = "C4"
# Tetragonal
S4 = Symmetry(C4)
S4.improper = [0, 1, 0, 1]
S4.name = "-4"
+S4._schoenflies = "S4"
+# include redundant point group C4i == S4
+C4i = Symmetry(C4)
+C4i.improper = [0, 1, 0, 1]
+C4i.name = "-4"
+C4i._schoenflies = "C4i"
C4h = Symmetry.from_generators(C4, Cs)
C4h.name = "4/m"
+C4h._schoenflies = "C4h"
D4 = Symmetry.from_generators(C4, C2x, C2y)
D4.name = "422"
+D4._schoenflies = "D4"
C4v = Symmetry.from_generators(C4, Csx)
C4v.name = "4mm"
+C4v._schoenflies = "C4v"
D2d = Symmetry.from_generators(D2, _mirror_xy)
D2d.name = "-42m"
+D2d._schoenflies = "D2d"
D4h = Symmetry.from_generators(C4h, Csx, Csy)
D4h.name = "4/mmm"
+D4h._schoenflies = "D4h"
# 3-fold rotations
-C3x = Symmetry([(1, 0, 0, 0), (0.5, 0.75**0.5, 0, 0), (-0.5, 0.75**0.5, 0, 0)])
-C3y = Symmetry([(1, 0, 0, 0), (0.5, 0, 0.75**0.5, 0), (-0.5, 0, 0.75**0.5, 0)])
-C3z = Symmetry([(1, 0, 0, 0), (0.5, 0, 0, 0.75**0.5), (-0.5, 0, 0, 0.75**0.5)])
+C3x = Symmetry([(1, 0, 0, 0), (0.5, 0.75**0.5, 0, 0), (0.5, -(0.75**0.5), 0, 0)])
+C3y = Symmetry([(1, 0, 0, 0), (0.5, 0, 0.75**0.5, 0), (0.5, 0, -(0.75**0.5), 0)])
+C3z = Symmetry([(1, 0, 0, 0), (0.5, 0, 0, 0.75**0.5), (0.5, 0, 0, -(0.75**0.5))])
C3 = Symmetry(C3z)
C3.name = "3"
+C3._schoenflies = "C3"
# Trigonal
+C3i = Symmetry.from_generators(C3, Ci)
+C3i.name = "-3"
+C3i._schoenflies = "C3i"
+# include redundant point group S6==C3i
S6 = Symmetry.from_generators(C3, Ci)
S6.name = "-3"
+S6._schoenflies = "S6"
D3x = Symmetry.from_generators(C3, C2x)
D3x.name = "321"
+D3x._schoenflies = "D3x"
D3y = Symmetry.from_generators(C3, C2y)
D3y.name = "312"
+D3y._schoenflies = "D3y"
D3 = Symmetry(D3x)
D3.name = "32"
+D3._schoenflies = "D3"
C3v = Symmetry.from_generators(C3, Csx)
C3v.name = "3m"
+C3v._schoenflies = "C3v"
D3d = Symmetry.from_generators(S6, Csx)
D3d.name = "-3m"
+D3d._schoenflies = "D3d"
# Hexagonal
C6 = Symmetry.from_generators(C3, C2)
C6.name = "6"
+C6._schoenflies = "C6"
C3h = Symmetry.from_generators(C3, Cs)
C3h.name = "-6"
+C3h._schoenflies = "C3h"
C6h = Symmetry.from_generators(C6, Cs)
C6h.name = "6/m"
+C6h._schoenflies = "C6h"
D6 = Symmetry.from_generators(C6, C2x, C2y)
D6.name = "622"
+D6._schoenflies = "D6"
C6v = Symmetry.from_generators(C6, Csx)
C6v.name = "6mm"
+C6v._schoenflies = "C6v"
D3h = Symmetry.from_generators(C3, C2y, Csz)
D3h.name = "-6m2"
+D3h._schoenflies = "D3h"
D6h = Symmetry.from_generators(D6, Csz)
D6h.name = "6/mmm"
+D6h._schoenflies = "D6h"
# Cubic
T = Symmetry.from_generators(C2, _cubic)
T.name = "23"
+T._schoenflies = "T"
Th = Symmetry.from_generators(T, Ci)
Th.name = "m-3"
+Th._schoenflies = "Th"
O = Symmetry.from_generators(C4, _cubic, C2x)
O.name = "432"
+O._schoenflies = "O"
Td = Symmetry.from_generators(T, _mirror_xy)
Td.name = "-43m"
+Td._schoenflies = "Td"
Oh = Symmetry.from_generators(O, Ci)
Oh.name = "m-3m"
+Oh._schoenflies = "Oh"
+
+# a dictionary of several common point group sets. This is used by
+# PointGroups to create default subsets, as well as by Symmetry to
+# determine the Laue and Proper groups/subgroups of classes.
+_point_groups_dictionary = {
+ "permutations_repeated": [
+ # Triclinic
+ C1,
+ Ci,
+ S2, # redundant
+ # Monoclinic
+ C2,
+ D1, # redundant
+ C2x,
+ C2y,
+ C2z, # redundant
+ Cs,
+ Csx,
+ Csy,
+ Csz, # redundant
+ C2h,
+ # Orthorhombic
+ D2,
+ C2v,
+ D2h,
+ # Tetragonal
+ C4,
+ S4,
+ C4i, # redundant
+ C4h,
+ D4,
+ C4v,
+ D2d,
+ D4h,
+ # Trigonal
+ C3,
+ C3i,
+ S6, # redundant
+ D3,
+ D3x,
+ D3y,
+ C3v,
+ D3d,
+ # Hexagonal
+ C6,
+ C3h,
+ C6h,
+ D6,
+ C6v,
+ D3h,
+ D6h,
+ # cubic
+ T,
+ Th,
+ O,
+ Td,
+ Oh,
+ ],
+ "permutations": [
+ # Triclinic
+ C1,
+ Ci,
+ # Monoclinic
+ C2,
+ C2x,
+ C2y,
+ Cs,
+ Csx,
+ Csy,
+ C2h,
+ # Orthorhombic
+ D2,
+ C2v,
+ D2h,
+ # Tetragonal
+ C4,
+ S4,
+ C4h,
+ D4,
+ C4v,
+ D2d,
+ D4h,
+ # Trigonal
+ C3,
+ C3i,
+ D3,
+ D3y,
+ C3v,
+ D3d,
+ # Hexagonal
+ C6,
+ C3h,
+ C6h,
+ D6,
+ C6v,
+ D3h,
+ D6h,
+ # cubic
+ T,
+ Th,
+ O,
+ Td,
+ Oh,
+ ],
+ "groups": [
+ # Triclinic
+ C1,
+ Ci,
+ # Monoclinic
+ C2,
+ Cs,
+ C2h,
+ # Orthorhombic
+ D2,
+ C2v,
+ D2h,
+ # Tetragonal
+ C4,
+ S4,
+ C4h,
+ D4,
+ C4v,
+ D2d,
+ D4h,
+ # Trigonal
+ C3,
+ C3i,
+ D3,
+ C3v,
+ D3d,
+ # Hexagonal
+ C6,
+ C3h,
+ C6h,
+ D6,
+ C6v,
+ D3h,
+ D6h,
+ # cubic
+ T,
+ Th,
+ O,
+ Td,
+ Oh,
+ ],
+ "proper_groups": [
+ # Triclinic
+ C1,
+ # Monoclinic
+ C2,
+ # Orthorhombic
+ D2,
+ D4,
+ # Tetragonal
+ C4,
+ # Trigonal
+ C3,
+ D3,
+ # Hexagonal
+ C6,
+ D6,
+ # cubic
+ T,
+ O,
+ ],
+ "proper_permutations": [
+ # Triclinic
+ C1,
+ # Monoclinic
+ C2,
+ C2x,
+ C2y,
+ # Orthorhombic
+ D2,
+ # Tetragonal
+ C4,
+ # Trigonal
+ C3,
+ D3,
+ D3x,
+ D3y,
+ # Hexagonal
+ C6,
+ D6,
+ # cubic
+ T,
+ O,
+ ],
+ "laue": [
+ # Triclinic
+ Ci,
+ # Monoclinic
+ C2h,
+ # Orthorhombic
+ D2h,
+ D4h,
+ # Tetragonal
+ C4h,
+ # Trigonal
+ C3i,
+ D3d,
+ # Hexagonal
+ C6h,
+ D6h,
+ # cubic
+ Th,
+ Oh,
+ ],
+ "procedural": [
+ # Cyclic
+ C1,
+ C2,
+ C3,
+ C4,
+ C6,
+ # Dihedral
+ D2,
+ D3,
+ D4,
+ D6,
+ # Cyclic plus inversion (\ba{n})
+ Ci,
+ Cs,
+ C3i,
+ S4,
+ C3h,
+ # Cyclic plus perpendicular mirrors (n/m)
+ C2h,
+ C4h,
+ C6h,
+ # Cyclic plus vertical mirrors (nm)
+ C2v,
+ C3v,
+ C4v,
+ C6v,
+ # Dihedral plus diagonal mirrors (\bar{n} m)
+ D3d,
+ D2d,
+ D3h,
+ # Dihedral with vertical and perpendicular mirros (n/m m)
+ D2h,
+ D4h,
+ D6h,
+ # Combining cyclic (n1 n2)
+ T,
+ O,
+ # combining cyclic and mirrors
+ Th,
+ Td,
+ Oh,
+ ],
+}
-# Collections of groups for convenience
-# fmt: off
-_groups = [
- # Schoenflies Crystal system International Laue class Proper point group
- C1, # Triclinic 1 -1 1
- Ci, # Triclinic -1 -1 1
- C2x, # Monoclinic 211 2/m 211
- C2y, # Monoclinic 121 2/m 121
- C2z, # Monoclinic 112 2/m 112
- Csx, # Monoclinic m11 2/m 1
- Csy, # Monoclinic 1m1 2/m 1
- Csz, # Monoclinic 11m 2/m 1
- C2h, # Monoclinic 2/m 2/m 112
- D2, # Orthorhombic 222 mmm 222
- C2v, # Orthorhombic mm2 mmm 211
- D2h, # Orthorhombic mmm mmm 222
- C4, # Tetragonal 4 4/m 4
- S4, # Tetragonal -4 4/m 112
- C4h, # Tetragonal 4/m 4/m 4
- D4, # Tetragonal 422 4/mmm 422
- C4v, # Tetragonal 4mm 4/mmm 4
- D2d, # Tetragonal -42m 4/mmm 222
- D4h, # Tetragonal 4/mmm 4/mmm 422
- C3, # Trigonal 3 -3 3
- S6, # Trigonal -3 -3 3
- D3x, # Trigonal 321 -3m 32
- D3y, # Trigonal 312 -3m 312
- D3, # Trigonal 32 -3m 32
- C3v, # Trigonal 3m -3m 3
- D3d, # Trigonal -3m -3m 32
- C6, # Hexagonal 6 6/m 6
- C3h, # Hexagonal -6 6/m 6
- C6h, # Hexagonal 6/m 6/m 622
- D6, # Hexagonal 622 6/mmm 622
- C6v, # Hexagonal 6mm 6/mmm 6
- D3h, # Hexagonal -6m2 6/mmm 312
- D6h, # Hexagonal 6/mmm 6/mmm 622
- T, # Cubic 23 m-3 23
- Th, # Cubic m-3 m-3 23
- O, # Cubic 432 m-3m 432
- Td, # Cubic -43m m-3m 23
- Oh, # Cubic m-3m m-3m 432
-]
-# fmt: on
-_proper_groups = [C1, C2, C2x, C2y, C2z, D2, C4, D4, C3, D3x, D3y, D3, C6, D6, T, O]
-
-
-def get_distinguished_points(s1: Symmetry, s2: Symmetry = C1) -> Rotation:
- """Return points symmetrically equivalent to identity with respect
- to ``s1`` and ``s2``.
-
- Parameters
- ----------
- s1
- First symmetry.
- s2
- Second symmetry.
-
- Returns
- -------
- distinguished_points
- Distinguished points.
- """
- distinguished_points = s1.outer(s2).antipodal.unique(antipodal=False)
- return distinguished_points[distinguished_points.angle > 0]
-
-
-spacegroup2pointgroup_dict = {
+# Dictionary used to convert diffpy.structure space group names to their
+# equivalent orix.symmetry.Symmetry objects.
+_spacegroup2pointgroup_dict = {
"PG1": {"proper": C1, "improper": C1},
"PG1bar": {"proper": C1, "improper": Ci},
"PG2": {"proper": C2, "improper": C2},
@@ -788,39 +1328,302 @@ def get_distinguished_points(s1: Symmetry, s2: Symmetry = C1) -> Rotation:
}
-def get_point_group(space_group_number: int, proper: bool = False) -> Symmetry:
- """Map a space group number to its (proper) point group.
+# TODO: there is no need for this to subclass list
+class PointGroups:
+ # make a lookup table of common subsets of Point Groups
+ subset_names = _point_groups_dictionary.keys()
+ _point_group_names = dict(
+ [(x.name, x) for x in _point_groups_dictionary["permutations_repeated"]]
+ ).keys()
+
+ def __init__(self, symmetry_list: list | Symmetry | str = "groups"):
+ """A group of symmetry operators with convenence functions
+ for parsing entries and displaying information.
+
+ This class is primarily intended to be called using
+ PointGroups.subset(), or to return a single Symmetry
+ object using PointGroups.get(). However, a list of Symmetry
+ objects can also be passed in to create a custom PointGroup.
+
+ Parameters
+ ----------
+ symmetry_list
+ Either a string matching one of the keys in
+ `PointGroups.subset_names`, or a list of
+ orix.quaternion.symmetry.Symmetry objects. Default is
+ 'groups', which returns the 32 crystalographic point
+ groups in the order given in the International Tables
+ for Crystallography, Chapter 10.
+ """
+ if isinstance(symmetry_list, str):
+ pgs = self.get_set(symmetry_list)
+ self.__init__(pgs.symms)
+ elif isinstance(symmetry_list, Symmetry):
+ self.symms = [symmetry_list]
+ elif isinstance(symmetry_list, list):
+ if np.all([isinstance(y, Symmetry) for y in symmetry_list]):
+ self.symms = symmetry_list
+ else:
+ raise ValueError(
+ "All entries in 'symmetry_list' must be Symmetry objects"
+ )
+ else:
+ raise ValueError(
+ "symmetry list must either be one of"
+ + f"{', '.join(map(str, PointGroups.subset_names))}, or a list of"
+ + f"symmetry operators, not '{symmetry_list}'"
+ )
+
+ def __repr__(self):
+ str_data = (
+ "| Name | System | HM | Laue | Proper |\n"
+ + "=" * 49
+ + "\n"
+ + "\n".join(
+ [
+ "| "
+ + "| ".join(
+ [
+ str(x._schoenflies).ljust(6),
+ str(x.system).ljust(13),
+ str(x.name).ljust(6),
+ str(x.laue.name).ljust(6),
+ str(x.proper_subgroup.name).ljust(7),
+ ]
+ )
+ + "|"
+ for x in self.symms
+ ]
+ )
+ )
+
+ return str_data
+
+ def __iter__(self) -> Generator[Symmetry]:
+ return iter(self.symms)
+
+ def __getitem__(self, index) -> PointGroups:
+ pg_subset = PointGroups(self.symms[index])
+ return pg_subset
+
+ def __len__(self):
+ return len(self.symms)
+
+ def to_list(self):
+ """Return the symmetry operators as a list.
+
+ Returns
+ -------
+ symmetry_list
+ returns the symmetry operators in this :class:PointGroup
+ object as a list of :class:Symmetry instances.
+ """
+ return self.symms
+
+ def get(name: Literal[PointGroups._point_group_names]):
+ """
+ Given a string or integer representation, this function will attempt
+ to return an associated Symmetry object.
+
+ This is done by first checking the labels defined in orix, which
+ includes Hermann-Mauguin ('m3m' or '2', etc.) and Shoenflies
+ ('C6' or D3h', etc.).
+
+ Next, it will check through common point group Aliases used by EDAX.
+
+ If none of the anove result in a match, it will attempt to look
+ up the space group name using diffpy's GetSpaceGroup, and relate that back
+ to a point group. this is equivalent to PointGroups.from_space_group(name)
+
+ Parameters
+ ----------
+ name : string in PointGroups._point_group_names
+ either the Hermann-Maugin or Shoenflies name for a crystallographic
+ point gorup.
+
+ Returns
+ -------
+ point_group
+ an object of Class `Symmetry` representing the requested
+ crystallographic point group.
+ """
+ # check the 'groups' list first, then 'permutations',
+ # then 'permutations_repeated'.
+ str_name = str(name).lower()
+ for subset in ["groups", "permutations", "permutations_repeated"]:
+ pgs = _point_groups_dictionary[subset]
+ pg_dict = dict([(x.name, x) for x in pgs])
+ if str_name in pg_dict.keys():
+ return pg_dict[str_name]
+ # repeat check with Shoenflies notation
+ pg_dict_s = dict([(x._schoenflies.lower(), x) for x in pgs])
+ if str_name in pg_dict_s.keys():
+ return pg_dict_s[str_name]
+ # If the name doesn't exist in orix, try EDAX
+ for key, aliases in _EDAX_POINT_GROUP_ALIASES.items():
+ if str_name in aliases:
+ return pg_dict[key]
+ # Finally, try diffpy
+ try:
+ return PointGroups.from_space_group(name)
+ # If the name still cannot be found, return a ValueError
+ except ValueError:
+ raise ValueError(
+ f"'{name}' could not be interpreted as a point group name. Valid options "
+ + f"are {', '.join(map(str, pg_dict.keys()))}, "
+ + f"{', '.join(map(str, pg_dict_s.keys()))}, or a value recognizable by "
+ + "diffpy.structure.spacegroups.GetSpaceGroup."
+ )
+
+ def from_space_group(
+ space_group_number: Union(int, str), proper: bool = False
+ ) -> Symmetry:
+ """
+ Maps a space group number or name to a crystallographic point group.
+
+ Parameters
+ ----------
+ space_group_number: int between 1-231, or str
+ If is an int(n) or str(int(n)) where n is between 1 and 231, it will
+ return the point group of the nth space group, as defined by the
+ International Tables for Crystallogrphy. Otherwise, it will be passed
+ to diffpy's dictionary of space group names for interpretation.
+
+ Thus, 222 and '222' will both return symmetry.Oh (ie "432", the point
+ group of SG#222==Pn-3n), but 'P222' will return symmetry.D2
+ (ie "222", the proper point group of SG#16=='P222').
+
+ proper: bool
+ Whether to return the point group with proper rotations only
+ (``True``), or the full point group (``False``). Default is
+ ``False``.
+
+ Returns
+ -------
+ point_group
+ One of the 11 proper or 32 point groups.
+
+ Notes:
+ ----------
+ This function uses diffpy.structure.spacegroups to convert names to
+ space group IDs, and has some allowances for spelling and spacing
+ differences. Thus, variations like "Pm-3m", 221, "PM3m", and "Pn3n" all
+ map to symmetry.Oh == 'm-3m'. To see a full list of all name options
+ avaiable, use the following snippet:
+
+ >>> import diffpy.structure.spacegroups as sg
+ >>> sg._buildSGLookupTable()
+ >>> sg._sg_lookup_table.keys()
+
+ Examples
+ --------
+ >>> from orix.quaternion.symmetry import get_point_group
+ >>> pgOh = get_point_group(225)
+ >>> pgOh.name
+ 'm-3m'
+ >>> pgO = get_point_group(225, proper=True)
+ >>> pgO.name
+ '432'
+ """
+ spg = GetSpaceGroup(space_group_number)
+ pgn = spg.point_group_name
+ if proper:
+ return _spacegroup2pointgroup_dict[pgn]["proper"]
+ else:
+ return _spacegroup2pointgroup_dict[pgn]["improper"]
+
+ @classmethod
+ def get_set(self, name: Literal[PointGroups.subset_names] = "groups"):
+ """
+ returns different subsets of the 32 crystallographic point groups.
+ By default, this returns all 32 in the order they appear in the
+ International Tables for Crystallography (ITC).
+
+ Parameters
+ ----------
+ subset : str, optional
+ the point group list to return. The options are as follows:
+ "groups" (default):
+ All 32 point groups in the order they appear in ITC's
+ space groups. As a result, they are grouped by
+ crystal system and Laue class
+ "permutations":
+ All 32 points groups, plus common axis-specific
+ permutations for non-centrosymmetric groups (ie,
+ C2 plus C2x and C2y) for a total of 37 point group
+ projections. These are given in the same order as
+ ITC Table 10.2.2
+ "permutations_repeated":
+ The 37 point group projections, plus the redundant
+ Schonflies and Hermann-Mauguin group names. For example,
+ both Ci and S2 are included, as well as D3 =="32" and
+ D3x == "321". NOTE: this means several entries are
+ symmetrically identical.
+ "proper":
+ The 11 proper point groups given in the same order as ITC
+ table 10.2.2.
+ same order as "unique", which in turn aligns with Table 3.1
+ of ITC
+ "proper_all":
+ The 11 proper point groups, plus axis-specific permutations.
+ "laue":
+ The point groups corresponding to the 11 Laue groups, using
+ the same ordering and definitions as Table 3.1 of ITC. These
+ are equivalent to adding an inversion symmetry to each op
+ the 11 proper point groups
+ "procedural":
+ The 32 point groups, but presented in the procedural ordering
+ described in "Structure of Materials" and other books, where
+ point groups are created from successive applications of
+ symmetry elements to the Cyclic (C_n) and Dihedral (D_n)
+ groups.
+
+ Returns
+ -------
+ point groups: PointGroups
+ A PointGroup class containing the requested symmetries
+ """
+ if name in self.subset_names:
+ return PointGroups(_point_groups_dictionary[name])
+ elif name.lower() in self.subset_names:
+ return PointGroups(_point_groups_dictionary[name.lower()])
+ # if the name doesn't exist, return a ValueError
+ raise ValueError(
+ "'name' must be one of "
+ + f"{', '.join(map(str, self.subset_names))}, not '{name}'"
+ )
+
+
+def get_distinguished_points(s1: Symmetry, s2: Symmetry = C1) -> Rotation:
+ """Return points symmetrically equivalent to identity with respect
+ to ``s1`` and ``s2``.
Parameters
----------
- space_group_number
- Between 1 and 231.
- proper
- Whether to return the point group with proper rotations only
- (``True``), or just the point group (``False``). Default is
- ``False``.
+ s1
+ First symmetry.
+ s2
+ Second symmetry.
Returns
-------
- point_group
- One of the 11 proper or 32 point groups.
-
- Examples
- --------
- >>> from orix.quaternion.symmetry import get_point_group
- >>> pgOh = get_point_group(225)
- >>> pgOh.name
- 'm-3m'
- >>> pgO = get_point_group(225, proper=True)
- >>> pgO.name
- '432'
+ distinguished_points
+ Distinguished points.
+ """
+ distinguished_points = s1.outer(s2).antipodal.unique(antipodal=False)
+ return distinguished_points[distinguished_points.angle > 0]
+
+
+@deprecated(
+ since="0.15",
+ removal="0.16",
+ alternative="PointGroups.from_space_group",
+)
+def get_point_group(space_group_number: int, proper: bool = False) -> Symmetry:
+ """
+ This function has been moved to the PointGroups class
"""
- spg = GetSpaceGroup(space_group_number)
- pgn = spg.point_group_name
- if proper:
- return spacegroup2pointgroup_dict[pgn]["proper"]
- else:
- return spacegroup2pointgroup_dict[pgn]["improper"]
+ return PointGroups.from_space_group(space_group_number, proper)
# Point group alias mapping. This is needed because in EDAX TSL OIM
@@ -839,30 +1642,33 @@ def get_point_group(space_group_number: int, proper: bool = False) -> Symmetry:
def _get_laue_group_name(name: str) -> str | None:
- if name in ["1", "-1"]:
- return "-1"
- elif name in ["2", "211", "121", "112", "m11", "1m1", "11m", "2/m"]:
- return "2/m"
- elif name in ["222", "mm2", "mmm"]:
- return "mmm"
- elif name in ["4", "-4", "4/m"]:
- return "4/m"
- elif name in ["422", "4mm", "-42m", "4/mmm"]:
- return "4/mmm"
- elif name in ["3", "-3"]:
- return "-3"
- elif name in ["321", "312", "32", "3m", "-3m"]:
- return "-3m"
- elif name in ["6", "-6", "6/m"]:
- return "6/m"
- elif name in ["6mm", "-6m2", "6/mmm", "622"]:
- return "6/mmm"
- elif name in ["23", "m-3"]:
- return "m-3"
- elif name in ["432", "-43m", "m-3m"]:
- return "m-3m"
- else:
- return None
+ # search through all the point groups defined in orix for one with a
+ # matching name.
+ valid_name = False
+ for g in _point_groups_dictionary["permutations_repeated"]:
+ if g.name == name:
+ valid_name = True
+ break
+ if valid_name is False:
+ raise ValueError(f"{name} is not a valid point group name")
+ # if the matching point group as a Schoenflies name that ends in an x,y, or z,
+ # it's a permutation of a point group. trade it for an unpermutated one.
+ if np.isin(g._schoenflies[-1], ["x", "y", "z"]):
+ s_name = g._schoenflies[:-1]
+ for g in _point_groups_dictionary["permutations_repeated"]:
+ if g._schoenflies == s_name:
+ break
+ # add an inversion to get the laue group.
+ g_laue = _get_unique_symmetry_elements(g, Ci)
+ # find a laue group with matching operators
+ for laue in _point_groups_dictionary["laue"]:
+ # first check for length
+ if g_laue.shape != laue.shape:
+ continue
+ # then check for identical operators (regardless of order)
+ if np.min(g_laue.outer(laue).angle ** 2, 1).max() < 1e-4:
+ if np.min(g_laue.outer(laue).angle ** 2, 0).max() < 1e-4:
+ return laue.name
def _get_unique_symmetry_elements(
diff --git a/orix/sampling/S2_sampling.py b/orix/sampling/S2_sampling.py
index a0c24518..4aa93521 100644
--- a/orix/sampling/S2_sampling.py
+++ b/orix/sampling/S2_sampling.py
@@ -573,8 +573,7 @@ def sample_S2_icosahedral_mesh(resolution: float) -> Vector3d:
)
_sampling_S2_method_names.add(f":func:`orix.sampling.{_func.__name__}`")
-_s2_sampling_docstring = (
- """Return unit vectors that sample S2 with a specific angular
+_s2_sampling_docstring = ("""Return unit vectors that sample S2 with a specific angular
resolution.
Parameters
@@ -596,8 +595,7 @@ def sample_S2_icosahedral_mesh(resolution: float) -> Vector3d:
See Also
--------
{}
- """
-).format(
+ """).format(
", ".join(map(lambda x: f'``"{x}"``', sample_S2_methods)),
"\n ".join(_sampling_S2_method_names),
)
diff --git a/orix/tests/test_crystal_map/test_crystal_map.py b/orix/tests/test_crystal_map/test_crystal_map.py
index a4513196..7dd890a6 100644
--- a/orix/tests/test_crystal_map/test_crystal_map.py
+++ b/orix/tests/test_crystal_map/test_crystal_map.py
@@ -677,9 +677,9 @@ def test_orientations(self, crystal_map_input, phase_list):
"point_group, rotation, expected_orientation",
[
(C2, [(0.6088, 0, 0, 0.7934)], [(-0.7934, 0, 0, 0.6088)]),
- (C3, [(0.6088, 0, 0, 0.7934)], [(-0.9914, 0, 0, 0.1305)]),
- (C4, [(0.6088, 0, 0, 0.7934)], [(-0.9914, 0, 0, -0.1305)]),
- (O, [(0.6088, 0, 0, 0.7934)], [(-0.9914, 0, 0, -0.1305)]),
+ (C3, [(0.6088, 0, 0, 0.7934)], [(0.9914, 0, 0, -0.1305)]),
+ (C4, [(0.6088, 0, 0, 0.7934)], [(0.9914, 0, 0, 0.1305)]),
+ (O, [(0.6088, 0, 0, 0.7934)], [(0.9914, 0, 0, 0.1305)]),
],
)
def test_orientations_symmetry(self, point_group, rotation, expected_orientation):
diff --git a/orix/tests/test_crystal_map/test_phase.py b/orix/tests/test_crystal_map/test_phase.py
index fa841b73..ae25420c 100644
--- a/orix/tests/test_crystal_map/test_phase.py
+++ b/orix/tests/test_crystal_map/test_phase.py
@@ -22,7 +22,10 @@
import pytest
from orix.crystal_map import Phase
-from orix.crystal_map._phase import default_lattice, new_structure_matrix_from_alignment
+from orix.crystal_map._phase import (
+ default_lattice,
+ new_structure_matrix_from_alignment,
+)
from orix.quaternion.symmetry import O, Symmetry
@@ -61,7 +64,14 @@ class TestPhase:
],
)
def test_init_phase(
- self, name, point_group, space_group, color, color_name, color_rgb, structure
+ self,
+ name,
+ point_group,
+ space_group,
+ color,
+ color_name,
+ color_rgb,
+ structure,
):
p = Phase(
name=name,
@@ -153,14 +163,17 @@ def test_set_phase_color(self, color, color_alias, color_rgb, fails):
def test_set_phase_point_group(self, point_group, point_group_name, fails):
p = Phase()
if fails:
- with pytest.raises(ValueError, match=f"'{point_group}' must be of type"):
+ with pytest.raises(
+ ValueError, match=f"'{point_group}' could not be interpreted as"
+ ):
p.point_group = point_group
else:
p.point_group = point_group
assert p.point_group.name == point_group_name
@pytest.mark.parametrize(
- "structure", [Structure(), Structure(lattice=Lattice(1, 2, 3, 90, 120, 90))]
+ "structure",
+ [Structure(), Structure(lattice=Lattice(1, 2, 3, 90, 120, 90))],
)
def test_set_structure(self, structure):
p = Phase()
@@ -239,7 +252,14 @@ def test_phase_init_non_matching_space_group_point_group(self):
@pytest.mark.parametrize(
"space_group_no, desired_point_group_name",
- [(1, "1"), (50, "mmm"), (100, "4mm"), (150, "32"), (200, "m-3"), (225, "m-3m")],
+ [
+ (1, "1"),
+ (50, "mmm"),
+ (100, "4mm"),
+ (150, "32"),
+ (200, "m-3"),
+ (225, "m-3m"),
+ ],
)
def test_point_group_derived_from_space_group(
self, space_group_no, desired_point_group_name
@@ -414,7 +434,9 @@ def test_from_cif(self, cif_file):
lattice = phase.structure.lattice
assert np.allclose(lattice.abcABG(), [15.5, 4.05, 6.74, 90, 105.3, 90])
assert np.allclose(
- lattice.base, [[15.5, 0, 0], [0, 4.05, 0], [-1.779, 0, 6.501]], atol=1e-3
+ lattice.base,
+ [[15.5, 0, 0], [0, 4.05, 0], [-1.779, 0, 6.501]],
+ atol=1e-3,
)
def test_from_cif_same_structure(self, cif_file):
diff --git a/orix/tests/test_io/test_ang.py b/orix/tests/test_io/test_ang.py
index b7dbd48f..892e0d0c 100644
--- a/orix/tests/test_io/test_ang.py
+++ b/orix/tests/test_io/test_ang.py
@@ -49,7 +49,10 @@ class TestAngReader:
np.zeros(5 * 3, dtype=int), # phase_id
5, # n_unknown_columns
np.array(
- [[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]
+ [
+ [1.59942, 2.37748, 4.53419],
+ [1.59331, 2.37417, 4.53628],
+ ]
), # rotations as rows of Euler angle triplets
),
(5, 3),
@@ -67,7 +70,10 @@ class TestAngReader:
np.zeros(8 * 4, dtype=int), # phase_id
1, # n_unknown_columns
np.array(
- [[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]
+ [
+ [5.81107, 2.34188, 4.47345],
+ [6.16205, 0.79936, 1.31702],
+ ]
), # rotations as rows of Euler angle triplets
),
(8, 4),
@@ -82,7 +88,13 @@ class TestAngReader:
indirect=["angfile_tsl"],
)
def test_load_ang_tsl(
- self, angfile_tsl, map_shape, step_sizes, phase_id, n_unknown_cols, example_rot
+ self,
+ angfile_tsl,
+ map_shape,
+ step_sizes,
+ phase_id,
+ n_unknown_cols,
+ example_rot,
):
xmap = load(angfile_tsl)
@@ -155,7 +167,10 @@ def test_load_ang_tsl(
(4.5, 4.5),
np.ones(9 * 3, dtype=int),
np.array(
- [[1.895079, 0.739496, 1.413542], [1.897871, 0.742638, 1.413717]]
+ [
+ [1.895079, 0.739496, 1.413542],
+ [1.897871, 0.742638, 1.413717],
+ ]
),
),
(
@@ -174,7 +189,10 @@ def test_load_ang_tsl(
(10, 10),
np.ones(11 * 13, dtype=int),
np.array(
- [[1.621760, 2.368935, 4.559324], [1.604481, 2.367539, 4.541870]]
+ [
+ [1.621760, 2.368935, 4.559324],
+ [1.604481, 2.367539, 4.541870],
+ ]
),
),
],
@@ -253,7 +271,10 @@ def test_load_ang_astar(
)
),
np.array(
- [[1.895079, 0.739496, 1.413542], [1.897871, 0.742638, 1.413717]]
+ [
+ [1.895079, 0.739496, 1.413542],
+ [1.897871, 0.742638, 1.413717],
+ ]
),
),
(
@@ -267,7 +288,10 @@ def test_load_ang_astar(
)
), # phase_id
np.array(
- [[1.62176, 2.36894, 1.72386], [1.60448, 2.36754, 1.72386]]
+ [
+ [1.62176, 2.36894, 1.72386],
+ [1.60448, 2.36754, 1.72386],
+ ]
),
),
(3, 6),
@@ -362,14 +386,24 @@ def test_get_header(self, temp_ang_file):
],
ANGFILE_TSL_HEADER,
),
- ("astar", ["ind", "rel", "phase_id", "relx100"], ANGFILE_ASTAR_HEADER),
+ (
+ "astar",
+ ["ind", "rel", "phase_id", "relx100"],
+ ANGFILE_ASTAR_HEADER,
+ ),
("emsoft", ["iq", "dp", "phase_id"], ANGFILE_EMSOFT_HEADER),
],
)
def test_get_vendor_columns(
self, expected_vendor, expected_columns, vendor_header, temp_ang_file
):
- expected_columns = ["euler1", "euler2", "euler3", "x", "y"] + expected_columns
+ expected_columns = [
+ "euler1",
+ "euler2",
+ "euler3",
+ "x",
+ "y",
+ ] + expected_columns
n_cols_file = len(expected_columns)
temp_ang_file.write(vendor_header)
@@ -630,7 +664,7 @@ def test_extra_phases(self, crystal_map, tmp_path, extra_phase_names):
del pl[-1]
assert xmap_reload.phases.names == pl.names
- @pytest.mark.parametrize("point_group", ["432", "121", "222", "321", "622"])
+ @pytest.mark.parametrize("point_group", ["432", "121", "222", "622"])
def test_point_group_aliases(self, crystal_map, tmp_path, point_group):
crystal_map.phases[0].point_group = point_group
fname = tmp_path / "test_point_group_aliases.ang"
diff --git a/orix/tests/test_plot/test_stereographic_plot.py b/orix/tests/test_plot/test_stereographic_plot.py
index e4ec91b7..8cd9ee48 100644
--- a/orix/tests/test_plot/test_stereographic_plot.py
+++ b/orix/tests/test_plot/test_stereographic_plot.py
@@ -25,20 +25,9 @@
import numpy as np
import pytest
-from orix import plot
-
-# fmt: off
-# isort: off
-from orix.plot.stereographic_plot import (
- TwoFoldMarker,
- ThreeFoldMarker,
- FourFoldMarker,
- SixFoldMarker,
-)
-# isort: on
-# fmt: on
-from orix.quaternion import Orientation, symmetry
-from orix.vector import Vector3d
+import orix.plot as opl
+import orix.quaternion as oqu
+import orix.vector as ove
plt.rcParams["axes.grid"] = True
PROJ_NAME = "stereographic"
@@ -54,7 +43,7 @@ def test_vector_scatter(self):
assert ax.can_pan()
assert ax.can_zoom()
- v = Vector3d([[0, 0, 1], [2, 0, 2]])
+ v = ove.Vector3d([[0, 0, 1], [2, 0, 2]])
ax.scatter(v[0])
ax.scatter(v[1].azimuth, v[1].polar)
@@ -64,8 +53,8 @@ def test_vector_scatter(self):
plt.close("all")
def test_ori_scatter(self):
- oris = Orientation.from_axes_angles(
- [1, 2, 3], [5, 15, 70], degrees=True, symmetry=symmetry.D6
+ oris = oqu.Orientation.from_axes_angles(
+ [1, 2, 3], [5, 15, 70], degrees=True, symmetry=oqu.symmetry.D6
)
fig0 = oris.scatter(return_figure=True)
@@ -94,7 +83,7 @@ def test_ori_scatter(self):
@pytest.mark.flaky(reruns=5)
def test_scatter_coloring(self):
# Marked as flaky since no vectors may be visible
- v = Vector3d.random(10)
+ v = ove.Vector3d.random(10)
visible = v.z >= 0
# Four ways to color vectors
@@ -143,9 +132,9 @@ def test_scatter_coloring(self):
def test_text(self):
_, ax = plt.subplots(subplot_kw=dict(projection=PROJ_NAME))
- v = Vector3d([[0, 0, 1], [-1, 0, 1], [1, 1, 1]])
+ v = ove.Vector3d([[0, 0, 1], [-1, 0, 1], [1, 1, 1]])
ax.scatter(v)
- labels = plot.format_labels(v.data, ("[", "]"), use_latex=False)
+ labels = opl.format_labels(v.data, ("[", "]"), use_latex=False)
for i in range(v.size):
ax.text(v[i], s=labels[i])
@@ -158,9 +147,9 @@ def test_text(self):
def test_text_offset(self):
_, ax = plt.subplots(subplot_kw=dict(projection=PROJ_NAME))
- v = Vector3d([[0, 0, 1], [-1, 0, 1], [1, 1, 1]])
+ v = ove.Vector3d([[0, 0, 1], [-1, 0, 1], [1, 1, 1]])
ax.scatter(v)
- labels = plot.format_labels(v.data)
+ labels = opl.format_labels(v.data)
offset = (-0.02, 0.05)
for i in range(v.size):
ax.text(v[i], s=labels[i], offset=offset)
@@ -291,7 +280,7 @@ def test_show_hemisphere_label(self):
label_xy = [-0.71, 0.71]
- ax[0].scatter(Vector3d([0, 0, 1]))
+ ax[0].scatter(ove.Vector3d([0, 0, 1]))
ax[0].show_hemisphere_label()
label_up = ax[0].texts[0]
assert label_up.get_text() == "upper"
@@ -299,7 +288,7 @@ def test_show_hemisphere_label(self):
assert np.allclose([label_up._x, label_up._y], label_xy)
ax[1].hemisphere = "lower"
- ax[1].scatter(Vector3d([0, 0, -1]))
+ ax[1].scatter(ove.Vector3d([0, 0, -1]))
ax[1].show_hemisphere_label(color="r")
label_low = ax[1].texts[0]
assert label_low.get_text() == "lower"
@@ -309,7 +298,8 @@ def test_show_hemisphere_label(self):
plt.close("all")
@pytest.mark.parametrize(
- "hemisphere, pole, hemi_str", [("uPPer", -1, "upper"), ("loweR", 1, "lower")]
+ "hemisphere, pole, hemi_str",
+ [("uPPer", -1, "upper"), ("loweR", 1, "lower")],
)
def test_hemisphere_pole(self, hemisphere, pole, hemi_str):
_, ax = plt.subplots(subplot_kw=dict(projection=PROJ_NAME))
@@ -346,7 +336,7 @@ def test_format_coord(self):
plt.close("all")
def test_empty_scatter(self):
- v = Vector3d([0, 0, 1])
+ v = ove.Vector3d([0, 0, 1])
_, ax = plt.subplots(subplot_kw=dict(projection=PROJ_NAME))
ax.hemisphere = "lower"
@@ -361,14 +351,14 @@ def test_empty_scatter(self):
@pytest.mark.parametrize("shape", [(5, 10), (2, 3)])
def test_multidimensional_vector(self, shape):
n = np.prod(shape)
- v = Vector3d(np.random.normal(size=3 * n).reshape(*shape, 3))
+ v = ove.Vector3d(np.random.normal(size=3 * n).reshape(*shape, 3))
v.scatter()
v.draw_circle()
plt.close("all")
def test_order_in_hemisphere(self):
- v = Vector3d.from_polar(
+ v = ove.Vector3d.from_polar(
azimuth=np.radians([45, 90, 135, 180]),
polar=np.radians([50, 45, 140, 135]),
)
@@ -399,7 +389,7 @@ def test_order_in_hemisphere(self):
def test_color_parameter(self):
"""Pass either ``color`` or ``c`` to color scatter points."""
- v = Vector3d([[1, 0, 0], [1, 1, 0], [1, 1, 1]])
+ v = ove.Vector3d([[1, 0, 0], [1, 1, 0], [1, 1, 1]])
colors = [f"C{i}" for i in range(v.size)]
colors_rgba = np.array([mcolors.to_rgba(c) for c in colors])
@@ -412,7 +402,7 @@ def test_color_parameter(self):
def test_size_parameter(self):
"""Pass either ``sizes`` or ``s`` to set scatter points sizes."""
- v = Vector3d([[1, 0, 0], [1, 1, 0], [1, 1, 1]])
+ v = ove.Vector3d([[1, 0, 0], [1, 1, 0], [1, 1, 1]])
sizes = np.arange(v.size)
fig = v.scatter(sizes=sizes, return_figure=True)
@@ -423,84 +413,41 @@ def test_size_parameter(self):
class TestSymmetryMarker:
- def test_properties(self):
- v2fold = Vector3d([[1, 0, 1], [0, 1, 1]])
- marker2fold = TwoFoldMarker(v2fold)
- assert np.allclose(v2fold.data, marker2fold._vector.data)
- assert marker2fold.fold == 2
- assert marker2fold.n == 2
- assert np.allclose(marker2fold.size, [1.55, 1.55], atol=1e-2)
- assert isinstance(marker2fold._marker[0], mpath.Path)
-
- v3fold = Vector3d([1, 1, 1])
- marker3fold = ThreeFoldMarker(v3fold, size=5)
- assert np.allclose(v3fold.data, marker3fold._vector.data)
- assert marker3fold.fold == 3
- assert marker3fold.n == 1
- assert np.allclose(marker3fold.size, 5)
-
- # Iterating over markers
- for i, (vec, mark, size) in enumerate(marker3fold):
- assert np.allclose(vec.data, v3fold[i].data)
- assert np.allclose(mark, (3, 0, 45 + 90))
- assert size == 5
-
- v4fold = Vector3d([[0, 0, 1], [1, 0, 0], [0, 1, 0]])
- marker4fold = FourFoldMarker(v4fold, size=11)
- assert np.allclose(v4fold.data, marker4fold._vector.data)
- assert marker4fold.fold == 4
- assert marker4fold.n == 3
- assert np.allclose(marker4fold.size, [11, 11, 11])
- assert marker4fold._marker == ["D"] * 3
-
- marker6fold = SixFoldMarker([0, 0, 1], size=15)
- assert isinstance(marker6fold._vector, Vector3d)
- assert np.allclose(marker6fold._vector.data, [0, 0, 1])
- assert marker6fold.fold == 6
- assert marker6fold.n == 1
- assert marker6fold.size == 15
- assert marker6fold._marker == ["h"]
-
- plt.close("all")
+ @pytest.mark.parametrize("v_data", [[0, 0, 1], [1, 0, 0], [1, 1, 0], [1, 1, 1]])
+ @pytest.mark.parametrize("folds", [1, 2, 3, 4, 6])
+ @pytest.mark.parametrize("modifier", [None, "none", "rotoinversion", "inversion"])
+ def test_main_properties(self, v_data, folds, modifier):
+ v = ove.Vector3d(v_data)
+ marker = opl.stereographic_plot._SymmetryMarker(
+ v, folds=folds, modifier=modifier
+ )
+ assert np.allclose(v.data, marker._vector.data)
+ assert marker._folds == folds
+ assert marker._inner_shape == modifier
+ assert (marker.angle_deg - (np.rad2deg(v.azimuth) + 90)) ** 2 < 1e-4
+ # check errors
+ with pytest.raises(ValueError, match="Folds must"):
+ opl.stereographic_plot._SymmetryMarker([0, 0, 1], folds=5)
+ with pytest.raises(ValueError, match="Modifier must"):
+ opl.stereographic_plot._SymmetryMarker([0, 0, 1], modifier="banana")
def test_plot_symmetry_marker(self):
_, ax = plt.subplots(subplot_kw=dict(projection=PROJ_NAME))
ax.stereographic_grid(False)
marker_size = 500
- v4fold = Vector3d([[0, 0, 1], [1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0]])
- ax.symmetry_marker(v4fold, fold=4, c="C4", s=marker_size)
-
- v3fold = Vector3d([[1, 1, 1], [1, -1, 1], [-1, -1, 1], [-1, 1, 1]])
- ax.symmetry_marker(v3fold, fold=3, c="C3", s=marker_size)
-
- v2fold = 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.symmetry_marker(v2fold, fold=2, c="C2", s=marker_size)
-
- ax.symmetry_marker([0, 0, 1], fold=6, s=marker_size)
+ v = ove.Vector3d([[1, 0, 0], [0, 1, 1]])
+ for i in [1, 2, 3, 4, 6]:
+ ax.symmetry_marker(v[0], folds=i, s=marker_size, color="k")
+ ax.symmetry_marker(v[1], folds=i, modifier="inversion", s=marker_size)
+ ax.symmetry_marker(
+ v, folds=1, modifier="inversion", color="C1", s=marker_size
+ )
+ for i in [4, 6]:
+ ax.symmetry_marker(v, folds=i, modifier="rotoinversion", s=marker_size)
markers = ax.collections
- assert len(markers) == 18
- assert np.allclose(markers[0]._sizes, marker_size)
- assert np.allclose(markers[-1]._sizes, marker_size)
- assert np.allclose(markers[0]._facecolors, mcolors.to_rgba("C4"))
- assert np.allclose(markers[5]._facecolors, mcolors.to_rgba("C3"))
- assert np.allclose(markers[-2]._facecolors, mcolors.to_rgba("C2"))
- assert np.allclose(markers[-1]._facecolors, mcolors.to_rgba("C0"))
-
- with pytest.raises(ValueError, match="Can only plot 2"):
- ax.symmetry_marker([0, 0, 1], fold=5)
+ assert len(markers) == 43
plt.close("all")
@@ -523,12 +470,12 @@ class TestDrawCircle:
)
def test_visible_in_hemisphere(self, pole, polar, desired_array):
assert np.allclose(
- plot.stereographic_plot._is_visible(polar, pole), desired_array
+ opl.stereographic_plot._is_visible(polar, pole), desired_array
)
def test_draw_circle(self):
- v1 = Vector3d([[0, 0, 1], [1, 0, 1], [1, 1, 1]])
- v2 = Vector3d(np.append(v1.data, -v1.data, axis=0))
+ v1 = ove.Vector3d([[0, 0, 1], [1, 0, 1], [1, 1, 1]])
+ v2 = ove.Vector3d(np.append(v1.data, -v1.data, axis=0))
upper_steps = 100
lower_steps = 150
@@ -546,13 +493,19 @@ def test_draw_circle(self):
assert len(ax[1].lines) == 3
assert ax[0].lines[0]._path._vertices.shape == (upper_steps, 2)
assert ax[1].lines[0]._path._vertices.shape == (lower_steps, 2)
- assert ax[1].lines[1]._path._vertices.shape == (lower_steps // 2 + 1, 2)
- assert ax[1].lines[1]._path._vertices.shape == (lower_steps // 2 + 1, 2)
+ assert ax[1].lines[1]._path._vertices.shape == (
+ lower_steps // 2 + 1,
+ 2,
+ )
+ assert ax[1].lines[1]._path._vertices.shape == (
+ lower_steps // 2 + 1,
+ 2,
+ )
plt.close("all")
def test_draw_circle_empty(self):
- v1 = Vector3d([[0, 0, 1], [1, 0, 1], [1, 1, 1]])
+ v1 = ove.Vector3d([[0, 0, 1], [1, 0, 1], [1, 1, 1]])
_, ax = plt.subplots(subplot_kw=dict(projection=PROJ_NAME))
ax.hemisphere = "lower"
ax.draw_circle(v1)
@@ -562,7 +515,7 @@ def test_draw_circle_empty(self):
def test_draw_circle_opening_angle_array(self):
"""Passing an opening angle per vector as an array works."""
- v = Vector3d([(0, 0, 1), (0, 0, -1), (1, 0, 1)])
+ v = ove.Vector3d([(0, 0, 1), (0, 0, -1), (1, 0, 1)])
fig = v.draw_circle(
opening_angle=np.array([np.pi / 2, np.pi / 4, np.pi / 2]),
return_figure=True,
@@ -576,7 +529,7 @@ def test_draw_circle_opening_angle_array(self):
plt.close("all")
def test_pdf_args(self):
- v = Vector3d.random(10)
+ v = ove.Vector3d.random(10)
resolution = 5
fig, ax = plt.subplots(ncols=2, subplot_kw=dict(projection="stereographic"))
# vector arg
@@ -596,7 +549,8 @@ def test_pdf_args(self):
def test_pdf_args_raises(self):
fig, ax = plt.subplots(subplot_kw=dict(projection="stereographic"))
with pytest.raises(
- TypeError, match="If one argument is passed it must be an instance of "
+ TypeError,
+ match="If one argument is passed it must be an instance of ",
):
ax.pole_density_function("test")
@@ -614,13 +568,13 @@ def test_restrict_to_fundamental_sector(self):
# C1 has no fundamental sector, so the circle marking the
# edge of the axis region should be unchanged
_, ax2 = plt.subplots(subplot_kw=dict(projection=PROJ_NAME))
- ax2.restrict_to_sector(symmetry.C1.fundamental_sector)
+ ax2.restrict_to_sector(oqu.symmetry.C1.fundamental_sector)
assert np.allclose(vertices, ax2.patches[0].get_verts())
# C6 fundamental sector is 1 / 6 of the unit sphere, with
# half of it in the upper hemisphere
_, ax3 = plt.subplots(ncols=2, subplot_kw=dict(projection=PROJ_NAME))
- ax3[0].restrict_to_sector(symmetry.C6.fundamental_sector)
+ ax3[0].restrict_to_sector(oqu.symmetry.C6.fundamental_sector)
assert not np.allclose(vertices[:10], ax3[0].patches[0].get_verts()[:10])
assert ax3[0].patches[1].get_label() == "_stereographic_sector"
@@ -632,7 +586,7 @@ def test_restrict_to_fundamental_sector(self):
# Oh's fundamental sector is only in the upper hemisphere,
# so the same as C1's sector applies for the lower hemisphere
- fs = symmetry.Oh.fundamental_sector
+ fs = oqu.symmetry.Oh.fundamental_sector
_, ax4 = plt.subplots(subplot_kw=dict(projection=PROJ_NAME))
ax4.restrict_to_sector(fs)
upper_patches4 = ax4.patches
@@ -658,18 +612,18 @@ def test_restrict_to_fundamental_sector(self):
plt.close("all")
def test_restrict_to_sector_pad(self):
- v = Vector3d.zvector()
+ v = ove.Vector3d.zvector()
fig = v.scatter(return_figure=True)
ax = fig.axes[0]
assert np.allclose(ax.get_xlim(), [-1.05, 1.05])
# No change since the sector is the equator
- ax.restrict_to_sector(symmetry.C1.fundamental_sector)
+ ax.restrict_to_sector(oqu.symmetry.C1.fundamental_sector)
assert np.allclose(ax.get_xlim(), [-1.05, 1.05])
# Default
- fs_m3m = symmetry.Oh.fundamental_sector
+ fs_m3m = oqu.symmetry.Oh.fundamental_sector
ax.restrict_to_sector(fs_m3m)
assert np.allclose(ax.get_xlim(), [-0.0103, 0.4245], atol=1e-4)
@@ -680,19 +634,19 @@ def test_restrict_to_sector_pad(self):
plt.close("all")
def test_restrict_to_sector_edges(self):
- v = Vector3d.zvector()
+ v = ove.Vector3d.zvector()
fig = v.scatter(return_figure=True)
ax = fig.axes[0]
- ax.restrict_to_sector(symmetry.Oh.fundamental_sector, show_edges=False)
+ ax.restrict_to_sector(oqu.symmetry.Oh.fundamental_sector, show_edges=False)
assert len(ax.patches) == 1
assert ax.patches[0].get_label() == "_stereographic_border"
plt.close("all")
def test_restrict_to_sector_full_projection(self):
- v = Vector3d.zvector()
+ v = ove.Vector3d.zvector()
fig = v.scatter(return_figure=True)
ax = fig.axes[0]
@@ -701,12 +655,12 @@ def test_restrict_to_sector_full_projection(self):
# Ensure padding changes little (ideally to +/- 1.01, but it
# does not on Windows...)
- ax.restrict_to_sector(symmetry.Ci.fundamental_sector)
+ ax.restrict_to_sector(oqu.symmetry.Ci.fundamental_sector)
xmin2, xmax2 = ax.get_xlim()
assert all([(xmin + xmin2) <= 0.05, (xmax - xmax2) <= 0.05])
# Upper part of projection, azimuthal in [0, 180]
- ax.restrict_to_sector(symmetry.C2h.fundamental_sector)
+ ax.restrict_to_sector(oqu.symmetry.C2h.fundamental_sector)
assert np.allclose(ax.get_ylim(), [-0.01, 1.01], atol=1e-3)
plt.close("all")
diff --git a/orix/tests/test_quaternion/test_orientation.py b/orix/tests/test_quaternion/test_orientation.py
index 89b0d566..dbc3591e 100644
--- a/orix/tests/test_quaternion/test_orientation.py
+++ b/orix/tests/test_quaternion/test_orientation.py
@@ -40,13 +40,15 @@
T,
O,
Oh,
- _groups,
- _proper_groups,
+ PointGroups,
)
# isort: on
from orix.vector import Miller, Vector3d
+groups = PointGroups.get_set("permutations_repeated")
+proper_groups = PointGroups.get_set("proper_groups")
+
@pytest.fixture
def vector(request):
@@ -92,11 +94,11 @@ def test_quaternion_subclasses_copy_constructor_casting():
# 7pi/12 -C2-> # 7pi/12
([(0.6088, 0, 0, 0.7934)], C2, [(-0.7934, 0, 0, 0.6088)]),
# 7pi/12 -C3-> # 7pi/12
- ([(0.6088, 0, 0, 0.7934)], C3, [(-0.9914, 0, 0, 0.1305)]),
+ ([(0.6088, 0, 0, 0.7934)], C3, [(0.9914, 0, 0, -0.1305)]),
# 7pi/12 -C4-> # pi/12
- ([(0.6088, 0, 0, 0.7934)], C4, [(-0.9914, 0, 0, -0.1305)]),
+ ([(0.6088, 0, 0, 0.7934)], C4, [(0.9914, 0, 0, 0.1305)]),
# 7pi/12 -O-> # pi/12
- ([(0.6088, 0, 0, 0.7934)], O, [(-0.9914, 0, 0, -0.1305)]),
+ ([(0.6088, 0, 0, 0.7934)], O, [(0.9914, 0, 0, 0.1305)]),
],
indirect=["orientation"],
)
@@ -292,7 +294,8 @@ def test_symmetry_property_wrong_type_orientation():
@pytest.mark.parametrize(
- "error_type, value", [(ValueError, (1, 2)), (ValueError, (C1, 2)), (TypeError, 1)]
+ "error_type, value",
+ [(ValueError, (1, 2)), (ValueError, (C1, 2)), (TypeError, 1)],
)
def test_symmetry_property_wrong_type_misorientation(error_type, value):
mori = Misorientation.random((3, 2))
@@ -412,7 +415,7 @@ def test_get_distance_matrix_progressbar_chunksize(self):
angle2 = m.get_distance_matrix(chunk_size=10, progressbar=False)
assert np.allclose(angle1, angle2)
- @pytest.mark.parametrize("symmetry", _groups[:-1])
+ @pytest.mark.parametrize("symmetry", groups[:-1])
def test_get_distance_matrix_equal_explicit_calculation(self, symmetry):
# do not test Oh, as this takes ~4 GB
m = Misorientation.random((5,))
@@ -564,13 +567,15 @@ def test_from_matrix_symmetry(self):
)
o1 = Orientation.from_matrix(om)
assert np.allclose(
- o1.data, np.array([1, 0, 0, 0] * 2 + [0, 1, 0, 0] * 2).reshape(4, 4)
+ o1.data,
+ np.array([1, 0, 0, 0] * 2 + [0, 1, 0, 0] * 2).reshape(4, 4),
)
assert o1.symmetry.name == "1"
o2 = Orientation.from_matrix(om, symmetry=Oh)
o2 = o2.reduce()
assert np.allclose(
- o2.data, np.array([1, 0, 0, 0] * 2 + [-1, 0, 0, 0] * 2).reshape(4, 4)
+ o2.data,
+ np.array([1, 0, 0, 0] * 2 + [-1, 0, 0, 0] * 2).reshape(4, 4),
)
assert o2.symmetry.name == "m-3m"
o3 = Orientation(o1.data, symmetry=Oh)
@@ -860,7 +865,7 @@ def test_in_fundamental_region(self):
(-0.3874, 0.6708, -0.1986, 0.6004),
)
)
- for pg in _proper_groups:
+ for pg in proper_groups:
ori.symmetry = pg
region = np.radians(pg.euler_fundamental_region)
assert np.all(np.max(ori.in_euler_fundamental_region(), axis=0) <= region)
diff --git a/orix/tests/test_quaternion/test_orientation_region.py b/orix/tests/test_quaternion/test_orientation_region.py
index bbe63b56..8049cc38 100644
--- a/orix/tests/test_quaternion/test_orientation_region.py
+++ b/orix/tests/test_quaternion/test_orientation_region.py
@@ -37,8 +37,8 @@
[
[0.5, 0, 0, 0.866],
[-0.5, 0, 0, -0.866],
- [-0.5, 0, 0, 0.866],
[0.5, 0, 0, -0.866],
+ [-0.5, 0, 0, 0.866],
],
),
(
@@ -47,14 +47,14 @@
[
[0.5, 0.0, 0.0, 0.866],
[-0.5, 0.0, 0.0, -0.866],
- [-0.5, 0.0, 0.0, 0.866],
- [0.5, -0.0, -0.0, -0.866],
+ [0.5, 0.0, 0.0, -0.866],
+ [-0.5, -0.0, -0.0, 0.866],
[0.0, 1.0, 0.0, 0.0],
[-0.0, -1.0, -0.0, -0.0],
[0.0, 0.5, 0.866, 0.0],
- [-0.0, -0.5, -0.866, 0.0],
- [0.0, -0.5, 0.866, 0.0],
+ [-0.0, -0.5, -0.866, -0.0],
[0.0, 0.5, -0.866, 0.0],
+ [-0.0, -0.5, 0.866, -0.0],
],
),
],
diff --git a/orix/tests/test_quaternion/test_symmetry.py b/orix/tests/test_quaternion/test_symmetry.py
index d27a2096..e730df5d 100644
--- a/orix/tests/test_quaternion/test_symmetry.py
+++ b/orix/tests/test_quaternion/test_symmetry.py
@@ -25,6 +25,7 @@
import pytest
from orix.quaternion import Rotation, Symmetry, get_point_group
+from orix.quaternion.symmetry import _spacegroup2pointgroup_dict
# fmt: off
# isort: off
@@ -36,12 +37,17 @@
C3, S6, D3x, D3y, D3, C3v, D3d, # trigonal
C6, C3h, C6h, D6, C6v, D3h, D6h, # hexagonal
T, Th, O, Td, Oh, # cubic
- spacegroup2pointgroup_dict, _groups, _get_unique_symmetry_elements
+ PointGroups,
+ _get_unique_symmetry_elements
)
# isort: on
# fmt: on
from orix.vector import Vector3d
+# fmt: onfrom orix.vector import Vector3d
+
+_groups = PointGroups.get_set("permutations")
+
@pytest.fixture(params=[(1, 2, 3)])
def vector(request):
@@ -56,13 +62,13 @@ def all_symmetries(request):
@pytest.mark.parametrize(
"symmetry, vector, expected",
[
- (Ci, (1, 2, 3), [(1, 2, 3), (-1, -2, -3)]),
- (Csx, (1, 2, 3), [(1, 2, 3), (-1, 2, 3)]),
- (Csy, (1, 2, 3), [(1, 2, 3), (1, -2, 3)]),
- (Csz, (1, 2, 3), [(1, 2, 3), (1, 2, -3)]),
- (C2, (1, 2, 3), [(1, 2, 3), (-1, -2, 3)]),
+ (PointGroups.get("Ci"), (1, 2, 3), [(1, 2, 3), (-1, -2, -3)]),
+ (PointGroups.get("Csx"), (1, 2, 3), [(1, 2, 3), (-1, 2, 3)]),
+ (PointGroups.get("Csy"), (1, 2, 3), [(1, 2, 3), (1, -2, 3)]),
+ (PointGroups.get("Csz"), (1, 2, 3), [(1, 2, 3), (1, 2, -3)]),
+ (PointGroups.get("C2"), (1, 2, 3), [(1, 2, 3), (-1, -2, 3)]),
(
- C2v,
+ PointGroups.get("C2v"),
(1, 2, 3),
[
(1, 2, 3),
@@ -72,7 +78,7 @@ def all_symmetries(request):
],
),
(
- C4v,
+ PointGroups.get("C4v"),
(1, 2, 3),
[
(1, 2, 3),
@@ -86,7 +92,7 @@ def all_symmetries(request):
],
),
(
- D4,
+ PointGroups.get("D4"),
(1, 2, 3),
[
(1, 2, 3),
@@ -100,7 +106,7 @@ def all_symmetries(request):
],
),
(
- C6,
+ PointGroups.get("C6"),
(1, 2, 3),
[
(1, 2, 3),
@@ -112,7 +118,7 @@ def all_symmetries(request):
],
),
(
- Td,
+ PointGroups.get("Td"),
(1, 2, 3),
[
(1, 2, 3),
@@ -142,7 +148,7 @@ def all_symmetries(request):
],
),
(
- Oh,
+ PointGroups.get("Oh"),
(1, 2, 3),
[
(1, 2, 3),
@@ -278,8 +284,8 @@ def test_is_proper(symmetry, expected):
"symmetry, expected",
[
(C1, [C1]),
- (D2, [C1, C2x, C2y, C2z, D2]),
- (C6v, [C1, C2z, Csx, Csy, C2v, C3, C3v, C6, C6v]),
+ (D2, [C1, C2, C2x, C2y, D2]),
+ (C6v, [C1, C2, Csx, Csy, C2v, C3, C3v, C6, C6v]),
],
)
def test_subgroups(symmetry, expected):
@@ -290,8 +296,8 @@ def test_subgroups(symmetry, expected):
"symmetry, expected",
[
(C1, [C1]),
- (D2, [C1, C2x, C2y, C2z, D2]),
- (C6v, [C1, C2z, C3, C6]),
+ (D2, [C1, C2x, C2y, C2, D2]),
+ (C6v, [C1, C2, C3, C6]),
],
)
def test_proper_subgroups(symmetry, expected):
@@ -446,8 +452,8 @@ def test_get_point_group():
sg = GetSpaceGroup(sg_number)
pg = get_point_group(sg_number, proper=False)
- assert proper_pg == spacegroup2pointgroup_dict[sg.point_group_name]["proper"]
- assert pg == spacegroup2pointgroup_dict[sg.point_group_name]["improper"]
+ assert proper_pg == _spacegroup2pointgroup_dict[sg.point_group_name]["proper"]
+ assert pg == _spacegroup2pointgroup_dict[sg.point_group_name]["improper"]
def test_unique_symmetry_elements_subgroups(all_symmetries):
@@ -514,33 +520,78 @@ def test_hash_persistence():
assert all(h1a == h2a for h1a, h2a in zip(h1, h2))
-@pytest.mark.parametrize("pg", [C1, C4, Oh])
-def test_symmetry_plot(pg):
+@pytest.mark.parametrize(
+ "pg, n_elements",
+ [
+ (C1, 2),
+ (Ci, 4),
+ (S4, 4),
+ (S6, 4),
+ (D3, 16),
+ (T, 30),
+ (Th, 20),
+ (O, 52),
+ (Td, 20),
+ (Oh, 36),
+ ],
+)
+def test_symmetry_plot(pg, n_elements):
+ plt.close("all")
fig = pg.plot(return_figure=True)
assert isinstance(fig, plt.Figure)
assert len(fig.axes) == 1
ax = fig.axes[0]
-
- c0 = ax.collections[0]
- assert len(c0.get_offsets()) == np.count_nonzero(~pg.improper)
- assert c0.get_label().lower() == "upper"
- if not pg.is_proper:
- c1 = ax.collections[1]
- assert len(c1.get_offsets()) == np.count_nonzero(pg.improper)
- assert c1.get_label().lower() == "lower"
-
- assert len(ax.texts) == 2
- assert ax.texts[0].get_text() == "$e_1$"
- assert ax.texts[1].get_text() == "$e_2$"
-
+ assert len(ax.collections) == n_elements
+
+ fig, plt_axis = plt.subplots(subplot_kw={"projection": "stereographic"})
+ v = Vector3d.from_polar(0.5, 0.3)
+ v_symm = pg.outer(v)
+ pg.plot(v, ax=plt_axis)
+ c1 = plt_axis.collections[-1]
+ c2 = plt_axis.collections[-2]
+ assert len(c1.get_offsets()) == np.sum(v_symm.z >= 0)
+ if pg.size > 1:
+ assert len(c2.get_offsets()) == np.sum(v_symm.z < 0)
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 TestPointGroups:
+ def test_repr(self):
+ pg_list = PointGroups.get_set("permutations_repeated")
+ assert len(pg_list) == 44
+ pg_list = PointGroups.get_set("GROUPS")
+ assert len(pg_list) == 32
+ docs = pg_list.__repr__()
+ lines = docs.split("\n")
+ assert len(lines) == 34
+ for l in lines[2:]:
+ assert len(l.split()) == 11
+
+ def test_get(self):
+ assert PointGroups.get("c1").laue.name == "-1"
+ assert PointGroups.get("C1").laue.name == "-1"
+ assert PointGroups.get("32").laue.name == "-3m"
+ assert PointGroups.get(230).laue.name == "m-3m"
+ with pytest.raises(ValueError):
+ x = PointGroups.get("banana")
+
+ def test_init(self):
+ for method in [Oh, [Oh], [D3, C6, O], "groups"]:
+ pgs = PointGroups(method)
+ assert isinstance(pgs, PointGroups)
+ with pytest.raises(ValueError, match="'name' must be one of"):
+ pgs = PointGroups("banana")
+ with pytest.raises(ValueError, match="All entries"):
+ pgs = PointGroups([Oh, "banana"])
+ with pytest.raises(ValueError, match="All entries"):
+ pgs = PointGroups(["banana"])
+ with pytest.raises(ValueError, match="symmetry list must"):
+ pgs = PointGroups(33)
+
+ def test_other_functions(self):
+ pgs = PointGroups()
+ assert isinstance(pgs.to_list(), list)
class TestFundamentalSectorFromSymmetry:
@@ -647,10 +698,13 @@ def test_fundamental_sector_d2d(self):
pg = D2d # -42m
fs = pg.fundamental_sector
assert np.allclose(
- fs.data, [[0, 0, 1], [0.7071, 0.7071, 0], [0.7071, -0.7071, 0]], atol=1e-4
+ fs.data,
+ [[0, 0, 1], [0.7071, 0.7071, 0], [0.7071, -0.7071, 0]],
+ atol=1e-4,
)
assert np.allclose(
- fs.vertices.data, [[0.7071, -0.7071, 0], [0, 0, 1], [0.7071, 0.7071, 0]]
+ fs.vertices.data,
+ [[0.7071, -0.7071, 0], [0, 0, 1], [0.7071, 0.7071, 0]],
)
assert np.allclose(fs.center.data, [[0.4714, 0, 1 / 3]], atol=1e-4)
@@ -661,7 +715,9 @@ def test_fundamental_sector_d4h(self):
fs.data, [[0, 0, 1], [0, 1, 0], [0.7071, -0.7071, 0]], atol=1e-4
)
assert np.allclose(
- fs.vertices.data, [[1, 0, 0], [0, 0, 1], [0.7071, 0.7071, 0]], atol=1e-4
+ fs.vertices.data,
+ [[1, 0, 0], [0, 0, 1], [0.7071, 0.7071, 0]],
+ atol=1e-4,
)
assert np.allclose(fs.center.data, [[0.569, 0.2357, 1 / 3]], atol=1e-3)
@@ -677,7 +733,9 @@ def test_fundamental_sector_s6(self):
fs = pg.fundamental_sector
assert np.allclose(fs.data, [[0, 0, 1], [0, 1, 0], [0.866, 0.5, 0]], atol=1e-3)
assert np.allclose(
- fs.vertices.data, [[1, 0, 0], [0, 0, 1], [-0.5, 0.866, 0]], atol=1e-4
+ fs.vertices.data,
+ [[1, 0, 0], [0, 0, 1], [-0.5, 0.866, 0]],
+ atol=1e-4,
)
assert np.allclose(fs.center.data, [[1 / 6, 0.2887, 1 / 3]], atol=1e-4)
@@ -686,7 +744,9 @@ def test_fundamental_sector_d3(self):
fs = pg.fundamental_sector
assert np.allclose(fs.data, [[0, 0, 1], [0, 1, 0], [0.866, 0.5, 0]], atol=1e-3)
assert np.allclose(
- fs.vertices.data, [[1, 0, 0], [0, 0, 1], [-0.5, 0.866, 0]], atol=1e-4
+ fs.vertices.data,
+ [[1, 0, 0], [0, 0, 1], [-0.5, 0.866, 0]],
+ atol=1e-4,
)
assert np.allclose(fs.center.data, [[1 / 6, 0.2887, 1 / 3]], atol=1e-4)
@@ -704,7 +764,9 @@ def test_fundamental_sector_d3d(self):
fs.data, [[0, 0, 1], [0.5, 0.866, 0], [0.5, -0.866, 0]], atol=1e-3
)
assert np.allclose(
- fs.vertices.data, [[0.866, -0.5, 0], [0, 0, 1], [0.866, 0.5, 0]], atol=1e-3
+ fs.vertices.data,
+ [[0.866, -0.5, 0], [0, 0, 1], [0.866, 0.5, 0]],
+ atol=1e-3,
)
assert np.allclose(fs.center.data, [[0.577, 0, 1 / 3]], atol=1e-3)
@@ -720,7 +782,9 @@ def test_fundamental_sector_c3h(self):
fs = pg.fundamental_sector
assert np.allclose(fs.data, [[0, 0, 1], [0, 1, 0], [0.866, 0.5, 0]], atol=1e-3)
assert np.allclose(
- fs.vertices.data, [[1, 0, 0], [0, 0, 1], [-0.5, 0.866, 0]], atol=1e-3
+ fs.vertices.data,
+ [[1, 0, 0], [0, 0, 1], [-0.5, 0.866, 0]],
+ atol=1e-3,
)
assert np.allclose(fs.center.data, [[1 / 6, 0.2887, 1 / 3]], atol=1e-4)
@@ -729,7 +793,9 @@ def test_fundamental_sector_c6h(self):
fs = pg.fundamental_sector
assert np.allclose(fs.data, [[0, 0, 1], [0, 1, 0], [0.866, -0.5, 0]], atol=1e-3)
assert np.allclose(
- fs.vertices.data, [[1, 0, 0], [0, 0, 1], [0.5, 0.866, 0]], atol=1e-3
+ fs.vertices.data,
+ [[1, 0, 0], [0, 0, 1], [0.5, 0.866, 0]],
+ atol=1e-3,
)
assert np.allclose(fs.center.data, [[0.5, 0.2887, 1 / 3]], atol=1e-4)
@@ -738,7 +804,9 @@ def test_fundamental_sector_d6(self):
fs = pg.fundamental_sector
assert np.allclose(fs.data, [[0, 0, 1], [0, 1, 0], [0.866, -0.5, 0]], atol=1e-3)
assert np.allclose(
- fs.vertices.data, [[1, 0, 0], [0, 0, 1], [0.5, 0.866, 0]], atol=1e-3
+ fs.vertices.data,
+ [[1, 0, 0], [0, 0, 1], [0.5, 0.866, 0]],
+ atol=1e-3,
)
assert np.allclose(fs.center.data, [[0.5, 0.2887, 1 / 3]], atol=1e-4)
@@ -754,7 +822,9 @@ def test_fundamental_sector_d3h(self):
fs = pg.fundamental_sector
assert np.allclose(fs.data, [[0, 0, 1], [0, 1, 0], [0.866, -0.5, 0]], atol=1e-3)
assert np.allclose(
- fs.vertices.data, [[1, 0, 0], [0, 0, 1], [0.5, 0.866, 0]], atol=1e-3
+ fs.vertices.data,
+ [[1, 0, 0], [0, 0, 1], [0.5, 0.866, 0]],
+ atol=1e-3,
)
assert np.allclose(fs.center.data, [[0.5, 0.2887, 1 / 3]], atol=1e-4)
@@ -763,7 +833,9 @@ def test_fundamental_sector_d6h(self):
fs = pg.fundamental_sector
assert np.allclose(fs.data, [[0, 0, 1], [0, 1, 0], [0.5, -0.866, 0]], atol=1e-3)
assert np.allclose(
- fs.vertices.data, [[1, 0, 0], [0, 0, 1], [0.866, 0.5, 0]], atol=1e-3
+ fs.vertices.data,
+ [[1, 0, 0], [0, 0, 1], [0.866, 0.5, 0]],
+ atol=1e-3,
)
assert np.allclose(fs.center.data, [[0.622, 0.1667, 1 / 3]], atol=1e-4)
@@ -773,7 +845,12 @@ def test_fundamental_sector_t(self):
assert np.allclose(fs.data, [[1, 1, 0], [1, -1, 0], [0, -1, 1], [0, 1, 1]])
assert np.allclose(
fs.vertices.data,
- [[0, 0, 1], [0.5774, 0.5774, 0.5774], [1, 0, 0], [0.5774, -0.5774, 0.5774]],
+ [
+ [0, 0, 1],
+ [0.5774, 0.5774, 0.5774],
+ [1, 0, 0],
+ [0.5774, -0.5774, 0.5774],
+ ],
atol=1e-4,
)
assert np.allclose(fs.center.data, [[0.7076, -0.0004, 0.7067]], atol=1e-4)
@@ -862,6 +939,48 @@ def test_equality(symmetry):
assert Rotation(symmetry) == symmetry
+@pytest.mark.parametrize(
+ ["subset", "length", "proper_count"],
+ [
+ ["groups", 32, 11],
+ ["permutations", 37, 14],
+ ["permutations_repeated", 44, 17],
+ ["proper_groups", 11, 11],
+ ["proper_permutations", 14, 14],
+ ["laue", 11, 0],
+ ["procedural", 32, 11],
+ ],
+)
+def test_get_point_groups(subset, length, proper_count):
+ # check that we get the expected number of proper and total point groups.
+ group = PointGroups.get_set(subset)
+ assert len(group) == length
+ assert np.sum([x.is_proper for x in group]) == proper_count
+
+
+def test_get_point_groups_unique():
+ group = PointGroups.get_set("groups")
+ # this is just a check to see if each element is unique, and if there are
+ # 32 of them.
+ assert np.all(
+ np.sum(
+ [
+ [
+ _get_unique_symmetry_elements(a, b) == b
+ and _get_unique_symmetry_elements(a, b) == a
+ for a in group
+ ]
+ for b in group
+ ],
+ 1,
+ )
+ == np.ones(32)
+ )
+ # additional test that nonsense returns nonsense
+ with pytest.raises(ValueError):
+ PointGroups.get_set("banana")
+
+
class TestLaueGroup:
def test_crystal_system(self):
assert Ci.system == "triclinic"
@@ -885,7 +1004,8 @@ def test_laue_group_name(self):
assert D6h.laue.name == "6/mmm"
assert Th.laue.name == "m-3"
assert Oh.laue.name == "m-3m"
- assert Symmetry(((1, 0, 0, 0), (1, 1, 0, 0))).laue.name is None
+ with pytest.raises(ValueError):
+ Symmetry(((1, 0, 0, 0), (1, 1, 0, 0))).laue.name
class TestEulerFundamentalRegion:
@@ -921,7 +1041,7 @@ def test_euler_fundamental_region(self):
# All point groups provide a region
for pg in _groups:
angles = pg.euler_fundamental_region
- if pg.name in ["1", "-1", "2", "m11", "1m1", "11m"]:
+ if pg.name in ["1", "-1", "m11", "1m1", "11m", "m"]:
assert np.allclose(angles, (360, 180, 360))
else:
assert not np.allclose(angles, (360, 180, 360))
diff --git a/orix/tests/test_vector3d/test_miller.py b/orix/tests/test_vector3d/test_miller.py
index bfccc4bf..f5d117c7 100644
--- a/orix/tests/test_vector3d/test_miller.py
+++ b/orix/tests/test_vector3d/test_miller.py
@@ -24,10 +24,16 @@
from orix.crystal_map import Phase
from orix.quaternion import Orientation, symmetry
from orix.vector import Miller
-from orix.vector.miller import _round_indices, _transform_space, _UVTW2uvw, _uvw2UVTW
+from orix.vector.miller import (
+ _round_indices,
+ _transform_space,
+ _UVTW2uvw,
+ _uvw2UVTW,
+)
TRIGONAL_PHASE = Phase(
- point_group="321", structure=Structure(lattice=Lattice(4.9, 4.9, 5.4, 90, 90, 120))
+ point_group="321",
+ structure=Structure(lattice=Lattice(4.9, 4.9, 5.4, 90, 90, 120)),
)
TETRAGONAL_LATTICE = Lattice(0.5, 0.5, 1, 90, 90, 90)
TETRAGONAL_PHASE = Phase(
@@ -484,7 +490,8 @@ def test_trigonal_crystal(self):
nround = n.round()
assert np.allclose(nround.UVTW, [3, 3, -6, 11])
assert np.allclose(
- [nround.U[0], nround.V[0], nround.T[0], nround.W[0]], [3, 3, -6, 11]
+ [nround.U[0], nround.V[0], nround.T[0], nround.W[0]],
+ [3, 3, -6, 11],
)
# Examples from MTEX' documentation: