From 31b13be9bd6e858524f279fb21795eda8d092892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Wed, 9 Jul 2025 14:52:47 +0200 Subject: [PATCH 1/9] Update RTD objects.inv link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- doc/conf.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index 2b81c2d3..697bd307 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,3 +1,22 @@ +# +# Copyright 2019-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 . +# + # Configuration file for the Sphinx documentation app. # See the documentation for a full list of configuration options: # https://www.sphinx-doc.org/en/master/usage/configuration.html @@ -66,7 +85,7 @@ "pytest": ("https://docs.pytest.org/en/stable", None), "python": ("https://docs.python.org/3", None), "pyxem": ("https://pyxem.readthedocs.io/en/latest", None), - "readthedocs": ("https://docs.readthedocs.io/en/stable", None), + "readthedocs": ("https://docs.readthedocs.com/platform/stable/objects.inv", None), "scipy": ("https://docs.scipy.org/doc/scipy", None), "sklearn": ("https://scikit-learn.org/stable", None), "sphinx": ("https://www.sphinx-doc.org/en/master", None), From 55be041fa5d2f7386737efc97af9d3d8e6aaac7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Wed, 9 Jul 2025 14:58:13 +0200 Subject: [PATCH 2/9] Disallow numpydoc v1.9.0 (#570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- doc/conf.py | 2 +- pyproject.toml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 697bd307..9d6aa2bd 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -85,7 +85,7 @@ "pytest": ("https://docs.pytest.org/en/stable", None), "python": ("https://docs.python.org/3", None), "pyxem": ("https://pyxem.readthedocs.io/en/latest", None), - "readthedocs": ("https://docs.readthedocs.com/platform/stable/objects.inv", None), + "readthedocs": ("https://docs.readthedocs.com/platform/stable/", None), "scipy": ("https://docs.scipy.org/doc/scipy", None), "sklearn": ("https://scikit-learn.org/stable", None), "sphinx": ("https://www.sphinx-doc.org/en/master", None), diff --git a/pyproject.toml b/pyproject.toml index ec28e9d9..bd4e90af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,8 @@ doc = [ "memory_profiler", "nbconvert >= 7.16.4", "nbsphinx >= 0.7", - "numpydoc", + # Restriction due to https://github.com/pyxem/orix/issues/570 + "numpydoc != 1.9.0", "pydata-sphinx-theme", "scikit-image", "scikit-learn", From 556a8ad91b83b7defec3ffdb665e16e724c42969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Thu, 10 Jul 2025 10:12:38 +0200 Subject: [PATCH 3/9] Specify custom pytest markers in pyproject.toml, simplify naming (#570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- .github/workflows/build.yml | 2 +- orix/quaternion/symmetry.py | 14 ++++---- orix/tests/conftest.py | 66 +++++++++++++++++-------------------- pyproject.toml | 4 +++ 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5abda71b..03e93c1d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -110,7 +110,7 @@ jobs: - name: Run tests run: | - pytest --pyargs orix --runslow --reruns 2 -n 2 --cov=orix + pytest --pyargs orix --slow --reruns 2 -n 2 --cov=orix - name: Generate line coverage if: ${{ matrix.os == 'ubuntu-latest' }} diff --git a/orix/quaternion/symmetry.py b/orix/quaternion/symmetry.py index ec3061ef..417f04ad 100644 --- a/orix/quaternion/symmetry.py +++ b/orix/quaternion/symmetry.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,11 +10,12 @@ # # 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 +# 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 . +# along with orix. If not, see . +# from __future__ import annotations @@ -24,11 +26,11 @@ import numpy as np from orix.quaternion.rotation import Rotation -from orix.vector import Vector3d +from orix.vector.vector3d import Vector3d if TYPE_CHECKING: # pragma: no cover - from orix.quaternion import Orientation - from orix.vector import FundamentalSector + from orix.quaternion.orientation import Orientation + from orix.vector.fundamental_sector import FundamentalSector class Symmetry(Rotation): diff --git a/orix/tests/conftest.py b/orix/tests/conftest.py index 7684e6d6..defdf5a8 100644 --- a/orix/tests/conftest.py +++ b/orix/tests/conftest.py @@ -16,80 +16,74 @@ # You should have received a copy of the GNU General Public License # along with orix. If not, see . # - - -import os -from tempfile import TemporaryDirectory - from diffpy.structure import Atom, Lattice, Structure from h5py import File import matplotlib.pyplot as plt import numpy as np import pytest -from orix import constants -from orix.crystal_map import CrystalMap, PhaseList, create_coordinate_arrays -from orix.quaternion import Rotation +from orix.constants import installed +from orix.crystal_map.crystal_map import CrystalMap, create_coordinate_arrays +from orix.crystal_map.phase_list import PhaseList +from orix.quaternion.rotation import Rotation # --------------------------- pytest hooks --------------------------- # -def pytest_sessionstart(session): # pragma: no cover +def pytest_sessionstart(session): plt.rcParams["backend"] = "agg" # -------------------- Control of test selection --------------------- # skipif_numpy_quaternion_present = pytest.mark.skipif( - constants.installed["numpy-quaternion"], reason="numpy-quaternion installed" + installed["numpy-quaternion"], reason="numpy-quaternion installed" ) skipif_numpy_quaternion_missing = pytest.mark.skipif( - not constants.installed["numpy-quaternion"], reason="numpy-quaternion not installed" + not installed["numpy-quaternion"], reason="numpy-quaternion not installed" ) -# ---------------------------- IO fixtures --------------------------- # - -# ----------------------------- .ang file ---------------------------- # - def pytest_addoption(parser): parser.addoption( - "--runslow", action="store_true", default=False, help="run slow tests" + "--slow", action="store_true", default=False, help="Run slow tests" ) -def pytest_configure(config): - config.addinivalue_line("markers", "slow: mark test as slow to run") +MARKERS = ["slow"] -def pytest_collection_modifyitems(config, items): - if config.getoption("--runslow"): - # --runslow given in cli: do not skip slow tests - return - else: # pragma: no cover - skip_slow = pytest.mark.skip(reason="need --runslow option to run") - for item in items: - if "slow" in item.keywords: - item.add_marker(skip_slow) +def pytest_runtest_setup(item): + # Skip certain tests when flag is missing: + # https://docs.pytest.org/en/stable/reference/reference.html#pytest.hookspec.pytest_runtest_setup + for marker in MARKERS: + marker_str = f"--{marker}" + if marker in item.keywords and not item.config.getoption(marker_str): + pytest.skip(f"Needs {marker_str} flag to run") + + +# ---------------------------- IO fixtures --------------------------- # + +# ----------------------------- .ang file ---------------------------- # @pytest.fixture() -def temp_ang_file(): - with TemporaryDirectory() as tempdir: - f = open(os.path.join(tempdir, "temp_ang_file.ang"), mode="w+") +def temp_ang_file(tmpdir): + fname = tmpdir.join("temp_ang_file.ang") + with open(fname, mode="w+") as f: yield f @pytest.fixture(params=["h5"]) -def temp_file_path(request): +def temp_file_path(request, tmpdir): """Temporary file in a temporary directory for use when tests need to write, and sometimes read again, data to, and from, a file. """ ext = request.param - with TemporaryDirectory() as tmp: - file_path = os.path.join(tmp, "data_temp." + ext) - yield file_path + fname = tmpdir.join(f"data_temp.{ext}") + with open(fname, mode="w+") as f: + yield f ANGFILE_TSL_HEADER = r"""# TEM_PIXperUM 1.000000 @@ -392,7 +386,7 @@ def angfile_emsoft(tmpdir, request): # Variable map shape and step sizes CTF_OXFORD_HEADER = r"""Channel Text File Prj standard steel sample -Author +Author JobMode Grid XCells %i YCells %i @@ -754,7 +748,7 @@ def ctf_emsoft(tmpdir, request): AcqE1 0.0000 AcqE2 0.0000 AcqE3 0.0000 -Euler angles refer to Sample Coordinate system (CS0)! Mag 0.0000 Coverage 0 Device 0 KV 0.0000 TiltAngle 0.0000 TiltAxis 0 DetectorOrientationE1 0.0000 DetectorOrientationE2 0.0000 DetectorOrientationE3 0.0000 WorkingDistance 0.0000 InsertionDistance 0.0000 +Euler angles refer to Sample Coordinate system (CS0)! Mag 0.0000 Coverage 0 Device 0 KV 0.0000 TiltAngle 0.0000 TiltAxis 0 DetectorOrientationE1 0.0000 DetectorOrientationE2 0.0000 DetectorOrientationE3 0.0000 WorkingDistance 0.0000 InsertionDistance 0.0000 Phases 1 4.079;4.079;4.079 90.000;90.000;90.000 Gold 11 0 Created from mtex Phase X Y Bands Error Euler1 Euler2 Euler3 MAD BC BS""" diff --git a/pyproject.toml b/pyproject.toml index bd4e90af..4a85f9a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,12 +101,16 @@ addopts = [ "-ra", "--ignore=doc/_static/img/colormap_banners/create_colormap_banners.py", "--ignore=examples/*/*.py", + "--strict-markers", ] doctest_optionflags = "NORMALIZE_WHITESPACE" filterwarnings = [ "ignore:Deprecated call to `pkg_resources:DeprecationWarning", "ignore:pkg_resources is deprecated as an API:DeprecationWarning", ] +markers = [ + "slow: mark test as slow", +] [tool.isort] profile = "black" From c87ece2afc0246dead54a14c85f90ea7475c98bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Thu, 10 Jul 2025 11:26:40 +0200 Subject: [PATCH 4/9] Fix rst formatting typo (#570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- doc/conf.py | 1 + doc/dev/running_writing_tests.rst | 32 +++++++++++++++---------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 9d6aa2bd..9dafd02c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -83,6 +83,7 @@ "numpydoc": ("https://numpydoc.readthedocs.io/en/latest", None), "pooch": ("https://www.fatiando.org/pooch/latest", None), "pytest": ("https://docs.pytest.org/en/stable", None), + "pytest-xdist": ("https://pytest-xdist.readthedocs.io/en/stable/", None), "python": ("https://docs.python.org/3", None), "pyxem": ("https://pyxem.readthedocs.io/en/latest", None), "readthedocs": ("https://docs.readthedocs.com/platform/stable/", None), diff --git a/doc/dev/running_writing_tests.rst b/doc/dev/running_writing_tests.rst index ab205cf4..da832cb8 100644 --- a/doc/dev/running_writing_tests.rst +++ b/doc/dev/running_writing_tests.rst @@ -3,11 +3,12 @@ Run and write tests All functionality in orix is tested with :doc:`pytest `. The tests reside in a ``tests`` module. -Tests are short methods that call functions in ``orix`` and compare resulting output -values with known answers. +Tests are short methods that call functions in orix and compare resulting output values +with known answers. + Install necessary dependencies to run the tests:: - pip install --editable ".[tests]" + pip install -e ".[tests]" Some useful :doc:`fixtures ` are available in the ``conftest.py`` file. @@ -22,29 +23,28 @@ Some useful :doc:`fixtures ` are available in the To run the tests:: - pytest --cov --pyargs orix -n auto + pytest --cov --pyargs orix -n auto -The ``-n auto`` is an optional flag to enable parallelized testing. -The ``--cov`` flag makes :doc:`coverage.py ` print a nice report. -For an even nicer presentation, you can use ``coverage.py`` directly:: +The ``-n auto`` is an optional flag to enable parallelized testing with +:doc:`pytest-xdist `. +We aim to cover all lines when all :ref:`dependencies` are installed. +The ``--cov`` flag makes :doc:`coverage.py ` print a nice coverage +report. +For an even nicer presentation, you can use coverage.py directly:: - coverage html + coverage html Coverage can then be inspected in the browser by opening ``htmlcov/index.html``. -We strive for 100% test coverage of lines when all dependencies are installed. - -If you have a test that takes a long time to run, you can mark it to skip it from running by default - -.. code-block:: Python +If a test takes a long time to run, you can mark it to skip it from running by default:: @pytest.mark.slow def test_slow_function(): - pass + ... -Then you can run the tests with the ``--runslow`` option to skip slow tests:: +To run tests marked as slow, add the flag when running pytest:: - pytest --runslow + pytest --slow Docstring examples are tested with :doc:`pytest ` as well. :mod:`numpy` and :mod:`matplotlib.pyplot` should not be imported in examples as they are From ea29fb2b6df61c63f004c1557079b97a35d78f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Thu, 10 Jul 2025 11:27:25 +0200 Subject: [PATCH 5/9] Fix docstring of new expand asymmetric unit function (#570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/crystal_map/phase_list.py | 42 ++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/orix/crystal_map/phase_list.py b/orix/crystal_map/phase_list.py index ff99ee89..2cd2ecec 100644 --- a/orix/crystal_map/phase_list.py +++ b/orix/crystal_map/phase_list.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,11 +10,12 @@ # # 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 +# 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 . +# along with orix. If not, see . +# from __future__ import annotations @@ -21,7 +23,7 @@ import copy from itertools import islice from pathlib import Path -from typing import Generator +from typing import Generator, Self import warnings from diffpy.structure import Lattice, Structure @@ -37,7 +39,8 @@ _groups, get_point_group, ) -from orix.vector import Miller, Vector3d +from orix.vector.miller import Miller +from orix.vector.vector3d import Vector3d # All named Matplotlib colors (tableau and xkcd already lower case hex) ALL_COLORS = mcolors.TABLEAU_COLORS @@ -333,7 +336,7 @@ def __repr__(self) -> str: ) @classmethod - def from_cif(cls, filename: str | Path) -> Phase: + def from_cif(cls, filename: str | Path) -> Self: """Return a new phase from a CIF file using :mod:`diffpy.structure`'s CIF file parser. @@ -359,25 +362,33 @@ def from_cif(cls, filename: str | Path) -> Phase: warnings.warn(f"Could not read space group from CIF file {path!r}") return cls(name, space_group, structure=structure) - def deepcopy(self) -> Phase: + def deepcopy(self) -> Self: """Return a deep copy using :py:func:`~copy.deepcopy` function. """ return copy.deepcopy(self) - def expand_asymmetric_unit(self) -> Phase: - """Return new instance with all symmetrically equivalent atoms. + def expand_asymmetric_unit(self) -> Self: + """Return a new phase with all symmetrically equivalent atoms. + + Returns + ------- + expanded_phase + New phase with the a :attr:`structure` with the unit cell + filled with symmetrically equivalent atoms. Examples -------- + >>> from diffpy.structure import Atom, Lattice, Structure + >>> import orix.crystal_map as ocm >>> atoms = [Atom("Si", xyz=(0, 0, 1))] >>> lattice = Lattice(4.04, 4.04, 4.04, 90, 90, 90) >>> structure = Structure(atoms = atoms,lattice=lattice) - >>> phase = Phase(structure=structure, space_group=227) + >>> phase = ocm.Phase(structure=structure, space_group=227) >>> phase.structure [Si 0.000000 0.000000 1.000000 1.0000] - >>> expanded = phase.expand_asymmetric_unit() - >>> expanded.structure + >>> expanded_phase = phase.expand_asymmetric_unit() + >>> expanded_phase.structure [Si 0.000000 0.000000 0.000000 1.0000, Si 0.000000 0.500000 0.500000 1.0000, Si 0.500000 0.500000 0.000000 1.0000, @@ -411,9 +422,10 @@ def expand_asymmetric_unit(self) -> Phase: diffpy_structure.append(new_atom) # This handles conversion back to correct alignment - out = Phase(self) - out.structure = diffpy_structure - return out + expanded_phase = self.__class__(self) + expanded_phase.structure = diffpy_structure + + return expanded_phase class PhaseList: From 10db4669341ce5693a98feac225d9fb1dd674e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Thu, 10 Jul 2025 11:19:29 +0200 Subject: [PATCH 6/9] Fix docstring of new to/from SciPy rotation functions (#570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/quaternion/quaternion.py | 152 +++++++++++++++++----------------- orix/tests/conftest.py | 1 + 2 files changed, 79 insertions(+), 74 deletions(-) diff --git a/orix/quaternion/quaternion.py b/orix/quaternion/quaternion.py index 9e0b260d..21bf933a 100644 --- a/orix/quaternion/quaternion.py +++ b/orix/quaternion/quaternion.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,15 +10,16 @@ # # 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 +# 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 . +# along with orix. If not, see . +# from __future__ import annotations -from typing import Any, Optional, Tuple, Union +from typing import Any, Self import warnings import dask.array as da @@ -29,7 +31,9 @@ from orix._base import Object3d from orix.constants import installed from orix.quaternion import _conversions -from orix.vector import AxAngle, Homochoric, Miller, Rodrigues, Vector3d +from orix.vector.miller import Miller +from orix.vector.neo_euler import AxAngle, Homochoric, Rodrigues +from orix.vector.vector3d import Vector3d class Quaternion(Object3d): @@ -167,12 +171,12 @@ def angle(self) -> np.ndarray: return 2 * np.nan_to_num(np.arccos(np.abs(self.a))) @property - def antipodal(self) -> Quaternion: + def antipodal(self) -> Self: """Return the quaternion and its antipodal.""" return self.__class__(np.stack([self.data, -self.data])) @property - def conj(self) -> Quaternion: + def conj(self) -> Self: r"""Return the conjugate of the quaternion :math:`Q^{*} = a - bi - cj - dk`. """ @@ -190,12 +194,10 @@ def conj(self) -> Quaternion: # ------------------------ Dunder methods ------------------------ # - def __invert__(self) -> Quaternion: + def __invert__(self) -> Self: return self.__class__(self.conj.data / (self.norm**2)[..., np.newaxis]) - def __mul__( - self, other: Union[Quaternion, Vector3d] - ) -> Union[Quaternion, Vector3d]: + def __mul__(self, other: Quaternion | Vector3d) -> Self | Vector3d: if isinstance(other, Quaternion): if installed["numpy-quaternion"]: import quaternion @@ -228,10 +230,10 @@ def __mul__( return other.__class__(v) return NotImplemented - def __neg__(self) -> Quaternion: + def __neg__(self) -> Self: return self.__class__(-self.data) - def __eq__(self, other: Union[Any, Quaternion]) -> bool: + def __eq__(self, other: Any | Quaternion) -> bool: """Check if quaternions have equal shapes and components.""" if ( isinstance(other, Quaternion) @@ -247,10 +249,10 @@ def __eq__(self, other: Union[Any, Quaternion]) -> bool: @classmethod def from_axes_angles( cls, - axes: Union[np.ndarray, Vector3d, tuple, list], - angles: Union[np.ndarray, tuple, list, float], + axes: np.ndarray | Vector3d | tuple | list, + angles: np.ndarray | tuple | list | float, degrees: bool = False, - ) -> Quaternion: + ) -> Self: r"""Create unit quaternions from axis-angle pairs :math:`(\hat{\mathbf{n}}, \omega)` :cite:`rowenhorst2015consistent`. @@ -299,9 +301,8 @@ def from_axes_angles( @classmethod def from_homochoric( - cls, - ho: Union[Vector3d, Homochoric, np.ndarray, tuple, list], - ) -> Quaternion: + cls, ho: Vector3d | Homochoric | np.ndarray | tuple | list + ) -> Self: r"""Create unit quaternions from homochoric vectors :math:`\mathbf{h}` :cite:`rowenhorst2015consistent`. @@ -346,9 +347,9 @@ def from_homochoric( @classmethod def from_rodrigues( cls, - ro: Union[np.ndarray, Vector3d, tuple, list], - angles: Union[np.ndarray, tuple, list, float, None] = None, - ) -> Quaternion: + ro: np.ndarray | Vector3d | tuple | list, + angles: np.ndarray | tuple | list | float | None = None, + ) -> Self: r"""Create unit quaternions from three-component Rodrigues vectors :math:`\hat{\mathbf{n}}` or four-component Rodrigues-Frank vectors :math:`\mathbf{\rho}` @@ -448,10 +449,10 @@ def from_rodrigues( @classmethod def from_euler( cls, - euler: Union[np.ndarray, tuple, list], + euler: np.ndarray | tuple | list, direction: str = "lab2crystal", degrees: bool = False, - ) -> Quaternion: + ) -> Self: """Create unit quaternions from Euler angle sets :cite:`rowenhorst2015consistent`. @@ -505,7 +506,7 @@ def from_euler( return Q @classmethod - def from_matrix(cls, matrix: Union[np.ndarray, tuple, list]) -> Quaternion: + def from_matrix(cls, matrix: np.ndarray | tuple | list) -> Self: """Create unit quaternions from orientation matrices :cite:`rowenhorst2015consistent`. @@ -541,7 +542,7 @@ def from_matrix(cls, matrix: Union[np.ndarray, tuple, list]) -> Quaternion: return Q @classmethod - def from_scipy_rotation(cls, rotation: SciPyRotation) -> Quaternion: + def from_scipy_rotation(cls, rotation: SciPyRotation) -> Self: """Create unit quaternions from :class:`scipy.spatial.transform.Rotation`. @@ -552,9 +553,13 @@ def from_scipy_rotation(cls, rotation: SciPyRotation) -> Quaternion: Returns ------- - quaternion + Q Quaternions. + See Also + -------- + to_scipy_rotation + Notes ----- The SciPy rotation is inverted to be consistent with the orix @@ -600,17 +605,17 @@ def from_scipy_rotation(cls, rotation: SciPyRotation) -> Quaternion: @classmethod def from_align_vectors( cls, - other: Union[Vector3d, tuple, list], - initial: Union[Vector3d, tuple, list], - weights: Optional[np.ndarray] = None, + other: Vector3d | tuple | list, + initial: Vector3d | tuple | list, + weights: np.ndarray | None = None, return_rmsd: bool = False, return_sensitivity: bool = False, - ) -> Union[ - Quaternion, - Tuple[Quaternion, float], - Tuple[Quaternion, np.ndarray], - Tuple[Quaternion, float, np.ndarray], - ]: + ) -> ( + Self + | tuple[Self, float] + | tuple[Self, np.ndarray] + | tuple[Self, float, np.ndarray] + ): """Estimate a quaternion to optimally align two sets of vectors. This method wraps @@ -681,7 +686,7 @@ def from_align_vectors( return out[0] if len(out) == 1 else tuple(out) @classmethod - def triple_cross(cls, q1: Quaternion, q2: Quaternion, q3: Quaternion) -> Quaternion: + def triple_cross(cls, q1: Quaternion, q2: Quaternion, q3: Quaternion) -> Self: """Pointwise cross product of three quaternions. Parameters @@ -739,7 +744,7 @@ def triple_cross(cls, q1: Quaternion, q2: Quaternion, q3: Quaternion) -> Quatern return Q @classmethod - def identity(cls, shape: Union[int, tuple] = (1,)) -> Quaternion: + def identity(cls, shape: int | tuple = (1,)) -> Self: """Create identity quaternions. Parameters @@ -836,7 +841,7 @@ def to_axes_angles(self) -> AxAngle: ax = AxAngle(axes * angles) return ax - def to_rodrigues(self, frank: bool = False) -> Union[Rodrigues, np.ndarray]: + def to_rodrigues(self, frank: bool = False) -> Rodrigues | np.ndarray: r"""Return the unit quaternions as Rodrigues or Rodrigues-Frank vectors :cite:`rowenhorst2015consistent`. @@ -946,45 +951,44 @@ def to_homochoric(self) -> Homochoric: return ho def to_scipy_rotation(self) -> SciPyRotation: - r"""Return the unit quaternions as - :class:`scipy.spatial.transform.Rotation` objects used in scipy's - spatial module. + r"""Return unit quaternions as a SciPy rotation. Returns ------- - SciPy_Rotation - a Rotation object generated from the unit quaternion data - (i.e, unaffected by symmetry, phase, or length). + scipy_rotation + A SciPy rotation (flattened) given by the unit quaternions + without considering any symmetry. + + See Also + -------- + from_scipy_rotation Notes ----- - SciPy by default uses the Active rotation convention along with the - vector-scalar quaternion definition, as opposed to ORIX's passive, - scalar-vector convention. Thus, the following quaternion in orix: - :math: `q_{orix} = [q_0, q_1, q_2, q_3]` - represents the same operations as the following quaternion in scipy: - :math: `q_{SciPy} = [-q_1, -q_2, -q_3, q_0]` - - See the function description for Quaternion.from_scipy_rotation - for an example of how these differing parameterizations still produce - identical rotation operations. - - Additionally, note that Orix enforces :math: `q_0 >= 0` whereas - SciPy does not. Thus, the operation - - >>> Quaternion.from_scipy_rotation(r).to_scipy_rotation.as_quat() - - will produce an identical rotation operation, but not - necessarily an idential quaternion. Look up "quaternion double cover" - for more information on why this occurs. - - Finally, ORIX supports N-dimensional arrays, whereas SciPy - currently supports only 1-dimensional vectors. Thus, this function - will also flatten arrays when converting to SciPy Rotations. + SciPy by default uses the active rotation interpretation along + with the vector-scalar quaternion definition, as opposed to + orix's passive one, scalar-vector interpretation. Thus, the + following quaternion in orix, + :math:`Q_{orix} = [q_0, q_1, q_2, q_3]` represents the same + transformation as the following quaternion in SciPy: + :math:`Q_{SciPy} = [-q_1, -q_2, -q_3, q_0]` + + See the function description for :meth:`from_scipy_rotation` for + an example of how these differing parameterizations still + produce identical transformations. + + Additionally, note that orix enforces :math:`Q_0 \geq 0` whereas + SciPy does not. Thus, the operation:: + + Quaternion.from_scipy_rotation(r).to_scipy_rotation.as_quat() + + will produce an identical transformation, but not necessarily an + idential quaternion. Look up "quaternion double cover" for more + information on why this occurs. """ if self.ndim > 1: warnings.warn( - "\n {} dimension greater than 1. ".format(self.__class__.__name__) + f"\n {self.__class__.__name__} dimension greater than 1. " + "Flattening into a 1-dimensional vector" ) self = self.flatten() @@ -1054,7 +1058,7 @@ def dot_outer(self, other: Quaternion) -> np.ndarray: dots = np.tensordot(self.data, other.data, axes=(-1, -1)) return dots - def mean(self) -> Quaternion: + def mean(self) -> Self: """Return the mean quaternion with unitary weights. Returns @@ -1075,11 +1079,11 @@ def mean(self) -> Quaternion: def outer( self, - other: Union[Quaternion, Vector3d], + other: Quaternion | Vector3d, lazy: bool = False, chunk_size: int = 20, progressbar: bool = True, - ) -> Union[Quaternion, Vector3d]: + ) -> Self | Vector3d: """Return the outer products of the quaternions and the other quaternions or vectors. @@ -1162,7 +1166,7 @@ def outer( "with `other` of type `Quaternion` or `Vector3d`" ) - def inv(self) -> Quaternion: + def inv(self) -> Self: r"""Return the inverse quaternions :math:`Q^{-1} = a - bi - cj - dk`. """ @@ -1171,7 +1175,7 @@ def inv(self) -> Quaternion: # -------------------- Other private methods --------------------- # def _outer_dask( - self, other: Union[Quaternion, Vector3d], chunk_size: int = 20 + self, other: Quaternion | Vector3d, chunk_size: int = 20 ) -> da.Array: """Compute the product of every quaternion in this instance to every quaternion or vector in another instance, returned as a diff --git a/orix/tests/conftest.py b/orix/tests/conftest.py index defdf5a8..fd028c28 100644 --- a/orix/tests/conftest.py +++ b/orix/tests/conftest.py @@ -51,6 +51,7 @@ def pytest_addoption(parser): ) +# Markers are defined in package configuration MARKERS = ["slow"] From 7632cefc3c55291d17376d3470ab4b3f506aa220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Thu, 10 Jul 2025 11:19:48 +0200 Subject: [PATCH 7/9] Update type hints in rotation class (#570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/quaternion/rotation.py | 48 ++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/orix/quaternion/rotation.py b/orix/quaternion/rotation.py index 64ce43e8..95dfe662 100644 --- a/orix/quaternion/rotation.py +++ b/orix/quaternion/rotation.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,23 +10,24 @@ # # 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 +# 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 . +# along with orix. If not, see . +# from __future__ import annotations -from typing import Any, Tuple, Union +from typing import Any, Self import dask.array as da from dask.diagnostics import ProgressBar import numpy as np from scipy.special import hyp0f1 -from orix.quaternion import Quaternion -from orix.vector import Vector3d +from orix.quaternion.quaternion import Quaternion +from orix.vector.vector3d import Vector3d class Rotation(Quaternion): @@ -73,7 +75,7 @@ class Rotation(Quaternion): True """ - def __init__(self, data: Union[np.ndarray, Rotation, list, tuple]): + def __init__(self, data: np.ndarray | Rotation | list | tuple) -> None: super().__init__(data) self._data = np.concatenate((self.data, np.zeros(self.shape + (1,))), axis=-1) if isinstance(data, Rotation): @@ -95,7 +97,7 @@ def improper(self, value: np.ndarray): self._data[..., -1] = value @property - def antipodal(self) -> Rotation: + def antipodal(self) -> Self: """Return the rotation and its antipodal.""" R = self.__class__(np.stack([self.data, -self.data])) R.improper = self.improper @@ -104,8 +106,8 @@ def antipodal(self) -> Rotation: # ------------------------ Dunder methods ------------------------ # def __mul__( - self, other: Union[Rotation, Quaternion, Vector3d, np.ndarray, int, list] - ): + self, other: Rotation | Quaternion | Vector3d | np.ndarray | int | list + ) -> Self | Quaternion | Vector3d: # Combine rotations self * other as first other, then self if isinstance(other, Rotation): Q = Quaternion(self) * Quaternion(other) @@ -131,22 +133,22 @@ def __mul__( return R return NotImplemented - def __neg__(self) -> Rotation: + def __neg__(self) -> Self: R = self.__class__(self.data) R.improper = np.logical_not(self.improper) return R - def __getitem__(self, key) -> Rotation: + def __getitem__(self, key) -> Self: R = super().__getitem__(key) R.improper = self.improper[key] return R - def __invert__(self) -> Rotation: + def __invert__(self) -> Self: R = super().__invert__() R.improper = self.improper return R - def __eq__(self, other: Union[Any, Rotation]) -> bool: + def __eq__(self, other: Any | Rotation) -> bool: """Check if the rotations have equal shapes and values.""" if ( isinstance(other, Rotation) @@ -163,10 +165,10 @@ def __eq__(self, other: Union[Any, Rotation]) -> bool: @classmethod def random_vonmises( cls, - shape: Union[int, tuple] = (1,), + shape: int | tuple = (1,), alpha: float = 1.0, - reference: Union[list, tuple, Rotation] = (1, 0, 0, 0), - ) -> Rotation: + reference: Rotation | list | tuple = (1, 0, 0, 0), + ) -> Self: """Return random rotations with a simplified Von Mises-Fisher distribution. @@ -206,11 +208,7 @@ def unique( return_index: bool = False, return_inverse: bool = False, antipodal: bool = True, - ) -> Union[ - Rotation, - Tuple[Rotation, np.ndarray], - Tuple[Rotation, np.ndarray, np.ndarray], - ]: + ) -> Self | tuple[Self, np.ndarray] | tuple[Self, np.ndarray, np.ndarray]: """Return the unique rotations from these rotations. Two rotations are not unique if they have the same propriety @@ -343,11 +341,11 @@ def angle_with_outer(self, other: Rotation, degrees: bool = False) -> np.ndarray def outer( self, - other: Union[Rotation, Vector3d], + other: Rotation | Vector3d, lazy: bool = False, chunk_size: int = 20, progressbar: bool = True, - ) -> Union[Rotation, Vector3d]: + ) -> Self | Vector3d: """Return the outer rotation products of the rotations and the other rotations or vectors. @@ -392,7 +390,7 @@ def outer( return R - def flatten(self) -> Rotation: + def flatten(self) -> Self: """Return a new rotation instance collapsed into one dimension. Returns From 2fe05da95bc773b295a0acf234521d31f3753b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Fri, 11 Jul 2025 09:00:14 +0200 Subject: [PATCH 8/9] Remove test fixture that unnecessarily complicated use of files (#570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/tests/conftest.py | 11 ------ orix/tests/io/test_ang.py | 49 ++++++++++++----------- orix/tests/io/test_h5ebsd.py | 15 ++++--- orix/tests/io/test_io.py | 69 +++++++++++++++++---------------- orix/tests/io/test_orix_hdf5.py | 52 +++++++++++++++---------- 5 files changed, 103 insertions(+), 93 deletions(-) diff --git a/orix/tests/conftest.py b/orix/tests/conftest.py index fd028c28..6852fd3d 100644 --- a/orix/tests/conftest.py +++ b/orix/tests/conftest.py @@ -76,17 +76,6 @@ def temp_ang_file(tmpdir): yield f -@pytest.fixture(params=["h5"]) -def temp_file_path(request, tmpdir): - """Temporary file in a temporary directory for use when tests need - to write, and sometimes read again, data to, and from, a file. - """ - ext = request.param - fname = tmpdir.join(f"data_temp.{ext}") - with open(fname, mode="w+") as f: - yield f - - ANGFILE_TSL_HEADER = r"""# TEM_PIXperUM 1.000000 # x-star 0.413900 # y-star 0.729100 diff --git a/orix/tests/io/test_ang.py b/orix/tests/io/test_ang.py index b224bfda..f1f57e78 100644 --- a/orix/tests/io/test_ang.py +++ b/orix/tests/io/test_ang.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,11 +10,12 @@ # # 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 +# 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 . +# along with orix. If not, see . +# import numpy as np import pytest @@ -371,23 +373,24 @@ def test_load_ang_emsoft( def test_get_header(self, temp_ang_file): temp_ang_file.write(ANGFILE_ASTAR_HEADER) temp_ang_file.close() - assert _get_header(open(temp_ang_file.name)) == [ - "# File created from ACOM RES results", - "# ni-dislocations.res", - "# ".rstrip(), - "# ".rstrip(), - "# MaterialName Nickel", - "# Formula", - "# Symmetry 43", - "# LatticeConstants 3.520 3.520 3.520 90.000 90.000 90.000", - "# NumberFamilies 4", - "# hklFamilies 1 1 1 1 0.000000", - "# hklFamilies 2 0 0 1 0.000000", - "# hklFamilies 2 2 0 1 0.000000", - "# hklFamilies 3 1 1 1 0.000000", - "#", - "# GRID: SqrGrid#", - ] + with open(temp_ang_file.name) as f: + assert _get_header(f) == [ + "# File created from ACOM RES results", + "# ni-dislocations.res", + "# ".rstrip(), + "# ".rstrip(), + "# MaterialName Nickel", + "# Formula", + "# Symmetry 43", + "# LatticeConstants 3.520 3.520 3.520 90.000 90.000 90.000", + "# NumberFamilies 4", + "# hklFamilies 1 1 1 1 0.000000", + "# hklFamilies 2 0 0 1 0.000000", + "# hklFamilies 2 2 0 1 0.000000", + "# hklFamilies 3 1 1 1 0.000000", + "#", + "# GRID: SqrGrid#", + ] @pytest.mark.parametrize( "expected_vendor, expected_columns, vendor_header", @@ -419,7 +422,8 @@ def test_get_vendor_columns( temp_ang_file.write(vendor_header) temp_ang_file.close() - header = _get_header(open(temp_ang_file.name)) + with open(temp_ang_file.name) as f: + header = _get_header(f) vendor, column_names = _get_vendor_columns(header, n_cols_file) assert vendor == expected_vendor @@ -429,7 +433,8 @@ def test_get_vendor_columns( def test_get_vendor_columns_unknown(self, temp_ang_file, n_cols_file): temp_ang_file.write("Look at me!\nI'm Mr. .ang file!\n") temp_ang_file.close() - header = _get_header(open(temp_ang_file.name)) + with open(temp_ang_file.name) as f: + header = _get_header(f) with pytest.warns(UserWarning, match=f"Number of columns, {n_cols_file}, "): vendor, column_names = _get_vendor_columns(header, n_cols_file) assert vendor == "unknown" diff --git a/orix/tests/io/test_h5ebsd.py b/orix/tests/io/test_h5ebsd.py index c364e71a..b309bbec 100644 --- a/orix/tests/io/test_h5ebsd.py +++ b/orix/tests/io/test_h5ebsd.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,11 +10,12 @@ # # 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 +# 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 . +# along with orix. If not, see . +# from h5py import File @@ -22,12 +24,13 @@ class TestH5ebsd: - def test_hdf5group2dict_update_dict(self, temp_file_path, crystal_map): + def test_hdf5group2dict_update_dict(self, tmp_path, crystal_map): """Can read datasets from an HDF5 file into an existing dictionary. """ - save(temp_file_path, crystal_map) - with File(temp_file_path, mode="r") as f: + fname = tmp_path / "test.h5" + save(fname, crystal_map) + with File(fname, mode="r") as f: this_dict = {"hello": "there"} this_dict = hdf5group2dict(f["crystal_map"], dictionary=this_dict) assert this_dict["hello"] == "there" diff --git a/orix/tests/io/test_io.py b/orix/tests/io/test_io.py index fe276927..46258910 100644 --- a/orix/tests/io/test_io.py +++ b/orix/tests/io/test_io.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,11 +10,12 @@ # # 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 +# 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 . +# along with orix. If not, see . +# from collections import OrderedDict from contextlib import contextmanager @@ -73,11 +75,11 @@ def test_load_no_filename_match(self): with pytest.raises(IOError, match=f"No filename matches '{fname}'."): _ = load(fname) - @pytest.mark.parametrize("temp_file_path", ["ktf"], indirect=["temp_file_path"]) - def test_load_unsupported_format(self, temp_file_path): - np.savetxt(temp_file_path, X=np.random.rand(100, 8)) - with pytest.raises(IOError, match=f"Could not read "): - _ = load(temp_file_path) + def test_load_unsupported_format(self, tmp_path): + fname = tmp_path / "unsupported_file.ktf" + np.savetxt(fname, X=np.random.rand(100, 8)) + with pytest.raises(IOError, match="Could not read "): + _ = load(fname) @pytest.mark.parametrize( "manufacturer, expected_plugin", @@ -88,61 +90,62 @@ def test_load_unsupported_format(self, temp_file_path): ("Oxford", None), ], ) - def test_plugin_from_manufacturer( - self, temp_file_path, manufacturer, expected_plugin - ): + def test_plugin_from_manufacturer(self, manufacturer, expected_plugin, tmp_path): h5ebsd_plugin_list = [bruker_h5ebsd, emsoft_h5ebsd, orix_hdf5] - with File(temp_file_path, mode="w") as f: + fname = tmp_path / "test.h5" + with File(fname, mode="w") as f: f.create_dataset(name="Manufacturer", data=manufacturer) assert ( - _plugin_from_manufacturer(temp_file_path, plugins=h5ebsd_plugin_list) + _plugin_from_manufacturer(fname, plugins=h5ebsd_plugin_list) is expected_plugin ) - def test_overwrite_or_not(self, crystal_map, temp_file_path): - save(temp_file_path, crystal_map) + def test_overwrite_or_not(self, crystal_map, tmp_path): + fname = tmp_path / "test.h5" + save(fname, crystal_map) with pytest.warns(UserWarning, match="Not overwriting, since your terminal "): - _overwrite_or_not(temp_file_path) + _overwrite_or_not(fname) @pytest.mark.parametrize( "answer, expected", [("y", True), ("n", False), ("m", None)] ) - def test_overwrite_or_not_input( - self, crystal_map, temp_file_path, answer, expected - ): - save(temp_file_path, crystal_map) + def test_overwrite_or_not_input(self, crystal_map, answer, expected, tmp_path): + fname = tmp_path / "test.h5" + save(fname, crystal_map) if answer == "m": with replace_stdin(StringIO(answer)): with pytest.raises(EOFError): - _overwrite_or_not(temp_file_path) + _overwrite_or_not(fname) else: with replace_stdin(StringIO(answer)): - assert _overwrite_or_not(temp_file_path) is expected + assert _overwrite_or_not(fname) is expected - @pytest.mark.parametrize("temp_file_path", ["angs", "hdf4", "h6"]) - def test_save_unsupported_raises(self, temp_file_path, crystal_map): - _, ext = os.path.splitext(temp_file_path) + @pytest.mark.parametrize("ext", ["angs", "hdf4", "h6"]) + def test_save_unsupported_raises(self, ext, crystal_map, tmp_path): + fname = tmp_path / f"test.{ext}" with pytest.raises(IOError, match=f"'{ext}' does not correspond to any "): - save(temp_file_path, crystal_map) + save(fname, crystal_map) - def test_save_overwrite_raises(self, temp_file_path, crystal_map): + def test_save_overwrite_raises(self, crystal_map, tmp_path): with pytest.raises(ValueError, match="`overwrite` parameter can only be "): - save(temp_file_path, crystal_map, overwrite=1) + save(tmp_path / "test.h5", crystal_map, overwrite=1) @pytest.mark.parametrize( "overwrite, expected_phase_name", [(True, "hepp"), (False, "")] ) def test_save_overwrite( - self, temp_file_path, crystal_map, overwrite, expected_phase_name + self, crystal_map, overwrite, expected_phase_name, tmp_path ): + fname = tmp_path / "test.h5" + assert crystal_map.phases[0].name == "" - save(temp_file_path, crystal_map) - assert os.path.isfile(temp_file_path) is True + save(fname, crystal_map) + assert os.path.isfile(fname) is True crystal_map.phases[0].name = "hepp" - save(temp_file_path, crystal_map, overwrite=overwrite) + save(fname, crystal_map, overwrite=overwrite) - crystal_map2 = load(temp_file_path) + crystal_map2 = load(fname) assert crystal_map2.phases[0].name == expected_phase_name diff --git a/orix/tests/io/test_orix_hdf5.py b/orix/tests/io/test_orix_hdf5.py index 11796600..8640788c 100644 --- a/orix/tests/io/test_orix_hdf5.py +++ b/orix/tests/io/test_orix_hdf5.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,11 +10,12 @@ # # 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 +# 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 . +# along with orix. If not, see . +# from diffpy.structure.spacegroups import GetSpaceGroup from h5py import File @@ -22,7 +24,7 @@ from orix import __version__ as orix_version from orix.crystal_map import CrystalMap, Phase -from orix.io import load, save +import orix.io as oio from orix.io.plugins.orix_hdf5 import ( atom2dict, crystalmap2dict, @@ -42,10 +44,12 @@ class TestOrixHDF5Plugin: - def test_file_writer(self, crystal_map, temp_file_path): - save(filename=temp_file_path, object2write=crystal_map) + def test_file_writer(self, crystal_map, tmp_path): + fname = tmp_path / "test.h5" + + oio.save(filename=fname, object2write=crystal_map) - with File(temp_file_path) as f: + with File(fname) as f: assert f["manufacturer"][()][0].decode() == "orix" assert f["version"][()][0].decode() == orix_version @@ -57,10 +61,12 @@ def test_file_writer(self, crystal_map, temp_file_path): ], indirect=["crystal_map_input"], ) - def test_write_read_masked(self, crystal_map_input, temp_file_path): + def test_write_read_masked(self, crystal_map_input, tmp_path): + fname = tmp_path / "test.h5" + xmap = CrystalMap(**crystal_map_input) - save(filename=temp_file_path, object2write=xmap[xmap.x > 2]) - xmap2 = load(temp_file_path) + oio.save(filename=fname, object2write=xmap[xmap.x > 2]) + xmap2 = oio.load(fname) assert xmap2.size != xmap.size with pytest.raises(ValueError, match="operands could not be broadcast"): @@ -70,13 +76,14 @@ def test_write_read_masked(self, crystal_map_input, temp_file_path): assert xmap2.size == xmap.size assert np.allclose(xmap2.x, xmap.x) - def test_file_writer_raises(self, temp_file_path, crystal_map): + def test_file_writer_raises(self, crystal_map, tmp_path): + fname = tmp_path / "test.h5" with pytest.raises(OSError, match="Cannot write to the already open file "): - with File(temp_file_path, mode="w") as _: - save(temp_file_path, crystal_map, overwrite=True) + with File(fname, mode="w") as _: + oio.save(fname, crystal_map, overwrite=True) - def test_dict2hdf5group(self, temp_file_path): - with File(temp_file_path, mode="w") as f: + def test_dict2hdf5group(self, tmp_path): + with File(tmp_path / "test.h5", mode="w") as f: group = f.create_group(name="a_group") with pytest.warns(UserWarning, match="The orix HDF5 writer could not"): dict2hdf5group( @@ -141,9 +148,11 @@ def test_structure2dict(self, phase_list): assert np.allclose(lattice1["baserot"], lattice2["baserot"]) assert_dictionaries_are_equal(structure_dict["atoms"], this_dict["atoms"]) - def test_file_reader(self, crystal_map, temp_file_path): - save(filename=temp_file_path, object2write=crystal_map) - xmap2 = load(filename=temp_file_path) + def test_file_reader(self, crystal_map, tmp_path): + fname = tmp_path / "test.h5" + + oio.save(filename=fname, object2write=crystal_map) + xmap2 = oio.load(filename=fname) assert_dictionaries_are_equal(crystal_map.__dict__, xmap2.__dict__) def test_dict2crystalmap(self, crystal_map): @@ -207,7 +216,7 @@ def test_dict2atom(self, phase_list): assert str(atom.element) == str(atom2.element) assert np.allclose(atom.xyz, atom2.xyz) - def test_write_read_nd_crystalmap_properties(self, temp_file_path, crystal_map): + def test_write_read_nd_crystalmap_properties(self, crystal_map, tmp_path): """Crystal map properties with more than one value in each point (e.g. top matching scores from dictionary indexing) can be written and read from file correctly. @@ -225,8 +234,9 @@ def test_write_read_nd_crystalmap_properties(self, temp_file_path, crystal_map): prop3d = np.arange(map_size * 4).reshape(prop3d_shape) xmap.prop[prop3d_name] = prop3d - save(filename=temp_file_path, object2write=xmap) - xmap2 = load(temp_file_path) + fname = tmp_path / "test.h5" + oio.save(filename=fname, object2write=xmap) + xmap2 = oio.load(fname) assert np.allclose(xmap2.prop[prop2d_name], xmap.prop[prop2d_name]) assert np.allclose(xmap2.prop[prop3d_name], xmap.prop[prop3d_name]) From d9074cbce561e163b79054fc78610f54dc20403e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Fri, 11 Jul 2025 09:18:15 +0200 Subject: [PATCH 9/9] Remove use of typing.Self, which is not available until Python 3.11 (#570) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/crystal_map/phase_list.py | 8 ++--- orix/quaternion/quaternion.py | 42 +++++++++++++------------- orix/quaternion/rotation.py | 22 +++++++------- orix/vector/miller.py | 54 ++++++++++++++++------------------ pyproject.toml | 2 -- 5 files changed, 63 insertions(+), 65 deletions(-) diff --git a/orix/crystal_map/phase_list.py b/orix/crystal_map/phase_list.py index 2cd2ecec..ce7e3b91 100644 --- a/orix/crystal_map/phase_list.py +++ b/orix/crystal_map/phase_list.py @@ -23,7 +23,7 @@ import copy from itertools import islice from pathlib import Path -from typing import Generator, Self +from typing import Generator import warnings from diffpy.structure import Lattice, Structure @@ -336,7 +336,7 @@ def __repr__(self) -> str: ) @classmethod - def from_cif(cls, filename: str | Path) -> Self: + def from_cif(cls, filename: str | Path) -> Phase: """Return a new phase from a CIF file using :mod:`diffpy.structure`'s CIF file parser. @@ -362,13 +362,13 @@ def from_cif(cls, filename: str | Path) -> Self: warnings.warn(f"Could not read space group from CIF file {path!r}") return cls(name, space_group, structure=structure) - def deepcopy(self) -> Self: + def deepcopy(self) -> Phase: """Return a deep copy using :py:func:`~copy.deepcopy` function. """ return copy.deepcopy(self) - def expand_asymmetric_unit(self) -> Self: + def expand_asymmetric_unit(self) -> Phase: """Return a new phase with all symmetrically equivalent atoms. Returns diff --git a/orix/quaternion/quaternion.py b/orix/quaternion/quaternion.py index 21bf933a..b0b67730 100644 --- a/orix/quaternion/quaternion.py +++ b/orix/quaternion/quaternion.py @@ -19,7 +19,7 @@ from __future__ import annotations -from typing import Any, Self +from typing import Any import warnings import dask.array as da @@ -171,12 +171,12 @@ def angle(self) -> np.ndarray: return 2 * np.nan_to_num(np.arccos(np.abs(self.a))) @property - def antipodal(self) -> Self: + def antipodal(self) -> Quaternion: """Return the quaternion and its antipodal.""" return self.__class__(np.stack([self.data, -self.data])) @property - def conj(self) -> Self: + def conj(self) -> Quaternion: r"""Return the conjugate of the quaternion :math:`Q^{*} = a - bi - cj - dk`. """ @@ -194,10 +194,10 @@ def conj(self) -> Self: # ------------------------ Dunder methods ------------------------ # - def __invert__(self) -> Self: + def __invert__(self) -> Quaternion: return self.__class__(self.conj.data / (self.norm**2)[..., np.newaxis]) - def __mul__(self, other: Quaternion | Vector3d) -> Self | Vector3d: + def __mul__(self, other: Quaternion | Vector3d) -> Quaternion | Vector3d: if isinstance(other, Quaternion): if installed["numpy-quaternion"]: import quaternion @@ -230,7 +230,7 @@ def __mul__(self, other: Quaternion | Vector3d) -> Self | Vector3d: return other.__class__(v) return NotImplemented - def __neg__(self) -> Self: + def __neg__(self) -> Quaternion: return self.__class__(-self.data) def __eq__(self, other: Any | Quaternion) -> bool: @@ -252,7 +252,7 @@ def from_axes_angles( axes: np.ndarray | Vector3d | tuple | list, angles: np.ndarray | tuple | list | float, degrees: bool = False, - ) -> Self: + ) -> Quaternion: r"""Create unit quaternions from axis-angle pairs :math:`(\hat{\mathbf{n}}, \omega)` :cite:`rowenhorst2015consistent`. @@ -302,7 +302,7 @@ def from_axes_angles( @classmethod def from_homochoric( cls, ho: Vector3d | Homochoric | np.ndarray | tuple | list - ) -> Self: + ) -> Quaternion: r"""Create unit quaternions from homochoric vectors :math:`\mathbf{h}` :cite:`rowenhorst2015consistent`. @@ -349,7 +349,7 @@ def from_rodrigues( cls, ro: np.ndarray | Vector3d | tuple | list, angles: np.ndarray | tuple | list | float | None = None, - ) -> Self: + ) -> Quaternion: r"""Create unit quaternions from three-component Rodrigues vectors :math:`\hat{\mathbf{n}}` or four-component Rodrigues-Frank vectors :math:`\mathbf{\rho}` @@ -452,7 +452,7 @@ def from_euler( euler: np.ndarray | tuple | list, direction: str = "lab2crystal", degrees: bool = False, - ) -> Self: + ) -> Quaternion: """Create unit quaternions from Euler angle sets :cite:`rowenhorst2015consistent`. @@ -506,7 +506,7 @@ def from_euler( return Q @classmethod - def from_matrix(cls, matrix: np.ndarray | tuple | list) -> Self: + def from_matrix(cls, matrix: np.ndarray | tuple | list) -> Quaternion: """Create unit quaternions from orientation matrices :cite:`rowenhorst2015consistent`. @@ -542,7 +542,7 @@ def from_matrix(cls, matrix: np.ndarray | tuple | list) -> Self: return Q @classmethod - def from_scipy_rotation(cls, rotation: SciPyRotation) -> Self: + def from_scipy_rotation(cls, rotation: SciPyRotation) -> Quaternion: """Create unit quaternions from :class:`scipy.spatial.transform.Rotation`. @@ -611,10 +611,10 @@ def from_align_vectors( return_rmsd: bool = False, return_sensitivity: bool = False, ) -> ( - Self - | tuple[Self, float] - | tuple[Self, np.ndarray] - | tuple[Self, float, np.ndarray] + Quaternion + | tuple[Quaternion, float] + | tuple[Quaternion, np.ndarray] + | tuple[Quaternion, float, np.ndarray] ): """Estimate a quaternion to optimally align two sets of vectors. @@ -686,7 +686,7 @@ def from_align_vectors( return out[0] if len(out) == 1 else tuple(out) @classmethod - def triple_cross(cls, q1: Quaternion, q2: Quaternion, q3: Quaternion) -> Self: + def triple_cross(cls, q1: Quaternion, q2: Quaternion, q3: Quaternion) -> Quaternion: """Pointwise cross product of three quaternions. Parameters @@ -744,7 +744,7 @@ def triple_cross(cls, q1: Quaternion, q2: Quaternion, q3: Quaternion) -> Self: return Q @classmethod - def identity(cls, shape: int | tuple = (1,)) -> Self: + def identity(cls, shape: int | tuple = (1,)) -> Quaternion: """Create identity quaternions. Parameters @@ -1058,7 +1058,7 @@ def dot_outer(self, other: Quaternion) -> np.ndarray: dots = np.tensordot(self.data, other.data, axes=(-1, -1)) return dots - def mean(self) -> Self: + def mean(self) -> Quaternion: """Return the mean quaternion with unitary weights. Returns @@ -1083,7 +1083,7 @@ def outer( lazy: bool = False, chunk_size: int = 20, progressbar: bool = True, - ) -> Self | Vector3d: + ) -> Quaternion | Vector3d: """Return the outer products of the quaternions and the other quaternions or vectors. @@ -1166,7 +1166,7 @@ def outer( "with `other` of type `Quaternion` or `Vector3d`" ) - def inv(self) -> Self: + def inv(self) -> Quaternion: r"""Return the inverse quaternions :math:`Q^{-1} = a - bi - cj - dk`. """ diff --git a/orix/quaternion/rotation.py b/orix/quaternion/rotation.py index 95dfe662..811f79a0 100644 --- a/orix/quaternion/rotation.py +++ b/orix/quaternion/rotation.py @@ -19,7 +19,7 @@ from __future__ import annotations -from typing import Any, Self +from typing import Any import dask.array as da from dask.diagnostics import ProgressBar @@ -97,7 +97,7 @@ def improper(self, value: np.ndarray): self._data[..., -1] = value @property - def antipodal(self) -> Self: + def antipodal(self) -> Rotation: """Return the rotation and its antipodal.""" R = self.__class__(np.stack([self.data, -self.data])) R.improper = self.improper @@ -107,7 +107,7 @@ def antipodal(self) -> Self: def __mul__( self, other: Rotation | Quaternion | Vector3d | np.ndarray | int | list - ) -> Self | Quaternion | Vector3d: + ) -> Rotation | Quaternion | Vector3d: # Combine rotations self * other as first other, then self if isinstance(other, Rotation): Q = Quaternion(self) * Quaternion(other) @@ -133,17 +133,17 @@ def __mul__( return R return NotImplemented - def __neg__(self) -> Self: + def __neg__(self) -> Rotation: R = self.__class__(self.data) R.improper = np.logical_not(self.improper) return R - def __getitem__(self, key) -> Self: + def __getitem__(self, key) -> Rotation: R = super().__getitem__(key) R.improper = self.improper[key] return R - def __invert__(self) -> Self: + def __invert__(self) -> Rotation: R = super().__invert__() R.improper = self.improper return R @@ -168,7 +168,7 @@ def random_vonmises( shape: int | tuple = (1,), alpha: float = 1.0, reference: Rotation | list | tuple = (1, 0, 0, 0), - ) -> Self: + ) -> Rotation: """Return random rotations with a simplified Von Mises-Fisher distribution. @@ -208,7 +208,9 @@ def unique( return_index: bool = False, return_inverse: bool = False, antipodal: bool = True, - ) -> Self | tuple[Self, np.ndarray] | tuple[Self, np.ndarray, np.ndarray]: + ) -> ( + Rotation | tuple[Rotation, np.ndarray] | tuple[Rotation, np.ndarray, np.ndarray] + ): """Return the unique rotations from these rotations. Two rotations are not unique if they have the same propriety @@ -345,7 +347,7 @@ def outer( lazy: bool = False, chunk_size: int = 20, progressbar: bool = True, - ) -> Self | Vector3d: + ) -> Rotation | Vector3d: """Return the outer rotation products of the rotations and the other rotations or vectors. @@ -390,7 +392,7 @@ def outer( return R - def flatten(self) -> Self: + def flatten(self) -> Rotation: """Return a new rotation instance collapsed into one dimension. Returns diff --git a/orix/vector/miller.py b/orix/vector/miller.py index 897898ce..2511a95a 100644 --- a/orix/vector/miller.py +++ b/orix/vector/miller.py @@ -1,4 +1,5 @@ -# Copyright 2018-2024 the orix developers +# +# Copyright 2019-2025 the orix developers # # This file is part of orix. # @@ -9,11 +10,12 @@ # # 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 +# 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 . +# along with orix. If not, see . +# from __future__ import annotations @@ -21,12 +23,6 @@ from itertools import product from typing import TYPE_CHECKING, Optional, Tuple, Union -try: - # New in Python 3.11 - from typing import Self -except ImportError: # pragma: no cover - from typing_extensions import Self - from diffpy.structure import Lattice import numpy as np @@ -344,7 +340,7 @@ def is_hexagonal(self) -> bool: return self.phase.is_hexagonal @property - def unit(self) -> Self: + def unit(self) -> Miller: """Return unit vectors.""" m = self.__class__(xyz=super().unit.data, phase=self.phase) m.coordinate_format = self.coordinate_format @@ -363,7 +359,7 @@ def __repr__(self) -> str: f"{name} {shape}, point group {symmetry}, {coordinate_format}\n" f"{data}" ) - def __getitem__(self, key) -> Self: + def __getitem__(self, key) -> Miller: """NumPy fancy indexing of vectors.""" m = self.__class__(xyz=self.data[key], phase=self.phase).deepcopy() m.coordinate_format = self.coordinate_format @@ -378,7 +374,7 @@ def from_highest_indices( uvw: Union[np.ndarray, list, tuple, None] = None, hkl: Union[np.ndarray, list, tuple, None] = None, include_zero_vector: bool = False, - ) -> Self: + ) -> Miller: """Create a set of unique direct or reciprocal lattice vectors from three highest indices and a phase (crystal lattice and symmetry). @@ -405,7 +401,7 @@ def from_highest_indices( return cls(**init_kw).unique() @classmethod - def from_min_dspacing(cls, phase: "Phase", min_dspacing: float = 0.05) -> Self: + def from_min_dspacing(cls, phase: "Phase", min_dspacing: float = 0.05) -> Miller: """Create a set of unique reciprocal lattice vectors with a a direct space interplanar spacing greater than a lower threshold. @@ -432,7 +428,7 @@ def random( phase: "Phase", shape: Union[int, tuple] = 1, coordinate_format: str = "xyz", - ) -> Self: + ) -> Miller: """Create random Miller indices. Parameters @@ -466,11 +462,11 @@ def random( # --------------------- Other public methods --------------------- # - def deepcopy(self) -> Self: + def deepcopy(self) -> Miller: """Return a deepcopy of the instance.""" return deepcopy(self) - def round(self, max_index: int = 20) -> Self: + def round(self, max_index: int = 20) -> Miller: """Round a set of index triplet (Miller) or quartet (Miller-Bravais/Weber) to the *closest* smallest integers. @@ -498,7 +494,9 @@ def symmetrise( unique: bool = False, return_multiplicity: bool = False, return_index: bool = False, - ) -> Union[Self, Tuple[Self, np.ndarray], Tuple[Self, np.ndarray, np.ndarray]]: + ) -> Union[ + Miller, Tuple[Miller, np.ndarray], Tuple[Miller, np.ndarray, np.ndarray] + ]: """Return vectors symmetrically equivalent to the vectors. Parameters @@ -585,7 +583,7 @@ def symmetrise( def angle_with( self, - other: Self, + other: Miller, use_symmetry: bool = False, degrees: bool = False, ) -> np.ndarray: @@ -630,7 +628,7 @@ def angle_with( return angles - def cross(self, other: Self) -> Self: + def cross(self, other: Miller) -> Miller: """Return the cross products of the vectors with the other vectors, which is considered the zone axes between the vectors. @@ -652,7 +650,7 @@ def cross(self, other: Self) -> Self: m.coordinate_format = new_fmt[self.coordinate_format] return m - def dot(self, other: Self) -> np.ndarray: + def dot(self, other: Miller) -> np.ndarray: """Return the dot products of the vectors and the other vectors. Parameters @@ -669,7 +667,7 @@ def dot(self, other: Self) -> np.ndarray: self._compatible_with(other, raise_error=True) return super().dot(other) - def dot_outer(self, other: Self) -> np.ndarray: + def dot_outer(self, other: Miller) -> np.ndarray: """Return the outer dot products of the vectors and the other vectors. @@ -687,7 +685,7 @@ def dot_outer(self, other: Self) -> np.ndarray: self._compatible_with(other, raise_error=True) return super().dot_outer(other) - def flatten(self) -> Self: + def flatten(self) -> Miller: """Return the flattened vectors. Returns @@ -699,7 +697,7 @@ def flatten(self) -> Self: m.coordinate_format = self.coordinate_format return m - def transpose(self, *axes: Optional[int]) -> Self: + def transpose(self, *axes: Optional[int]) -> Miller: """Return a new instance with the data transposed. The order may be undefined if :attr:`ndim` is originally 2. In @@ -725,7 +723,7 @@ def get_nearest(self, *args) -> NotImplemented: """NotImplemented.""" return NotImplemented - def mean(self, use_symmetry: bool = False) -> Self: + def mean(self, use_symmetry: bool = False) -> Miller: """Return the mean vector of the set of vectors. Parameters @@ -745,7 +743,7 @@ def mean(self, use_symmetry: bool = False) -> Self: m.coordinate_format = self.coordinate_format return m - def reshape(self, *shape: Union[int, tuple]) -> Self: + def reshape(self, *shape: Union[int, tuple]) -> Miller: """Return a new instance with the vectors reshaped. Parameters @@ -764,7 +762,7 @@ def reshape(self, *shape: Union[int, tuple]) -> Self: def unique( self, use_symmetry: bool = False, return_index: bool = False - ) -> Union[Self, Tuple[Self, np.ndarray]]: + ) -> Union[Miller, Tuple[Miller, np.ndarray]]: """Unique vectors in ``self``. Parameters @@ -809,7 +807,7 @@ def unique( else: return m - def in_fundamental_sector(self, symmetry: Optional["Symmetry"] = None) -> Self: + def in_fundamental_sector(self, symmetry: Optional["Symmetry"] = None) -> Miller: """Project Miller indices to a symmetry's fundamental sector (inverse pole figure). @@ -856,7 +854,7 @@ def in_fundamental_sector(self, symmetry: Optional["Symmetry"] = None) -> Self: # -------------------- Other private methods --------------------- # - def _compatible_with(self, other: Self, raise_error: bool = False) -> bool: + def _compatible_with(self, other: Miller, raise_error: bool = False) -> bool: """Whether ``self`` and ``other`` are the same (the same crystal lattice and symmetry) with vectors in the same space. diff --git a/pyproject.toml b/pyproject.toml index 4a85f9a6..314d5025 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,8 +36,6 @@ dependencies = [ "pycifrw", "scipy", "tqdm", - # TODO: Remove once Python >= 3.11 - "typing_extensions", ] [project.optional-dependencies]