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: