diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f721f637..e3a422eb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,13 +14,13 @@ jobs: name: code style runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: psf/black@stable - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.12' - uses: isort/isort-action@master with: @@ -34,23 +34,6 @@ jobs: run: | black --diff --line-length 77 doc/tutorials/*.ipynb - # Make sure all necessary files will be included in a release - manifest: - name: check manifest - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - uses: actions/setup-python@v4 - - - name: Install dependencies - run: | - pip install manifix - - - name: Check MANIFEST.in file - run: | - python setup.py manifix - build-with-pip: name: ${{ matrix.os }}-py${{ matrix.python-version }}${{ matrix.LABEL }} runs-on: ${{ matrix.os }} @@ -61,24 +44,33 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ['3.10', '3.11'] + python-version: ['3.11', '3.12'] include: - os: ubuntu-latest - python-version: 3.8 - DEPENDENCIES: diffpy.structure==3.0.2 matplotlib==3.5 + python-version: '3.10' + DEPENDENCIES: diffpy.structure==3.0.2 matplotlib==3.6.1 LABEL: -oldest + - os: ubuntu-latest + python-version: '3.12' + LABEL: -minimum_requirement steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install depedencies and package + - name: Install core depedencies and package + shell: bash + run: | + pip install -U -e .'[tests,coverage]' + + - name: Install optional dependencies + if: ${{ !contains(matrix.LABEL, 'minimum_requirement') }} shell: bash run: | - pip install -U -e .'[doc, tests]' + pip install -e .'[all]' - name: Install oldest supported version if: ${{ contains(matrix.LABEL, 'oldest') }} diff --git a/.gitignore b/.gitignore index 244b1526..0f6d691e 100644 --- a/.gitignore +++ b/.gitignore @@ -63,7 +63,7 @@ instance/ .scrapy # Sphinx documentation -doc/build/ +_build/ doc/examples/ doc/reference/generated/ doc/source/_autosummary/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b4cd29ca..ae7f0bd8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black - id: black-jupyter diff --git a/.zenodo.json b/.zenodo.json index 783e087a..02d910a0 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -3,7 +3,7 @@ { "name": "Håkon Wiik Ånes", "orcid": "0000-0002-1213-2911", - "affiliation": "Norwegian University of Science and Technology" + "affiliation": "Xnovo Technology ApS" }, { "name": "Ben Martineau" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ee0fb9b3..92023e40 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,27 @@ All user facing changes to this project are documented in this file. The format on `Keep a Changelog `__, and this project tries its best to adhere to `Semantic Versioning `__. +2024-09-20 - version 0.13.1 +=========================== + +Added +----- +- Support for Python 3.12. + +Changed +------- +- numpy-quaternion is now an optional dependency and will not be installed with ``pip`` + unless ``pip install orix[all]`` is used. + +Removed +------- +- Support for Python 3.8 and 3.9. + +Fixed +----- +- ``Phase.from_cif()`` still gives a valid phase even though the space group could not + be read. + 2024-09-03 - version 0.13.0 =========================== diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 9c3daa8f..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,14 +0,0 @@ -include CHANGELOG.rst -include CONTRIBUTING.rst -include environment.yml -include LICENSE -include README.rst -include RELEASE.rst -include readthedocs.yaml -include setup.cfg -include setup.py -include tutorials/README.rst - -recursive-include doc Makefile make.bat *.rst *.py *.ipynb *.bib *.txt *.cfg *.sh *.yml -recursive-include doc/_static *.png *.jpb *.svg *.css *.sh -recursive-include examples *.txt *.py diff --git a/README.rst b/README.rst index cd486d6d..5ec3f626 100644 --- a/README.rst +++ b/README.rst @@ -1,15 +1,8 @@ -.. raw:: html - -

-

- orix logo - orix -

-

