From 693416cadd975aed5826b7b8748523528f83509a Mon Sep 17 00:00:00 2001 From: Austin Gerlt Date: Thu, 9 Apr 2026 16:14:27 -0600 Subject: [PATCH 01/11] Improve the docstrings for Phase --- orix/crystal_map/_phase.py | 262 +++++++++++++++++++++++++------------ 1 file changed, 179 insertions(+), 83 deletions(-) diff --git a/orix/crystal_map/_phase.py b/orix/crystal_map/_phase.py index 7ac984ed..d40fbda8 100644 --- a/orix/crystal_map/_phase.py +++ b/orix/crystal_map/_phase.py @@ -25,7 +25,7 @@ from pathlib import Path import warnings -from diffpy.structure import Lattice, Structure +import diffpy.structure as dst from diffpy.structure.parsers import p_cif from diffpy.structure.spacegroups import GetSpaceGroup, SpaceGroup from diffpy.structure.symmetryutilities import ExpandAsymmetricUnit @@ -43,6 +43,54 @@ from orix.vector.miller import Miller from orix.vector.vector3d import Vector3d +# ======================== # +# Repeatedly used docstrings +# ======================== # +# DEVELOPER NOTE: as of python 3.14, due to an intentional choice by the python +# developers, classmethod docstrings cannot be f-strings, and cannot be +# edited within the method. Thus, only docstrings used repeatedly by properties +# or functions should be added here. +_xtal2cart_docstring = r"""There are multiple valid algorithms for converting between + crystallographic and cartesian reference frames. ORIX uses + the form defined in the International Tables for + Crystallography, Volume A, section 1.5, which matches equation + 7.33 from Structure of Materials, second edition: + + .. math:: + \bf{e_1} = \frac{\bf{a_1}}{ |\bf{a_1}|} \quad\quad + \bf{e_2} = \bf{e_3} \times \bf{e_1} \quad\quad + \bf{e_3} = \frac{\bf{a_3^{*}}}{ |\bf{a_3^{*}}|} + + Note the equations in both sources are using a different notation + than what is used in orix to define coordinate axes. + :math:`\bf{a_{i}}` represents the :math:`(a, b, c)` direct lattice basis vectors, + :math:`\bf{a_{i}^{*}}` represents the :math:`(a^{*}, b^{*}, c^{*})` reciprocal lattice basis vectors, + and :math:`\bf{e_{i}}` represents the :math:`(x, y, z)` cartesion basis vectors. + Additionally, :math:`| |` represents the magnitude (ie, length) of a vector. + Notably, this sets: + + 1) :math:`\bf{a_1}` parallel with :math:`\bf{e_1}` + 2) :math:`\bf{e_2}` perpendicular to :math:`\bf{a_3}` and :math:`\bf{a_1}` + 3) :math:`\bf{e_3}` perpendicular to :math:`\bf{a_1}` and :math:`\bf{a_2}` + + This is different from some conventions popular in x-ray + diffraction references that choose to instead align :math:`\bf{a_3}` + parallel to :math:`\bf{e_3}`. This makes no difference for + cubic, tetragonal, and orthorhombic systems, but can lead to + unintented results in all other systems. Users should + take care to determine which convention was used when importing + data from other sources. + + Regardless of choice, the reciprocal basis vectors :math:`{\bf{a^{*}}}` + are then defined as + + .. math:: + \bf{a_i} * \bf{a_j^{*}}=\delta_{ij} + + where :math:`\delta_{ij}` is the Kronecker delta. Details on how + to solves these equations for every crystal system can be found + in Structure of Materials, second edition, chapters 6 and 7.""" + class Phase: """Symmetry and unit cell of a phase in a crystallographic map. @@ -53,29 +101,44 @@ class Phase: Parameters ---------- name - Phase name. Overwrites the name in the *structure*. A phase can - also be given, in which case a copy is returned and all other - parameters are ignored. + The name to give to the Phase. If None, a name will be inhereted + from *structure* if possible. If name is a Phase object, a copy + of that phase is returned instead, and all further arguments + are ignored. + space_group - Space group describing the symmetry operations resulting from - associating the point group with a Bravais lattice, according - to the International Tables for Crystallography. If not given, - it is set to None. + Space group describing the symmetry of the Phase. This can + either be a number 1-230 corresponding to the 230 space groups + defined in the International Tables for Crystallography, or a + diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. + If not given, the phase's space group is set to None. + point_group - Point group describing the symmetry operations of the phase's - crystal structure, according to the International Tables for - Crystallography. If neither this nor *space_group* is given, it - is set to None. If not given but *space_group* is, it is derived - from the space group. If both this and *space_group* is given, - the space group must to be derived from adding translational - symmetry to the point group. + Point group describing the non-translational symmetry of the + Phase. This can be an orix :class:`~orix.quaternion.symmetry.Symmetry` + object or one of the known point + group aliases (see the Notes section for details). It must be + compatable with *space_group* if both are given. If + None, the correct point group will either be derived from + *space_group*, or left as None if *space_group* is None. + structure - Unit cell with atoms and a lattice. If not given, a default - :class:`~diffpy.structure.structure.Structure` compatible with - the symmetry is used. + :class:`~diffpy.structure.structure.Structure` describing the + lattice and atomic positions. If None, a default + :class:`~diffpy.structure.structure.Structure` is used. If + a space group or point group has been defined, the structure + will also include a :class:`~diffpy.structure.lattice.Lattice` + with compatible default values. + color - Phase color. If not given, it is set to the first default - Matplotlib color "tab:blue". + The colour to use when plotting this phase. Default is blue. + + Notes + ----- + The list of known point group aliases can be seen using the + following command: + >>> import orix.quaternion as oqu + >>> [point_group.name for point_group in oqu.symmetry._groups] """ def __init__( @@ -83,7 +146,7 @@ def __init__( name: str | Phase | None = None, space_group: int | SpaceGroup | None = None, point_group: int | str | Symmetry | None = None, - structure: Structure | None = None, + structure: dst.Structure | None = None, color: str | None = None, ) -> None: if isinstance(name, Phase): @@ -98,7 +161,6 @@ def __init__( self.space_group = space_group # Needs to be set before point group self.point_group = point_group - self.color = color if color is not None else "tab:blue" if structure is None: @@ -106,43 +168,47 @@ def __init__( if pg is not None and pg.system is not None: lat = default_lattice(pg.system) else: - lat = Lattice() - structure = Structure(lattice=lat) + lat = dst.Lattice() + structure = dst.Structure(lattice=lat) self.structure = structure if name is not None: self.name = name + # ------------------------------ # + # Property Getters and Setters # + # ------------------------------ # @property - def structure(self) -> Structure: - r"""Return or set the crystal structure containing a lattice - (:class:`~diffpy.structure.lattice.Lattice`) and possibly many - atoms (:class:`~diffpy.structure.atom.Atom`). + def structure(self) -> dst.Structure: + """The crystallographic unit cell. - Parameters - ---------- - value : ~diffpy.structure.Structure - Crystal structure. The cartesian reference frame of the - crystal lattice is assumed to align :math:`a` with - :math:`e_1` and :math:`c*` with :math:`e_3`. This alignment - is assumed when transforming direct, reciprocal and - cartesian vectors between these spaces. + The unit cell is defined using the diffpy + :class:`~diffpy.structure.Structure` class. For reciprocal + space calculations, the structure must include a + :class:`~diffpy.structure.Lattice`. For atomic distance + calculations, the structure must include one or more + :class:`~diffpy.structure.atom.Atom`. + + Notes + ----- + %s """ return self._structure @structure.setter - def structure(self, value: Structure) -> None: - """Set the crystal structure.""" - if not isinstance(value, Structure): + def structure(self, value: dst.Structure) -> None: + if not isinstance(value, dst.Structure): raise ValueError(f"{value} must be a diffpy.structure.Structure") # Ensure correct alignment old_matrix = value.lattice.base - new_matrix = new_structure_matrix_from_alignment(old_matrix, x="a", z="c*") + new_matrix = new_structure_matrix_from_alignment( + old_matrix, x="a", z="c*" + ) new_value = value.copy() # Ensure atom positions are expressed in the new basis - new_value.placeInLattice(Lattice(base=new_matrix)) + new_value.placeInLattice(dst.Lattice(base=new_matrix)) # Store old lattice for expand_asymmetric_unit self._diffpy_lattice = old_matrix @@ -151,20 +217,15 @@ def structure(self, value: Structure) -> None: self._structure = new_value + structure.__doc__ %= _xtal2cart_docstring + @property def name(self) -> str: - """Return or set the phase name. - - Parameters - ---------- - value : str - Phase name. - """ + """The name of the phase.""" return self.structure.title @name.setter def name(self, value: str) -> None: - """Set the phase name.""" self.structure.title = str(value) @property @@ -193,19 +254,17 @@ def color_rgb(self) -> tuple: @property def space_group(self) -> SpaceGroup | None: - """Return or set the space group. + """The space group symmetry. - Parameters - ---------- - value : int, SpaceGroup or None - Space group. If an integer is passed, it must be between - 1-230. + Space groups are stored as diffpy :class:`~diffpy.spacegroup.SpaceGroup` + objects, but can be set using an integer between 1 and 230. + Note that changing the space group does not automatically update + the point group or structure. """ return self._space_group @space_group.setter def space_group(self, value: int | SpaceGroup | None) -> None: - """Set the space group.""" if isinstance(value, int): value = GetSpaceGroup(value) if not isinstance(value, SpaceGroup) and value is not None: @@ -217,12 +276,11 @@ def space_group(self, value: int | SpaceGroup | None) -> None: @property def point_group(self) -> Symmetry | None: - """Return or set the point group. + """The point group symmetry. - Parameters - ---------- - value : int, str, Symmetry or None - Point group. + Point Groups are stored as :class:`~orix.quaternion.symmetry.Symmetry` + objects, but can be set using common shorthand names such as + "m3m", "622", etc. """ if self.space_group is not None: return get_point_group(self.space_group.number) @@ -231,7 +289,6 @@ def point_group(self) -> Symmetry | None: @point_group.setter def point_group(self, value: int | str | Symmetry | None) -> None: - """Set the point group.""" if isinstance(value, int): value = str(value) if isinstance(value, str): @@ -262,53 +319,95 @@ def point_group(self, value: int | str | Symmetry | None) -> None: @property def is_hexagonal(self) -> bool: - """Return whether the crystal structure is hexagonal/trigonal or - not. - """ + """Returns True for hexagonal and trigonal crystal structures.""" return np.allclose(self.structure.lattice.abcABG()[3:], [90, 90, 120]) @property def a_axis(self) -> Miller: - """Return the direct lattice vector :math:`a` in the cartesian - reference frame of the crystal lattice :math:`e_i`. + """The :math:`a` axis of the crystal lattice. + + This is the vector describing the :math:`a` axis of the crystal + lattice, expressed in the standard cartesian frame. + + Notes + ----- + %s """ return Miller(uvw=(1, 0, 0), phase=self) @property def b_axis(self) -> Miller: - """Return the direct lattice vector :math:`b` in the cartesian - reference frame of the crystal lattice :math:`e_i`. + """The :math:`b` axis of the crystal lattice. + + This is the vector describing the :math:`b` axis of the crystal + lattice, expressed in the standard cartesian frame. + + Notes + ----- + %s """ return Miller(uvw=(0, 1, 0), phase=self) @property def c_axis(self) -> Miller: - """Return the direct lattice vector :math:`c` in the cartesian - reference frame of the crystal lattice :math:`e_i`. + """The :math:`c` axis of the crystal lattice. + + This is the vector describing the :math:`c` axis of the crystal + lattice, expressed in the standard cartesian frame. + + Notes + ----- + %s """ return Miller(uvw=(0, 0, 1), phase=self) @property def ar_axis(self) -> Miller: - """Return the reciprocal lattice vector :math:`a^{*}` in the - cartesian reference frame of the crystal lattice :math:`e_i`. + """The :math:`a^{*}` axis of the reciprocal lattice. + + This is the vector describing the :math:`a^*` axis of the + reciprocal lattice, expressed in the standard cartesian frame. + + Notes + ----- + %s """ return Miller(hkl=(1, 0, 0), phase=self) @property def br_axis(self) -> Miller: - """Return the reciprocal lattice vector :math:`b^{*}` in the - cartesian reference frame of the crystal lattice :math:`e_i`. + """The :math:`b^*` axis of the reciprocal lattice. + + This is the vector describing the :math:`b^*` axis of the + reciprocal lattice, expressed in the standard cartesian frame. + + Notes + ----- + %s """ return Miller(hkl=(0, 1, 0), phase=self) @property def cr_axis(self) -> Miller: - """Return the reciprocal lattice vector :math:`c^{*}` in the - cartesian reference frame of the crystal lattice :math:`e_i`. + """The :math:`c^*` axis of the reciprocal lattice. + + This is the vector describing the :math:`c^*` axis of the + reciprocal lattice, expressed in the standard cartesian frame. + + Notes + ----- + %s """ return Miller(hkl=(0, 0, 1), phase=self) + # add repeated text to axis docstrings. + a_axis.__doc__ %= _xtal2cart_docstring + b_axis.__doc__ %= _xtal2cart_docstring + c_axis.__doc__ %= _xtal2cart_docstring + ar_axis.__doc__ %= _xtal2cart_docstring + br_axis.__doc__ %= _xtal2cart_docstring + cr_axis.__doc__ %= _xtal2cart_docstring + def __repr__(self) -> str: if self.point_group is not None: pg_name = self.point_group.name @@ -327,8 +426,7 @@ def __repr__(self) -> str: @classmethod def from_cif(cls, filename: str | Path) -> Phase: - r"""Return a new phase from a Crystallographic Information File - (CIF). + r"""Create a Phase from a Crystallographic Information File (CIF). Parameters ---------- @@ -361,9 +459,7 @@ def from_cif(cls, filename: str | Path) -> Phase: return cls(name, space_group, structure=structure) def deepcopy(self) -> Phase: - """Return a deep copy using :py:func:`~copy.deepcopy` - function. - """ + """Return a deep copy using :py:func:`~copy.deepcopy`.""" return copy.deepcopy(self) def expand_asymmetric_unit(self) -> Phase: @@ -401,7 +497,7 @@ def expand_asymmetric_unit(self) -> Phase: # Ensure atom positions are expressed in diffpy's convention diffpy_structure = self.structure.copy() - diffpy_structure.placeInLattice(Lattice(base=self._diffpy_lattice)) + diffpy_structure.placeInLattice(dst.Lattice(base=self._diffpy_lattice)) xyz = diffpy_structure.xyz diffpy_structure.clear() From 32d413eb00c49f2967fd7fcb05cbc9d7664555c4 Mon Sep 17 00:00:00 2001 From: Austin Gerlt Date: Thu, 9 Apr 2026 16:16:31 -0600 Subject: [PATCH 02/11] update default lattice parameters used by Phase --- orix/crystal_map/_phase.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/orix/crystal_map/_phase.py b/orix/crystal_map/_phase.py index d40fbda8..18cad1af 100644 --- a/orix/crystal_map/_phase.py +++ b/orix/crystal_map/_phase.py @@ -152,11 +152,11 @@ def __init__( if isinstance(name, Phase): return Phase.__init__( self, - name.name, - name.space_group, - name.point_group, - name.structure.copy(), - name.color, + name=name.name, + space_group=name.space_group, + point_group=name.point_group, + structure=name.structure.copy(), + color=name.color, ) self.space_group = space_group # Needs to be set before point group @@ -581,11 +581,18 @@ def new_structure_matrix_from_alignment( return new_matrix -def default_lattice(system: VALID_SYSTEMS) -> Lattice: - if system in ["triclinic", "monoclinic", "orthorhombic", "tetragonal", "cubic"]: - lat = Lattice() - elif system in ["trigonal", "hexagonal"]: - lat = Lattice(1, 1, 1, 90, 90, 120) - else: +_default_lattices = { + "triclinic": dst.Lattice(1.0, 1.1, 1.2, 85, 82, 80), + "monoclinic": dst.Lattice(1.0, 1.1, 1.2, 90, 82, 90), + "orthorhombic": dst.Lattice(1.0, 1.1, 1.2, 90, 90, 90), + "tetragonal": dst.Lattice(1.0, 1.0, 1.2, 90, 90, 90), + "trigonal": dst.Lattice(1.0, 1.0, 1.0, 90, 90, 120), + "hexagonal": dst.Lattice(1.0, 1.0, 1.5, 90, 90, 120), + "cubic": dst.Lattice(1.0, 1.0, 1.0, 90, 90, 90), +} + + +def default_lattice(system: VALID_SYSTEMS) -> dst.Lattice: + if system not in _default_lattices: raise ValueError(f"Unknown crystal system {system!r}") - return lat + return _default_lattices[system] From 026334bf4e65dbc418f98900d96471c92c6fe525 Mon Sep 17 00:00:00 2001 From: Austin Gerlt Date: Thu, 9 Apr 2026 16:18:15 -0600 Subject: [PATCH 03/11] Add crystal-system-based classmethods to Phase --- orix/crystal_map/_phase.py | 517 +++++++++++++++++++++++++++++++++++++ 1 file changed, 517 insertions(+) diff --git a/orix/crystal_map/_phase.py b/orix/crystal_map/_phase.py index 18cad1af..2831213d 100644 --- a/orix/crystal_map/_phase.py +++ b/orix/crystal_map/_phase.py @@ -424,6 +424,523 @@ def __repr__(self) -> str: f"proper point group: {ppg_name}. color: {self.color}>" ) + # ------------------------ # + # Class creation Methods # + # ------------------------ # + @classmethod + def triclinic( + cls, + name: str | None = None, + space_group: int | SpaceGroup | None = None, + point_group: int | str | Symmetry | None = None, + color: str | None = None, + a: float | None = None, + b: float | None = None, + c: float | None = None, + alpha: float | None = None, + beta: float | None = None, + gamma: float | None = None, + ) -> None: + """Create a Phase with a triclinic bravais lattice. + + Parameters + ---------- + name + The name to give the Phase. + + space_group + Space group describing the symmetry of the Phase. This can + either be the integers 1 or 2, corresponding to the two + triclinic space group IDs, or a + diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. + If not given, the space group is set to SG#1 (P1). + + point_group + Point group describing the non-translational symmetry of the + phase. If not given, the point group is set to C1 ("1"). + + color + The colour to use when plotting this phase. Default is blue. + + a + The unit cell length a (arbitrary units). + + b + The unit cell length b (arbitrary units). + + c + The unit cell length c (arbitrary units). + + alpha + The angle (in degrees) between the b and c axes of the unit cell. + + beta + The angle (in degrees) between the a and c axes of the unit cell. + + gamma + The angle (in degrees) between the a and b axes of the unit cell. + + Returns + ------- + phase + a Phase object with triclinic symmetry + + Notes + ----- + Any missing lattice parameters will be imported from a + dictonary of default values, which can be seen using the following + snippet: + + >>> import diffpy.structure as dst + >>> dst.Lattice(*_default_lattices["triclinic"].abcABG()) + """ + if space_group is None and point_group is None: + space_group = 1 + default = _default_lattices["triclinic"].abcABG() + given = [a, b, c, alpha, beta, gamma] + abcABG = [(j if j is not None else i) for i, j in zip(default, given)] + lat = dst.Lattice(*abcABG) + phase = cls( + name=name, + space_group=space_group, + point_group=point_group, + color=color, + structure=dst.Structure(lattice=lat), + ) + if phase.point_group.system != "triclinic": + raise ValueError( + f"{phase.point_group.name} is not a triclinic symmetry." + ) + return phase + + @classmethod + def monoclinic( + cls, + name: str | Phase | None = None, + space_group: int | SpaceGroup | None = None, + point_group: int | str | Symmetry | None = None, + color: str | None = None, + a: float = None, + b: float = None, + c: float = None, + beta: float = None, + ) -> None: + """Create a Phase with a monoclinic bravais lattice. + + Parameters + ---------- + name + The name to give the Phase. + + space_group + Space group describing the symmetry of the Phase. This can + either be an integer between 2 and 16 corresponding to one + of the 13 monoclinic space group IDs, or a + diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. + If not given, the space group is set to SG#3 (P2). + + point_group + Point group describing the non-translational symmetry of the + phase. If not given, the point group is set to C2 ("2"). + + color + The colour to use when plotting this phase. Default is blue. + + a + The unit cell length a (arbitrary units). + + b + The unit cell length b (arbitrary units). + + c + The unit cell length c (arbitrary units). + + beta + The angle (in degrees) between the a and c axes of the unit cell. + + Returns + ------- + phase + New phase. + + Notes + ----- + Any missing lattice parameters will be imported from a + dictonary of default values, which can be seen using the following + snippet: + + >>> import diffpy.structure as dst + >>> dst.Lattice(*_default_lattices["monoclinic"].abcABG()) + """ + if space_group is None and point_group is None: + space_group = 3 + default = _default_lattices["monoclinic"].abcABG() + given = [a, b, c, None, beta, None] + abcABG = [(j if j is not None else i) for i, j in zip(default, given)] + lat = dst.Lattice(*abcABG) + phase = cls( + name=name, + space_group=space_group, + point_group=point_group, + color=color, + structure=dst.Structure(lattice=lat), + ) + if phase.point_group.system != "monoclinic": + raise ValueError( + f"{phase.point_group.name} is not a monoclinic symmetry." + ) + return phase + + @classmethod + def orthorhombic( + cls, + name: str | Phase | None = None, + space_group: int | SpaceGroup | None = None, + point_group: int | str | Symmetry | None = None, + color: str | None = None, + a: float = None, + b: float = None, + c: float = None, + ) -> None: + """Create a Phase with an orthorhombic bravais lattice. + + Parameters + ---------- + name + The name to give the Phase. + + space_group + Space group describing the symmetry of the Phase. This can + either be an integer between 15 and 75 corresponding to one + of the 59 orthorhombic space group IDs, or a + diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. + If not given, the space group is set to SG#15 (P222). + + point_group + Point group describing the non-translational symmetry of the + phase. If not given, the point group is set to D2 ("222"). + + color + The colour to use when plotting this phase. Default is blue. + + a + The unit cell length a (arbitrary units). + + b + The unit cell length b (arbitrary units). + + c + The unit cell length c (arbitrary units). + + Returns + ------- + phase + New phase. + + Notes + ----- + Any missing lattice parameters will be imported from a + dictonary of default values, which can be seen using the following + snippet: + + >>> import diffpy.structure as dst + >>> dst.Lattice(*_default_lattices["orthorhombic"].abcABG()) + """ + if space_group is None and point_group is None: + space_group = 16 + default = _default_lattices["orthorhombic"].abcABG() + given = [a, b, c, None, None, None] + abcABG = [(j if j is not None else i) for i, j in zip(default, given)] + lat = dst.Lattice(*abcABG) + phase = cls( + name=name, + space_group=space_group, + point_group=point_group, + color=color, + structure=dst.Structure(lattice=lat), + ) + if phase.point_group.system != "orthorhombic": + raise ValueError( + f"{phase.point_group.name} is not an orthorhombic symmetry." + ) + return phase + + @classmethod + def tetragonal( + cls, + name: str | Phase | None = None, + space_group: int | SpaceGroup | None = None, + point_group: int | str | Symmetry | None = None, + color: str | None = None, + a: float = None, + c: float = None, + ) -> None: + """Create a Phase with a tetragonal bravais lattice. + + Parameters + ---------- + name + The name to give the Phase. + + space_group + Space group describing the symmetry of the Phase. This can + either be an integer between 74 and 143 corresponding to one + of the 68 tetragonal space group IDs, or a + diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. + If not given, the space group is set to SG#75 (P4). + + point_group + Point group describing the non-translational symmetry of the + phase. If not given, the point group is set to C4 ("4"). + + color + The colour to use when plotting this phase. Default is blue. + + a + The unit cell length a (arbitrary units). + + c + The unit cell length c (arbitrary units). + + Returns + ------- + phase + New phase. + + Notes + ----- + Any missing lattice parameters will be imported from a + dictonary of default values, which can be seen using the following + snippet: + + >>> import diffpy.structure as dst + >>> dst.Lattice(*_default_lattices["tetragonal"].abcABG()) + """ + if space_group is None and point_group is None: + space_group = 75 + default = _default_lattices["tetragonal"].abcABG() + given = [a, a, c, None, None, None] + abcABG = [(j if j is not None else i) for i, j in zip(default, given)] + lat = dst.Lattice(*abcABG) + phase = cls( + name=name, + space_group=space_group, + point_group=point_group, + color=color, + structure=dst.Structure(lattice=lat), + ) + if phase.point_group.system != "tetragonal": + raise ValueError( + f"{phase.point_group.name} is not a tetragonal symmetry." + ) + return phase + + @classmethod + def trigonal( + cls, + name: str | Phase | None = None, + space_group: int | SpaceGroup | None = None, + point_group: int | str | Symmetry | None = None, + color: str | None = None, + a: float = None, + alpha: float = None, + ) -> None: + """Create a Phase with trigonal (rhombohedral) bravais lattice. + + Parameters + ---------- + name + The name to give the Phase. + + space_group + Space group describing the symmetry of the Phase. This can + either be an integer between 142 and 168 corresponding to one + of the 25 trigonal space group IDs, or a + diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. + If not given, the space group is set to SG#143 (P3). + + point_group + Point group describing the non-translational symmetry of the + phase. If not given, the point group is set to C3 ("3"). + + color + The colour to use when plotting this phase. Default is blue. + + a + The unit cell length a (arbitrary units). + + alpha + The angle (in degrees) between the b and c axes of the unit cell. + + Returns + ------- + phase + New phase. + + Notes + ----- + Any missing lattice parameters will be imported from a + dictonary of default values, which can be seen using the following + snippet: + + >>> import diffpy.structure as dst + >>> dst.Lattice(*_default_lattices["trigonal"].abcABG()) + """ + if space_group is None and point_group is None: + space_group = 143 + default = _default_lattices["trigonal"].abcABG() + given = [a, a, a, alpha, alpha, alpha] + abcABG = [(j if j is not None else i) for i, j in zip(default, given)] + lat = dst.Lattice(*abcABG) + phase = cls( + name=name, + space_group=space_group, + point_group=point_group, + color=color, + structure=dst.Structure(lattice=lat), + ) + if phase.point_group.system != "trigonal": + raise ValueError( + f"{phase.point_group.name} is not a trigonal (rhombohedral) symmetry." + ) + return phase + + @classmethod + def hexagonal( + cls, + name: str | Phase | None = None, + space_group: int | SpaceGroup | None = None, + point_group: int | str | Symmetry | None = None, + color: str | None = None, + a: float = None, + c: float = None, + ) -> None: + """Create a Phase with a hexagonal bravais lattice. + + Parameters + ---------- + name + The name to give the Phase. + + space_group + Space group describing the symmetry of the Phase. This can + either be an integer between 167 and 195 corresponding to one + of the 27 hexagonal space group IDs, or a + diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. + If not given, the space group is set to SG#168 (P6). + + point_group + Point group describing the non-translational symmetry of the + phase. If not given, the point group is set to C6 ("6"). + + color + The colour to use when plotting this phase. Default is blue. + + a + The unit cell length a (arbitrary units). + + c + The unit cell length c (arbitrary units). + + Returns + ------- + phase + New phase. + + Notes + ----- + Any missing lattice parameters will be imported from a + dictonary of default values, which can be seen using the following + snippet: + + >>> import diffpy.structure as dst + >>> dst.Lattice(*_default_lattices["hexagonal"].abcABG()) + """ + if space_group is None and point_group is None: + space_group = 168 + default = _default_lattices["hexagonal"].abcABG() + given = [a, a, c, None, None, None] + abcABG = [(j if j is not None else i) for i, j in zip(default, given)] + lat = dst.Lattice(*abcABG) + phase = cls( + name=name, + space_group=space_group, + point_group=point_group, + color=color, + structure=dst.Structure(lattice=lat), + ) + if phase.point_group.system != "hexagonal": + raise ValueError( + f"{phase.point_group.name} is not a hexagonal symmetry." + ) + return phase + + @classmethod + def cubic( + cls, + name: str | Phase | None = None, + space_group: int | SpaceGroup | None = None, + point_group: int | str | Symmetry | None = None, + color: str | None = None, + a: float = None, + ) -> None: + """Create a Phase with a cubic bravais lattice. + + Parameters + ---------- + name + The name to give the Phase. + + space_group + Space group describing the symmetry of the Phase. This can + either be an integer between 194 and 231 corresponding to one + of the 36 cubic space group IDs, or a + diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. + If not given, the space group is set to SG#195 (P23). + + point_group + Point group describing the non-translational symmetry of the + phase. If not given, the point group is set to T ("23"). + + color + The colour to use when plotting this phase. Default is blue. + + a + The unit cell length a (arbitrary units). + + Returns + ------- + phase + New phase. + + Notes + ----- + Any missing lattice parameters will be imported from a + dictonary of default values, which can be seen using the following + snippet: + + >>> import diffpy.structure as dst + >>> dst.Lattice(*_default_lattices["cubic"].abcABG()) + """ + if space_group is None and point_group is None: + space_group = 195 + default = _default_lattices["cubic"].abcABG() + given = [a, a, a, None, None, None] + abcABG = [(j if j is not None else i) for i, j in zip(default, given)] + lat = dst.Lattice(*abcABG) + phase = cls( + name=name, + space_group=space_group, + point_group=point_group, + color=color, + structure=dst.Structure(lattice=lat), + ) + if phase.point_group.system != "cubic": + raise ValueError( + f"{phase.point_group.name} is not a cubic symmetry." + ) + return phase + @classmethod def from_cif(cls, filename: str | Path) -> Phase: r"""Create a Phase from a Crystallographic Information File (CIF). From 7b5cb47fadbf784e2d3eb8fbdeaba521e2c6949e Mon Sep 17 00:00:00 2001 From: Austin Gerlt Date: Thu, 9 Apr 2026 16:22:10 -0600 Subject: [PATCH 04/11] update imports and styling in test_phase --- orix/tests/test_crystal_map/test_phase.py | 225 +++++++++++++--------- 1 file changed, 137 insertions(+), 88 deletions(-) diff --git a/orix/tests/test_crystal_map/test_phase.py b/orix/tests/test_crystal_map/test_phase.py index fa841b73..6a33de48 100644 --- a/orix/tests/test_crystal_map/test_phase.py +++ b/orix/tests/test_crystal_map/test_phase.py @@ -17,12 +17,15 @@ # along with orix. If not, see . # -from diffpy.structure import Atom, Lattice, Structure, loadStructure +import diffpy.structure as dst import numpy as np import pytest -from orix.crystal_map import Phase -from orix.crystal_map._phase import default_lattice, new_structure_matrix_from_alignment +import orix.crystal_map as ocm +from orix.crystal_map._phase import ( + default_lattice, + new_structure_matrix_from_alignment, +) from orix.quaternion.symmetry import O, Symmetry @@ -37,9 +40,11 @@ class TestPhase: None, "tab:blue", (0.121568, 0.466666, 0.705882), - Structure(title="Super", lattice=Lattice(1, 1, 1, 90, 90, 90)), + dst.Structure( + title="Super", lattice=dst.Lattice(1, 1, 1, 90, 90, 90) + ), ), - (None, "1", 1, "blue", "blue", (0, 0, 1), Structure()), + (None, "1", 1, "blue", "blue", (0, 0, 1), dst.Structure()), ( "al", "43", @@ -47,7 +52,9 @@ class TestPhase: "xkcd:salmon", "xkcd:salmon", (1, 0.474509, 0.423529), - Structure(title="ni", lattice=Lattice(1, 2, 3, 90, 90, 90)), + dst.Structure( + title="ni", lattice=dst.Lattice(1, 2, 3, 90, 90, 90) + ), ), ( "My awes0me phase!", @@ -61,9 +68,16 @@ 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( + p = ocm.Phase( name=name, point_group=point_group, space_group=space_group, @@ -93,19 +107,19 @@ def test_init_phase( if structure is not None: assert p.structure == structure else: - assert p.structure == Structure() + assert p.structure == dst.Structure() def test_copy_constructor_phase(self): - p1 = Phase( + p1 = ocm.Phase( "test", 225, "m-3m", - Structure( - [Atom("Al", (0, 0, 0))], - Lattice(10, 10, 10, 90, 90, 90), + dst.Structure( + [dst.Atom("Al", (0, 0, 0))], + dst.Lattice(10, 10, 10, 90, 90, 90), ), ) - p2 = Phase(p1) + p2 = ocm.Phase(p1) assert p1 is not p2 assert repr(p1) == repr(p2) @@ -118,7 +132,7 @@ def test_copy_constructor_phase(self): @pytest.mark.parametrize("name", [None, "al", 1, np.arange(2)]) def test_set_phase_name(self, name): - p = Phase(name=name) + p = ocm.Phase(name=name) if name is None: name = "" assert p.name == str(name) @@ -132,7 +146,7 @@ def test_set_phase_name(self, name): ], ) def test_set_phase_color(self, color, color_alias, color_rgb, fails): - p = Phase() + p = ocm.Phase() if fails: with pytest.raises(ValueError, match="Invalid RGBA argument: "): p.color = color @@ -151,33 +165,43 @@ 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() + p = ocm.Phase() if fails: - with pytest.raises(ValueError, match=f"'{point_group}' must be of type"): + with pytest.raises( + ValueError, match=f"'{point_group}' must be of type" + ): 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", + [ + dst.Structure(), + dst.Structure(lattice=dst.Lattice(1, 2, 3, 90, 120, 90)), + ], ) def test_set_structure(self, structure): - p = Phase() + p = ocm.Phase() p.structure = structure assert p.structure == structure def test_set_structure_phase_name(self): name = "al" - p = Phase(name=name) - p.structure = Structure(lattice=Lattice(*([0.405] * 3 + [90] * 3))) + p = ocm.Phase(name=name) + p.structure = dst.Structure( + lattice=dst.Lattice(*([0.405] * 3 + [90] * 3)) + ) assert p.name == name assert p.structure.title == name def test_set_structure_raises(self): - p = Phase() - with pytest.raises(ValueError, match=".* must be a diffpy.structure.Structure"): + p = ocm.Phase() + with pytest.raises( + ValueError, match=".* must be a diffpy.structure.Structure" + ): p.structure = [1, 2, 3, 90, 90, 90] @pytest.mark.parametrize( @@ -191,7 +215,7 @@ def test_set_structure_raises(self): def test_phase_repr_str( self, name, space_group, desired_sg_str, desired_pg_str, desired_ppg_str ): - p = Phase(name=name, space_group=space_group, color="C0") + p = ocm.Phase(name=name, space_group=space_group, color="C0") desired = ( f" Date: Thu, 9 Apr 2026 17:00:09 -0600 Subject: [PATCH 05/11] Unit tests for Phase class methods --- orix/tests/test_crystal_map/test_phase.py | 113 +++++++++++++++++++--- 1 file changed, 99 insertions(+), 14 deletions(-) diff --git a/orix/tests/test_crystal_map/test_phase.py b/orix/tests/test_crystal_map/test_phase.py index 6a33de48..6352699e 100644 --- a/orix/tests/test_crystal_map/test_phase.py +++ b/orix/tests/test_crystal_map/test_phase.py @@ -26,7 +26,7 @@ default_lattice, new_structure_matrix_from_alignment, ) -from orix.quaternion.symmetry import O, Symmetry +import orix.quaternion.symmetry as osm class TestPhase: @@ -58,7 +58,7 @@ class TestPhase: ), ( "My awes0me phase!", - O, + osm.O, 211, "C1", "tab:orange", @@ -97,7 +97,7 @@ def test_init_phase( if point_group == "43": point_group = "432" - if isinstance(point_group, Symmetry): + if isinstance(point_group, osm.Symmetry): point_group = point_group.name assert p.point_group.name == point_group @@ -1016,16 +1016,101 @@ def test_expand_asymmetric_unit_raise_if_no_point_group(self): phase.expand_asymmetric_unit() def test_default_lattice(self): - for S in ["1", "2", "222", "422", "432"]: - phase = Phase(point_group=S) - lattice_parameters = phase.structure.lattice.abcABG() - assert np.allclose([1, 1, 1, 90, 90, 90], lattice_parameters) + for symmetry, system in [ + ["1", "triclinic"], + ["2", "monoclinic"], + ["222", "orthorhombic"], + ["422", "tetragonal"], + ["432", "cubic"], + ["3", "trigonal"], + ["622", "hexagonal"], + ]: + phase = ocm.Phase(point_group=symmetry) + actual_abcABG = phase.structure.lattice.abcABG() + expected_abcABG = ocm._phase._default_lattices[system].abcABG() + assert np.allclose(actual_abcABG, expected_abcABG) + abcABG = ocm._phase.default_lattice("hexagonal").abcABG() + assert np.allclose(abcABG, [1, 1, 1.5, 90, 90, 120]) + with pytest.raises(ValueError, match="Unknown"): + ocm._phase.default_lattice("banana") - for S in ["3", "622"]: - phase = Phase(point_group=S) - lattice_parameters = phase.structure.lattice.abcABG() - assert np.allclose([1, 1, 1, 90, 90, 120], lattice_parameters) + def test_triclinic_classmethod(self): + out = ocm.Phase.triclinic() + out = ocm.Phase.triclinic("aa", space_group=1) + out = ocm.Phase.triclinic(space_group=dst.spacegroups.GetSpaceGroup(2)) + out = ocm.Phase.triclinic(point_group=osm.Ci) + out = ocm.Phase.triclinic(point_group="1") + with pytest.raises(ValueError): + out = ocm.Phase.triclinic(space_group=90) + out = ocm.Phase.triclinic(point_group=osm.D6) + del out - def test_default_lattice_raises(self): - with pytest.raises(ValueError, match="Unknown crystal system 'rhombohedral'"): - default_lattice("rhombohedral") + def test_monoclinic_classmethod(self): + out = ocm.Phase.monoclinic() + out = ocm.Phase.monoclinic("aa", space_group=3) + out = ocm.Phase.monoclinic(space_group=dst.spacegroups.GetSpaceGroup(15)) + out = ocm.Phase.monoclinic(point_group=osm.C2) + out = ocm.Phase.monoclinic(point_group="2") + with pytest.raises(ValueError): + out = ocm.Phase.monoclinic(space_group=90) + out = ocm.Phase.monoclinic(point_group=osm.D6) + del out + + def test_orthorhombic_classmethod(self): + out = ocm.Phase.orthorhombic() + out = ocm.Phase.orthorhombic("aa", space_group=16) + out = ocm.Phase.orthorhombic( + space_group=dst.spacegroups.GetSpaceGroup(74) + ) + out = ocm.Phase.orthorhombic(point_group=osm.D2) + out = ocm.Phase.orthorhombic(point_group="222") + with pytest.raises(ValueError): + out = ocm.Phase.orthorhombic(space_group=90) + out = ocm.Phase.orthorhombic(point_group=osm.D6) + del out + + def test_tetragonal_classmethod(self): + out = ocm.Phase.tetragonal() + out = ocm.Phase.tetragonal("aa", space_group=75) + out = ocm.Phase.tetragonal( + space_group=dst.spacegroups.GetSpaceGroup(142) + ) + out = ocm.Phase.tetragonal(point_group=osm.D4) + out = ocm.Phase.tetragonal(point_group="422") + with pytest.raises(ValueError): + out = ocm.Phase.tetragonal(space_group=9) + out = ocm.Phase.tetragonal(point_group=osm.D6) + del out + + def test_cubic_classmethod(self): + out = ocm.Phase.cubic() + out = ocm.Phase.cubic("aa", space_group=195) + out = ocm.Phase.cubic(space_group=dst.spacegroups.GetSpaceGroup(230)) + out = ocm.Phase.cubic(point_group=osm.Oh) + out = ocm.Phase.cubic(point_group="m3m") + with pytest.raises(ValueError): + out = ocm.Phase.cubic(space_group=90) + out = ocm.Phase.cubic(point_group=osm.D6) + del out + + def test_trigonal_classmethod(self): + out = ocm.Phase.trigonal() + out = ocm.Phase.trigonal("aa", space_group=143) + out = ocm.Phase.trigonal(space_group=dst.spacegroups.GetSpaceGroup(167)) + out = ocm.Phase.trigonal(point_group=osm.C3) + out = ocm.Phase.trigonal(point_group="3") + with pytest.raises(ValueError): + out = ocm.Phase.trigonal(space_group=90) + out = ocm.Phase.trigonal(point_group=osm.D6) + del out + + def test_hexagonal_classmethod(self): + out = ocm.Phase.hexagonal() + out = ocm.Phase.hexagonal("aa", space_group=168) + out = ocm.Phase.hexagonal(space_group=dst.spacegroups.GetSpaceGroup(194)) + out = ocm.Phase.hexagonal(point_group=osm.D6) + out = ocm.Phase.hexagonal(point_group="622") + with pytest.raises(ValueError): + out = ocm.Phase.hexagonal(space_group=90) + out = ocm.Phase.hexagonal(point_group=osm.D4) + del out From 555723c83968550e682feeaadb33bce3331a9e73 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 23:38:28 +0000 Subject: [PATCH 06/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- orix/crystal_map/_phase.py | 24 +++-------- orix/tests/test_crystal_map/test_phase.py | 52 ++++++----------------- 2 files changed, 19 insertions(+), 57 deletions(-) diff --git a/orix/crystal_map/_phase.py b/orix/crystal_map/_phase.py index 2831213d..c0e29f3d 100644 --- a/orix/crystal_map/_phase.py +++ b/orix/crystal_map/_phase.py @@ -202,9 +202,7 @@ def structure(self, value: dst.Structure) -> None: # Ensure correct alignment old_matrix = value.lattice.base - new_matrix = new_structure_matrix_from_alignment( - old_matrix, x="a", z="c*" - ) + new_matrix = new_structure_matrix_from_alignment(old_matrix, x="a", z="c*") new_value = value.copy() # Ensure atom positions are expressed in the new basis @@ -508,9 +506,7 @@ def triclinic( structure=dst.Structure(lattice=lat), ) if phase.point_group.system != "triclinic": - raise ValueError( - f"{phase.point_group.name} is not a triclinic symmetry." - ) + raise ValueError(f"{phase.point_group.name} is not a triclinic symmetry.") return phase @classmethod @@ -586,9 +582,7 @@ def monoclinic( structure=dst.Structure(lattice=lat), ) if phase.point_group.system != "monoclinic": - raise ValueError( - f"{phase.point_group.name} is not a monoclinic symmetry." - ) + raise ValueError(f"{phase.point_group.name} is not a monoclinic symmetry.") return phase @classmethod @@ -730,9 +724,7 @@ def tetragonal( structure=dst.Structure(lattice=lat), ) if phase.point_group.system != "tetragonal": - raise ValueError( - f"{phase.point_group.name} is not a tetragonal symmetry." - ) + raise ValueError(f"{phase.point_group.name} is not a tetragonal symmetry.") return phase @classmethod @@ -870,9 +862,7 @@ def hexagonal( structure=dst.Structure(lattice=lat), ) if phase.point_group.system != "hexagonal": - raise ValueError( - f"{phase.point_group.name} is not a hexagonal symmetry." - ) + raise ValueError(f"{phase.point_group.name} is not a hexagonal symmetry.") return phase @classmethod @@ -936,9 +926,7 @@ def cubic( structure=dst.Structure(lattice=lat), ) if phase.point_group.system != "cubic": - raise ValueError( - f"{phase.point_group.name} is not a cubic symmetry." - ) + raise ValueError(f"{phase.point_group.name} is not a cubic symmetry.") return phase @classmethod diff --git a/orix/tests/test_crystal_map/test_phase.py b/orix/tests/test_crystal_map/test_phase.py index 6352699e..8d3306bf 100644 --- a/orix/tests/test_crystal_map/test_phase.py +++ b/orix/tests/test_crystal_map/test_phase.py @@ -40,9 +40,7 @@ class TestPhase: None, "tab:blue", (0.121568, 0.466666, 0.705882), - dst.Structure( - title="Super", lattice=dst.Lattice(1, 1, 1, 90, 90, 90) - ), + dst.Structure(title="Super", lattice=dst.Lattice(1, 1, 1, 90, 90, 90)), ), (None, "1", 1, "blue", "blue", (0, 0, 1), dst.Structure()), ( @@ -52,9 +50,7 @@ class TestPhase: "xkcd:salmon", "xkcd:salmon", (1, 0.474509, 0.423529), - dst.Structure( - title="ni", lattice=dst.Lattice(1, 2, 3, 90, 90, 90) - ), + dst.Structure(title="ni", lattice=dst.Lattice(1, 2, 3, 90, 90, 90)), ), ( "My awes0me phase!", @@ -167,9 +163,7 @@ 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 = ocm.Phase() if fails: - with pytest.raises( - ValueError, match=f"'{point_group}' must be of type" - ): + with pytest.raises(ValueError, match=f"'{point_group}' must be of type"): p.point_group = point_group else: p.point_group = point_group @@ -191,17 +185,13 @@ def test_set_structure(self, structure): def test_set_structure_phase_name(self): name = "al" p = ocm.Phase(name=name) - p.structure = dst.Structure( - lattice=dst.Lattice(*([0.405] * 3 + [90] * 3)) - ) + p.structure = dst.Structure(lattice=dst.Lattice(*([0.405] * 3 + [90] * 3))) assert p.name == name assert p.structure.title == name def test_set_structure_raises(self): p = ocm.Phase() - with pytest.raises( - ValueError, match=".* must be a diffpy.structure.Structure" - ): + with pytest.raises(ValueError, match=".* must be a diffpy.structure.Structure"): p.structure = [1, 2, 3, 90, 90, 90] @pytest.mark.parametrize( @@ -258,9 +248,7 @@ def test_shallow_copy_phase(self): assert p.__repr__() == p2.__repr__() def test_phase_init_non_matching_space_group_point_group(self): - with pytest.warns( - UserWarning, match="Setting space group to 'None', as" - ): + with pytest.warns(UserWarning, match="Setting space group to 'None', as"): _ = ocm.Phase(space_group=225, point_group="432") @pytest.mark.parametrize( @@ -282,9 +270,7 @@ def test_point_group_derived_from_space_group( def test_set_space_group_raises(self): space_group = "outer-space" - with pytest.raises( - ValueError, match=f"'{space_group}' must be of type " - ): + with pytest.raises(ValueError, match=f"'{space_group}' must be of type "): p = ocm.Phase() p.space_group = space_group @@ -335,9 +321,7 @@ def test_structure_matrix(self): # Getting new structure matrix without passing enough parameters # raises an error - with pytest.raises( - ValueError, match="At least two of x, y, z must be set." - ): + with pytest.raises(ValueError, match="At least two of x, y, z must be set."): _ = new_structure_matrix_from_alignment(lattice.base, x="a") def test_triclinic_structure_matrix(self): @@ -433,17 +417,13 @@ def test_atom_positions(self, lat, atoms): # however, Phase should (in many cases) change the basis. if np.allclose(structure.lattice.base, phase.structure.lattice.base): # In this branch we are in the same basis & all atoms should be the same - for atom_from_structure, atom_from_phase in zip( - structure, phase.structure - ): + for atom_from_structure, atom_from_phase in zip(structure, phase.structure): assert np.allclose(atom_from_structure.xyz, atom_from_phase.xyz) else: # Here we have differing basis, so xyz must disagree for at least some atoms disagreement_found = False - for atom_from_structure, atom_from_phase in zip( - structure, phase.structure - ): + for atom_from_structure, atom_from_phase in zip(structure, phase.structure): if not np.allclose(atom_from_structure.xyz, atom_from_phase.xyz): disagreement_found = True break @@ -468,9 +448,7 @@ def test_from_cif_same_structure(self, cif_file): phase1 = ocm.Phase.from_cif(cif_file) structure = dst.loadStructure(cif_file) phase2 = ocm.Phase(structure=structure) - assert np.allclose( - phase1.structure.lattice.base, phase2.structure.lattice.base - ) + assert np.allclose(phase1.structure.lattice.base, phase2.structure.lattice.base) assert np.allclose(phase1.structure.xyz, phase2.structure.xyz) @pytest.mark.parametrize( @@ -1059,9 +1037,7 @@ def test_monoclinic_classmethod(self): def test_orthorhombic_classmethod(self): out = ocm.Phase.orthorhombic() out = ocm.Phase.orthorhombic("aa", space_group=16) - out = ocm.Phase.orthorhombic( - space_group=dst.spacegroups.GetSpaceGroup(74) - ) + out = ocm.Phase.orthorhombic(space_group=dst.spacegroups.GetSpaceGroup(74)) out = ocm.Phase.orthorhombic(point_group=osm.D2) out = ocm.Phase.orthorhombic(point_group="222") with pytest.raises(ValueError): @@ -1072,9 +1048,7 @@ def test_orthorhombic_classmethod(self): def test_tetragonal_classmethod(self): out = ocm.Phase.tetragonal() out = ocm.Phase.tetragonal("aa", space_group=75) - out = ocm.Phase.tetragonal( - space_group=dst.spacegroups.GetSpaceGroup(142) - ) + out = ocm.Phase.tetragonal(space_group=dst.spacegroups.GetSpaceGroup(142)) out = ocm.Phase.tetragonal(point_group=osm.D4) out = ocm.Phase.tetragonal(point_group="422") with pytest.raises(ValueError): From 306ea16559b1b85141b22ee0192575962cd863c2 Mon Sep 17 00:00:00 2001 From: Austin Gerlt Date: Thu, 9 Apr 2026 19:23:15 -0600 Subject: [PATCH 07/11] update changelog --- CHANGELOG.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 647e4f60..56ef892f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,12 +12,15 @@ its best to adhere to `Semantic Versioning Added ----- +- Phases can be defined using lattice-based class methods, such as ``Phase.cubic`` - Non-lazy computation of dot products with ``Misorientation.get_distance_matrix(lazy=True)``. Currently opt-in, but will be the default in the next minor version. Changed ------- +- The symmetry-dependent default lattice parameters in ``Phase.structure`` have + been changed to crystallographically possible values. - ``Miller.get_nearest()`` now raises a ``NotImplementedError`` rather than returning ``NotImplemented``. - ``Mille.mean(use_symmetry=True)`` now raises a ``NotImplementedError`` rather than @@ -25,6 +28,12 @@ Changed - Improved (faster and using less memory) non-lazy computation of misorientation angles from ``Orientation.with_angle_outer()``. +Removed +------- + +Deprecated +---------- + Fixed ----- - (Mis)orientation reduction to the fundamental zone via ``reduce()`` now correctly From 27c90b06a01b59ca6c122985595eefc4fdbfd85c Mon Sep 17 00:00:00 2001 From: Austin Gerlt Date: Mon, 20 Apr 2026 16:16:39 -0600 Subject: [PATCH 08/11] Revert default lattices and improve docstrings --- orix/crystal_map/_phase.py | 189 ++++++++-------------- orix/tests/test_crystal_map/test_phase.py | 30 ++-- 2 files changed, 79 insertions(+), 140 deletions(-) diff --git a/orix/crystal_map/_phase.py b/orix/crystal_map/_phase.py index c0e29f3d..55c33a55 100644 --- a/orix/crystal_map/_phase.py +++ b/orix/crystal_map/_phase.py @@ -158,7 +158,6 @@ def __init__( structure=name.structure.copy(), color=name.color, ) - self.space_group = space_group # Needs to be set before point group self.point_group = point_group self.color = color if color is not None else "tab:blue" @@ -439,15 +438,15 @@ def triclinic( beta: float | None = None, gamma: float | None = None, ) -> None: - """Create a Phase with a triclinic bravais lattice. + """Create a phase with a triclinic lattice and symmetry. Parameters ---------- name - The name to give the Phase. + The name to give the phase. space_group - Space group describing the symmetry of the Phase. This can + Space group describing the symmetry of the phase. This can either be the integers 1 or 2, corresponding to the two triclinic space group IDs, or a diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. @@ -461,40 +460,34 @@ def triclinic( The colour to use when plotting this phase. Default is blue. a - The unit cell length a (arbitrary units). + The unit cell length a (arbitrary units). Default is 1. b - The unit cell length b (arbitrary units). + The unit cell length b (arbitrary units). Default is 1. c - The unit cell length c (arbitrary units). + The unit cell length c (arbitrary units). Default is 1. alpha - The angle (in degrees) between the b and c axes of the unit cell. + The angle (in degrees) between the b and c axes of the + unit cell. Default is 90. beta - The angle (in degrees) between the a and c axes of the unit cell. + The angle (in degrees) between the a and c axes of the + unit cell. Default is 90. gamma - The angle (in degrees) between the a and b axes of the unit cell. + The angle (in degrees) between the a and b axes of the + unit cell. Default is 90. Returns ------- phase - a Phase object with triclinic symmetry - - Notes - ----- - Any missing lattice parameters will be imported from a - dictonary of default values, which can be seen using the following - snippet: - - >>> import diffpy.structure as dst - >>> dst.Lattice(*_default_lattices["triclinic"].abcABG()) + A new triclinic phase. """ if space_group is None and point_group is None: space_group = 1 - default = _default_lattices["triclinic"].abcABG() + default = default_lattice("triclinic").abcABG() given = [a, b, c, alpha, beta, gamma] abcABG = [(j if j is not None else i) for i, j in zip(default, given)] lat = dst.Lattice(*abcABG) @@ -521,15 +514,15 @@ def monoclinic( c: float = None, beta: float = None, ) -> None: - """Create a Phase with a monoclinic bravais lattice. + """Create a phase with a monoclinic lattice and symmetry. Parameters ---------- name - The name to give the Phase. + The name to give the phase. space_group - Space group describing the symmetry of the Phase. This can + Space group describing the symmetry of the phase. This can either be an integer between 2 and 16 corresponding to one of the 13 monoclinic space group IDs, or a diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. @@ -543,34 +536,27 @@ def monoclinic( The colour to use when plotting this phase. Default is blue. a - The unit cell length a (arbitrary units). + The unit cell length a (arbitrary units). Default is 1. b - The unit cell length b (arbitrary units). + The unit cell length b (arbitrary units). Default is 1. c - The unit cell length c (arbitrary units). + The unit cell length c (arbitrary units). Default is 1. beta - The angle (in degrees) between the a and c axes of the unit cell. + The angle (in degrees) between the a and c axes of the + unit cell. Default is 90. Returns ------- phase - New phase. - - Notes - ----- - Any missing lattice parameters will be imported from a - dictonary of default values, which can be seen using the following - snippet: + A new monoclinic phase. - >>> import diffpy.structure as dst - >>> dst.Lattice(*_default_lattices["monoclinic"].abcABG()) """ if space_group is None and point_group is None: space_group = 3 - default = _default_lattices["monoclinic"].abcABG() + default = default_lattice("monoclinic").abcABG() given = [a, b, c, None, beta, None] abcABG = [(j if j is not None else i) for i, j in zip(default, given)] lat = dst.Lattice(*abcABG) @@ -596,15 +582,15 @@ def orthorhombic( b: float = None, c: float = None, ) -> None: - """Create a Phase with an orthorhombic bravais lattice. + """Create a phase with an orthorhombic lattice and symmetry. Parameters ---------- name - The name to give the Phase. + The name to give the phase. space_group - Space group describing the symmetry of the Phase. This can + Space group describing the symmetry of the phase. This can either be an integer between 15 and 75 corresponding to one of the 59 orthorhombic space group IDs, or a diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. @@ -618,31 +604,22 @@ def orthorhombic( The colour to use when plotting this phase. Default is blue. a - The unit cell length a (arbitrary units). + The unit cell length a (arbitrary units). Default is 1. b - The unit cell length b (arbitrary units). + The unit cell length b (arbitrary units). Default is 1. c - The unit cell length c (arbitrary units). + The unit cell length c (arbitrary units). Default is 1. Returns ------- phase - New phase. - - Notes - ----- - Any missing lattice parameters will be imported from a - dictonary of default values, which can be seen using the following - snippet: - - >>> import diffpy.structure as dst - >>> dst.Lattice(*_default_lattices["orthorhombic"].abcABG()) + A new orthorhombic phase. """ if space_group is None and point_group is None: space_group = 16 - default = _default_lattices["orthorhombic"].abcABG() + default = default_lattice("orthorhombic").abcABG() given = [a, b, c, None, None, None] abcABG = [(j if j is not None else i) for i, j in zip(default, given)] lat = dst.Lattice(*abcABG) @@ -669,15 +646,15 @@ def tetragonal( a: float = None, c: float = None, ) -> None: - """Create a Phase with a tetragonal bravais lattice. + """Create a phase with a tetragonal lattice and symmetry. Parameters ---------- name - The name to give the Phase. + The name to give the phase. space_group - Space group describing the symmetry of the Phase. This can + Space group describing the symmetry of the phase. This can either be an integer between 74 and 143 corresponding to one of the 68 tetragonal space group IDs, or a diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. @@ -691,28 +668,20 @@ def tetragonal( The colour to use when plotting this phase. Default is blue. a - The unit cell length a (arbitrary units). + The unit cell length a (arbitrary units). Default is 1. c - The unit cell length c (arbitrary units). + The unit cell length c (arbitrary units). Default is 1. Returns ------- phase - New phase. + A new tetragonal phase. - Notes - ----- - Any missing lattice parameters will be imported from a - dictonary of default values, which can be seen using the following - snippet: - - >>> import diffpy.structure as dst - >>> dst.Lattice(*_default_lattices["tetragonal"].abcABG()) """ if space_group is None and point_group is None: space_group = 75 - default = _default_lattices["tetragonal"].abcABG() + default = default_lattice("tetragonal").abcABG() given = [a, a, c, None, None, None] abcABG = [(j if j is not None else i) for i, j in zip(default, given)] lat = dst.Lattice(*abcABG) @@ -737,15 +706,15 @@ def trigonal( a: float = None, alpha: float = None, ) -> None: - """Create a Phase with trigonal (rhombohedral) bravais lattice. + """Create a phase with a trigonal (rhombohedral) lattice and symmetry. Parameters ---------- name - The name to give the Phase. + The name to give the phase. space_group - Space group describing the symmetry of the Phase. This can + Space group describing the symmetry of the phase. This can either be an integer between 142 and 168 corresponding to one of the 25 trigonal space group IDs, or a diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. @@ -759,28 +728,20 @@ def trigonal( The colour to use when plotting this phase. Default is blue. a - The unit cell length a (arbitrary units). - - alpha - The angle (in degrees) between the b and c axes of the unit cell. + The unit cell length a (arbitrary units). Default is 1. Returns ------- phase - New phase. + A new trigonal phase. Notes ----- - Any missing lattice parameters will be imported from a - dictonary of default values, which can be seen using the following - snippet: - - >>> import diffpy.structure as dst - >>> dst.Lattice(*_default_lattices["trigonal"].abcABG()) + Trigonal symmetry requires a=b=c, alpha=beta=90, and gamma=120 """ if space_group is None and point_group is None: space_group = 143 - default = _default_lattices["trigonal"].abcABG() + default = default_lattice("trigonal").abcABG() given = [a, a, a, alpha, alpha, alpha] abcABG = [(j if j is not None else i) for i, j in zip(default, given)] lat = dst.Lattice(*abcABG) @@ -807,15 +768,16 @@ def hexagonal( a: float = None, c: float = None, ) -> None: - """Create a Phase with a hexagonal bravais lattice. + """Create a phase with a hexagonal lattice and symmetry. + Parameters ---------- name - The name to give the Phase. + The name to give the phase. space_group - Space group describing the symmetry of the Phase. This can + Space group describing the symmetry of the phase. This can either be an integer between 167 and 195 corresponding to one of the 27 hexagonal space group IDs, or a diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. @@ -829,28 +791,24 @@ def hexagonal( The colour to use when plotting this phase. Default is blue. a - The unit cell length a (arbitrary units). + The unit cell length a (arbitrary units). Default is 1. c - The unit cell length c (arbitrary units). + The unit cell length c (arbitrary units). Default is 1. Returns ------- phase - New phase. + A new hexagonal phase. Notes ----- - Any missing lattice parameters will be imported from a - dictonary of default values, which can be seen using the following - snippet: + Hexagonal symmetry requires a=b, alpha=beta=90, and gamma=120 - >>> import diffpy.structure as dst - >>> dst.Lattice(*_default_lattices["hexagonal"].abcABG()) """ if space_group is None and point_group is None: space_group = 168 - default = _default_lattices["hexagonal"].abcABG() + default = default_lattice("hexagonal").abcABG() given = [a, a, c, None, None, None] abcABG = [(j if j is not None else i) for i, j in zip(default, given)] lat = dst.Lattice(*abcABG) @@ -874,7 +832,7 @@ def cubic( color: str | None = None, a: float = None, ) -> None: - """Create a Phase with a cubic bravais lattice. + """Create a phase with a cubic lattice and symmetry. Parameters ---------- @@ -882,7 +840,7 @@ def cubic( The name to give the Phase. space_group - Space group describing the symmetry of the Phase. This can + Space group describing the symmetry of the phase. This can either be an integer between 194 and 231 corresponding to one of the 36 cubic space group IDs, or a diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. @@ -896,25 +854,16 @@ def cubic( The colour to use when plotting this phase. Default is blue. a - The unit cell length a (arbitrary units). + The unit cell length a (arbitrary units). Default is 1. Returns ------- phase - New phase. - - Notes - ----- - Any missing lattice parameters will be imported from a - dictonary of default values, which can be seen using the following - snippet: - - >>> import diffpy.structure as dst - >>> dst.Lattice(*_default_lattices["cubic"].abcABG()) + A new cubic phase. """ if space_group is None and point_group is None: space_group = 195 - default = _default_lattices["cubic"].abcABG() + default = default_lattice("cubic").abcABG() given = [a, a, a, None, None, None] abcABG = [(j if j is not None else i) for i, j in zip(default, given)] lat = dst.Lattice(*abcABG) @@ -1086,18 +1035,12 @@ def new_structure_matrix_from_alignment( return new_matrix -_default_lattices = { - "triclinic": dst.Lattice(1.0, 1.1, 1.2, 85, 82, 80), - "monoclinic": dst.Lattice(1.0, 1.1, 1.2, 90, 82, 90), - "orthorhombic": dst.Lattice(1.0, 1.1, 1.2, 90, 90, 90), - "tetragonal": dst.Lattice(1.0, 1.0, 1.2, 90, 90, 90), - "trigonal": dst.Lattice(1.0, 1.0, 1.0, 90, 90, 120), - "hexagonal": dst.Lattice(1.0, 1.0, 1.5, 90, 90, 120), - "cubic": dst.Lattice(1.0, 1.0, 1.0, 90, 90, 90), -} - def default_lattice(system: VALID_SYSTEMS) -> dst.Lattice: - if system not in _default_lattices: + if system in ["triclinic", "monoclinic", "orthorhombic", "tetragonal", "cubic"]: + lat = dst.Lattice(1, 1, 1, 90, 90, 90) + elif system in ["trigonal", "hexagonal"]: + lat = dst.Lattice(1, 1, 1, 90, 90, 120) + else: raise ValueError(f"Unknown crystal system {system!r}") - return _default_lattices[system] + return lat diff --git a/orix/tests/test_crystal_map/test_phase.py b/orix/tests/test_crystal_map/test_phase.py index 8d3306bf..7abb7694 100644 --- a/orix/tests/test_crystal_map/test_phase.py +++ b/orix/tests/test_crystal_map/test_phase.py @@ -994,23 +994,19 @@ def test_expand_asymmetric_unit_raise_if_no_point_group(self): phase.expand_asymmetric_unit() def test_default_lattice(self): - for symmetry, system in [ - ["1", "triclinic"], - ["2", "monoclinic"], - ["222", "orthorhombic"], - ["422", "tetragonal"], - ["432", "cubic"], - ["3", "trigonal"], - ["622", "hexagonal"], - ]: - phase = ocm.Phase(point_group=symmetry) - actual_abcABG = phase.structure.lattice.abcABG() - expected_abcABG = ocm._phase._default_lattices[system].abcABG() - assert np.allclose(actual_abcABG, expected_abcABG) - abcABG = ocm._phase.default_lattice("hexagonal").abcABG() - assert np.allclose(abcABG, [1, 1, 1.5, 90, 90, 120]) - with pytest.raises(ValueError, match="Unknown"): - ocm._phase.default_lattice("banana") + for S in ["1", "2", "222", "422", "432"]: + phase = ocm.Phase(point_group=S) + lattice_parameters = phase.structure.lattice.abcABG() + assert np.allclose([1, 1, 1, 90, 90, 90], lattice_parameters) + + for S in ["3", "622"]: + phase = ocm.Phase(point_group=S) + lattice_parameters = phase.structure.lattice.abcABG() + assert np.allclose([1, 1, 1, 90, 90, 120], lattice_parameters) + + def test_default_lattice_raises(self): + with pytest.raises(ValueError, match="Unknown crystal system 'rhombohedral'"): + default_lattice("rhombohedral") def test_triclinic_classmethod(self): out = ocm.Phase.triclinic() From 9b44ede1d78307d8794ff991aac2ce23eb0d0b3f Mon Sep 17 00:00:00 2001 From: Austin Gerlt Date: Mon, 20 Apr 2026 16:52:39 -0600 Subject: [PATCH 09/11] Using more regular language in Phase docstrings --- orix/crystal_map/_phase.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/orix/crystal_map/_phase.py b/orix/crystal_map/_phase.py index 55c33a55..c1272567 100644 --- a/orix/crystal_map/_phase.py +++ b/orix/crystal_map/_phase.py @@ -101,13 +101,12 @@ class Phase: Parameters ---------- name - The name to give to the Phase. If None, a name will be inhereted - from *structure* if possible. If name is a Phase object, a copy - of that phase is returned instead, and all further arguments - are ignored. + The name to give the phase. If not given, a name will be inhereted + from *structure*, if possible. If a phase is given, a copy of that + phase is returned instead, and all further arguments are ignored. space_group - Space group describing the symmetry of the Phase. This can + Space group describing the symmetry of the phase. This can either be a number 1-230 corresponding to the 230 space groups defined in the International Tables for Crystallography, or a diffpy :class:`~diffpy.structure.spacegroup.SpaceGroup` object. @@ -115,16 +114,16 @@ class Phase: point_group Point group describing the non-translational symmetry of the - Phase. This can be an orix :class:`~orix.quaternion.symmetry.Symmetry` + phase. This can be an orix :class:`~orix.quaternion.symmetry.Symmetry` object or one of the known point group aliases (see the Notes section for details). It must be - compatable with *space_group* if both are given. If - None, the correct point group will either be derived from - *space_group*, or left as None if *space_group* is None. + compatable with *space_group* if both are given. If not given, + then either an appropriate point group will be derived from + *space_group*, or set to None if *space_group* is also None. structure :class:`~diffpy.structure.structure.Structure` describing the - lattice and atomic positions. If None, a default + lattice and atomic positions. If not given, a default :class:`~diffpy.structure.structure.Structure` is used. If a space group or point group has been defined, the structure will also include a :class:`~diffpy.structure.lattice.Lattice` @@ -133,12 +132,13 @@ class Phase: color The colour to use when plotting this phase. Default is blue. - Notes - ----- + Examples + -------- The list of known point group aliases can be seen using the following command: - >>> import orix.quaternion as oqu - >>> [point_group.name for point_group in oqu.symmetry._groups] + >>> import orix.quaternion.symmetry as osm + >>> pg_ aliases = [pg.name for pg in osm._groups] + >>> print(pg_aliases) """ def __init__( From 13a6c39685573fb6d3c4b300a79bd9cf5eeb9cbf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:55:58 +0000 Subject: [PATCH 10/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- orix/crystal_map/_phase.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/orix/crystal_map/_phase.py b/orix/crystal_map/_phase.py index c1272567..e0a7e36d 100644 --- a/orix/crystal_map/_phase.py +++ b/orix/crystal_map/_phase.py @@ -101,7 +101,7 @@ class Phase: Parameters ---------- name - The name to give the phase. If not given, a name will be inhereted + The name to give the phase. If not given, a name will be inhereted from *structure*, if possible. If a phase is given, a copy of that phase is returned instead, and all further arguments are ignored. @@ -1035,7 +1035,6 @@ def new_structure_matrix_from_alignment( return new_matrix - def default_lattice(system: VALID_SYSTEMS) -> dst.Lattice: if system in ["triclinic", "monoclinic", "orthorhombic", "tetragonal", "cubic"]: lat = dst.Lattice(1, 1, 1, 90, 90, 90) From fa61025f526e32fcb357f7991e8a7443ed14e4be Mon Sep 17 00:00:00 2001 From: Austin Gerlt Date: Mon, 20 Apr 2026 17:03:45 -0600 Subject: [PATCH 11/11] add changes from code review --- orix/crystal_map/_phase.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/orix/crystal_map/_phase.py b/orix/crystal_map/_phase.py index e0a7e36d..fbb19e2b 100644 --- a/orix/crystal_map/_phase.py +++ b/orix/crystal_map/_phase.py @@ -218,7 +218,13 @@ def structure(self, value: dst.Structure) -> None: @property def name(self) -> str: - """The name of the phase.""" + """The name of the phase. + + Parameters + ---------- + value : str + Phase name. + """ return self.structure.title @name.setter @@ -316,7 +322,7 @@ def point_group(self, value: int | str | Symmetry | None) -> None: @property def is_hexagonal(self) -> bool: - """Returns True for hexagonal and trigonal crystal structures.""" + """Return True for hexagonal and trigonal crystal structures.""" return np.allclose(self.structure.lattice.abcABG()[3:], [90, 90, 120]) @property