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
-
-
-
-.. 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"]},
-)