- -.. Content above here until EXCLUDE plus one line is excluded from the long description -.. in the source distributions uploaded to PyPI -.. EXCLUDE +|logo| orix +=========== + +.. |logo| image:: https://raw.githubusercontent.com/pyxem/orix/develop/doc/_static/img/orix_logo.png + :width: 50 orix is an open-source Python library for analysing orientations and crystal symmetry. diff --git a/RELEASE.rst b/RELEASE.rst index 81760ff6..b02b5b66 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,7 +1,7 @@ How to make a new release of ``orix`` ===================================== -After version 0.9.0, orix' branching model changed to one similar to the Gitflow +After version 0.9.0, orix's branching model changed to one similar to the Gitflow Workflow (`original blog post `__). diff --git a/doc/Makefile b/doc/Makefile index ab377f87..78a7cba8 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -6,7 +6,7 @@ SPHINXOPTS = SPHINXBUILD = PYDEVD_DISABLE_FILE_VALIDATION=1 python -Xfrozen_modules=off -m sphinx SOURCEDIR = . -BUILDDIR = build +BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: diff --git a/doc/conf.py b/doc/conf.py index a15b86f8..2b81c2d3 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -21,8 +21,8 @@ sys.path.append("../") project = "orix" -copyright = f"2018-{str(datetime.now().year)}, {orix.__author__}" -author = orix.__author__ +author = "orix developers" +copyright = f"2018-{str(datetime.now().year)}, {author}" release = orix.__version__ # Add any Sphinx extension module names here, as strings. They can be @@ -59,8 +59,10 @@ "matplotlib": ("https://matplotlib.org/stable", None), "nbsphinx": ("https://nbsphinx.readthedocs.io/en/latest", None), "nbval": ("https://nbval.readthedocs.io/en/latest", None), + "numba": ("https://numba.readthedocs.io/en/latest", None), "numpy": ("https://numpy.org/doc/stable", None), "numpydoc": ("https://numpydoc.readthedocs.io/en/latest", None), + "pooch": ("https://www.fatiando.org/pooch/latest", None), "pytest": ("https://docs.pytest.org/en/stable", None), "python": ("https://docs.python.org/3", None), "pyxem": ("https://pyxem.readthedocs.io/en/latest", None), @@ -78,7 +80,7 @@ # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [ - "build", + "_build", "Thumbs.db", ".DS_Store", # Suppress warnings from Sphinx regarding "duplicate source files": @@ -120,10 +122,10 @@ # modifications to point nbviewer and Binder to the GitHub develop # branch links when the documentation is launched from a kikuchipy # version with "dev" in the version -if "dev" in orix.__version__: +if "dev" in release: release_version = "develop" else: - release_version = "v" + orix.__version__ + release_version = "v" + release # This is processed by Jinja2 and inserted before each notebook nbsphinx_prolog = ( r""" @@ -234,14 +236,14 @@ def linkcode_resolve(domain, info): fn = relpath(fn, start=startdir).replace(os.path.sep, "/") if fn.startswith("orix/"): - m = re.match(r"^.*dev0\+([a-f\d]+)$", orix.__version__) + m = re.match(r"^.*dev0\+([a-f\d]+)$", release) pre_link = "https://github.com/pyxem/orix/blob/" if m: return pre_link + "%s/%s%s" % (m.group(1), fn, linespec) - elif "dev" in orix.__version__: + elif "dev" in release: return pre_link + "develop/%s%s" % (fn, linespec) else: - return pre_link + "v%s/%s%s" % (orix.__version__, fn, linespec) + return pre_link + "v%s/%s%s" % (release, fn, linespec) else: return None @@ -329,7 +331,7 @@ def _str_examples(self): "filename_pattern": "^((?!sgskip).)*$", "gallery_dirs": "examples", "reference_url": {"orix": None}, - "run_stale_examples": True, + "run_stale_examples": False, "show_memory": True, } autosummary_generate = True diff --git a/doc/dev/building_writing_documentation.rst b/doc/dev/building_writing_documentation.rst index 5bd8d66d..05815b42 100644 --- a/doc/dev/building_writing_documentation.rst +++ b/doc/dev/building_writing_documentation.rst @@ -112,7 +112,7 @@ We use :doc:`nbval ` for this. The tutorial notebooks can be run interactively in the browser with the help of Binder. When creating a server from the orix source code, Binder installs the packages listed in the ``environment.yml`` configuration file, which must include all ``doc`` dependencies -in ``setup.py`` necessary to run the notebooks. +in ``pyproject.toml`` necessary to run the notebooks. Writing API reference --------------------- diff --git a/doc/dev/running_writing_tests.rst b/doc/dev/running_writing_tests.rst index f1bffa23..2e6b5b7d 100644 --- a/doc/dev/running_writing_tests.rst +++ b/doc/dev/running_writing_tests.rst @@ -4,7 +4,8 @@ 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. Install necessary dependencies to run the tests:: +values with known answers. +Install necessary dependencies to run the tests:: pip install --editable ".[tests]" @@ -15,23 +16,24 @@ Some useful :doc:`fixtures ` are available in the Some :mod:`orix.data` module tests check that data not part of the package distribution can be downloaded from the web, thus downloading some small datasets to - your local cache. See the section on the - :ref:`data module ` for more details. + your local cache. + See the section on the :ref:`data module ` for more + details. To run the tests:: pytest --cov --pyargs orix -The ``--cov`` flag makes :doc:`coverage.py ` prints a nice report in the -terminal. +The ``--cov`` flag makes :doc:`coverage.py ` print a nice report. For an even nicer presentation, you can use ``coverage.py`` directly:: coverage html -Then, you can open the created ``htmlcov/index.html`` in the browser and inspect the -coverage in more detail. +Coverage can then be inspected in the browser by opening ``htmlcov/index.html``. -Docstring examples are tested :doc:`with pytest ` as well. +We strive for 100% test coverage of lines when all dependencies are installed. + +Docstring examples are tested with :doc:`pytest ` as well. :mod:`numpy` and :mod:`matplotlib.pyplot` should not be imported in examples as they are already available in the namespace as ``np`` and ``plt``, respectively. The docstring tests can be run from the top directory:: diff --git a/doc/tutorials/crystal_map.ipynb b/doc/tutorials/crystal_map.ipynb index a5f0602b..71deb9ed 100644 --- a/doc/tutorials/crystal_map.ipynb +++ b/doc/tutorials/crystal_map.ipynb @@ -1330,7 +1330,7 @@ " vmax=angles.max() - 10,\n", " overlay=xmap.iq,\n", " colorbar=True,\n", - " colorbar_label=\"Rotation angle, $\\omega$ [$^{\\circ}$]\",\n", + " colorbar_label=r\"Rotation angle, $\\omega$ [$\\degree$]\",\n", ")" ] }, @@ -1492,7 +1492,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.12.6" } }, "nbformat": 4, diff --git a/doc/tutorials/pole_density_function.ipynb b/doc/tutorials/pole_density_function.ipynb index 850b5b71..cb2b499b 100644 --- a/doc/tutorials/pole_density_function.ipynb +++ b/doc/tutorials/pole_density_function.ipynb @@ -236,19 +236,19 @@ "v = Vector3d.random(1_000_000)\n", "\n", "ax[0].pole_density_function(v, log=False, resolution=1)\n", - "ax[0].set(title=\"Sampling resolution: 1$\\degree$\")\n", + "ax[0].set(title=r\"Sampling resolution: 1$\\degree$\")\n", "\n", "# change sampling resolution on S2\n", "ax[1].pole_density_function(v, log=False, resolution=5)\n", - "ax[1].set(title=\"Sampling resolution: 5$\\degree$\")\n", + "ax[1].set(title=r\"Sampling resolution: 5$\\degree$\")\n", "\n", "# increase peak broadening\n", "ax[2].pole_density_function(v, log=False, resolution=1, sigma=15)\n", - "ax[2].set(title=\"Sampling resolution: 1$\\degree$\\n$\\sigma$: 15$\\degree$\")\n", + "ax[2].set(title=\"Sampling resolution: 1$\\\\degree$\\n$\\\\sigma$: 15$\\\\degree$\")\n", "\n", "# change colormap\n", "ax[3].pole_density_function(v, log=False, resolution=1, cmap=\"gray_r\")\n", - "ax[3].set(title='Sampling resolution: 1$\\degree$\\ncmap: \"gray_r\"')\n", + "ax[3].set(title=\"Sampling resolution: 1$\\\\degree$\\ncmap: 'gray_r'\")\n", "\n", "for a in ax:\n", " a.set_labels(\"X\", \"Y\", None)" @@ -352,7 +352,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.12.6" } }, "nbformat": 4, diff --git a/doc/user/installation.rst b/doc/user/installation.rst index 509a43d3..b5bbf555 100644 --- a/doc/user/installation.rst +++ b/doc/user/installation.rst @@ -4,7 +4,7 @@ Installation orix can be installed with `pip `__, `conda `__ or from source, and supports Python ->= 3.8. +>= 3.10. All alternatives are available on Windows, macOS and Linux. .. _install-with-pip: @@ -13,19 +13,27 @@ With pip ======== orix is availabe from the Python Package Index (PyPI), and can therefore be installed -with `pip `__. To install, run the following:: +with `pip `__. +To install all of orix's functionality, do:: + + pip install orix[all] + +To install only the strictly required dependencies with limited functionality, do:: pip install orix +See :ref:`dependencies` for the base and optional dependencies and alternatives for how +to install these. + To update orix to the latest release:: pip install --upgrade orix -To install a specific version of orix (say version 0.8.1):: +To install a specific version of orix (say version 0.12.1):: - pip install orix==0.8.1 + pip install orix==0.12.1 -.. _optional-dependencies: +.. _install-with-anaconda: With Anaconda ============= @@ -35,23 +43,30 @@ To install with Anaconda, we recommend you install it in a `conda environment with the `Miniconda distribution `__. To create an environment and activate it, run the following:: - conda create --name orix-env python=3.9 + conda create --name orix-env python=3.12 conda activate orix-env If you prefer a graphical interface to manage packages and environments, you can install the `Anaconda distribution `__ instead. -To install:: +To install all of orix's functionality, do:: conda install orix --channel conda-forge +To install only the strictly required dependencies with limited functionality, do:: + + conda install orix-base -c conda-forge + +See :ref:`dependencies` for the base and optional dependencies and alternatives for how +to install these. + To update orix to the latest release:: conda update orix -To install a specific version of orix (say version 0.8.1):: +To install a specific version of orix (say version 0.12.1):: - conda install orix==0.8.1 -c conda-forge + conda install orix==0.12.1 -c conda-forge .. _install-from-source: @@ -74,4 +89,43 @@ exchanged with ``zip``. See the :ref:`contributing guide ` for how to set up a development installation and keep it up to date. -.. _https://github.com/pyxem/orix/archive/v/orix-.tar.gz: https://github.com/pyxem/orix/archive/v/orix-.tar.gz \ No newline at end of file +.. _https://github.com/pyxem/orix/archive/v/orix-.tar.gz: https://github.com/pyxem/orix/archive/v/orix-.tar.gz + + +.. _dependencies: + +Dependencies +============ + +orix builds on the great work and effort of many people. +This is a list of core package dependencies: + +================================================ ================================================ +Package Purpose +================================================ ================================================ +:doc:`dask` Out-of-memory processing of data larger than RAM +:doc:`diffpy.structure ` Handling of crystal structures +:doc:`h5py ` Read/write of HDF5 files +:doc:`matplotlib ` Visualization +`matplotlib-scalebar`_ Scale bar for crystal map plots +:doc:`numba ` CPU acceleration +:doc:`numpy ` Handling of N-dimensional arrays +:doc:`pooch ` Downloading and caching of datasets +:doc:`scipy ` Optimization algorithms, filtering and more +`tqdm `__ Progressbars +================================================ ================================================ + +.. _matplotlib-scalebar: https://github.com/ppinard/matplotlib-scalebar + +Some functionality requires optional dependencies: + +=================== =========================================== +Package Purpose +=================== =========================================== +`numpy-quaternion`_ Faster quaternion and vector multiplication +=================== =========================================== + +.. _numpy-quaternion: https://quaternion.readthedocs.io/en/stable/ + +Optional dependencies can be installed either with ``pip install orix[all]`` or by +installing each dependency separately, such as ``pip install orix numpy-quaternion``. diff --git a/examples/plotting/interactive_xmap.py b/examples/plotting/interactive_xmap.py index d3925535..6b23c6a4 100644 --- a/examples/plotting/interactive_xmap.py +++ b/examples/plotting/interactive_xmap.py @@ -85,7 +85,7 @@ def on_click(event): plt.imshow(rgb_dp_2d) plt.plot(x, y, "+", c="k", markersize=15, markeredgewidth=3) plt.title( - f"Phase: {phase_name}, Euler angles: $(\phi_1, \Phi, \phi_2)$ = {eu_str}" + rf"Phase: {phase_name}, Euler angles: $(\phi_1, \Phi, \phi_2)$ = {eu_str}" ) plt.draw() diff --git a/examples/plotting/subplots.py b/examples/plotting/subplots.py index af64dddf..96a0bd4b 100644 --- a/examples/plotting/subplots.py +++ b/examples/plotting/subplots.py @@ -3,7 +3,7 @@ Subplots ======== -This example shows how to place different plots in the same figure using orix' various +This example shows how to place different plots in the same figure using orix's various :mod:`plot types `, which extend Matplotlib's plot types. By first creating a blank figure and then using diff --git a/examples/rotations/rotating_z_to_high_symmetry_directions.py b/examples/rotations/rotating_z_to_high_symmetry_directions.py index 70232090..2ffecf82 100644 --- a/examples/rotations/rotating_z_to_high_symmetry_directions.py +++ b/examples/rotations/rotating_z_to_high_symmetry_directions.py @@ -1,4 +1,4 @@ -""" +r""" ===================================================== Rotating z-vector to high-symmetry crystal directions ===================================================== @@ -26,7 +26,7 @@ from orix.crystal_map import Phase from orix.quaternion import Rotation -from orix.vector import Miller, Vector3d +from orix.vector import Miller phase = Phase(point_group="mmm") t = Miller.from_highest_indices(phase, uvw=[1, 1, 1]) diff --git a/examples/rotations/rotations_mapping_fundamental_sector.py b/examples/rotations/rotations_mapping_fundamental_sector.py index 8f195dad..940177c6 100644 --- a/examples/rotations/rotations_mapping_fundamental_sector.py +++ b/examples/rotations/rotations_mapping_fundamental_sector.py @@ -1,4 +1,4 @@ -""" +r""" ================================================ Rotations mapping the fundamental sector on *S2* ================================================ diff --git a/orix/__init__.py b/orix/__init__.py index 18ea610e..f7b604e7 100644 --- a/orix/__init__.py +++ b/orix/__init__.py @@ -1,8 +1,4 @@ -__name__ = "orix" -__version__ = "0.13.0" -__author__ = "orix developers" -__author_email__ = "pyxem.team@gmail.com" -__description__ = "orix is an open-source Python library for handling crystal orientation mapping data." +__version__ = "0.13.1" # Sorted by line contributions (ideally excluding lines in notebook files) __credits__ = [ "Håkon Wiik Ånes", diff --git a/orix/_base.py b/orix/_base.py index 508d172e..1b6c5a1b 100644 --- a/orix/_base.py +++ b/orix/_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -271,7 +270,7 @@ def reshape(self, *shape: Union[int, tuple]) -> Object3d: if len(shape) == 1 and isinstance(shape[0], tuple): shape = shape[0] obj = self.__class__(self.data.reshape(*shape, self.dim)) - obj._data = self._data.reshape(*shape, -1) + obj._data = self._data.reshape(*shape, self._data.shape[-1]) return obj def transpose(self, *axes: Optional[int]) -> Object3d: diff --git a/orix/_util.py b/orix/_util.py index f6c4fe62..f6bed563 100644 --- a/orix/_util.py +++ b/orix/_util.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/constants.py b/orix/constants.py new file mode 100644 index 00000000..cd5c2096 --- /dev/null +++ b/orix/constants.py @@ -0,0 +1,38 @@ +# Copyright 2018-2024 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 . + +"""Constants and such useful across modules.""" + +from importlib.metadata import version + +# NB! Update project config file if this list is updated! +optional_deps: list[str] = ["numpy-quaternion"] +installed: dict[str, bool] = {} +for pkg in optional_deps: + try: + _ = version(pkg) + installed[pkg] = True + except ImportError: # pragma: no cover + installed[pkg] = False + +# Typical tolerances for comparisons in need of a precision. We +# generally use the highest precision possible (allowed by testing on +# different OS and Python versions). +eps9 = 1e-9 +eps12 = 1e-12 + +del optional_deps diff --git a/orix/crystal_map/__init__.py b/orix/crystal_map/__init__.py index 1d08e48b..2b213ffa 100644 --- a/orix/crystal_map/__init__.py +++ b/orix/crystal_map/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/crystal_map/crystal_map.py b/orix/crystal_map/crystal_map.py index d74feb82..ec87d484 100644 --- a/orix/crystal_map/crystal_map.py +++ b/orix/crystal_map/crystal_map.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/crystal_map/crystal_map_properties.py b/orix/crystal_map/crystal_map_properties.py index a58f7b0c..02e397ec 100644 --- a/orix/crystal_map/crystal_map_properties.py +++ b/orix/crystal_map/crystal_map_properties.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/crystal_map/phase_list.py b/orix/crystal_map/phase_list.py index db99ab97..4e12185e 100644 --- a/orix/crystal_map/phase_list.py +++ b/orix/crystal_map/phase_list.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -21,8 +20,8 @@ from collections import OrderedDict import copy from itertools import islice -import os -from typing import Dict, List, Optional, Tuple, Union +from pathlib import Path +from typing import Generator import warnings from diffpy.structure import Structure @@ -96,13 +95,12 @@ class Phase: def __init__( self, - name: Optional[str] = None, - space_group: Union[int, SpaceGroup, None] = None, - point_group: Union[int, str, Symmetry, None] = None, - structure: Optional[Structure] = None, - color: Optional[str] = None, - ): - """Create a phase.""" + name: str | None = None, + space_group: int | SpaceGroup | None = None, + point_group: int | str | Symmetry | None = None, + structure: Structure | None = None, + color: str | None = None, + ) -> None: self.structure = structure if structure is not None else Structure() if name is not None: self.name = name @@ -128,7 +126,7 @@ def structure(self) -> Structure: return self._structure @structure.setter - def structure(self, value: Structure): + def structure(self, value: Structure) -> None: """Set the crystal structure.""" if isinstance(value, Structure): # Ensure correct alignment @@ -157,12 +155,12 @@ def name(self) -> str: return self.structure.title @name.setter - def name(self, value: str): + def name(self, value: str) -> None: """Set the phase name.""" self.structure.title = str(value) @property - def color(self): + def color(self) -> str: """Return or set the name of phase color. Parameters @@ -174,7 +172,7 @@ def color(self): return self._color @color.setter - def color(self, value: str): + def color(self, value: str) -> None: """Set the phase color.""" value_hex = mcolors.to_hex(value) for name, color_hex in ALL_COLORS.items(): @@ -188,7 +186,7 @@ def color_rgb(self) -> tuple: return mcolors.to_rgb(self.color) @property - def space_group(self) -> SpaceGroup: + def space_group(self) -> SpaceGroup | None: """Return or set the space group. Parameters @@ -200,7 +198,7 @@ def space_group(self) -> SpaceGroup: return self._space_group @space_group.setter - def space_group(self, value: Union[int, SpaceGroup, None]): + def space_group(self, value: int | SpaceGroup | None) -> None: """Set the space group.""" if isinstance(value, int): value = GetSpaceGroup(value) @@ -208,10 +206,11 @@ def space_group(self, value: Union[int, SpaceGroup, None]): raise ValueError( f"'{value}' must be of type {SpaceGroup}, an integer 1-230, or None." ) - self._space_group = value # Overwrites any point group set before + # Overwrites any point group set before + self._space_group: SpaceGroup | None = value @property - def point_group(self) -> Symmetry: + def point_group(self) -> Symmetry | None: """Return or set the point group. Parameters @@ -225,7 +224,7 @@ def point_group(self) -> Symmetry: return self._point_group @point_group.setter - def point_group(self, value: Union[int, str, Symmetry, None]): + def point_group(self, value: int | str | Symmetry | None) -> None: """Set the point group.""" if isinstance(value, int): value = str(value) @@ -321,7 +320,7 @@ def __repr__(self) -> str: ) @classmethod - def from_cif(cls, filename: str) -> Phase: + def from_cif(cls, filename: str | Path) -> Phase: """Return a new phase from a CIF file using :mod:`diffpy.structure`'s CIF file parser. @@ -336,10 +335,15 @@ def from_cif(cls, filename: str) -> Phase: phase New phase. """ + path = Path(filename) parser = p_cif.P_cif() - name = os.path.splitext(os.path.split(filename)[1])[0] - structure = parser.parseFile(filename) - space_group = parser.spacegroup.number + name = path.stem + structure = parser.parseFile(str(path)) + try: + space_group = parser.spacegroup.number + except AttributeError: # pragma: no cover + space_group = None + warnings.warn(f"Could not read space group from CIF file {path!r}") return cls(name, space_group, structure=structure) def deepcopy(self) -> Phase: @@ -432,16 +436,14 @@ class PhaseList: def __init__( self, - phases: Union[Phase, List[Phase], Dict[Phase], None] = None, - names: Union[str, List[str], None] = None, - space_groups: Union[int, SpaceGroup, List[Union[int, SpaceGroup]], None] = None, - point_groups: Union[ - str, int, Symmetry, List[Union[str, int, Symmetry]], None - ] = None, - colors: Union[str, List[str], None] = None, - ids: Union[int, List[int], np.ndarray, None] = None, - structures: Union[Structure, List[Structure], None] = None, - ): + phases: Phase | list[Phase] | dict[int, Phase] | None = None, + names: str | list[str] | None = None, + space_groups: int | SpaceGroup | list[int | SpaceGroup] | None = None, + point_groups: str | int | Symmetry | list[str | int | Symmetry] | None = None, + colors: str | list[str] | None = None, + ids: int | list[int] | np.ndarray | None = None, + structures: Structure | list[Structure] | None = None, + ) -> None: """Create a new phase list.""" d = {} if isinstance(phases, list): @@ -555,27 +557,27 @@ def __init__( self._dict = OrderedDict(sorted(d.items())) @property - def names(self) -> List[str]: + def names(self) -> list[str]: """Return the phases' names.""" return [phase.name for _, phase in self] @property - def space_groups(self) -> List[SpaceGroup]: + def space_groups(self) -> list[SpaceGroup]: """Return the phases' space groups.""" return [phase.space_group for _, phase in self] @property - def point_groups(self) -> List[Symmetry]: + def point_groups(self) -> list[Symmetry]: """Return the phases' point groups.""" return [phase.point_group for _, phase in self] @property - def colors(self) -> List[str]: + def colors(self) -> list[str]: """Return the phases' colors.""" return [phase.color for _, phase in self] @property - def colors_rgb(self) -> List[tuple]: + def colors_rgb(self) -> list[tuple]: """Return the phases' RGB color values.""" return [phase.color_rgb for _, phase in self] @@ -585,16 +587,16 @@ def size(self) -> int: return len(self._dict.items()) @property - def ids(self) -> List[int]: + def ids(self) -> list[int]: """Return the unique phase IDs in the list of phases.""" return list(self._dict.keys()) @property - def structures(self) -> List[Structure]: + def structures(self) -> list[Structure]: """Return the phases' structures.""" return [phase.structure for _, phase in self] - def __getitem__(self, key) -> Union[PhaseList, Phase]: + def __getitem__(self, key) -> PhaseList | Phase: """Return a PhaseList or a Phase object, depending on the number of phases in the list matches the `key`. """ @@ -641,7 +643,7 @@ def __getitem__(self, key) -> Union[PhaseList, Phase]: else: return PhaseList(d) - def __delitem__(self, key: Union[int, str]): + def __delitem__(self, key: int | str) -> None: """Delete a phase from the phase list. Parameters @@ -664,7 +666,7 @@ def __delitem__(self, key: Union[int, str]): else: raise TypeError(f"{key} is an invalid phase ID or name.") - def __iter__(self) -> Tuple[int, Phase]: + def __iter__(self) -> Generator[tuple[int, Phase]]: """Return a tuple with phase ID and Phase object, in that order.""" for phase_id, phase in self._dict.items(): yield phase_id, phase @@ -721,7 +723,7 @@ def deepcopy(self) -> PhaseList: """Return a deep copy using :func:`copy.deepcopy` function.""" return copy.deepcopy(self) - def add_not_indexed(self): + def add_not_indexed(self) -> None: """Add a dummy phase to assign to not indexed data points. The phase, named ``"not_indexed"``, has a @@ -731,7 +733,7 @@ def add_not_indexed(self): self._dict[-1] = Phase(name="not_indexed", color="white") self.sort_by_id() - def sort_by_id(self): + def sort_by_id(self) -> None: """Sort the list according to phase ID in-place.""" self._dict = OrderedDict(sorted(self._dict.items())) @@ -754,7 +756,7 @@ def id_from_name(self, name: str) -> int: return phase_id raise KeyError(f"'{name}' is not among the phase names {self.names}.") - def add(self, value: Union[Phase, List[Phase], PhaseList]): + def add(self, value: Phase | list[Phase] | PhaseList) -> None: """Add phases to the end of a phase list in-place, incrementing the phase IDs. @@ -810,9 +812,9 @@ def add(self, value: Union[Phase, List[Phase], PhaseList]): def _new_structure_matrix_from_alignment( old_matrix: np.ndarray, - x: Optional[str] = None, - y: Optional[str] = None, - z: Optional[str] = None, + x: str | None = None, + y: str | None = None, + z: str | None = None, ) -> np.ndarray: """Return a new structure matrix given the old structure matrix and at least two aligned axes x, y, or z. diff --git a/orix/data/__init__.py b/orix/data/__init__.py index 77f1160a..7813745a 100644 --- a/orix/data/__init__.py +++ b/orix/data/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/data/_registry.py b/orix/data/_registry.py index dc31b415..7d934fef 100644 --- a/orix/data/_registry.py +++ b/orix/data/_registry.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/io/__init__.py b/orix/io/__init__.py index c1434776..f6d658ae 100644 --- a/orix/io/__init__.py +++ b/orix/io/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/io/plugins/__init__.py b/orix/io/plugins/__init__.py index 6485b624..cbf77e0a 100644 --- a/orix/io/plugins/__init__.py +++ b/orix/io/plugins/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/io/plugins/_h5ebsd.py b/orix/io/plugins/_h5ebsd.py index 7167d1b7..17c8da96 100644 --- a/orix/io/plugins/_h5ebsd.py +++ b/orix/io/plugins/_h5ebsd.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/io/plugins/ang.py b/orix/io/plugins/ang.py index 521b27bb..c93c8b8c 100644 --- a/orix/io/plugins/ang.py +++ b/orix/io/plugins/ang.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -108,7 +107,7 @@ def file_reader(filename: str) -> CrystalMap: data_dict["phase_list"] = PhaseList(**phases) # Set which data points are not indexed - # TODO: Add not-indexed convention for INDEX ASTAR + # TODO: Add not-indexed convention for ASTAR INDEX if vendor in ["orix", "tsl"]: not_indexed = data_dict["prop"]["ci"] == -1 data_dict["phase_id"][not_indexed] = -1 diff --git a/orix/io/plugins/bruker_h5ebsd.py b/orix/io/plugins/bruker_h5ebsd.py index a1037eee..28c40a84 100644 --- a/orix/io/plugins/bruker_h5ebsd.py +++ b/orix/io/plugins/bruker_h5ebsd.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py index e271a5e7..dfdc7994 100644 --- a/orix/io/plugins/ctf.py +++ b/orix/io/plugins/ctf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/io/plugins/emsoft_h5ebsd.py b/orix/io/plugins/emsoft_h5ebsd.py index 56f8cf9d..5674ed2d 100644 --- a/orix/io/plugins/emsoft_h5ebsd.py +++ b/orix/io/plugins/emsoft_h5ebsd.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/io/plugins/orix_hdf5.py b/orix/io/plugins/orix_hdf5.py index b2877112..3cc68a06 100644 --- a/orix/io/plugins/orix_hdf5.py +++ b/orix/io/plugins/orix_hdf5.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -16,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with orix. If not, see . -"""Reader and writer of a crystal map to and from orix' own HDF5 file +"""Reader and writer of a crystal map to and from orix's own HDF5 file format. """ @@ -44,7 +43,7 @@ def file_reader(filename: str, **kwargs) -> CrystalMap: - """Return a crystal map from a file in orix' HDF5 file format. + """Return a crystal map from a file in orix's HDF5 file format. Parameters ---------- diff --git a/orix/measure/__init__.py b/orix/measure/__init__.py index 6124743c..84e49cc3 100644 --- a/orix/measure/__init__.py +++ b/orix/measure/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/measure/pole_density_function.py b/orix/measure/pole_density_function.py index 113db7cc..62076089 100644 --- a/orix/measure/pole_density_function.py +++ b/orix/measure/pole_density_function.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -34,7 +33,7 @@ def pole_density_function( hemisphere: str = "upper", symmetry: Optional[Symmetry] = None, log: bool = False, - mrd: bool = True + mrd: bool = True, ) -> Tuple[np.ma.MaskedArray, Tuple[np.ndarray, np.ndarray]]: """Compute the Pole Density Function (PDF) of vectors in the stereographic projection. See :cite:`rohrer2004distribution`. diff --git a/orix/plot/__init__.py b/orix/plot/__init__.py index 30fe7194..6aad3f5a 100644 --- a/orix/plot/__init__.py +++ b/orix/plot/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/plot/_symmetry_marker.py b/orix/plot/_symmetry_marker.py index d4f4684e..b02472c1 100644 --- a/orix/plot/_symmetry_marker.py +++ b/orix/plot/_symmetry_marker.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/plot/_util.py b/orix/plot/_util.py index 8669c432..c7d4d9f0 100644 --- a/orix/plot/_util.py +++ b/orix/plot/_util.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/plot/crystal_map_plot.py b/orix/plot/crystal_map_plot.py index 41984445..5a5929b6 100644 --- a/orix/plot/crystal_map_plot.py +++ b/orix/plot/crystal_map_plot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -428,8 +427,6 @@ def _override_status_bar( n_rows, n_cols = self._data_shape # Get rotations, ensuring correct masking - # TODO: Show orientations in Euler angles (computationally - # intensive...) r = crystal_map.get_map_data("rotations", decimals=3) # Get image data, overwriting potentially masked regions set to 0.0 diff --git a/orix/plot/direction_color_keys/__init__.py b/orix/plot/direction_color_keys/__init__.py index 52736ac0..661116f9 100644 --- a/orix/plot/direction_color_keys/__init__.py +++ b/orix/plot/direction_color_keys/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/plot/direction_color_keys/_util.py b/orix/plot/direction_color_keys/_util.py index c67c301f..47b228af 100644 --- a/orix/plot/direction_color_keys/_util.py +++ b/orix/plot/direction_color_keys/_util.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/plot/direction_color_keys/direction_color_key.py b/orix/plot/direction_color_keys/direction_color_key.py index 25f1c1ab..9d04ccac 100644 --- a/orix/plot/direction_color_keys/direction_color_key.py +++ b/orix/plot/direction_color_keys/direction_color_key.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/plot/direction_color_keys/direction_color_key_tsl.py b/orix/plot/direction_color_keys/direction_color_key_tsl.py index 75b7d145..96e15374 100644 --- a/orix/plot/direction_color_keys/direction_color_key_tsl.py +++ b/orix/plot/direction_color_keys/direction_color_key_tsl.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/plot/inverse_pole_figure_plot.py b/orix/plot/inverse_pole_figure_plot.py index d8a893f4..47175c7a 100644 --- a/orix/plot/inverse_pole_figure_plot.py +++ b/orix/plot/inverse_pole_figure_plot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/plot/orientation_color_keys/__init__.py b/orix/plot/orientation_color_keys/__init__.py index 232d6ec4..83c58334 100644 --- a/orix/plot/orientation_color_keys/__init__.py +++ b/orix/plot/orientation_color_keys/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/plot/orientation_color_keys/euler_color_key.py b/orix/plot/orientation_color_keys/euler_color_key.py index 90c6138a..e8dec3f0 100644 --- a/orix/plot/orientation_color_keys/euler_color_key.py +++ b/orix/plot/orientation_color_keys/euler_color_key.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/plot/orientation_color_keys/ipf_color_key.py b/orix/plot/orientation_color_keys/ipf_color_key.py index e7d6c392..69402e4e 100644 --- a/orix/plot/orientation_color_keys/ipf_color_key.py +++ b/orix/plot/orientation_color_keys/ipf_color_key.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/plot/orientation_color_keys/ipf_color_key_tsl.py b/orix/plot/orientation_color_keys/ipf_color_key_tsl.py index e44622bd..4c15a9c7 100644 --- a/orix/plot/orientation_color_keys/ipf_color_key_tsl.py +++ b/orix/plot/orientation_color_keys/ipf_color_key_tsl.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/plot/rotation_plot.py b/orix/plot/rotation_plot.py index 223af0ac..c46a593b 100644 --- a/orix/plot/rotation_plot.py +++ b/orix/plot/rotation_plot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/plot/stereographic_plot.py b/orix/plot/stereographic_plot.py index 85b9a53e..a1a1cb8c 100644 --- a/orix/plot/stereographic_plot.py +++ b/orix/plot/stereographic_plot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/plot/unit_cell_plot.py b/orix/plot/unit_cell_plot.py index 52e97c86..1925518d 100644 --- a/orix/plot/unit_cell_plot.py +++ b/orix/plot/unit_cell_plot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/projections/__init__.py b/orix/projections/__init__.py index d749f175..f87b1d08 100644 --- a/orix/projections/__init__.py +++ b/orix/projections/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/projections/stereographic.py b/orix/projections/stereographic.py index fe4c211d..05644c1d 100644 --- a/orix/projections/stereographic.py +++ b/orix/projections/stereographic.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/quaternion/__init__.py b/orix/quaternion/__init__.py index f6da231f..3a53b769 100644 --- a/orix/quaternion/__init__.py +++ b/orix/quaternion/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/quaternion/_conversions.py b/orix/quaternion/_conversions.py index 6fa2c1b6..e1e36b44 100644 --- a/orix/quaternion/_conversions.py +++ b/orix/quaternion/_conversions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -18,19 +17,6 @@ """Conversions of rotations between many common representations from :cite:`rowenhorst2015consistent`, accelerated with Numba. - -Conventions: - -1. Right-handed Cartesian reference frames -2. Rotation angles are taken to be positive for a counter-clockwise - rotation when viewing from the end point of the rotation axis unit - vector towards the origin. -3. Rotations are *interpreted* in the passive sense. This means that we - rotate reference frames with vectors fixed in space. Rotations are - basis transformations rather than coordinate transformations. -4. Euler angle triplets are implemented using the Bunge convention, with - angular ranges as [0, 2pi], [0, pi], and [0, 2pi]. -5. Rotation angles are limited to [0, pi]. """ from typing import Tuple @@ -38,10 +24,10 @@ import numba as nb import numpy as np -FLOAT_EPS = np.finfo(float).eps +from orix import constants -@nb.jit("int64(float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("int64(float64[:])", cache=True, fastmath=True, nogil=True) def get_pyramid_single(xyz: np.ndarray) -> int: """Determine to which out of six pyramids in the cube a (x, y, z) coordinate belongs. @@ -77,7 +63,7 @@ def get_pyramid_single(xyz: np.ndarray) -> int: return 6 -@nb.jit("int64[:](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("int64[:](float64[:, :])", cache=True, fastmath=True, nogil=True) def get_pyramid_2d(xyz: np.ndarray) -> np.ndarray: """Determine to which out of six pyramids in the cube a 2D array of (x, y, z) coordinates belongs. @@ -116,7 +102,7 @@ def get_pyramid(xyz: np.ndarray) -> np.ndarray: return pyramids -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def cu2ho_single(cu: np.ndarray) -> np.ndarray: """Convert a single set of cubochoric coordinates to un-normalized homochoric coordinates :cite:`singh2016orientation`. @@ -195,7 +181,7 @@ def cu2ho_single(cu: np.ndarray) -> np.ndarray: return np.roll(ho, -1) -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def cu2ho_2d(cu: np.ndarray) -> np.ndarray: """Convert multiple cubochoric coordinates to un-normalized homochoric coordinates :cite:`singh2016orientation`. @@ -234,7 +220,7 @@ def cu2ho(cu: np.ndarray) -> np.ndarray: return ho -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def ho2ax_single(ho: np.ndarray) -> np.ndarray: """Convert a single set of homochoric coordinates to an un-normalized axis-angle pair :cite:`rowenhorst2015consistent`. @@ -285,7 +271,7 @@ def ho2ax_single(ho: np.ndarray) -> np.ndarray: return ax -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def ho2ax_2d(ho: np.ndarray) -> np.ndarray: """Convert multiple homochoric coordinates to un-normalized axis-angle pairs :cite:`rowenhorst2015consistent`. @@ -325,7 +311,7 @@ def ho2ax(ho: np.ndarray) -> np.ndarray: return ax -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def ax2ro_single(ax: np.ndarray) -> np.ndarray: """Convert a single angle-axis pair to an un-normalized Rodrigues vector :cite:`rowenhorst2015consistent`. @@ -365,7 +351,7 @@ def ax2ro_single(ax: np.ndarray) -> np.ndarray: return ro -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def ax2ro_2d(ax: np.ndarray) -> np.ndarray: """Convert multiple axis-angle pairs to un-normalized Rodrigues vectors :cite:`rowenhorst2015consistent`. @@ -405,7 +391,7 @@ def ax2ro(ax: np.ndarray) -> np.ndarray: return ro -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def ro2ax_single(ro: np.ndarray) -> np.ndarray: """Convert a single Rodrigues vector to an un-normalized axis-angle pair :cite:`rowenhorst2015consistent`. @@ -434,7 +420,7 @@ def ro2ax_single(ro: np.ndarray) -> np.ndarray: return np.append(ro[:3] / norm, 2 * np.arctan(ro[3])) -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def ro2ax_2d(ro: np.ndarray) -> np.ndarray: """Convert multiple Rodrigues vectors to un-normalized axis-angle pairs :cite:`rowenhorst2015consistent`. @@ -474,7 +460,7 @@ def ro2ax(ro: np.ndarray) -> np.ndarray: return ax -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def ax2qu_single(ax: np.ndarray) -> np.ndarray: """Convert a single axis-angle pair to a unit quaternion :cite:`rowenhorst2015consistent`. @@ -505,7 +491,7 @@ def ax2qu_single(ax: np.ndarray) -> np.ndarray: return qu -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def ax2qu_2d(ax: np.ndarray) -> np.ndarray: """Convert multiple axis-angle pairs to unit quaternions :cite:`rowenhorst2015consistent`. @@ -582,7 +568,7 @@ def ax2qu(axes: np.ndarray, angles: np.ndarray) -> np.ndarray: return qu -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def qu2ax_single(qu: np.ndarray) -> np.ndarray: """Convert a single (un)normalized quaternion to a normalized axis-angle pair :cite:`rowenhorst2015consistent`. @@ -606,10 +592,10 @@ def qu2ax_single(qu: np.ndarray) -> np.ndarray: """ omega = 2 * np.arccos(qu[0]) - if omega < FLOAT_EPS: + if omega < constants.eps9: return np.array([0, 0, 1, 0], dtype=np.float64) - if np.abs(qu[0]) < FLOAT_EPS: + if np.abs(qu[0]) < constants.eps9: return np.array([qu[1], qu[2], qu[3], np.pi], dtype=np.float64) s = np.sqrt(np.sum(np.square(qu[1:]))) @@ -622,7 +608,7 @@ def qu2ax_single(qu: np.ndarray) -> np.ndarray: return ax -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def qu2ax_2d(qu: np.ndarray) -> np.ndarray: """Convert multiple (un)normalized quaternions to normalized axis-angle pairs :cite:`rowenhorst2015consistent`. @@ -685,7 +671,7 @@ def qu2ax(qu: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: return axes, angles -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def ho2ro_single(ho: np.ndarray) -> np.ndarray: """Convert a single set of homochoric coordinates to an un-normalized Rodrigues vector :cite:`rowenhorst2015consistent`. @@ -708,7 +694,7 @@ def ho2ro_single(ho: np.ndarray) -> np.ndarray: return ax2ro_single(ho2ax_single(ho)) -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def ho2ro_2d(ho: np.ndarray) -> np.ndarray: """Convert multiple homochoric coordinates to un-normalized Rodrigues vectors :cite:`rowenhorst2015consistent`. @@ -748,7 +734,7 @@ def ho2ro(ho: np.ndarray) -> np.ndarray: return ro -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def cu2ro_single(cu: np.ndarray) -> np.ndarray: """Convert a single set of cubochoric coordinates to an un-normalized Rodrigues vector :cite:`rowenhorst2015consistent`. @@ -774,7 +760,7 @@ def cu2ro_single(cu: np.ndarray) -> np.ndarray: return ho2ro_single(cu2ho_single(cu)) -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def cu2ro_2d(cu: np.ndarray) -> np.ndarray: """Convert multiple cubochoric coordinates to un-normalized Rodrigues vectors :cite:`rowenhorst2015consistent`. @@ -814,7 +800,7 @@ def cu2ro(cu: np.ndarray) -> np.ndarray: return ro -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def eu2qu_single(eu: np.ndarray) -> np.ndarray: """Convert three Euler angles (alpha, beta, gamma) to a unit quaternion :cite:`rowenhorst2015consistent`. @@ -854,7 +840,7 @@ def eu2qu_single(eu: np.ndarray) -> np.ndarray: return qu -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def eu2qu_2d(eu: np.ndarray) -> np.ndarray: """Convert multiple Euler angles (alpha, beta, gamma) to unit quaternions. @@ -894,7 +880,7 @@ def eu2qu(eu: np.ndarray) -> np.ndarray: return qu -@nb.jit("float64[:](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:, :])", cache=True, fastmath=True, nogil=True) def om2qu_single(om: np.ndarray) -> np.ndarray: """Convert a single (3, 3) rotation matrix to a unit quaternion :cite:`rowenhorst2015consistent`. @@ -923,26 +909,26 @@ def om2qu_single(om: np.ndarray) -> np.ndarray: qu = np.zeros(4, dtype=np.float64) - if a_almost < FLOAT_EPS: + if a_almost < constants.eps9: qu[0] = 0 else: qu[0] = 0.5 * np.sqrt(a_almost) - if b_almost < FLOAT_EPS: + if b_almost < constants.eps9: qu[1] = 0 elif om[2, 1] < om[1, 2]: qu[1] = -0.5 * np.sqrt(b_almost) else: qu[1] = 0.5 * np.sqrt(b_almost) - if c_almost < FLOAT_EPS: + if c_almost < constants.eps9: qu[2] = 0 elif om[0, 2] < om[2, 0]: qu[2] = -0.5 * np.sqrt(c_almost) else: qu[2] = 0.5 * np.sqrt(c_almost) - if d_almost < FLOAT_EPS: + if d_almost < constants.eps9: qu[3] = 0 elif om[1, 0] < om[0, 1]: qu[3] = -0.5 * np.sqrt(d_almost) @@ -955,7 +941,7 @@ def om2qu_single(om: np.ndarray) -> np.ndarray: return qu -@nb.jit("float64[:, :](float64[:, :, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :, :])", cache=True, fastmath=True, nogil=True) def om2qu_3d(om: np.ndarray) -> np.ndarray: """Convert multiple rotation matrices to unit quaternions :cite:`rowenhorst2015consistent`. @@ -995,7 +981,7 @@ def om2qu(om: np.ndarray) -> np.ndarray: return qu -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def qu2eu_single(qu: np.ndarray) -> np.ndarray: """Convert a unit quaternion to three Euler angles :cite:`rowenhorst2015consistent`. @@ -1025,8 +1011,8 @@ def qu2eu_single(qu: np.ndarray) -> np.ndarray: q_bc = (qu[1] * qu[1]) + (qu[2] * qu[2]) chi = np.sqrt(q_ad * q_bc) - if chi < FLOAT_EPS: - if q_bc < FLOAT_EPS: + if chi < constants.eps9: + if q_bc < constants.eps9: a = -2 * qu[0] * qu[3] b = qu[0] * qu[0] - qu[3] * qu[3] else: @@ -1045,12 +1031,12 @@ def qu2eu_single(qu: np.ndarray) -> np.ndarray: eu[1] = np.arctan2(2 * chi, q_ad - q_bc) eu[2] = np.arctan2(eu_2a, eu_2b) - eu[np.abs(eu) < FLOAT_EPS] = 0 + eu[np.abs(eu) < constants.eps9] = 0 return np.mod(eu, np.pi * 2) -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def qu2eu_2d(qu: np.ndarray) -> np.ndarray: """Convert multiple unit quaternions to Euler angles. @@ -1089,7 +1075,7 @@ def qu2eu(qu: np.ndarray) -> np.ndarray: return eu -@nb.jit("float64[:, :](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:])", cache=True, fastmath=True, nogil=True) def qu2om_single(qu: np.ndarray) -> np.ndarray: """Convert a unit quaternion to an orthogonal rotation matrix :cite:`rowenhorst2015consistent`. @@ -1137,7 +1123,7 @@ def qu2om_single(qu: np.ndarray) -> np.ndarray: return om -@nb.jit("float64[:, :, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def qu2om_2d(qu: np.ndarray) -> np.ndarray: """Convert multiple unit quaternions to orthogonal rotation matrices. @@ -1177,7 +1163,7 @@ def qu2om(qu: np.ndarray) -> np.ndarray: return om -@nb.jit("float64[:](float64[:])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:](float64[:])", cache=True, fastmath=True, nogil=True) def qu2ho_single(qu: np.ndarray) -> np.ndarray: """Convert a single (un)normalized quaternion to a normalized homochoric vector :cite:`rowenhorst2015consistent`. @@ -1201,7 +1187,7 @@ def qu2ho_single(qu: np.ndarray) -> np.ndarray: """ omega = 2 * np.arccos(qu[0]) - if omega < FLOAT_EPS: + if omega < constants.eps9: return np.zeros(3, dtype=np.float64) s = np.sqrt(np.sum(np.square(qu[1:]))) @@ -1212,7 +1198,7 @@ def qu2ho_single(qu: np.ndarray) -> np.ndarray: return ho -@nb.jit("float64[:, :](float64[:, :])", cache=True, nogil=True, nopython=True) +@nb.njit("float64[:, :](float64[:, :])", cache=True, fastmath=True, nogil=True) def qu2ho_2d(qu: np.ndarray) -> np.ndarray: """Convert multiple (un)normalized quaternions to normalized homochoric vectors :cite:`rowenhorst2015consistent`. diff --git a/orix/quaternion/misorientation.py b/orix/quaternion/misorientation.py index fb10c6ea..1437e9b0 100644 --- a/orix/quaternion/misorientation.py +++ b/orix/quaternion/misorientation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/quaternion/orientation.py b/orix/quaternion/orientation.py index d654f3d5..4a013248 100644 --- a/orix/quaternion/orientation.py +++ b/orix/quaternion/orientation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/quaternion/orientation_region.py b/orix/quaternion/orientation_region.py index 988105d4..ea91e6fa 100644 --- a/orix/quaternion/orientation_region.py +++ b/orix/quaternion/orientation_region.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -23,13 +22,12 @@ import numpy as np +from orix import constants from orix.quaternion import Quaternion from orix.quaternion.rotation import Rotation from orix.quaternion.symmetry import C1, Symmetry, get_distinguished_points from orix.vector import Rodrigues -_FLOAT_EPS = 1e-9 # Small number to avoid round off problems - def _get_large_cell_normals(s1, s2): dp = get_distinguished_points(s1, s2) @@ -133,8 +131,8 @@ def __gt__(self, other: OrientationRegion) -> np.ndarray: """ c = Quaternion(self).dot_outer(Quaternion(other)) inside = np.logical_or( - np.all(np.greater_equal(c, -_FLOAT_EPS), axis=0), - np.all(np.less_equal(c, +_FLOAT_EPS), axis=0), + np.all(np.greater_equal(c, -constants.eps9), axis=0), + np.all(np.less_equal(c, constants.eps9), axis=0), ) return inside @@ -204,8 +202,8 @@ def get_plot_data(self) -> Rotation: from orix.vector import Vector3d # Get a grid of vector directions - theta = np.linspace(0, 2 * np.pi - _FLOAT_EPS, 361) - rho = np.linspace(0, np.pi - _FLOAT_EPS, 181) + theta = np.linspace(0, 2 * np.pi - constants.eps9, 361) + rho = np.linspace(0, np.pi - constants.eps9, 181) theta, rho = np.meshgrid(theta, rho) g = Vector3d.from_polar(rho, theta) diff --git a/orix/quaternion/quaternion.py b/orix/quaternion/quaternion.py index 5b77a39c..f271dbf8 100644 --- a/orix/quaternion/quaternion.py +++ b/orix/quaternion/quaternion.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -23,11 +22,12 @@ import dask.array as da from dask.diagnostics import ProgressBar +import numba as nb import numpy as np -import quaternion from scipy.spatial.transform import Rotation as SciPyRotation from orix._base import Object3d +from orix.constants import installed from orix.quaternion import _conversions from orix.vector import AxAngle, Homochoric, Miller, Rodrigues, Vector3d @@ -48,9 +48,8 @@ class Quaternion(Object3d): .. math:: - i^2 = j^2 = k^2 = -1; - - ij = -ji = k; jk = -kj = i; ki = -ik = j. + i^2 &= j^2 = k^2 = -1;\\ + ij &= -ji = k; jk = -kj = i; ki = -ik = j. In orix, quaternions are stored with the scalar part first followed by the vector part, denoted :math:`Q = (a, b, c, d)`. @@ -61,12 +60,9 @@ class Quaternion(Object3d): .. math:: - a_3 = a_1 \cdot a_2 - b_1 \cdot b_2 - c_1 \cdot c_2 - d_1 \cdot d_2 - - b_3 = a_1 \cdot b_2 + b_1 \cdot a_2 + c_1 \cdot d_2 - d_1 \cdot c_2 - - c_3 = a_1 \cdot c_2 - b_1 \cdot d_2 + c_1 \cdot a_2 + d_1 \cdot b_2 - + a_3 = a_1 \cdot a_2 - b_1 \cdot b_2 - c_1 \cdot c_2 - d_1 \cdot d_2\\ + b_3 = a_1 \cdot b_2 + b_1 \cdot a_2 + c_1 \cdot d_2 - d_1 \cdot c_2\\ + c_3 = a_1 \cdot c_2 - b_1 \cdot d_2 + c_1 \cdot a_2 + d_1 \cdot b_2\\ d_3 = a_1 \cdot d_2 + b_1 \cdot c_2 - c_1 \cdot b_2 + d_1 \cdot a_2 Rotation of a 3D vector :math:`v = (x, y, z)` by a quaternion is @@ -74,11 +70,9 @@ class Quaternion(Object3d): .. math:: - v'_x = x(a^2 + b^2 - c^2 - d^2) + 2[z(a \cdot c + b \cdot d) + y(b \cdot c - a \cdot d)] - - v'_y = y(a^2 - b^2 + c^2 - d^2) + 2[x(a \cdot d + b \cdot c) + z(c \cdot d - a \cdot b)] - - v'_z = z(a^2 - b^2 - c^2 + d^2) + 2[y(a \cdot b + c \cdot d) + x(b \cdot d - a \cdot c)] + v'_x = x + 2a(cz - dy) - 2d(dx - bz) + 2c(by - cx)\\ + v'_y = y + 2d(cz - dy) + 2a(dx - bz) - 2b(by - cx)\\ + v'_z = z - 2c(cz - dy) + 2b(dx - bz) + 2a(by - cx) The norm of a quaternion is defined as @@ -120,66 +114,38 @@ class Quaternion(Object3d): @property def a(self) -> np.ndarray: - """Return or set the scalar quaternion component. - - Parameters - ---------- - value : numpy.ndarray - Scalar quaternion component. - """ + """Return or set the scalar quaternion component.""" return self.data[..., 0] @a.setter - def a(self, value: np.ndarray): - """Set the scalar quaternion component.""" + def a(self, value: np.ndarray) -> None: self.data[..., 0] = value @property def b(self) -> np.ndarray: - """Return or set the first vector quaternion component. - - Parameters - ---------- - value : numpy.ndarray - First vector quaternion component. - """ + """Return or set the first vector quaternion component.""" return self.data[..., 1] @b.setter - def b(self, value: np.ndarray): - """Set the first vector quaternion component.""" + def b(self, value: np.ndarray) -> None: self.data[..., 1] = value @property def c(self) -> np.ndarray: - """Return or set the second vector quaternion component. - - Parameters - ---------- - value : numpy.ndarray - Second vector quaternion component. - """ + """Return or set the second vector quaternion component.""" return self.data[..., 2] @c.setter - def c(self, value: np.ndarray): - """Set the second vector quaternion component.""" + def c(self, value: np.ndarray) -> None: self.data[..., 2] = value @property def d(self) -> np.ndarray: - """Return or set the third vector quaternion component. - - Parameters - ---------- - value : numpy.ndarray - Third vector quaternion component. - """ + """Return or set the third vector quaternion component.""" return self.data[..., 3] @d.setter - def d(self, value: np.ndarray): - """Set the third vector quaternion component.""" + def d(self, value: np.ndarray) -> None: self.data[..., 3] = value @property @@ -208,29 +174,52 @@ def antipodal(self) -> Quaternion: @property def conj(self) -> Quaternion: r"""Return the conjugate of the quaternion - :math:`Q^* = a - bi - cj - dk`. + :math:`Q^{*} = a - bi - cj - dk`. """ - Q = quaternion.from_float_array(self.data).conj() - return self.__class__(quaternion.as_float_array(Q)) + if installed["numpy-quaternion"]: + import quaternion + + qu2 = quaternion.from_float_array(self.data).conj() + qu2 = quaternion.as_float_array(qu2) + else: # pragma: no cover + qu1 = self.data.astype(np.float64) + qu2 = np.empty_like(qu1) + qu_conj_gufunc(qu1, qu2) + Q = self.__class__(qu2) + return Q # ------------------------ Dunder methods ------------------------ # def __invert__(self) -> Quaternion: return self.__class__(self.conj.data / (self.norm**2)[..., np.newaxis]) - def __mul__(self, other: Union[Quaternion, Vector3d]): + def __mul__( + self, other: Union[Quaternion, Vector3d] + ) -> Union[Quaternion, Vector3d]: if isinstance(other, Quaternion): - Q1 = quaternion.from_float_array(self.data) - Q2 = quaternion.from_float_array(other.data) - return other.__class__(quaternion.as_float_array(Q1 * Q2)) + if installed["numpy-quaternion"]: + import quaternion + + qu1 = quaternion.from_float_array(self.data) + qu2 = quaternion.from_float_array(other.data) + qu12 = quaternion.as_float_array(qu1 * qu2) + else: # pragma: no cover + qu12 = qu_multiply(self.data, other.data) + Q = self.__class__(qu12) + return Q elif isinstance(other, Vector3d): - # check broadcast shape is correct before calculation, as - # quaternion.rotat_vectors will perform outer product - # this keeps current __mul__ broadcast behaviour - Q1 = quaternion.from_float_array(self.data) - v = quaternion.as_vector_part( - (Q1 * quaternion.from_vector_part(other.data)) * ~Q1 - ) + if installed["numpy-quaternion"]: + import quaternion + + # Don't use rotate_vectors as it may perform an outer + # product. The following keeps current __mul__ broadcast + # behavior. + qu = quaternion.from_float_array(self.data) + v = quaternion.as_vector_part( + (qu * quaternion.from_vector_part(other.data)) * ~qu + ) + else: # pragma: no cover + v = qu_rotate_vec(self.unit.data, other.data) if isinstance(other, Miller): m = other.__class__(xyz=v, phase=other.phase) m.coordinate_format = other.coordinate_format @@ -243,7 +232,7 @@ def __neg__(self) -> Quaternion: return self.__class__(-self.data) def __eq__(self, other: Union[Any, Quaternion]) -> bool: - """Check if quaternions have equal shapes and values.""" + """Check if quaternions have equal shapes and components.""" if ( isinstance(other, Quaternion) and self.shape == other.shape @@ -302,8 +291,8 @@ def from_axes_angles( if degrees: angles = np.deg2rad(angles) - Q = _conversions.ax2qu(axes, angles) - Q = cls(Q) + qu = _conversions.ax2qu(axes, angles) + Q = cls(qu) Q = Q.unit return Q @@ -1077,37 +1066,49 @@ def outer( if isinstance(other, Quaternion): if lazy: darr = self._outer_dask(other, chunk_size=chunk_size) - arr = np.empty(darr.shape) + qu = np.empty(darr.shape) if progressbar: with ProgressBar(): - da.store(darr, arr) + da.store(darr, qu) else: - da.store(darr, arr) + da.store(darr, qu) else: - Q1 = quaternion.from_float_array(self.data) - Q2 = quaternion.from_float_array(other.data) - # np.outer works with flattened array - Q = np.outer(Q1, Q2).reshape(Q1.shape + Q2.shape) - arr = quaternion.as_float_array(Q) - return other.__class__(arr) + if installed["numpy-quaternion"]: + import quaternion + + qu1 = quaternion.from_float_array(self.data) + qu2 = quaternion.from_float_array(other.data) + # np.outer works with flattened array + qu12 = np.outer(qu1, qu2).reshape(*qu1.shape, *qu2.shape) + qu = quaternion.as_float_array(qu12) + else: # pragma: no cover + Q12 = Quaternion(self).reshape(-1, 1) * other.reshape(1, -1) + qu = Q12.data.reshape(*self.shape, *other.shape, 4) + return other.__class__(qu) elif isinstance(other, Vector3d): if lazy: darr = self._outer_dask(other, chunk_size=chunk_size) - arr = np.empty(darr.shape) + v_arr = np.empty(darr.shape) if progressbar: with ProgressBar(): - da.store(darr, arr) + da.store(darr, v_arr) else: - da.store(darr, arr) + da.store(darr, v_arr) else: - Q = quaternion.from_float_array(self.data) - arr = quaternion.rotate_vectors(Q, other.data) + if installed["numpy-quaternion"]: + import quaternion + + qu = quaternion.from_float_array(self.data) + v_arr = quaternion.rotate_vectors(qu, other.data) + else: # pragma: no cover + v = Quaternion(self).reshape(-1, 1) * other.reshape(1, -1) + v_arr = v.reshape(*self.shape, *other.shape).data if isinstance(other, Miller): - m = other.__class__(xyz=arr, phase=other.phase) + m = other.__class__(xyz=v_arr, phase=other.phase) m.coordinate_format = other.coordinate_format return m else: - return other.__class__(arr) + return other.__class__(v_arr) else: raise NotImplementedError( "This operation is currently not avaliable in orix, please use outer " @@ -1237,3 +1238,73 @@ def _outer_dask( new_chunks = tuple(chunks1[:-1]) + tuple(chunks2[:-1]) + (-1,) return out.rechunk(new_chunks) + + +# ------------------- Numba accelerated functions -------------------- # +# Functions with Numba decorators are compiled to machine code at run +# time (just-in-time) and cached for later calls. +# +# Some functions are generalized universal functions (gufuncs), +# https://numba.readthedocs.io/en/stable/user/vectorize.html. +# Array shapes are determined from signatures such as (n)->(n), meaning +# the input and output arrays both have single dimensions of size n. +# The final input parameter (array) is overwritten inside the function, +# with no return. +# Ensure float64 to avoid surprising errors (some occured during +# testing). + + +@nb.guvectorize("(n)->(n)", cache=True) +def qu_conj_gufunc(qu: np.ndarray, qu2: np.ndarray) -> None: # pragma: no cover + qu2[0] = qu[0] + qu2[1] = -qu[1] + qu2[2] = -qu[2] + qu2[3] = -qu[3] + + +@nb.guvectorize("(n),(n)->(n)", cache=True) +def qu_multiply_gufunc( + qu1: np.ndarray, qu2: np.ndarray, qu12: np.ndarray +) -> None: # pragma: no cover + qu12[0] = qu1[0] * qu2[0] - qu1[1] * qu2[1] - qu1[2] * qu2[2] - qu1[3] * qu2[3] + qu12[1] = qu1[1] * qu2[0] + qu1[0] * qu2[1] - qu1[3] * qu2[2] + qu1[2] * qu2[3] + qu12[2] = qu1[2] * qu2[0] + qu1[3] * qu2[1] + qu1[0] * qu2[2] - qu1[1] * qu2[3] + qu12[3] = qu1[3] * qu2[0] - qu1[2] * qu2[1] + qu1[1] * qu2[2] + qu1[0] * qu2[3] + + +def qu_multiply(qu1: np.ndarray, qu2: np.ndarray) -> np.ndarray: # pragma: no cover + shape = np.broadcast_shapes(qu1.shape, qu2.shape) + if not np.issubdtype(qu1.dtype, np.float64): + qu1 = qu1.astype(np.float64) + if not np.issubdtype(qu2.dtype, np.float64): + qu2 = qu2.astype(np.float64) + qu12 = np.empty(shape, dtype=np.float64) + qu_multiply_gufunc(qu1, qu2, qu12) + return qu12 + + +@nb.guvectorize("(n),(m)->(m)", cache=True) +def qu_rotate_vec_gufunc( + qu: np.ndarray, v1: np.ndarray, v2: np.ndarray +) -> None: # pragma: no cover + a, b, c, d = qu + x, y, z = v1 + tx = 2 * (c * z - d * y) + ty = 2 * (d * x - b * z) + tz = 2 * (b * y - c * x) + v2[0] = x + a * tx - d * ty + c * tz + v2[1] = y + d * tx + a * ty - b * tz + v2[2] = z - c * tx + b * ty + a * tz + + +def qu_rotate_vec(qu: np.ndarray, v: np.ndarray) -> np.ndarray: # pragma: no cover + qu = np.atleast_2d(qu) + v = np.atleast_2d(v) + shape = np.broadcast_shapes(qu.shape[:-1], v.shape[:-1]) + (3,) + if not np.issubdtype(qu.dtype, np.float64): + qu = qu.astype(np.float64) + if not np.issubdtype(v.dtype, np.float64): + v = v.astype(np.float64) + v2 = np.empty(shape, dtype=np.float64) + qu_rotate_vec_gufunc(qu, v, v2) + return v2 diff --git a/orix/quaternion/rotation.py b/orix/quaternion/rotation.py index 3042d3e5..4c6701a8 100644 --- a/orix/quaternion/rotation.py +++ b/orix/quaternion/rotation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/quaternion/symmetry.py b/orix/quaternion/symmetry.py index 1e940255..9f13ab31 100644 --- a/orix/quaternion/symmetry.py +++ b/orix/quaternion/symmetry.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/sampling/S2_sampling.py b/orix/sampling/S2_sampling.py index 83616961..b92b3848 100644 --- a/orix/sampling/S2_sampling.py +++ b/orix/sampling/S2_sampling.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/sampling/SO3_sampling.py b/orix/sampling/SO3_sampling.py index 366e082e..92c07ebf 100644 --- a/orix/sampling/SO3_sampling.py +++ b/orix/sampling/SO3_sampling.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -31,7 +30,7 @@ def uniform_SO3_sample( resolution: Union[int, float], method: str = "cubochoric", unique: bool = True, - **kwargs + **kwargs, ) -> Rotation: r"""Uniform sampling of *SO(3)* by a number of methods. diff --git a/orix/sampling/__init__.py b/orix/sampling/__init__.py index 0f051f15..29c50fa0 100644 --- a/orix/sampling/__init__.py +++ b/orix/sampling/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/sampling/_cubochoric_sampling.py b/orix/sampling/_cubochoric_sampling.py index 9b9f8f9d..032c21bf 100644 --- a/orix/sampling/_cubochoric_sampling.py +++ b/orix/sampling/_cubochoric_sampling.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/sampling/_polyhedral_sampling.py b/orix/sampling/_polyhedral_sampling.py index 2e60b9fd..362c30eb 100644 --- a/orix/sampling/_polyhedral_sampling.py +++ b/orix/sampling/_polyhedral_sampling.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/sampling/sample_generators.py b/orix/sampling/sample_generators.py index 6bdffae9..20cb4a3b 100644 --- a/orix/sampling/sample_generators.py +++ b/orix/sampling/sample_generators.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -32,7 +31,7 @@ def get_sample_fundamental( point_group: Optional[Symmetry] = None, space_group: Optional[int] = None, method: str = "cubochoric", - **kwargs + **kwargs, ) -> Rotation: """Return an equispaced grid of rotations within a fundamental zone. @@ -96,7 +95,7 @@ def get_sample_local( center: Optional[Rotation] = None, grid_width: Union[int, float] = 10, method: str = "cubochoric", - **kwargs + **kwargs, ) -> Rotation: """Return a grid of rotations about a given rotation. diff --git a/orix/tests/conftest.py b/orix/tests/conftest.py index da5b7a40..12f610a7 100644 --- a/orix/tests/conftest.py +++ b/orix/tests/conftest.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -25,23 +24,26 @@ 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 +# --------------------------- pytest hooks --------------------------- # + def pytest_sessionstart(session): # pragma: no cover plt.rcParams["backend"] = "agg" -@pytest.fixture -def rotations(): - return Rotation([(2, 4, 6, 8), (-1, -3, -5, -7)]) - +# -------------------- Control of test selection --------------------- # -@pytest.fixture() -def eu(): - return np.random.rand(10, 3) +skipif_numpy_quaternion_present = pytest.mark.skipif( + constants.installed["numpy-quaternion"], reason="numpy-quaternion installed" +) +skipif_numpy_quaternion_missing = pytest.mark.skipif( + not constants.installed["numpy-quaternion"], reason="numpy-quaternion not installed" +) # ---------------------------- IO fixtures --------------------------- # @@ -1164,7 +1166,7 @@ def crystal_map(crystal_map_input): # ---------- Rotation representations for conversion tests ----------- # -# NOTE to future test writers on unittest data: +# NOTE: to future test writers on unittest data: # All the data below can be recreated using 3Drotations, which is # available at # https://github.com/marcdegraef/3Drotations/blob/master/src/python. @@ -1372,6 +1374,19 @@ def euler_angles(): # ------- End of rotation representations for conversion tests ------- # +# ------------------------ Geometry fixtures ------------------------- # + + +@pytest.fixture +def rotations(): + return Rotation([(2, 4, 6, 8), (-1, -3, -5, -7)]) + + +@pytest.fixture() +def eu(): + return np.random.rand(10, 3) + + @pytest.fixture(autouse=True) def import_to_namespace(doctest_namespace): """Make :mod:`numpy` and :mod:`matplotlib.pyplot` available in diff --git a/orix/tests/data/__init__.py b/orix/tests/data/__init__.py index cd58d0cb..913f79e7 100644 --- a/orix/tests/data/__init__.py +++ b/orix/tests/data/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/data/test_data.py b/orix/tests/data/test_data.py index 226f8e3d..2c6cf531 100644 --- a/orix/tests/data/test_data.py +++ b/orix/tests/data/test_data.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/io/__init__.py b/orix/tests/io/__init__.py index cd58d0cb..913f79e7 100644 --- a/orix/tests/io/__init__.py +++ b/orix/tests/io/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/io/test_ang.py b/orix/tests/io/test_ang.py index 4e091f93..bcbb39ea 100644 --- a/orix/tests/io/test_ang.py +++ b/orix/tests/io/test_ang.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/io/test_bruker_h5ebsd.py b/orix/tests/io/test_bruker_h5ebsd.py index b186f0d5..b2d2b7f5 100644 --- a/orix/tests/io/test_bruker_h5ebsd.py +++ b/orix/tests/io/test_bruker_h5ebsd.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/io/test_emsoft_h5ebsd.py b/orix/tests/io/test_emsoft_h5ebsd.py index 83829218..443dcb51 100644 --- a/orix/tests/io/test_emsoft_h5ebsd.py +++ b/orix/tests/io/test_emsoft_h5ebsd.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/io/test_h5ebsd.py b/orix/tests/io/test_h5ebsd.py index 89ce8bbf..c364e71a 100644 --- a/orix/tests/io/test_h5ebsd.py +++ b/orix/tests/io/test_h5ebsd.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/io/test_io.py b/orix/tests/io/test_io.py index 12719c47..b0c9aa78 100644 --- a/orix/tests/io/test_io.py +++ b/orix/tests/io/test_io.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/io/test_orix_hdf5.py b/orix/tests/io/test_orix_hdf5.py index 90cebe66..11796600 100644 --- a/orix/tests/io/test_orix_hdf5.py +++ b/orix/tests/io/test_orix_hdf5.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/plot/__init__.py b/orix/tests/plot/__init__.py index cd58d0cb..913f79e7 100644 --- a/orix/tests/plot/__init__.py +++ b/orix/tests/plot/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/plot/test_crystal_map_plot.py b/orix/tests/plot/test_crystal_map_plot.py index 3766a32e..bf706425 100644 --- a/orix/tests/plot/test_crystal_map_plot.py +++ b/orix/tests/plot/test_crystal_map_plot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -375,7 +374,8 @@ def test_status_bar_silence_default_format_coord(self, crystal_map): fig = plt.figure() ax = fig.add_subplot(projection=PLOT_MAP) _ = ax.plot_map(crystal_map) - assert ax.format_coord(0, 0) == "(x, y) = (0, 0)" + # TODO: Remove (x, y) = (0, 0) when requuring matplotlib > 3.8 + assert ax.format_coord(0, 0) in ["(x, y) = (0, 0)", "x=0 y=0"] fig = plt.figure() ax = fig.add_subplot(projection=PLOT_MAP) diff --git a/orix/tests/plot/test_direction_color_keys.py b/orix/tests/plot/test_direction_color_keys.py index e2b1624f..f8b80358 100644 --- a/orix/tests/plot/test_direction_color_keys.py +++ b/orix/tests/plot/test_direction_color_keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/plot/test_inverse_pole_figure_plot.py b/orix/tests/plot/test_inverse_pole_figure_plot.py index e5d0fd8a..9effca39 100644 --- a/orix/tests/plot/test_inverse_pole_figure_plot.py +++ b/orix/tests/plot/test_inverse_pole_figure_plot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/plot/test_orientation_color_keys.py b/orix/tests/plot/test_orientation_color_keys.py index 7c360012..6bd58254 100644 --- a/orix/tests/plot/test_orientation_color_keys.py +++ b/orix/tests/plot/test_orientation_color_keys.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/plot/test_plotting_utilities.py b/orix/tests/plot/test_plotting_utilities.py index 5c865e61..f3a860c3 100644 --- a/orix/tests/plot/test_plotting_utilities.py +++ b/orix/tests/plot/test_plotting_utilities.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/plot/test_rotation_plot.py b/orix/tests/plot/test_rotation_plot.py index 8fe36e1c..5a26e024 100644 --- a/orix/tests/plot/test_rotation_plot.py +++ b/orix/tests/plot/test_rotation_plot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/plot/test_stereographic_plot.py b/orix/tests/plot/test_stereographic_plot.py index 4be1cd9b..3239682d 100644 --- a/orix/tests/plot/test_stereographic_plot.py +++ b/orix/tests/plot/test_stereographic_plot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/plot/test_unit_cell_plot.py b/orix/tests/plot/test_unit_cell_plot.py index 25caa2da..cba3e265 100644 --- a/orix/tests/plot/test_unit_cell_plot.py +++ b/orix/tests/plot/test_unit_cell_plot.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/quaternion/__init__.py b/orix/tests/quaternion/__init__.py index cd58d0cb..913f79e7 100644 --- a/orix/tests/quaternion/__init__.py +++ b/orix/tests/quaternion/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/quaternion/test_conversions.py b/orix/tests/quaternion/test_conversions.py index 6411f429..11a701f8 100644 --- a/orix/tests/quaternion/test_conversions.py +++ b/orix/tests/quaternion/test_conversions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/quaternion/test_orientation.py b/orix/tests/quaternion/test_orientation.py index 2a87982b..1fac045c 100644 --- a/orix/tests/quaternion/test_orientation.py +++ b/orix/tests/quaternion/test_orientation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/quaternion/test_orientation_region.py b/orix/tests/quaternion/test_orientation_region.py index c1513490..e377cd40 100644 --- a/orix/tests/quaternion/test_orientation_region.py +++ b/orix/tests/quaternion/test_orientation_region.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/quaternion/test_quaternion.py b/orix/tests/quaternion/test_quaternion.py index e3959164..c48b122c 100644 --- a/orix/tests/quaternion/test_quaternion.py +++ b/orix/tests/quaternion/test_quaternion.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -48,11 +47,6 @@ def quaternion(request): return Quaternion(request.param) -@pytest.fixture -def identity(): - return Quaternion((1, 0, 0, 0)) - - @pytest.fixture( params=[ (0.881, 0.665, 0.123, 0.517), @@ -105,8 +99,8 @@ def test_mul(self, quaternion, something): assert np.allclose(q1.c, qa * sc - qb * sd + qc * sa + qd * sb) assert np.allclose(q1.d, qa * sd + qb * sc - qc * sb + qd * sa) - def test_mul_identity(self, quaternion, identity): - assert np.allclose((quaternion * identity).data, quaternion.data) + def test_mul_identity(self, quaternion): + assert np.allclose((quaternion * Quaternion.identity()).data, quaternion.data) def test_no_multiplicative_inverse(self, quaternion, something): q1 = quaternion * something @@ -162,10 +156,21 @@ def test_dot_outer(self, quaternion, something): ], ) def test_multiply_vector(self, quaternion, vector, expected): - q = Quaternion(quaternion) - v = Vector3d(vector) - v_new = q * v - assert np.allclose(v_new.data, expected) + Q1 = Quaternion(quaternion) + v1 = Vector3d(vector) + v2 = Q1 * v1 + assert np.allclose(v2.data, expected) + + def test_multiply_vector_float32(self): + Q1 = Quaternion.random() + v1 = Vector3d.random() + + Q2 = Quaternion(Q1) + Q2._data = Q2._data.astype(np.float32) + + v2 = Q1 * v1 + v3 = Q2 * v1 + assert np.allclose(v3.data, v2.data, atol=1e-6) def test_abcd_properties(self): quat = Quaternion([2, 2, 2, 2]) diff --git a/orix/tests/quaternion/test_rotation.py b/orix/tests/quaternion/test_rotation.py index ada61c02..e24b0dd2 100644 --- a/orix/tests/quaternion/test_rotation.py +++ b/orix/tests/quaternion/test_rotation.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/quaternion/test_symmetry.py b/orix/tests/quaternion/test_symmetry.py index 0f649428..bd7ac463 100644 --- a/orix/tests/quaternion/test_symmetry.py +++ b/orix/tests/quaternion/test_symmetry.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/sampling/__init__.py b/orix/tests/sampling/__init__.py index cd58d0cb..913f79e7 100644 --- a/orix/tests/sampling/__init__.py +++ b/orix/tests/sampling/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/sampling/test_cubochoric_sampling.py b/orix/tests/sampling/test_cubochoric_sampling.py index 623fa151..d682b04d 100644 --- a/orix/tests/sampling/test_cubochoric_sampling.py +++ b/orix/tests/sampling/test_cubochoric_sampling.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/sampling/test_s2_sampling.py b/orix/tests/sampling/test_s2_sampling.py index 9ceab5b6..b3e28638 100644 --- a/orix/tests/sampling/test_s2_sampling.py +++ b/orix/tests/sampling/test_s2_sampling.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/sampling/test_sampling.py b/orix/tests/sampling/test_sampling.py index 770827a5..207e1486 100644 --- a/orix/tests/sampling/test_sampling.py +++ b/orix/tests/sampling/test_sampling.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -204,7 +203,7 @@ def test_get_sample_reduced_fundamental(self): # Some rotations have a phi1 Euler angle of multiples of pi, # presumably due to rounding errors phi1_C1 = R_C1.to_euler()[:, 0].round(7) - assert np.allclose(np.unique(phi1_C1), [0, 2 * np.pi], atol=1e-7) + assert np.allclose(np.unique(phi1_C1), 0, atol=1e-7) phi1_C4 = R_C4.to_euler()[:, 0].round(7) assert np.allclose(np.unique(phi1_C4), [0, np.pi / 2], atol=1e-7) phi1_C6 = R_C6.to_euler()[:, 0].round(7) diff --git a/orix/tests/test_axangle.py b/orix/tests/test_axangle.py index 0a2a6cc4..27f02497 100644 --- a/orix/tests/test_axangle.py +++ b/orix/tests/test_axangle.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/test_constants.py b/orix/tests/test_constants.py new file mode 100644 index 00000000..aa171ecc --- /dev/null +++ b/orix/tests/test_constants.py @@ -0,0 +1,30 @@ +# Copyright 2018-2024 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 . + +from orix import constants + +from .conftest import skipif_numpy_quaternion_missing, skipif_numpy_quaternion_present + + +class TestConstants: + @skipif_numpy_quaternion_present + def test_numpy_quaternion_not_installed(self): + assert not constants.installed["numpy-quaternion"] + + @skipif_numpy_quaternion_missing + def test_numpy_quaternion_installed(self): + assert constants.installed["numpy-quaternion"] diff --git a/orix/tests/test_crystal_map.py b/orix/tests/test_crystal_map.py index a04c65d8..c4881108 100644 --- a/orix/tests/test_crystal_map.py +++ b/orix/tests/test_crystal_map.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/test_crystal_map_properties.py b/orix/tests/test_crystal_map_properties.py index 74f13ed6..d3b936d0 100644 --- a/orix/tests/test_crystal_map_properties.py +++ b/orix/tests/test_crystal_map_properties.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/test_fundamental_sector.py b/orix/tests/test_fundamental_sector.py index 00d16ae7..6a47169c 100644 --- a/orix/tests/test_fundamental_sector.py +++ b/orix/tests/test_fundamental_sector.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/test_measure.py b/orix/tests/test_measure.py index 59a922ea..81f987d4 100644 --- a/orix/tests/test_measure.py +++ b/orix/tests/test_measure.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/test_miller.py b/orix/tests/test_miller.py index 2e23068e..d1696966 100644 --- a/orix/tests/test_miller.py +++ b/orix/tests/test_miller.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/test_neoeuler.py b/orix/tests/test_neoeuler.py index d6ba0540..5338ec40 100644 --- a/orix/tests/test_neoeuler.py +++ b/orix/tests/test_neoeuler.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/test_object3d.py b/orix/tests/test_object3d.py index db4104ed..27e5c460 100644 --- a/orix/tests/test_object3d.py +++ b/orix/tests/test_object3d.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/test_phase_list/test_phase.py b/orix/tests/test_phase_list/test_phase.py new file mode 100644 index 00000000..be61e7d1 --- /dev/null +++ b/orix/tests/test_phase_list/test_phase.py @@ -0,0 +1,402 @@ +# Copyright 2018-2024 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 . + +from diffpy.structure import Atom, Lattice, Structure, loadStructure +import numpy as np +import pytest + +from orix.crystal_map import Phase +from orix.crystal_map.phase_list import _new_structure_matrix_from_alignment +from orix.quaternion.symmetry import O, Symmetry + + +class TestPhase: + @pytest.mark.parametrize( + "name, point_group, space_group, color, color_alias, color_rgb, structure", + [ + ( + None, + "m-3m", + None, + None, + "tab:blue", + (0.121568, 0.466666, 0.705882), + Structure(title="Super", lattice=Lattice(1, 1, 1, 90, 90, 90)), + ), + (None, "1", 1, "blue", "b", (0, 0, 1), Structure()), + ( + "al", + "43", + 207, + "xkcd:salmon", + "xkcd:salmon", + (1, 0.474509, 0.423529), + Structure(title="ni", lattice=Lattice(1, 2, 3, 90, 90, 90)), + ), + ( + "My awes0me phase!", + O, + 211, + "C1", + "tab:orange", + (1, 0.498039, 0.054901), + None, + ), + ], + ) + def test_init_phase( + self, name, point_group, space_group, color, color_alias, color_rgb, structure + ): + p = Phase( + name=name, + point_group=point_group, + space_group=space_group, + structure=structure, + color=color, + ) + + if name is None: + assert p.name == structure.title + else: + assert p.name == str(name) + + if space_group is None: + assert p.space_group is None + else: + assert p.space_group.number == space_group + + if point_group == "43": + point_group = "432" + if isinstance(point_group, Symmetry): + point_group = point_group.name + assert p.point_group.name == point_group + + assert p.color == color_alias + assert np.allclose(p.color_rgb, color_rgb, atol=1e-6) + + if structure is not None: + assert p.structure == structure + else: + assert p.structure == Structure() + + @pytest.mark.parametrize("name", [None, "al", 1, np.arange(2)]) + def test_set_phase_name(self, name): + p = Phase(name=name) + if name is None: + name = "" + assert p.name == str(name) + + @pytest.mark.parametrize( + "color, color_alias, color_rgb, fails", + [ + ("some-color", None, None, True), + ("c1", None, None, True), + ("C1", "tab:orange", (1, 0.498039, 0.054901), False), + ], + ) + def test_set_phase_color(self, color, color_alias, color_rgb, fails): + p = Phase() + if fails: + with pytest.raises(ValueError, match="Invalid RGBA argument: "): + p.color = color + else: + p.color = color + assert p.color == color_alias + assert np.allclose(p.color_rgb, color_rgb, atol=1e-6) + + @pytest.mark.parametrize( + "point_group, point_group_name, fails", + [ + (43, "432", False), + ("4321", None, True), + ("m3m", "m-3m", False), + ("43", "432", False), + ], + ) + def test_set_phase_point_group(self, point_group, point_group_name, fails): + p = Phase() + if fails: + with pytest.raises(ValueError, match=f"'{point_group}' must be of type"): + 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))] + ) + def test_set_structure(self, structure): + p = 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))) + 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.structure = [1, 2, 3, 90, 90, 90] + + @pytest.mark.parametrize( + "name, space_group, desired_sg_str, desired_pg_str, desired_ppg_str", + [ + ("al", None, "None", "None", "None"), + ("", 207, "P432", "432", "432"), + ("ni", 225, "Fm-3m", "m-3m", "432"), + ], + ) + 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") + desired = ( + f"" + ) + assert p.__repr__() == desired + assert p.__str__() == desired + + def test_deepcopy_phase(self): + p = Phase(name="al", space_group=225, color="C1") + p2 = p.deepcopy() + + desired_p_repr = ( + "" + ) + assert p.__repr__() == desired_p_repr + + p.name = "austenite" + p.space_group = 229 + p.color = "C2" + + new_desired_p_repr = ( + "" + ) + assert p.__repr__() == new_desired_p_repr + assert p2.__repr__() == desired_p_repr + + def test_shallow_copy_phase(self): + p = Phase(name="al", point_group="m-3m", color="C1") + p2 = p + + p2.name = "austenite" + p2.point_group = 43 + p2.color = "C2" + + 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"): + _ = Phase(space_group=225, point_group="432") + + @pytest.mark.parametrize( + "space_group_no, desired_point_group_name", + [(1, "1"), (50, "mmm"), (100, "4mm"), (150, "32"), (200, "m-3"), (225, "m-3m")], + ) + def test_point_group_derived_from_space_group( + self, space_group_no, desired_point_group_name + ): + p = Phase(space_group=space_group_no) + assert p.point_group.name == desired_point_group_name + + def test_set_space_group_raises(self): + space_group = "outer-space" + with pytest.raises(ValueError, match=f"'{space_group}' must be of type "): + p = Phase() + p.space_group = space_group + + def test_is_hexagonal(self): + p1 = Phase( + point_group="321", + structure=Structure(lattice=Lattice(1, 1, 2, 90, 90, 120)), + ) + p2 = Phase( + point_group="m-3m", + structure=Structure(lattice=Lattice(1, 1, 1, 90, 90, 90)), + ) + assert p1.is_hexagonal + assert not p2.is_hexagonal + + def test_structure_matrix(self): + """Structure matrix is updated assuming e1 || a, e3 || c*.""" + trigonal_lattice = Lattice(1.7, 1.7, 1.4, 90, 90, 120) + phase = Phase(point_group="321", structure=Structure(lattice=trigonal_lattice)) + lattice = phase.structure.lattice + + # Lattice parameters are unchanged + assert np.allclose(lattice.abcABG(), [1.7, 1.7, 1.4, 90, 90, 120]) + + # Structure matrix has changed internally, but not the input + # `Lattice` instance + assert not np.allclose(lattice.base, trigonal_lattice.base) + + # The expected structure matrix + # fmt: off + assert np.allclose( + lattice.base, + [ + [ 1.7, 0, 0 ], + [-0.85, 1.472, 0 ], + [ 0, 0, 1.4] + ], + atol=1e-3 + ) + # fmt: on + + # Setting the structure also updates the lattice + phase2 = phase.deepcopy() + phase2.structure = Structure(lattice=trigonal_lattice) + assert np.allclose(phase2.structure.lattice.base, lattice.base) + + # 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."): + _ = _new_structure_matrix_from_alignment(lattice.base, x="a") + + def test_triclinic_structure_matrix(self): + """Update a triclinic structure matrix.""" + # diffpy.structure aligns e1 || a*, e3 || c* by default + lat = Lattice(2, 3, 4, 70, 100, 120) + # fmt: off + assert np.allclose( + lat.base, + [ + [1.732, -0.938, -0.347], + [0, 2.819, 1.026], + [0, 0, 4 ] + ], + atol=1e-3 + ) + assert np.allclose( + _new_structure_matrix_from_alignment(lat.base, x="a", z="c*"), + [ + [ 2, 0, 0 ], + [-1.5, 2.598, 0 ], + [-0.695, 1.179, 3.759] + ], + atol=1e-3 + ) + assert np.allclose( + _new_structure_matrix_from_alignment(lat.base, x="b", z="c*"), + [ + [-1, -1.732, 0 ], + [ 3, 0, 0 ], + [+1.368, 0.012, 3.759] + ], + atol=1e-3 + ) + # fmt: on + + def test_lattice_vectors(self): + """Correct direct and reciprocal lattice vectors.""" + trigonal_lattice = Lattice(1.7, 1.7, 1.4, 90, 90, 120) + phase = Phase(point_group="321", structure=Structure(lattice=trigonal_lattice)) + + a, b, c = phase.a_axis, phase.b_axis, phase.c_axis + ar, br, cr = phase.ar_axis, phase.br_axis, phase.cr_axis + # Coordinates in direct and reciprocal crystal reference frames + assert np.allclose([a.coordinates, ar.coordinates], [1, 0, 0]) + assert np.allclose([b.coordinates, br.coordinates], [0, 1, 0]) + assert np.allclose([c.coordinates, cr.coordinates], [0, 0, 1]) + # Coordinates in cartesian crystal reference frame + assert np.allclose(a.data, [1.7, 0, 0]) + assert np.allclose(b.data, [-0.85, 1.472, 0], atol=1e-3) + assert np.allclose(c.data, [0, 0, 1.4]) + assert np.allclose(ar.data, [0.588, 0.340, 0], atol=1e-3) + assert np.allclose(br.data, [0, 0.679, 0], atol=1e-3) + assert np.allclose(cr.data, [0, 0, 0.714], atol=1e-3) + + @pytest.mark.parametrize( + ["lat", "atoms"], + [ + [ + Lattice(1, 1, 1, 90, 90, 90), + [ + Atom("C", [0, 0, 0]), + Atom("C", [0.5, 0.5, 0.5]), + Atom("C", [0.5, 0, 0]), + ], + ], + [ + Lattice(1, 1, 1, 90, 90, 120), + [ + Atom("C", [0, 0, 0]), + Atom("C", [0.5, 0, 0]), + Atom("C", [0.5, 0.5, 0.5]), + ], + ], + [ + Lattice(1, 2, 3, 90, 90, 60), + [ + Atom("C", [0, 0, 0]), + Atom("C", [0.1, 0.1, 0.6]), + Atom("C", [0.5, 0, 0]), + ], + ], + ], + ) + def test_atom_positions(self, lat, atoms): + structure = Structure(atoms, lat) + phase = Phase(structure=structure) + # xyz_cartn is independent of basis + assert np.allclose(phase.structure.xyz_cartn, structure.xyz_cartn) + + # 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): + 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): + if not np.allclose(atom_from_structure.xyz, atom_from_phase.xyz): + disagreement_found = True + break + + assert disagreement_found + + def test_from_cif(self, cif_file): + """CIF files parsed correctly with space group and all.""" + phase = Phase.from_cif(cif_file) + assert phase.space_group.number == 12 + assert phase.point_group.name == "2/m" + assert len(phase.structure) == 22 # Number of atoms + lattice = phase.structure.lattice + assert np.allclose(lattice.abcABG(), [15.5, 4.05, 6.74, 90, 105.3, 90]) + assert np.allclose( + lattice.base, [[15.5, 0, 0], [0, 4.05, 0], [-1.779, 0, 6.501]], atol=1e-3 + ) + + def test_from_cif_same_structure(self, cif_file): + phase1 = Phase.from_cif(cif_file) + structure = loadStructure(cif_file) + phase2 = Phase(structure=structure) + assert np.allclose(phase1.structure.lattice.base, phase2.structure.lattice.base) + assert np.allclose(phase1.structure.xyz, phase2.structure.xyz) diff --git a/orix/tests/test_phase_list.py b/orix/tests/test_phase_list/test_phase_list.py similarity index 53% rename from orix/tests/test_phase_list.py rename to orix/tests/test_phase_list/test_phase_list.py index 5e3782bf..5774beff 100644 --- a/orix/tests/test_phase_list.py +++ b/orix/tests/test_phase_list/test_phase_list.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -16,392 +15,12 @@ # You should have received a copy of the GNU General Public License # along with orix. If not, see . -from diffpy.structure import Atom, Lattice, Structure, loadStructure +from diffpy.structure import Lattice, Structure from diffpy.structure.spacegroups import GetSpaceGroup import numpy as np import pytest from orix.crystal_map import Phase, PhaseList -from orix.crystal_map.phase_list import _new_structure_matrix_from_alignment -from orix.quaternion.symmetry import O, Symmetry - - -class TestPhase: - @pytest.mark.parametrize( - "name, point_group, space_group, color, color_alias, color_rgb, structure", - [ - ( - None, - "m-3m", - None, - None, - "tab:blue", - (0.121568, 0.466666, 0.705882), - Structure(title="Super", lattice=Lattice(1, 1, 1, 90, 90, 90)), - ), - (None, "1", 1, "blue", "b", (0, 0, 1), Structure()), - ( - "al", - "43", - 207, - "xkcd:salmon", - "xkcd:salmon", - (1, 0.474509, 0.423529), - Structure(title="ni", lattice=Lattice(1, 2, 3, 90, 90, 90)), - ), - ( - "My awes0me phase!", - O, - 211, - "C1", - "tab:orange", - (1, 0.498039, 0.054901), - None, - ), - ], - ) - def test_init_phase( - self, name, point_group, space_group, color, color_alias, color_rgb, structure - ): - p = Phase( - name=name, - point_group=point_group, - space_group=space_group, - structure=structure, - color=color, - ) - - if name is None: - assert p.name == structure.title - else: - assert p.name == str(name) - - if space_group is None: - assert p.space_group is None - else: - assert p.space_group.number == space_group - - if point_group == "43": - point_group = "432" - if isinstance(point_group, Symmetry): - point_group = point_group.name - assert p.point_group.name == point_group - - assert p.color == color_alias - assert np.allclose(p.color_rgb, color_rgb, atol=1e-6) - - if structure is not None: - assert p.structure == structure - else: - assert p.structure == Structure() - - @pytest.mark.parametrize("name", [None, "al", 1, np.arange(2)]) - def test_set_phase_name(self, name): - p = Phase(name=name) - if name is None: - name = "" - assert p.name == str(name) - - @pytest.mark.parametrize( - "color, color_alias, color_rgb, fails", - [ - ("some-color", None, None, True), - ("c1", None, None, True), - ("C1", "tab:orange", (1, 0.498039, 0.054901), False), - ], - ) - def test_set_phase_color(self, color, color_alias, color_rgb, fails): - p = Phase() - if fails: - with pytest.raises(ValueError, match="Invalid RGBA argument: "): - p.color = color - else: - p.color = color - assert p.color == color_alias - assert np.allclose(p.color_rgb, color_rgb, atol=1e-6) - - @pytest.mark.parametrize( - "point_group, point_group_name, fails", - [ - (43, "432", False), - ("4321", None, True), - ("m3m", "m-3m", False), - ("43", "432", False), - ], - ) - def test_set_phase_point_group(self, point_group, point_group_name, fails): - p = Phase() - if fails: - with pytest.raises(ValueError, match=f"'{point_group}' must be of type"): - 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))] - ) - def test_set_structure(self, structure): - p = 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))) - 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.structure = [1, 2, 3, 90, 90, 90] - - @pytest.mark.parametrize( - "name, space_group, desired_sg_str, desired_pg_str, desired_ppg_str", - [ - ("al", None, "None", "None", "None"), - ("", 207, "P432", "432", "432"), - ("ni", 225, "Fm-3m", "m-3m", "432"), - ], - ) - 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") - desired = ( - f"" - ) - assert p.__repr__() == desired - assert p.__str__() == desired - - def test_deepcopy_phase(self): - p = Phase(name="al", space_group=225, color="C1") - p2 = p.deepcopy() - - desired_p_repr = ( - "" - ) - assert p.__repr__() == desired_p_repr - - p.name = "austenite" - p.space_group = 229 - p.color = "C2" - - new_desired_p_repr = ( - "" - ) - assert p.__repr__() == new_desired_p_repr - assert p2.__repr__() == desired_p_repr - - def test_shallow_copy_phase(self): - p = Phase(name="al", point_group="m-3m", color="C1") - p2 = p - - p2.name = "austenite" - p2.point_group = 43 - p2.color = "C2" - - 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"): - _ = Phase(space_group=225, point_group="432") - - @pytest.mark.parametrize( - "space_group_no, desired_point_group_name", - [(1, "1"), (50, "mmm"), (100, "4mm"), (150, "32"), (200, "m-3"), (225, "m-3m")], - ) - def test_point_group_derived_from_space_group( - self, space_group_no, desired_point_group_name - ): - p = Phase(space_group=space_group_no) - assert p.point_group.name == desired_point_group_name - - def test_set_space_group_raises(self): - space_group = "outer-space" - with pytest.raises(ValueError, match=f"'{space_group}' must be of type "): - p = Phase() - p.space_group = space_group - - def test_is_hexagonal(self): - p1 = Phase( - point_group="321", - structure=Structure(lattice=Lattice(1, 1, 2, 90, 90, 120)), - ) - p2 = Phase( - point_group="m-3m", - structure=Structure(lattice=Lattice(1, 1, 1, 90, 90, 90)), - ) - assert p1.is_hexagonal - assert not p2.is_hexagonal - - def test_structure_matrix(self): - """Structure matrix is updated assuming e1 || a, e3 || c*.""" - trigonal_lattice = Lattice(1.7, 1.7, 1.4, 90, 90, 120) - phase = Phase(point_group="321", structure=Structure(lattice=trigonal_lattice)) - lattice = phase.structure.lattice - - # Lattice parameters are unchanged - assert np.allclose(lattice.abcABG(), [1.7, 1.7, 1.4, 90, 90, 120]) - - # Structure matrix has changed internally, but not the input - # `Lattice` instance - assert not np.allclose(lattice.base, trigonal_lattice.base) - - # The expected structure matrix - # fmt: off - assert np.allclose( - lattice.base, - [ - [ 1.7, 0, 0 ], - [-0.85, 1.472, 0 ], - [ 0, 0, 1.4] - ], - atol=1e-3 - ) - # fmt: on - - # Setting the structure also updates the lattice - phase2 = phase.deepcopy() - phase2.structure = Structure(lattice=trigonal_lattice) - assert np.allclose(phase2.structure.lattice.base, lattice.base) - - # 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."): - _ = _new_structure_matrix_from_alignment(lattice.base, x="a") - - def test_triclinic_structure_matrix(self): - """Update a triclinic structure matrix.""" - # diffpy.structure aligns e1 || a*, e3 || c* by default - lat = Lattice(2, 3, 4, 70, 100, 120) - # fmt: off - assert np.allclose( - lat.base, - [ - [1.732, -0.938, -0.347], - [0, 2.819, 1.026], - [0, 0, 4 ] - ], - atol=1e-3 - ) - assert np.allclose( - _new_structure_matrix_from_alignment(lat.base, x="a", z="c*"), - [ - [ 2, 0, 0 ], - [-1.5, 2.598, 0 ], - [-0.695, 1.179, 3.759] - ], - atol=1e-3 - ) - assert np.allclose( - _new_structure_matrix_from_alignment(lat.base, x="b", z="c*"), - [ - [-1, -1.732, 0 ], - [ 3, 0, 0 ], - [+1.368, 0.012, 3.759] - ], - atol=1e-3 - ) - # fmt: on - - def test_lattice_vectors(self): - """Correct direct and reciprocal lattice vectors.""" - trigonal_lattice = Lattice(1.7, 1.7, 1.4, 90, 90, 120) - phase = Phase(point_group="321", structure=Structure(lattice=trigonal_lattice)) - - a, b, c = phase.a_axis, phase.b_axis, phase.c_axis - ar, br, cr = phase.ar_axis, phase.br_axis, phase.cr_axis - # Coordinates in direct and reciprocal crystal reference frames - assert np.allclose([a.coordinates, ar.coordinates], [1, 0, 0]) - assert np.allclose([b.coordinates, br.coordinates], [0, 1, 0]) - assert np.allclose([c.coordinates, cr.coordinates], [0, 0, 1]) - # Coordinates in cartesian crystal reference frame - assert np.allclose(a.data, [1.7, 0, 0]) - assert np.allclose(b.data, [-0.85, 1.472, 0], atol=1e-3) - assert np.allclose(c.data, [0, 0, 1.4]) - assert np.allclose(ar.data, [0.588, 0.340, 0], atol=1e-3) - assert np.allclose(br.data, [0, 0.679, 0], atol=1e-3) - assert np.allclose(cr.data, [0, 0, 0.714], atol=1e-3) - - @pytest.mark.parametrize( - ["lat", "atoms"], - [ - [ - Lattice(1, 1, 1, 90, 90, 90), - [ - Atom("C", [0, 0, 0]), - Atom("C", [0.5, 0.5, 0.5]), - Atom("C", [0.5, 0, 0]), - ], - ], - [ - Lattice(1, 1, 1, 90, 90, 120), - [ - Atom("C", [0, 0, 0]), - Atom("C", [0.5, 0, 0]), - Atom("C", [0.5, 0.5, 0.5]), - ], - ], - [ - Lattice(1, 2, 3, 90, 90, 60), - [ - Atom("C", [0, 0, 0]), - Atom("C", [0.1, 0.1, 0.6]), - Atom("C", [0.5, 0, 0]), - ], - ], - ], - ) - def test_atom_positions(self, lat, atoms): - structure = Structure(atoms, lat) - phase = Phase(structure=structure) - # xyz_cartn is independent of basis - assert np.allclose(phase.structure.xyz_cartn, structure.xyz_cartn) - - # 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): - 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): - if not np.allclose(atom_from_structure.xyz, atom_from_phase.xyz): - disagreement_found = True - break - - assert disagreement_found - - def test_from_cif(self, cif_file): - """CIF files parsed correctly with space group and all.""" - phase = Phase.from_cif(cif_file) - assert phase.space_group.number == 12 - assert phase.point_group.name == "2/m" - assert len(phase.structure) == 22 # Number of atoms - lattice = phase.structure.lattice - assert np.allclose(lattice.abcABG(), [15.5, 4.05, 6.74, 90, 105.3, 90]) - assert np.allclose( - lattice.base, [[15.5, 0, 0], [0, 4.05, 0], [-1.779, 0, 6.501]], atol=1e-3 - ) - - def test_from_cif_same_structure(self, cif_file): - phase1 = Phase.from_cif(cif_file) - structure = loadStructure(cif_file) - phase2 = Phase(structure=structure) - assert np.allclose(phase1.structure.lattice.base, phase2.structure.lattice.base) - assert np.allclose(phase1.structure.xyz, phase2.structure.xyz) class TestPhaseList: diff --git a/orix/tests/test_spherical_region.py b/orix/tests/test_spherical_region.py index 21c028a6..b58e2100 100644 --- a/orix/tests/test_spherical_region.py +++ b/orix/tests/test_spherical_region.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/test_stereographic_projection.py b/orix/tests/test_stereographic_projection.py index 9757e240..9172961b 100644 --- a/orix/tests/test_stereographic_projection.py +++ b/orix/tests/test_stereographic_projection.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/test_util.py b/orix/tests/test_util.py index 579f0bf3..e4e87774 100644 --- a/orix/tests/test_util.py +++ b/orix/tests/test_util.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/tests/test_vector3d.py b/orix/tests/test_vector3d.py index 7ed4c386..197af051 100644 --- a/orix/tests/test_vector3d.py +++ b/orix/tests/test_vector3d.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -763,27 +762,29 @@ def test_scatter_projection(self): def test_scatter_reproject(self): o = Orientation.from_axes_angles((-1, 8, 1), 65, degrees=True) v = (symmetry.Oh * o) * Vector3d.zvector() - # normal scatter: half of the vectors are shown - fig1 = v.scatter(hemisphere="upper", return_figure=True) - assert ( - sum(len(c.get_offsets()) for c in fig1.axes[0].collections) == v.size // 2 - ) - # reproject: all of the vectors are shown - fig2 = v.scatter(hemisphere="upper", reproject=True, return_figure=True, c="r") - assert sum(len(c.get_offsets()) for c in fig2.axes[0].collections) == v.size - # (1, 0, 0, 1) is red in RGBA - assert all( - np.allclose(c.get_edgecolor(), (1, 0, 0, 1)) - for c in fig2.axes[0].collections - ) - # reproject: all of the vectors are shown + + # Normal scatter: half of the vectors are shown + fig1 = v.scatter(return_figure=True) + assert fig1.axes[0].collections[0].get_offsets().shape == (v.size // 2, 2) + + # Reproject: all of the vectors are shown + fig2 = v.scatter(reproject=True, return_figure=True, c="r") + colls = fig2.axes[0].collections[:2] + for coll in colls[:2]: + assert coll.get_offsets().shape == (v.size // 2, 2) + assert np.allclose(coll.get_edgecolor(), (1, 0, 0, 1)) + + # Reproject: all of the vectors are shown fig3 = v.scatter(hemisphere="lower", reproject=True, return_figure=True) - assert sum(len(c.get_offsets()) for c in fig3.axes[0].collections) == v.size - # reproject hemisphere="both": reprojection is ignored so - # half of the vectors are shown on each axes as normal + colls = fig3.axes[0].collections[:2] + for coll in colls: + assert coll.get_offsets().shape == (v.size // 2, 2) + + # Reproject hemisphere="both": reprojection is ignored so half + # of the vectors are shown on each axes as normal fig4 = v.scatter(hemisphere="both", reproject=True, return_figure=True) for ax in fig4.axes: - assert sum(len(c.get_offsets()) for c in ax.collections) == v.size // 2 + assert ax.collections[0].get_offsets().shape == (v.size // 2, 2) def test_draw_circle(self): v = self.v diff --git a/orix/vector/__init__.py b/orix/vector/__init__.py index 5f45bc5a..d2154a04 100644 --- a/orix/vector/__init__.py +++ b/orix/vector/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/vector/fundamental_sector.py b/orix/vector/fundamental_sector.py index 8cd48d46..6a5a03af 100644 --- a/orix/vector/fundamental_sector.py +++ b/orix/vector/fundamental_sector.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/vector/miller.py b/orix/vector/miller.py index e65e68bd..1d8cd160 100644 --- a/orix/vector/miller.py +++ b/orix/vector/miller.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/vector/neo_euler.py b/orix/vector/neo_euler.py index 7aab52b0..123a6a9b 100644 --- a/orix/vector/neo_euler.py +++ b/orix/vector/neo_euler.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/vector/spherical_region.py b/orix/vector/spherical_region.py index 17d2508f..33c9a512 100644 --- a/orix/vector/spherical_region.py +++ b/orix/vector/spherical_region.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. diff --git a/orix/vector/vector3d.py b/orix/vector/vector3d.py index ea62b36d..882509a5 100644 --- a/orix/vector/vector3d.py +++ b/orix/vector/vector3d.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018-2024 the orix developers # # This file is part of orix. @@ -27,6 +26,7 @@ import matplotlib.pyplot as plt import numpy as np +from orix import constants from orix._base import Object3d @@ -159,9 +159,21 @@ def _tuples(self) -> Set: @property def perpendicular(self) -> Vector3d: - """Return the perpendicular vectors.""" - if np.any(self.x == 0) and np.any(self.y == 0): - if np.any(self.z == 0): + r"""Return the perpendicular vectors. + + Notes + ----- + The following convention is used: + + .. math:: + + (x, y, z) &\rightarrow (-y, x, 0),\\ + (0, 0, z) &\rightarrow (1, 0, 0). + """ + if np.any(abs(self.x) < constants.eps12) and np.any( + abs(self.y) < constants.eps12 + ): + if np.any(abs(self.z) < constants.eps12): raise ValueError("No vectors are perpendicular") return Vector3d.xvector() x = -self.y @@ -432,7 +444,7 @@ def from_path_ends( satisfy these two conditions .. math:: - (v_1 \times v_i) \cdot (v_1 \times v_2) \geq 0, + (v_1 \times v_i) \cdot (v_1 \times v_2) \geq 0,\\ (v_2 \times v_i) \cdot (v_2 \times v_1) \geq 0. """ v = Vector3d(vectors).flatten() @@ -448,8 +460,8 @@ def from_path_ends( v_normal = v1.cross(v2) v_circle = v_normal.get_circle(steps=steps) - cond1 = v1.cross(v_circle).dot(v1.cross(v2)) >= 0 - cond2 = v2.cross(v_circle).dot(v2.cross(v1)) >= 0 + cond1 = v1.cross(v_circle).dot(v_normal) >= 0 + cond2 = v2.cross(v_circle).dot(-v_normal) >= 0 v_path = v_circle[cond1 & cond2] @@ -466,31 +478,43 @@ def from_path_ends( # --------------------- Other public methods --------------------- # def dot(self, other: Vector3d) -> np.ndarray: - """Return the dot products of the vectors and the other vectors. + r"""Return the dot products :math:`D` of the vectors. Parameters ---------- other - Other vectors with a compatible shape. + Other vectors. Shapes must be broadcastable. Returns ------- - dot_products + D Dot products. + Notes + ----- + The dot product :math:`D` between :math:`\mathbf{v_1}` and + :math:`\mathbf{v_2}` is given by + + .. math:: + + D = \mathbf{v_1}\cdot\mathbf{v_2} = |\mathbf{v_1}|\:|\mathbf{v_2}|\cos{\omega}, + + where :math:`\omega` is the angle between the two vectors. + Examples -------- >>> from orix.vector import Vector3d - >>> v = Vector3d((0, 0, 1.0)) - >>> w = Vector3d(((0, 0, 0.5), (0.4, 0.6, 0))) - >>> v.dot(w) + >>> v1 = Vector3d([0, 0, 1]) + >>> v2 = Vector3d([[0, 0, 0.5], [0.4, 0.6, 0]]) + >>> v1.dot(v2) array([0.5, 0. ]) - >>> w.dot(v) + >>> v2.dot(v1) array([0.5, 0. ]) """ if not isinstance(other, Vector3d): - raise ValueError("{} is not a vector!".format(other)) - return np.sum(self.data * other.data, axis=-1) + raise ValueError(f"{other} is not a vector") + D = np.sum(self.data * other.data, axis=-1) + return D def dot_outer( self, @@ -596,8 +620,8 @@ def angle_with(self, other: Vector3d, degrees: bool = False) -> np.ndarray: def rotate( self, - axis: Union[np.ndarray, Vector3d] = None, - angle: Union[List[float], float, np.np.ndarray] = 0, + axis: Union[np.ndarray, Vector3d, None] = None, + angle: Union[List[float], float, np.ndarray] = 0, ) -> Vector3d: """Convenience function for rotating this vector. @@ -824,11 +848,18 @@ def get_circle( vector to the current vector at ``opening_angle`` and (2) about the current vector in a full circle. """ + from orix.quaternion.rotation import Rotation + circles = self.zero((self.size, steps)) full_circle = np.linspace(0, 2 * np.pi, num=steps) opening_angles = np.ones(self.size) * opening_angle - for i, (v, oa) in enumerate(zip(self.flatten(), opening_angles)): - circles[i] = v.rotate(v.perpendicular, oa).rotate(v, full_circle) + v = self.flatten() + for i in range(v.size): + vi = v[i] + R1 = Rotation.from_axes_angles(vi.perpendicular, opening_angles[i]) + R2 = Rotation.from_axes_angles(vi, full_circle) + circles[i] = R2 * R1 * vi + return circles def inverse_pole_density_function( diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..1d815d4c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,112 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "orix" +authors = [{name = "orix developers"}] +description = "orix is an open-source Python library for handling crystal orientation mapping data" +license = {file = "LICENSE"} +readme = {file = "README.rst", content-type = "text/x-rst"} +dynamic = ["version"] +requires-python = ">= 3.10" +classifiers=[ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Natural Language :: English", + "Operating System :: OS Independent", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Physics", +] +dependencies = [ + "dask[array]", + "diffpy.structure >= 3.0.2", + "h5py", + "matplotlib >= 3.6.1", + "matplotlib-scalebar", + "numba", + "numpy", + "pooch >= 0.13", + # TODO: Remove once diffpy.structure >= 3.2.1 + "pycifrw", + "scipy", + "tqdm", + # TODO: Remove once Python >= 3.11 + "typing_extensions", +] + +[project.optional-dependencies] +all = [ # NB! Update constants.py if this list is updated! + "numpy-quaternion", +] +doc = [ + "ipykernel", # Used by nbsphinx to execute notebooks + "memory_profiler", + "nbconvert >= 7.16.4", + "nbsphinx >= 0.7", + "numpydoc", + "pydata-sphinx-theme", + "scikit-image", + "scikit-learn", + "sphinx >= 3.0.2", + "sphinx-codeautolink[ipython]", + "sphinx-copybutton >= 0.2.5", + "sphinx-design", + "sphinx-gallery", + "sphinxcontrib-bibtex >= 1.0", +] +tests = [ + "numpydoc", + "pytest >= 5.4", + "pytest-rerunfailures", + "pytest-xdist", +] +coverage = [ + "coverage >= 5.0", + "pytest-cov >= 2.8.1", +] +dev = [ + "black[jupyter]", + "hatch", + "isort >= 5.10", + "outdated", + "pre-commit >= 1.16", + "orix[doc,tests,coverage]", +] + +[tool.hatch.version] +path = "orix/__init__.py" + +[tool.coverage.report] +precision = 2 + +[tool.coverage.run] +branch = false +source = ["orix"] +relative_files = true +omit = [ + "orix/__init__.py", + "orix/tests/**/*.py", +] + +[tool.pytest.ini_options] +addopts = [ + "-ra", + "--ignore=doc/_static/img/colormap_banners/create_colormap_banners.py", + "--ignore=examples/*/*.py", +] +doctest_optionflags = "NORMALIZE_WHITESPACE" +filterwarnings = [ + "ignore:Deprecated call to `pkg_resources:DeprecationWarning", + "ignore:pkg_resources is deprecated as an API:DeprecationWarning", +] + +[tool.isort] +profile = "black" +filter_files = true +force_sort_within_sections = true diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index f59f4c06..00000000 --- a/setup.cfg +++ /dev/null @@ -1,40 +0,0 @@ -# Note that Black does not support setup.cfg - -[tool:pytest] -addopts = - -ra - # Documentation scripts - --ignore=doc/_static/img/colormap_banners/create_colormap_banners.py - # Examples - --ignore=examples/*/*.py -doctest_optionflags = NORMALIZE_WHITESPACE -filterwarnings = - # From setuptools - ignore:Deprecated call to \`pkg_resources:DeprecationWarning - ignore:pkg_resources is deprecated as an API:DeprecationWarning - -[coverage:run] -source = orix -omit = - setup.py - orix/__init__.py - orix/tests/**/*.py -relative_files = True - -[coverage:report] -precision = 2 - -[manifix] -known_excludes = - .* - .*/** - *.code-workspace - **/*.pyc - **/*.nbi - **/*.nbc - **/__pycache__/** - doc/_build/** - doc/examples/** - doc/reference/generated/** - doc/.ipynb_checkpoints/** - htmlcov/** diff --git a/setup.py b/setup.py deleted file mode 100644 index dfcc4c09..00000000 --- a/setup.py +++ /dev/null @@ -1,103 +0,0 @@ -from itertools import chain - -from setuptools import find_packages, setup - -from orix import __author__, __author_email__, __description__, __name__, __version__ - -# Projects with optional features for building the documentation and running -# tests. From setuptools: -# https://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-extras-optional-features-with-their-own-dependencies -# fmt: off -extra_feature_requirements = { - "doc": [ - "ipykernel", # Used by nbsphinx to execute notebooks - "memory_profiler", - "nbconvert >= 7.16.4", - "nbsphinx >= 0.7", - "numpydoc", - "pydata-sphinx-theme", - "scikit-image", - "scikit-learn", - "sphinx >= 3.0.2", - "sphinx-codeautolink[ipython]", - "sphinx-copybutton >= 0.2.5", - "sphinx-design", - "sphinx-gallery", - "sphinxcontrib-bibtex >= 1.0", - ], - "tests": [ - "coverage >= 5.0", - "numpydoc", - "pytest >= 5.4", - "pytest-cov >= 2.8.1", - "pytest-rerunfailures", - "pytest-xdist", - ], -} -extra_feature_requirements["dev"] = [ - "black[jupyter]", - "isort >= 5.10", - "manifix", - "outdated", - "pre-commit >= 1.16", -] + list(chain(*list(extra_feature_requirements.values()))) -# fmt: on - -# Remove the "raw" ReStructuredText directive from the README so we can -# use it as the long_description on PyPI -readme = open("README.rst").read() -readme_split = readme.split("\n") -for i, line in enumerate(readme_split): - if line == ".. EXCLUDE": - break -long_description = "\n".join(readme_split[i + 2 :]) - -setup( - name=__name__, - version=str(__version__), - license="GPLv3", - url="https://orix.readthedocs.io", - author=__author__, - author_email=__author_email__, - description=__description__, - long_description=long_description, - long_description_content_type="text/x-rst", - classifiers=[ - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Development Status :: 4 - Beta", - "Intended Audience :: Science/Research", - ( - "License :: OSI Approved :: GNU General Public License v3 or later " - "(GPLv3+)" - ), - "Natural Language :: English", - "Operating System :: OS Independent", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Physics", - ], - python_requires=">=3.8", - packages=find_packages(exclude=["orix/tests"]), - extras_require=extra_feature_requirements, - # fmt: off - install_requires=[ - "dask[array]", - "diffpy.structure >= 3.0.2", - "h5py", - "matplotlib >= 3.5", - "matplotlib-scalebar", - "numba", - "numpy", - "numpy-quaternion", - "pooch >= 0.13", - # TODO: Remove once https://github.com/diffpy/diffpy.structure/issues/97 is fixed - "pycifrw", - "scipy", - "tqdm", - ], - # fmt: on - package_data={"": ["LICENSE", "README.rst", "readthedocs.yaml"], "orix": ["*.py"]}, -)