diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0325438f..56802eb1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -44,6 +44,9 @@ jobs: continue-on-error: true with: repository-url: https://test.pypi.org/legacy/ + attestations: false - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + with: + attestations: false diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c67ee3ea..73e76d42 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,22 +18,44 @@ Unreleased Added ----- -- Dependency on RosettaSciIO for read/write of some file formats. - (`#? `_) Changed ------- -- Minimum Python version is now 3.10. - (`#689 `_) Removed ------- +Deprecated +---------- + Fixed ----- -Deprecated ----------- +0.11.0 (2024-11-10) +=================== + +Added +----- +- HyperSpy 2.0 compatibility. (`#695 `_) +- Dependency on RosettaSciIO for read/write of HyperSpy's HDF5 and zarr files. + (`#694 `_) + +Changed +------- +- Minimum Python version is now 3.10. + (`#689 `_) +- Minimum HyperSpy version is now 2.2. + (`#695 `_) +- Markers returned from geometrical simulations and the virtual backscatter electron + imager use the new HyperSpy 2.0 markers. + (`#695 `_) +- Progressbar when calculating kinematical master pattern shows progress per pole. + +Removed +------- +- Contrasting background bounding box for zone axes labels returned as HyperSpy + markers. They are not supported by HyperSpy 2.0. + (`#695 `_) 0.10.0 (2024-06-02) =================== @@ -64,7 +86,7 @@ Fixed ``downsample()``, was previously previously incorrect, (n columns, n rows). This is now correct, (n rows, n columns). (`#674 `_) - + 0.9.0 (2023-11-03) ================== diff --git a/doc/tutorials/kinematical_ebsd_simulations.ipynb b/doc/tutorials/kinematical_ebsd_simulations.ipynb index 8e6ad61f..5fff7c6f 100644 --- a/doc/tutorials/kinematical_ebsd_simulations.ipynb +++ b/doc/tutorials/kinematical_ebsd_simulations.ipynb @@ -22,13 +22,13 @@ "In this tutorial, we will perform kinematical Kikuchi pattern simulations of nickel, a variant of the $\\sigma$-phase (Fe, Cr) in steels, and silicon carbide 6H.\n", "\n", "We can generate kinematical master patterns using [KikuchiPatternSimulator.calculate_master_pattern()](../reference/generated/kikuchipy.simulations.KikuchiPatternSimulator.calculate_master_pattern.rst).\n", - "The simulator must be created from a [ReciprocalLatticeVector](https://diffsims.readthedocs.io/en/stable/reference.html#diffsims.crystallography.ReciprocalLatticeVector) instance that satisfy the following conditions:\n", + "The simulator must be created from a [ReciprocalLatticeVector](https://diffsims.readthedocs.io/en/stable/reference/generated/diffsims.crystallography.ReciprocalLatticeVector.html) instance that satisfy the following conditions:\n", "\n", - "1. All atom positions are filled in the unit cell, i.e. the `structure` used to create the `phase` used in `ReciprocalLatticeVector`. This can be achieved by creating a `Phase` instance with all asymmetric atom positions listed, creating a reflector list, and then calling [ReciprocalLatticeVector.sanitise_phase()](https://diffsims.readthedocs.io/en/stable/reference.html#diffsims.crystallography.ReciprocalLatticeVector.sanitise_phase). The phase can be created manually or imported from a valid CIF file with [Phase.from_cif()](https://orix.readthedocs.io/en/stable/reference/generated/orix.crystal_map.Phase.from_cif.html).\n", + "1. All atom positions are filled in the unit cell, i.e. the `structure` used to create the `phase` used in `ReciprocalLatticeVector`. This can be achieved by creating a `Phase` instance with all asymmetric atom positions listed, creating a reflector list, and then calling [ReciprocalLatticeVector.sanitise_phase()](https://diffsims.readthedocs.io/en/stable/reference/generated/diffsims.crystallography.ReciprocalLatticeVector.sanitise_phase.html). The phase can be created manually or imported from a valid CIF file with [Phase.from_cif()](https://orix.readthedocs.io/en/stable/reference/generated/orix.crystal_map.Phase.from_cif.html).\n", "2. The atoms in the `structure` have their elements described by the symbol (Ni), not by the atomic number (28).\n", "3. The lattice parameters $(a, b, c)$ are given in Ångström.\n", - "4. Kinematical structure factors $F_{\\mathrm{hkl}}$ have been calculated with [ReciprocalLatticeVector.calculate_structure_factor()](https://diffsims.readthedocs.io/en/stable/reference.html#diffsims.crystallography.ReciprocalLatticeVector.calculate_structure_factor).\n", - "5. Bragg angles $\\theta_{\\mathrm{B}}$ have been calculated with [ReciprocalLatticeVector.calculate_theta()](https://diffsims.readthedocs.io/en/stable/reference.html#diffsims.crystallography.ReciprocalLatticeVector.calculate_theta).\n", + "4. Kinematical structure factors $F_{\\mathrm{hkl}}$ have been calculated with [ReciprocalLatticeVector.calculate_structure_factor()](https://diffsims.readthedocs.io/en/stable/reference/generated/diffsims.crystallography.ReciprocalLatticeVector.calculate_structure_factor.html).\n", + "5. Bragg angles $\\theta_{\\mathrm{B}}$ have been calculated with [ReciprocalLatticeVector.calculate_theta()](https://diffsims.readthedocs.io/en/stable/reference/generated/diffsims.crystallography.ReciprocalLatticeVector.calculate_theta.html).\n", "\n", "Let's import the necessary libraries" ] @@ -824,7 +824,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.6" + "version": "3.12.7" }, "widgets": { "application/vnd.jupyter.widget-state+json": { diff --git a/pyproject.toml b/pyproject.toml index 1db76da5..961ec228 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,8 +42,7 @@ dependencies = [ "dask[array] >= 2021.8.1", "diffpy.structure >= 3", "diffsims >= 0.5.2", - # TODO: Replace with HyperSpy 2.2 once it's out - "hyperspy @ git+https://github.com/hyperspy/hyperspy.git@RELEASE_next_patch", + "hyperspy >= 2.2", "h5py >= 2.10", "imageio", "lazy_loader", @@ -122,13 +121,7 @@ path = "src/kikuchipy/__init__.py" src = "" [tool.hatch.build.targets.wheel] -include = [ - "src/kikuchipy", -] - -# TODO: Remove once HyperSpy is not installed from a direct reference (GitHub) -[tool.hatch.metadata] -allow-direct-references = true +include = ["src/kikuchipy"] [tool.coverage.report] precision = 2 diff --git a/src/kikuchipy/__init__.py b/src/kikuchipy/__init__.py index 74142944..ef1444fa 100644 --- a/src/kikuchipy/__init__.py +++ b/src/kikuchipy/__init__.py @@ -30,7 +30,7 @@ "Carter Francis", "Magnus Nord", ] -__version__ = "0.11.dev0" +__version__ = "0.12.dev0" __getattr__, __dir__, __all__ = lazy_loader.attach_stub(__name__, __file__) diff --git a/src/kikuchipy/_rotation/__init__.py b/src/kikuchipy/_rotation/__init__.py deleted file mode 100644 index b527aad5..00000000 --- a/src/kikuchipy/_rotation/__init__.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright 2019-2024 The kikuchipy developers -# -# This file is part of kikuchipy. -# -# kikuchipy 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. -# -# kikuchipy 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 kikuchipy. If not, see . - -"""Private tools for handling crystal orientations and directions. - -The functions here are taken directly from orix, but modified to use -Numba. - -This module and documentation is only relevant for kikuchipy developers, -not for users. - -.. warning: - - This module and its submodules are for internal use only. Do not - use them in your own code. We may change the API at any time with no - warning. -""" - -from numba import njit -import numpy as np - - -@njit("float64[:](float64, float64, float64)", cache=True, nogil=True, fastmath=True) -def _rotation_from_rodrigues(rx: float, ry: float, rz: float) -> np.ndarray: - """Convert a Rodrigues-Frank vector to a unit quaternion. - - Taken from :meth:`orix.quaternion.Rotation.from_rodrigues`. - - Parameters - ---------- - rx - X component. - ry - Y component. - rz - Z component. - - Returns - ------- - rot - Unit quaternion. - - Notes - ----- - This function is optimized with Numba, so care must be taken with - array shapes and data types. - """ - rod = np.array([rx, ry, rz]) - norm = np.sqrt(np.sum(np.square(rod))) - half_angle = np.arctan(norm) - s = np.sin(half_angle) - - a = np.cos(half_angle) - b = s * rx / norm - c = s * ry / norm - d = s * rz / norm - rot = np.array([a, b, c, d], dtype="float64") - - if rot[0] < 0: - rot = -rot - - return rot - - -@njit("float64[:](float64, float64, float64)", cache=True, nogil=True, fastmath=True) -def _rotation_from_euler(alpha: float, beta: float, gamma: float) -> np.ndarray: - """Convert three Euler angles (alpha, beta, gamma) to a unit - quaternion. - - Taken from :meth:`orix.quaternion.Rotation.from_euler`. - - Parameters - ---------- - alpha, beta, gamma - Euler angles in the Bunge convention in radians. - - Returns - ------- - rotation - Unit quaternion. - - Notes - ----- - This function is optimized with Numba, so care must be taken with - array shapes and data types. - """ - sigma = 0.5 * (alpha + gamma) - delta = 0.5 * (alpha - gamma) - c = np.cos(0.5 * beta) - s = np.sin(0.5 * beta) - - # fmt: off - rotation = np.array( - ( - c * np.cos(sigma), - -s * np.cos(delta), - -s * np.sin(delta), - -c * np.sin(sigma) - ), - dtype=np.float64, - ) - # fmt: on - - if rotation[0] < 0: - rotation = -rotation - - return rotation - - -@njit("float64[:, :](float64[:], float64[:, :])", cache=True, nogil=True, fastmath=True) -def _rotate_vector(rotation: np.ndarray, vector: np.ndarray) -> np.ndarray: - """Rotation of vector(s) by a quaternion. - - Taken from :meth:`orix.quaternion.Quaternion.__mul__`. - - Parameters - ---------- - rotation - Quaternion rotation as an array of shape (4,) and data type - 64-bit floats. - vector - Vector(s) as an array of shape (n, 3) and data type 64-bit - floats. - - Returns - ------- - rotated_vector - Rotated vector. - - Notes - ----- - This function is optimized with Numba, so care must be taken with - array shapes and data types. - """ - a, b, c, d = rotation - x = vector[:, 0] - y = vector[:, 1] - z = vector[:, 2] - - aa = a**2 - bb = b**2 - cc = c**2 - dd = d**2 - ac = a * c - ab = a * b - ad = a * d - bc = b * c - bd = b * d - cd = c * d - - rotated_vector = np.zeros(vector.shape, dtype=np.float64) - rotated_vector[:, 0] = (aa + bb - cc - dd) * x + 2 * ((ac + bd) * z + (bc - ad) * y) - rotated_vector[:, 1] = (aa - bb + cc - dd) * y + 2 * ((ad + bc) * x + (cd - ab) * z) - rotated_vector[:, 2] = (aa - bb - cc + dd) * z + 2 * ((ab + cd) * y + (bd - ac) * x) - - return rotated_vector diff --git a/tests/test_util/__init__.py b/src/kikuchipy/_utils/__init__.py similarity index 83% rename from tests/test_util/__init__.py rename to src/kikuchipy/_utils/__init__.py index dce3f2c4..6b098073 100644 --- a/tests/test_util/__init__.py +++ b/src/kikuchipy/_utils/__init__.py @@ -14,3 +14,11 @@ # # You should have received a copy of the GNU General Public License # along with kikuchipy. If not, see . + +"""Internal tools.""" + +import lazy_loader + +__getattr__, __dir__, __all__ = lazy_loader.attach_stub(__name__, __file__) + +del lazy_loader diff --git a/src/kikuchipy/_util/__init__.pyi b/src/kikuchipy/_utils/__init__.pyi similarity index 93% rename from src/kikuchipy/_util/__init__.pyi rename to src/kikuchipy/_utils/__init__.pyi index 5be1a0bd..3cf04620 100644 --- a/src/kikuchipy/_util/__init__.pyi +++ b/src/kikuchipy/_utils/__init__.pyi @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with kikuchipy. If not, see . -from ._deprecated import deprecated, deprecated_argument +from .deprecated import deprecated, deprecated_argument __all__ = [ "deprecated", diff --git a/src/kikuchipy/_util/_transfer_axes.py b/src/kikuchipy/_utils/_transfer_axes.py similarity index 100% rename from src/kikuchipy/_util/_transfer_axes.py rename to src/kikuchipy/_utils/_transfer_axes.py diff --git a/src/kikuchipy/_util/_deprecated.py b/src/kikuchipy/_utils/deprecated.py similarity index 100% rename from src/kikuchipy/_util/_deprecated.py rename to src/kikuchipy/_utils/deprecated.py diff --git a/src/kikuchipy/_util/__init__.py b/src/kikuchipy/_utils/exceptions.py similarity index 53% rename from src/kikuchipy/_util/__init__.py rename to src/kikuchipy/_utils/exceptions.py index 9606be78..2325cb45 100644 --- a/src/kikuchipy/_util/__init__.py +++ b/src/kikuchipy/_utils/exceptions.py @@ -15,19 +15,22 @@ # You should have received a copy of the GNU General Public License # along with kikuchipy. If not, see . -"""Helper functions and classes for managing kikuchipy. +from typing import Any -This module and documentation is only relevant for kikuchipy developers, -not for users. -.. warning: - This module and its submodules are for internal use only. Do not - use them in your own code. We may change the API at any time with no - warning. -""" +class UnknownHemisphereError(ValueError): + def __init__(self, given: Any = None, *args: object) -> None: + msg = "Unknown hemisphere" + if given is not None: + msg += f" {given!r}" + msg += ", options are 'upper', 'lower', or 'both'" + super().__init__(msg, *args) -import lazy_loader -__getattr__, __dir__, __all__ = lazy_loader.attach_stub(__name__, __file__) - -del lazy_loader +class UnknownProjectionError(ValueError): + def __init__(self, given: Any = None, *args: object) -> None: + msg = "Unknown projection" + if given is not None: + msg += f" {given!r}" + msg += ", options are 'stereographic' and 'lambert'" + super().__init__(msg, *args) diff --git a/src/kikuchipy/_utils/numba.py b/src/kikuchipy/_utils/numba.py new file mode 100644 index 00000000..5796dec0 --- /dev/null +++ b/src/kikuchipy/_utils/numba.py @@ -0,0 +1,92 @@ +# Copyright 2019-2024 The kikuchipy developers +# +# This file is part of kikuchipy. +# +# kikuchipy 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. +# +# kikuchipy 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 kikuchipy. If not, see . + +"""Numba-accelerated functions useful across modules.""" + +import numba as nb +import numpy as np + +# ---------------------------- Rotations ----------------------------- # + + +@nb.njit("float64[:](float64, float64, float64)", cache=True, fastmath=True, nogil=True) +def rotation_from_rodrigues(rx: float, ry: float, rz: float) -> np.ndarray: + rod = np.array([rx, ry, rz]) + norm = np.sqrt(np.sum(np.square(rod))) + half_angle = np.arctan(norm) + s = np.sin(half_angle) + a = np.cos(half_angle) + b = s * rx / norm + c = s * ry / norm + d = s * rz / norm + rot = np.array([a, b, c, d], dtype="float64") + if rot[0] < 0: + for i in range(4): + rot[i] = -rot[i] + return rot + + +@nb.njit("float64[:](float64, float64, float64)", cache=True, fastmath=True, nogil=True) +def rotation_from_euler(alpha: float, beta: float, gamma: float) -> np.ndarray: + sigma = 0.5 * (alpha + gamma) + delta = 0.5 * (alpha - gamma) + c = np.cos(0.5 * beta) + s = np.sin(0.5 * beta) + rot = np.array( + [c * np.cos(sigma), -s * np.cos(delta), -s * np.sin(delta), -c * np.sin(sigma)], + dtype=np.float64, + ) + if rot[0] < 0: + for i in range(4): + rot[i] = -rot[i] + return rot + + +@nb.njit( + "float64[:, :](float64[:], float64[:, :])", cache=True, fastmath=True, nogil=True +) +def rotate_vector(rotation: np.ndarray, vector: np.ndarray) -> np.ndarray: + a, b, c, d = rotation + x = vector[:, 0] + y = vector[:, 1] + z = vector[:, 2] + aa = a**2 + bb = b**2 + cc = c**2 + dd = d**2 + ac = a * c + ab = a * b + ad = a * d + bc = b * c + bd = b * d + cd = c * d + vector2 = np.zeros(vector.shape, dtype=np.float64) + vector2[:, 0] = (aa + bb - cc - dd) * x + 2 * ((ac + bd) * z + (bc - ad) * y) + vector2[:, 1] = (aa - bb + cc - dd) * y + 2 * ((ad + bc) * x + (cd - ab) * z) + vector2[:, 2] = (aa - bb - cc + dd) * z + 2 * ((ab + cd) * y + (bd - ac) * x) + return vector2 + + +# ----------------------------- Vectors ------------------------------ # + + +@nb.njit("float64(float64[:], float64[:])", cache=True, fastmath=True, nogil=True) +def vec_dot(v1, v2): + D = 0.0 + for i in range(3): + D += v1[i] * v2[i] + return D diff --git a/src/kikuchipy/_utils/vector.py b/src/kikuchipy/_utils/vector.py new file mode 100644 index 00000000..a20c5415 --- /dev/null +++ b/src/kikuchipy/_utils/vector.py @@ -0,0 +1,60 @@ +# Copyright 2019-2024 The kikuchipy developers +# +# This file is part of kikuchipy. +# +# kikuchipy 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. +# +# kikuchipy 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 kikuchipy. If not, see . + +"""Vector-related tools useful across modules.""" + +from typing import Literal + +from kikuchipy._utils.exceptions import UnknownHemisphereError, UnknownProjectionError + +ValidHemispheres = Literal["upper", "lower", "both"] +ValidProjections = Literal["stereographic", "lambert"] + + +def poles_from_hemisphere(hemisphere: ValidHemispheres) -> list[int]: + """Return pole(s) (-1, 1) for the given hemisphere(s) (upper, lower, + or both). + + Raises + ------ + ValueError + If an unknown hemisphere is given. + """ + hemi = parse_hemisphere(hemisphere) + match hemi: + case "upper": + return [-1] + case "lower": + return [1] + case _: + return [-1, 1] + + +def parse_hemisphere(hemisphere: ValidHemispheres) -> str: + hemi = hemisphere.lower() + if hemi not in ["upper", "lower", "both"]: + raise UnknownHemisphereError(hemisphere) + else: + return hemi + + +def parse_projection(projection: ValidProjections) -> str: + proj = projection.lower() + if proj not in ["stereographic", "lambert"]: + raise UnknownProjectionError(projection) + else: + return proj diff --git a/src/kikuchipy/data/_data.py b/src/kikuchipy/data/_data.py index 4facb048..f2aef16c 100644 --- a/src/kikuchipy/data/_data.py +++ b/src/kikuchipy/data/_data.py @@ -353,8 +353,7 @@ def si_wafer( See Also -------- - silicon_ebsd_moving_screen_in, silicon_ebsd_moving_screen_out5mm, - silicon_ebsd_moving_screen_out10mm + silicon_ebsd_moving_screen Notes ----- @@ -382,7 +381,7 @@ def si_wafer( def nickel_ebsd_master_pattern_small(**kwargs) -> EBSDMasterPattern: """(401, 401) ``uint8`` square Lambert or stereographic projection - of the northern and southern hemisphere of a nickel master pattern + of the upper and lower hemisphere of a nickel master pattern at 20 keV accelerating voltage. The master pattern was simulated with *EMsoft* diff --git a/src/kikuchipy/detectors/ebsd_detector.py b/src/kikuchipy/detectors/ebsd_detector.py index 99ddf8d7..6d9a1a60 100644 --- a/src/kikuchipy/detectors/ebsd_detector.py +++ b/src/kikuchipy/detectors/ebsd_detector.py @@ -145,7 +145,7 @@ class EBSDDetector: x_B^* &= x_T^*,\\ y_B^* &= 1 - y_T^*,\\ - z_B^* &= \frac{min(N_x, N_y)}{N_y} z_T^*. + z_B^* &= \frac{\min(N_x, N_y)}{N_y} z_T^*. The conversion from Oxford Instruments to Bruker is given as @@ -227,13 +227,14 @@ def __init__( def __repr__(self) -> str: decimals = 3 pc_average = tuple(map(float, self.pc_average.round(decimals))) + shape = tuple(map(int, self.shape)) sample_tilt = np.round(self.sample_tilt, decimals) tilt = np.round(self.tilt, decimals) azimuthal = np.round(self.azimuthal, decimals) px_size = np.round(self.px_size, decimals) return ( f"{type(self).__name__}" - f"(shape={self.shape}, " + f"(shape={shape}, " f"pc={pc_average}, " f"sample_tilt={sample_tilt}, " f"tilt={tilt}, " diff --git a/src/kikuchipy/filters/window.py b/src/kikuchipy/filters/window.py index 49ef02fa..2ae835ec 100644 --- a/src/kikuchipy/filters/window.py +++ b/src/kikuchipy/filters/window.py @@ -43,18 +43,18 @@ class Window(np.ndarray): window Window type to create. Available types are listed in :func:`scipy.signal.windows.get_window` and includes - ``"rectangular"`` and ``"gaussian"``, in addition to a - ``"circular"`` window (default) filled with ones in which corner - data are set to zero, a ``"modified_hann"`` window and - ``"lowpass"`` and ``"highpass"`` FFT windows. A window element - is considered to be in a corner if its radial distance to the - origin (window center) is shorter or equal to the half width of - the windows's longest axis. A 1D or 2D :class:`numpy.ndarray` or - :class:`dask.array.Array` can also be passed. + "rectangular" and "gaussian", in addition to a "circular" window + (default) filled with ones in which corner data are set to zero, + a "modified_hann" window and "lowpass" and "highpass" FFT + windows. A window element is considered to be in a corner if its + radial distance to the origin (window center) is shorter or + equal to the half width of the windows's longest axis. A 1D or + 2D :class:`numpy.ndarray` or :class:`dask.array.Array` can also + be passed. shape Shape of the window. Not used if a custom window is passed to - ``window``. This can be either 1D or 2D, and can be - asymmetrical. Default is ``(3, 3)``. + *window*. This can be either 1D or 2D, and can be asymmetrical. + Default is (3, 3). **kwargs Required keyword arguments passed to the window type. @@ -127,9 +127,9 @@ def __new__( shape = (3, 3) elif "Nx" in kwargs.keys(): shape = (kwargs.pop("Nx"),) - else: # Ensure valid shape tuple - shape = tuple(shape) + else: try: + shape = tuple(shape) if any(np.array(shape) < 1): raise ValueError(f"All window axes {shape} must be > 0.") if any(isinstance(i, float) for i in np.array(shape)): @@ -143,14 +143,12 @@ def __new__( data = window elif isinstance(window, str): window_kwargs = {} - if window == "modified_hann": name = window window_func = modified_hann window_kwargs["Nx"] = shape[0] elif window in ["lowpass", "highpass"]: name = window - if window == "lowpass": window_func = lowpass_fft_filter else: @@ -165,45 +163,42 @@ def __new__( if window == "circular": exclude_window_corners = True window = "rectangular" - name = window window_func = get_window window_kwargs["fftbins"] = kwargs.pop("fftbins", False) window_kwargs["Nx"] = kwargs.pop("Nx", shape[0]) window_kwargs["window"] = (window,) + tuple(kwargs.values()) - data = window_func(**window_kwargs) - - # Add second axis to window if shape has two axes if len(shape) == 2 and data.ndim == 1: window_kwargs["Nx"] = shape[1] data = np.outer(data, window_func(**window_kwargs)) else: raise ValueError( f"Window {type(window)} must be of type numpy.ndarray, " - "dask.array.Array or a valid string." + "dask.array.Array, or a valid string" ) - # Create object obj = np.asarray(data).view(cls) obj._name = name - if exclude_window_corners: # Exclude window corners + if exclude_window_corners: obj.make_circular() return obj - def __array_finalize__(self, obj: Self | None) -> None: + def __array_finalize__(self, obj) -> None: if obj is None: return self._name = getattr(obj, "_name", None) self._circular = getattr(obj, "_circular", False) - def __array_wrap__(self, obj: Self) -> Window | np.ndarray: + def __array_wrap__( + self, obj, context=None, return_scalar=False + ) -> Window | np.ndarray: if obj.shape == (): return obj[()] else: - return np.ndarray.__array_wrap__(self, obj) + return np.ndarray.__array_wrap__(self, obj, context, return_scalar) def __repr__(self) -> str: cls = self.__class__.__name__ @@ -227,7 +222,6 @@ def distance_to_origin(self) -> np.ndarray: @property def is_valid(self) -> bool: """Return whether the window is in a valid state.""" - return ( isinstance(self.name, str) and (isinstance(self, np.ndarray) or isinstance(self, Array)) @@ -248,7 +242,7 @@ def name(self) -> str: return self._name @property - def origin(self) -> tuple: + def origin(self) -> tuple[int, ...]: """Return the window origin.""" return tuple(i // 2 for i in self.shape) @@ -288,17 +282,10 @@ def shape_compatible(self, shape: tuple[int, ...]) -> bool: is_compatible Whether the window shape is compatible with another shape. """ - # Number of window dimensions cannot be greater than data - # dimensions, and a window axis cannot be greater than the - # corresponding data axis - window_shape = self.shape - if len(window_shape) > len(shape) or any( - np.array(window_shape) > np.array(shape) - ): - is_compatible = False + if len(self.shape) > len(shape) or any(np.array(self.shape) > np.array(shape)): + return False else: - is_compatible = True - return is_compatible + return True def plot( self, @@ -321,9 +308,9 @@ def plot( Whether to show values as text in centre of element. Default is True. textcolors - A list of two color specifications. The first is used for - values below a threshold, the second for those above. If - not given (default), this is set to ["white", "black"]. + A list of two colors. The first is used for values below a + threshold, the second for those above. If not given + (default), this is set to ["white", "black"]. cmap A colormap to color data with, available in :class:`matplotlib.colors.ListedColormap`. Default is @@ -352,12 +339,10 @@ def plot( >>> fig.savefig('my_kernel.png') """ if not self.is_valid: - raise ValueError("Window is invalid.") + raise ValueError("Window is invalid") - # Copy and use this object - w = copy(self) + w = self.copy() - # Add axis if 1D if w.ndim == 1: w = np.expand_dims(w, axis=w.ndim) @@ -421,14 +406,12 @@ def distance_to_origin( """ if origin is None: origin = tuple(i // 2 for i in shape) - coordinates = np.ogrid[tuple(slice(None, i) for i in shape)] if len(origin) == 2: squared = [(i - o) ** 2 for i, o in zip(coordinates, origin)] distance = np.sqrt(np.add.outer(*squared).squeeze()) else: distance = abs(coordinates[0] - origin[0]) - return distance @@ -592,12 +575,9 @@ def highpass_fft_filter( True """ r = distance_to_origin(shape) - if cutoff_width is None: cutoff_width = cutoff / 2 - window = np.exp(-(((cutoff - r) / (np.sqrt(2) * cutoff_width / 2)) ** 2)) window[r < (cutoff - (2 * cutoff_width))] = 0 window[r > cutoff] = 1 - return window diff --git a/src/kikuchipy/imaging/vbse.py b/src/kikuchipy/imaging/vbse.py index 2d4d0efa..b6e3db82 100644 --- a/src/kikuchipy/imaging/vbse.py +++ b/src/kikuchipy/imaging/vbse.py @@ -22,7 +22,7 @@ import numpy as np from numpy.typing import NDArray -from kikuchipy._util._transfer_axes import _transfer_navigation_axes_to_signal_axes +from kikuchipy._utils._transfer_axes import _transfer_navigation_axes_to_signal_axes from kikuchipy.pattern._pattern import rescale_intensity from kikuchipy.signals.ebsd import EBSD, LazyEBSD from kikuchipy.signals.virtual_bse_image import VirtualBSEImage diff --git a/src/kikuchipy/indexing/_refinement/_objective_functions.py b/src/kikuchipy/indexing/_refinement/_objective_functions.py index be95aab3..8b94abd5 100644 --- a/src/kikuchipy/indexing/_refinement/_objective_functions.py +++ b/src/kikuchipy/indexing/_refinement/_objective_functions.py @@ -22,7 +22,7 @@ import numpy as np -from kikuchipy._rotation import _rotation_from_euler +from kikuchipy._utils.numba import rotation_from_euler from kikuchipy.indexing.similarity_metrics._normalized_cross_correlation import ( _ncc_single_patterns_1d_float32_exp_centered, ) @@ -45,8 +45,8 @@ def _refine_orientation_objective_function(x: np.ndarray, *args) -> float: function. The expected contents are: 0. 1D centered experimental pattern of 32-bit floats 1. 1D direction cosines - 2. 2D northern hemisphere of master pattern of 32-bit floats - 3. 2D southern hemisphere of master pattern of 32-bit floats + 2. 2D upper hemisphere of master pattern of 32-bit floats + 3. 2D lower hemisphere of master pattern of 32-bit floats 4. Number of master pattern columns 5. Number of master pattern rows 6. Master pattern scale @@ -58,7 +58,7 @@ def _refine_orientation_objective_function(x: np.ndarray, *args) -> float: Normalized cross-correlation score. """ simulated = _project_single_pattern_from_master_pattern( - rotation=_rotation_from_euler(*x), + rotation=rotation_from_euler(*x), direction_cosines=args[1], master_upper=args[2], master_lower=args[3], @@ -86,8 +86,8 @@ def _refine_pc_objective_function(x: np.ndarray, *args) -> float: function. The expected contents are: 0. 1D centered experimental pattern of 32-bit floats 1. 1D quaternion - 2. 2D northern hemisphere of master pattern of 32-bit floats - 3. 2D southern hemisphere of master pattern of 32-bit floats + 2. 2D upper hemisphere of master pattern of 32-bit floats + 3. 2D lower hemisphere of master pattern of 32-bit floats 4. Number of master pattern columns 5. Number of master pattern rows 6. Master pattern scale @@ -146,8 +146,8 @@ def _refine_orientation_pc_objective_function(x: np.ndarray, *args) -> float: Tuple of fixed parameters needed to completely specify the function. The expected contents are: 0. 1D experimental pattern of 32-bit floats - 1. 2D northern hemisphere of master pattern of 32-bit floats - 2. 2D southern hemisphere of master pattern of 32-bit floats + 1. 2D upper hemisphere of master pattern of 32-bit floats + 2. 2D lower hemisphere of master pattern of 32-bit floats 3. Number of master pattern columns 4. Number of master pattern rows 5. Master pattern scale @@ -176,7 +176,7 @@ def _refine_orientation_pc_objective_function(x: np.ndarray, *args) -> float: signal_mask=args[6], ) simulated = _project_single_pattern_from_master_pattern( - rotation=_rotation_from_euler(*x[:3]), + rotation=rotation_from_euler(*x[:3]), direction_cosines=dc, master_upper=args[1], master_lower=args[2], diff --git a/src/kikuchipy/io/plugins/_emsoft_master_pattern.py b/src/kikuchipy/io/plugins/_emsoft_master_pattern.py index 3a9b783d..33794c22 100644 --- a/src/kikuchipy/io/plugins/_emsoft_master_pattern.py +++ b/src/kikuchipy/io/plugins/_emsoft_master_pattern.py @@ -23,6 +23,12 @@ import h5py import numpy as np +from kikuchipy._utils.vector import ( + ValidHemispheres, + ValidProjections, + parse_hemisphere, + parse_projection, +) from kikuchipy.io.plugins._h5ebsd import _hdf5group2dict from kikuchipy.io.plugins.emsoft_ebsd._api import _crystaldata2phase @@ -36,14 +42,14 @@ def __init__( self, filename: str | Path, energy: range | None = None, - projection: str = "stereographic", - hemisphere: "str" = "upper", + projection: ValidProjections = "stereographic", + hemisphere: ValidHemispheres = "upper", lazy: bool = False, ) -> None: self.filename = filename self.energy = energy - self.projection = projection.lower() - self.hemisphere = hemisphere.lower() + self.projection = parse_projection(projection) + self.hemisphere = parse_hemisphere(hemisphere) self.lazy = lazy @property @@ -79,7 +85,7 @@ def read(self, **kwargs) -> list[dict]: mode = kwargs.pop("mode", "r") file = h5py.File(fpath, mode, **kwargs) - _check_file_format(file, self.diffraction_type) + check_file_format(file, self.diffraction_type) # Set data group names diff_type = self.diffraction_type @@ -116,8 +122,8 @@ def read(self, **kwargs) -> list[dict]: # Get data shape and slices data_group = file[data_group_path] energies = data_group[self.energy_string][()] - data_shape, data_slices = _get_data_shape_slices( - npx=nml_params[name_list_name]["npx"], energies=energies, energy=self.energy + data_shape, data_slices = get_data_shape_slices( + nml_params[name_list_name]["npx"], energies, self.energy ) i_min = data_slices[0].start i_min = 0 if i_min is None else i_min @@ -126,10 +132,7 @@ def read(self, **kwargs) -> list[dict]: projection = self.projection hemisphere = self.hemisphere - # Get HDF5 data sets - datasets = _get_datasets( - data_group=data_group, projection=projection, hemisphere=hemisphere - ) + datasets = get_datasets(data_group, projection, hemisphere) # TODO: Take EMsoft NML file parameter combinesites into account dataset_shape = data_shape @@ -220,7 +223,7 @@ def read(self, **kwargs) -> list[dict]: return [output] -def _check_file_format(file: h5py.File, diffraction_type: str) -> None: +def check_file_format(file: h5py.File, diffraction_type: str) -> None: """Raise an error if the HDF5 file is not in EMsoft's master pattern file format. @@ -248,7 +251,7 @@ def _check_file_format(file: h5py.File, diffraction_type: str) -> None: raise IOError(f"{file.filename!r} is not in EMsoft's master pattern format") -def _get_data_shape_slices( +def get_data_shape_slices( npx: int, energies: np.ndarray, energy: tuple | None = None, @@ -293,51 +296,20 @@ def _get_data_shape_slices( return data_shape, data_slices -def _get_datasets( - data_group: h5py.Group, projection: str, hemisphere: str +def get_datasets( + data_group: h5py.Group, projection: ValidProjections, hemisphere: ValidHemispheres ) -> list[h5py.Dataset]: - """Return datasets from projection and hemisphere. - - Parameters - ---------- - data_group - HDF5 data group with data sets. - projection - "stereographic" or "lambert" projection. - hemisphere - "upper", "lower", or "both" hemisphere(s). - - Returns - ------- - datasets - List of HDF5 data sets. - - Raises - ------ - ValueError - If *projection* or *hemisphere* is not among the options. - """ - projection = projection.lower() - projections = {"stereographic": "masterSP", "lambert": "mLP"} - if projection not in projections: - raise ValueError( - f"'projection' value {projection!r} must be one of {list(projections)}" - ) - - hemisphere = hemisphere.lower() + proj = parse_projection(projection) + hemi = parse_hemisphere(hemisphere) + if proj == "stereographic": + proj_label = "masterSP" + else: + proj_label = "mLP" hemispheres = {"upper": "NH", "lower": "SH"} - - projection_label = projections[projection] - if hemisphere == "both": - datasets = [] - for hemi_label in hemispheres.values(): - datasets.append(data_group[projection_label + hemi_label]) - elif hemisphere in hemispheres: - dset_name = projection_label + hemispheres[hemisphere] - datasets = [data_group[dset_name]] + datasets = [] + if hemi == "both": + datasets.append(data_group[f"{proj_label}NH"]) + datasets.append(data_group[f"{proj_label}SH"]) else: - raise ValueError( - f"'hemisphere' value {hemisphere!r} must be one of {list(hemispheres)}" - ) - + datasets.append(data_group[f"{proj_label}{hemispheres[hemi]}"]) return datasets diff --git a/src/kikuchipy/io/plugins/emsoft_ebsd_master_pattern/_api.py b/src/kikuchipy/io/plugins/emsoft_ebsd_master_pattern/_api.py index 95f5f83b..53c1bf19 100644 --- a/src/kikuchipy/io/plugins/emsoft_ebsd_master_pattern/_api.py +++ b/src/kikuchipy/io/plugins/emsoft_ebsd_master_pattern/_api.py @@ -20,6 +20,7 @@ from pathlib import Path +from kikuchipy._utils.vector import ValidHemispheres, ValidProjections from kikuchipy.io.plugins._emsoft_master_pattern import EMsoftMasterPatternReader ENERGY_ARG = """Desired beam energy or energy range. If not given (default), all @@ -51,8 +52,8 @@ def energy_string(self) -> str: def file_reader( filename: str | Path, energy: range | None = None, - projection: str = "stereographic", - hemisphere: str = "upper", + projection: ValidProjections = "stereographic", + hemisphere: ValidHemispheres = "upper", lazy: bool = False, **kwargs, ) -> list[dict]: @@ -84,11 +85,7 @@ def file_reader( Data, axes, metadata, and original metadata. """ reader = EMsoftEBSDMasterPatternReader( - filename=filename, - energy=energy, - projection=projection, - hemisphere=hemisphere, - lazy=lazy, + filename, energy, projection, hemisphere, lazy ) return reader.read(**kwargs) diff --git a/src/kikuchipy/io/plugins/emsoft_ecp_master_pattern/_api.py b/src/kikuchipy/io/plugins/emsoft_ecp_master_pattern/_api.py index 8643bb83..123f02d0 100644 --- a/src/kikuchipy/io/plugins/emsoft_ecp_master_pattern/_api.py +++ b/src/kikuchipy/io/plugins/emsoft_ecp_master_pattern/_api.py @@ -20,6 +20,7 @@ from pathlib import Path +from kikuchipy._utils.vector import ValidHemispheres, ValidProjections from kikuchipy.io.plugins._emsoft_master_pattern import EMsoftMasterPatternReader from kikuchipy.io.plugins.emsoft_ebsd_master_pattern._api import ( ENERGY_ARG, @@ -45,8 +46,8 @@ def energy_string(self) -> str: def file_reader( filename: str | Path, energy: range | None = None, - projection: str = "stereographic", - hemisphere: str = "upper", + projection: ValidProjections = "stereographic", + hemisphere: ValidHemispheres = "upper", lazy: bool = False, **kwargs, ) -> list[dict]: @@ -78,11 +79,7 @@ def file_reader( Data, axes, metadata and original metadata. """ reader = EMsoftECPMasterPatternReader( - filename=filename, - energy=energy, - projection=projection, - hemisphere=hemisphere, - lazy=lazy, + filename, energy, projection, hemisphere, lazy ) return reader.read(**kwargs) diff --git a/src/kikuchipy/io/plugins/emsoft_tkd_master_pattern/_api.py b/src/kikuchipy/io/plugins/emsoft_tkd_master_pattern/_api.py index a64d3490..5c123797 100644 --- a/src/kikuchipy/io/plugins/emsoft_tkd_master_pattern/_api.py +++ b/src/kikuchipy/io/plugins/emsoft_tkd_master_pattern/_api.py @@ -20,6 +20,7 @@ from pathlib import Path +from kikuchipy._utils.vector import ValidHemispheres, ValidProjections from kikuchipy.io.plugins._emsoft_master_pattern import EMsoftMasterPatternReader from kikuchipy.io.plugins.emsoft_ebsd_master_pattern._api import ( ENERGY_ARG, @@ -45,8 +46,8 @@ def energy_string(self) -> str: def file_reader( filename: str | Path, energy: range | None = None, - projection: str = "stereographic", - hemisphere: str = "upper", + projection: ValidProjections = "stereographic", + hemisphere: ValidHemispheres = "upper", lazy: bool = False, **kwargs, ) -> list[dict]: @@ -78,11 +79,7 @@ def file_reader( Data, axes, metadata and original metadata. """ reader = EMsoftTKDMasterPatternReader( - filename=filename, - energy=energy, - projection=projection, - hemisphere=hemisphere, - lazy=lazy, + filename, energy, projection, hemisphere, lazy ) return reader.read(**kwargs) diff --git a/src/kikuchipy/io/plugins/nordif/_api.py b/src/kikuchipy/io/plugins/nordif/_api.py index 7e025db0..cae99398 100644 --- a/src/kikuchipy/io/plugins/nordif/_api.py +++ b/src/kikuchipy/io/plugins/nordif/_api.py @@ -29,7 +29,6 @@ import numpy as np from orix.crystal_map import CrystalMap -from kikuchipy.constants import VisibleDeprecationWarning from kikuchipy.detectors.ebsd_detector import EBSDDetector if TYPE_CHECKING: # pragma: no cover @@ -107,7 +106,6 @@ def file_reader( "No setting file found and no scan_size or pattern_size detected in " "input arguments. These must be set if no setting file is provided" ) - warnings.filterwarnings("ignore", category=VisibleDeprecationWarning) md = {} omd = {} detector_dict = None diff --git a/src/kikuchipy/io/plugins/oxford_binary/_api.py b/src/kikuchipy/io/plugins/oxford_binary/_api.py index bf79b13a..c1773f74 100644 --- a/src/kikuchipy/io/plugins/oxford_binary/_api.py +++ b/src/kikuchipy/io/plugins/oxford_binary/_api.py @@ -422,12 +422,12 @@ def get_single_pattern_header(self, offset: int) -> tuple[bool, int, int, int]: Number of pattern bytes. """ self.file.seek(offset) - header = np.fromfile(self.file, dtype=self.pattern_header_dtype, count=1) + header = np.fromfile(self.file, dtype=self.pattern_header_dtype, count=1)[0] return ( bool(header["is_compressed"][0]), - int(header["nrows"]), - int(header["ncols"]), - int(header["n_bytes"]), + int(header["nrows"][0]), + int(header["ncols"][0]), + int(header["n_bytes"][0]), ) def get_version(self) -> int: diff --git a/src/kikuchipy/pattern/_pattern.py b/src/kikuchipy/pattern/_pattern.py index 515aa42c..0c976352 100644 --- a/src/kikuchipy/pattern/_pattern.py +++ b/src/kikuchipy/pattern/_pattern.py @@ -815,7 +815,8 @@ def _adaptive_histogram_equalization( ) -> np.ndarray: """Local contrast enhancement with adaptive histogram equalization. - This method makes use of :func:`skimage.exposure.equalize_adapthist`. + This method makes use of + :func:`skimage.exposure.equalize_adapthist`. Parameters ---------- @@ -834,15 +835,6 @@ def _adaptive_histogram_equalization( image_eq Image with enhanced contrast. """ - dtype_in = image.dtype.type - - image_eq = equalize_adapthist( - image, - kernel_size=kernel_size, - clip_limit=clip_limit, - nbins=nbins, - ) - - image_eq = rescale_intensity(image_eq, dtype_out=dtype_in) - + image_eq = equalize_adapthist(image, kernel_size, clip_limit, nbins) + image_eq = rescale_intensity(image_eq, dtype_out=image.dtype.type) return image_eq diff --git a/src/kikuchipy/signals/_kikuchi_master_pattern.py b/src/kikuchipy/signals/_kikuchi_master_pattern.py index ad3b5e49..f2357d2b 100644 --- a/src/kikuchipy/signals/_kikuchi_master_pattern.py +++ b/src/kikuchipy/signals/_kikuchi_master_pattern.py @@ -28,6 +28,12 @@ from scipy.interpolate import interpn from tqdm import tqdm +from kikuchipy._utils.vector import ( + ValidHemispheres, + ValidProjections, + parse_hemisphere, + parse_projection, +) from kikuchipy.signals._kikuchipy_signal import KikuchipySignal2D from kikuchipy.signals.util._master_pattern import _lambert2vector from kikuchipy.signals.util._overwrite_hyperspy_methods import insert_doc_disclaimer @@ -50,14 +56,14 @@ class KikuchiMasterPattern(KikuchipySignal2D, hs.signals.Signal2D): *args See :class:`~hyperspy._signals.signal2d.Signal2D`. hemisphere : str - Which hemisphere the data contains, either ``"upper"``, - ``"lower"``, or ``"both"``. + Which hemisphere the data contains, either "upper", "lower", or + "both". phase : ~orix.crystal_map.Phase The phase describing the crystal structure used in the master pattern simulation. projection : str - Which projection the pattern is in, ``"stereographic"`` or - ``"lambert"``. + Which projection the pattern is in, "stereographic" or + "lambert". **kwargs See :class:`~hyperspy._signals.signal2d.Signal2D`. """ @@ -78,7 +84,7 @@ def _has_multiple_energies(self) -> bool: def hemisphere(self) -> str: """Return or set which hemisphere(s) the signal contains. - Options are ``"upper"``, ``"lower"`` or ``"both"``. + Options are "upper", "lower", or "both". Parameters ---------- @@ -88,8 +94,8 @@ def hemisphere(self) -> str: return self._hemisphere @hemisphere.setter - def hemisphere(self, value: str) -> None: - self._hemisphere = value + def hemisphere(self, value: ValidHemispheres) -> None: + self._hemisphere = parse_hemisphere(value) @property def phase(self) -> Phase: @@ -114,14 +120,14 @@ def projection(self) -> str: Parameters ---------- value : str - Which projection the pattern is in, either - ``"stereographic"`` or ``"lambert"``. + Which projection the pattern is in, either "stereographic" + or "lambert". """ return self._projection @projection.setter - def projection(self, value: str) -> None: - self._projection = value + def projection(self, value: ValidProjections) -> None: + self._projection = parse_projection(value) def as_lambert(self, show_progressbar: bool | None = None) -> Any: """Return a new master pattern in the Lambert projection @@ -223,25 +229,25 @@ def plot_spherical( pattern to plot. If not given, the highest energy is used. return_figure Whether to return the :class:`pyvista.Plotter` instance for - further modification and then plotting. Default is - ``False``. If ``True``, the figure is not plotted. + further modification and then plotting. Default is False. If + True, the figure is not plotted. style - Visualization style of the mesh, either ``"surface"`` - (default), ``"wireframe"`` or ``"points"``. In general, - ``"surface"`` is recommended when zoomed out, while - ``"points"`` is recommended when zoomed in. See - :meth:`pyvista.Plotter.add_mesh` for details. + Visualization style of the mesh, either "surface" (default), + "wireframe" or "points". In general, "surface" is + recommended when zoomed out, while "points" is recommended + when zoomed in. See :meth:`pyvista.Plotter.add_mesh` for + details. plotter_kwargs Dictionary of keyword arguments passed to :class:`pyvista.Plotter`. show_kwargs Dictionary of keyword arguments passed to - :meth:`pyvista.Plotter.show` if ``return_figure=False``. + :meth:`pyvista.Plotter.show` if *return_figure* is False. Returns ------- pl - Only returned if ``return_figure=True``. + Only returned if *return_figure* is True. Notes ----- diff --git a/src/kikuchipy/signals/_kikuchipy_signal.py b/src/kikuchipy/signals/_kikuchipy_signal.py index 27e70eed..2b3e47a1 100644 --- a/src/kikuchipy/signals/_kikuchipy_signal.py +++ b/src/kikuchipy/signals/_kikuchipy_signal.py @@ -285,10 +285,10 @@ def normalize_intensity( >>> import kikuchipy as kp >>> s = kp.data.nickel_ebsd_small() >>> np.mean(s.data) - 146.0670987654321 + np.float64(146.0670987654321) >>> s.normalize_intensity(dtype_out=np.float32) >>> np.mean(s.data) - 0.0 + np.float32(0.0) """ if lazy_output and inplace: raise ValueError("'lazy_output=True' requires 'inplace=False'") @@ -409,23 +409,16 @@ def adaptive_histogram_equalization( if lazy_output and inplace: raise ValueError("'lazy_output=True' requires 'inplace=False'") - dtype_out = self.data.dtype - if np.issubdtype(dtype_out, np.floating): + if not self._lazy and np.isnan(self.data).any(): warnings.warn( - ( - "Equalization of signals with floating point data type has been " - "shown to give bad results. Rescaling intensities to integer " - "intensities is recommended." - ), - UserWarning, + "Equalization of signals with NaN data has been shown to give bad " + "results" ) - if not self._lazy and np.isnan(self.data).any(): + elif np.issubdtype(self.data.dtype, np.floating): warnings.warn( - ( - "Equalization of signals with NaN data has been shown to give bad " - "results" - ), - UserWarning, + "Equalization of signals with floating point data type has been shown " + "to give bad results. Rescaling intensities to integer intensities is " + "recommended." ) # Determine window size (shape of contextual region) @@ -440,7 +433,7 @@ def adaptive_histogram_equalization( map_kw = { "show_progressbar": show_progressbar, - "output_dtype": dtype_out, + "output_dtype": self.data.dtype, "kernel_size": kernel_size, "clip_limit": clip_limit, "nbins": nbins, diff --git a/src/kikuchipy/signals/ebsd.py b/src/kikuchipy/signals/ebsd.py index 395f6e49..406b0a7b 100644 --- a/src/kikuchipy/signals/ebsd.py +++ b/src/kikuchipy/signals/ebsd.py @@ -123,15 +123,15 @@ class EBSD(KikuchipySignal2D): ---------- *args See :class:`~hyperspy._signals.signal2d.Signal2D`. - detector : EBSDDetector, optional + detector : :class:`~kikuchipy.detectors.EBSDDetector`, optional Detector describing the EBSD detector-sample geometry. If not - given, this is a default detector (see :class:`EBSDDetector`). - static_background : ~numpy.ndarray or ~dask.array.Array, optional - Static background pattern. If not given, this is ``None``. + given, this is a default detector. + static_background : numpy.ndarray or dask.array.Array, optional + Static background pattern. If not given, this is None. xmap : ~orix.crystal_map.CrystalMap Crystal map containing the phases, unit cell rotations and auxiliary properties of the EBSD dataset. If not given, this is - ``None``. + None. **kwargs See :class:`~hyperspy._signals.signal2d.Signal2D`. @@ -141,12 +141,8 @@ class EBSD(KikuchipySignal2D): An EBSD signal with ``(3, 3)`` experimental nickel patterns. kikuchipy.data.nickel_ebsd_large : An EBSD signal with ``(55, 75)`` experimental nickel patterns. - kikuchipy.data.silicon_ebsd_moving_screen_in : - An EBSD signal with one experimental silicon pattern. - kikuchipy.data.silicon_ebsd_moving_screen_out5mm : - An EBSD signal with one experimental silicon pattern. - kikuchipy.data.silicon_ebsd_moving_screen_out10mm : - An EBSD signal with one experimental silicon pattern. + kikuchipy.data.si_ebsd_moving_screen : + An EBSD signal with experimental silicon patterns. Examples -------- diff --git a/src/kikuchipy/signals/ebsd_master_pattern.py b/src/kikuchipy/signals/ebsd_master_pattern.py index 4c98b444..91173927 100644 --- a/src/kikuchipy/signals/ebsd_master_pattern.py +++ b/src/kikuchipy/signals/ebsd_master_pattern.py @@ -58,14 +58,14 @@ class EBSDMasterPattern(KikuchiMasterPattern): *args See :class:`~hyperspy._signals.signal2d.Signal2D`. hemisphere : str - Which hemisphere the data contains, either ``"upper"``, - ``"lower"``, or ``"both"``. + Which hemisphere the data contains, either "upper", "lower", or + "both". phase : ~orix.crystal_map.Phase The phase describing the crystal structure used in the master pattern simulation. projection : str - Which projection the pattern is in, ``"stereographic"`` or - ``"lambert"``. + Which projection the pattern is in, "stereographic" or + "lambert". **kwargs See :class:`~hyperspy._signals.signal2d.Signal2D`. diff --git a/src/kikuchipy/signals/ecp_master_pattern.py b/src/kikuchipy/signals/ecp_master_pattern.py index 101fe935..e96bfeef 100644 --- a/src/kikuchipy/signals/ecp_master_pattern.py +++ b/src/kikuchipy/signals/ecp_master_pattern.py @@ -44,14 +44,14 @@ class ECPMasterPattern(KikuchiMasterPattern): *args See :class:`~hyperspy._signals.signal2d.Signal2D`. hemisphere : str - Which hemisphere the data contains, either ``"upper"``, - ``"lower"``, or ``"both"``. + Which hemisphere the data contains, either "upper", "lower", or + "both". phase : ~orix.crystal_map.Phase The phase describing the crystal structure used in the master pattern simulation. projection : str - Which projection the pattern is in, ``"stereographic"`` or - ``"lambert"``. + Which projection the pattern is in, "stereographic" or + "lambert". **kwargs See :class:`~hyperspy._signals.signal2d.Signal2D`. """ diff --git a/src/kikuchipy/signals/util/_master_pattern.py b/src/kikuchipy/signals/util/_master_pattern.py index d1445a4f..36d641ef 100644 --- a/src/kikuchipy/signals/util/_master_pattern.py +++ b/src/kikuchipy/signals/util/_master_pattern.py @@ -64,7 +64,7 @@ from numba import njit import numpy as np -from kikuchipy._rotation import _rotate_vector +from kikuchipy._utils.numba import rotate_vector from kikuchipy.pattern._pattern import _rescale_with_min_max if TYPE_CHECKING: # pragma: no cover @@ -542,7 +542,7 @@ def _project_single_pattern_from_master_pattern( array shapes and data types. """ # Rotate the detector's view of the crystal - dc_rotated = _rotate_vector(rotation, direction_cosines) + dc_rotated = rotate_vector(rotation, direction_cosines) (nii, nij, niip, nijp, di, dj, dim, djm) = _get_lambert_interpolation_parameters( v=dc_rotated, npx=npx, npy=npy, scale=scale diff --git a/src/kikuchipy/simulations/kikuchi_pattern_simulator.py b/src/kikuchipy/simulations/kikuchi_pattern_simulator.py index 48118c0c..a257752e 100644 --- a/src/kikuchipy/simulations/kikuchi_pattern_simulator.py +++ b/src/kikuchipy/simulations/kikuchi_pattern_simulator.py @@ -55,7 +55,7 @@ # ###################################################################### import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Literal import dask.array as da from dask.diagnostics import ProgressBar @@ -63,13 +63,17 @@ import matplotlib.colors as mcolors import matplotlib.figure as mfigure import matplotlib.pyplot as plt +import numba as nb import numpy as np from orix import projections from orix.crystal_map import Phase from orix.plot._util import Arrow3D from orix.quaternion import Rotation from orix.vector import Vector3d +from tqdm import tqdm +from kikuchipy._utils.numba import vec_dot +from kikuchipy._utils.vector import ValidHemispheres, poles_from_hemisphere from kikuchipy.constants import installed from kikuchipy.detectors.ebsd_detector import EBSDDetector from kikuchipy.signals.ebsd_master_pattern import EBSDMasterPattern @@ -116,13 +120,13 @@ def __repr__(self) -> str: def calculate_master_pattern( self, half_size: int = 500, - hemisphere: str = "upper", - scaling: str | None = "linear", + hemisphere: ValidHemispheres = "upper", + scaling: Literal["linear", "square"] | None = "linear", ) -> EBSDMasterPattern: - r"""Calculate a kinematical master pattern in the stereographic + r"""Return a kinematical master pattern in the stereographic projection. - Requires that the :attr:`reflectors` have structure factors + Requires that :attr:`reflectors` have structure factors (:attr:`~diffsims.crystallography.ReciprocalLatticeVector.structure_factor`) and Bragg angles (:attr:`~diffsims.crystallography.ReciprocalLatticeVector.theta`) @@ -132,15 +136,15 @@ def calculate_master_pattern( ---------- half_size Number of pixels along the x-direction of the square master - pattern. Default is ``500``. The full size will be - ``2 * half_size + 1``, given a master pattern of shape - ``(1001, 1001)`` for the default value. + pattern. Default is 500. The full size will be 2 * half_size + + 1, given a master pattern of shape (1001, 1001) for the + default value. hemisphere - Which hemisphere(s) to calculate. Options are ``"upper"`` - (default), ``"lower"`` or ``"both"``. + Which hemisphere(s) to calculate. Options are "upper" + (default), "lower", or "both". scaling Intensity scaling of the band kinematical intensities, - either ``"linear"`` (default), :math:`|F|`, ``"square"``, + either "linear" (default), :math:`|F|`, "square", :math:`|F|^2`, or None, giving all bands an intensity of 1. Returns @@ -158,80 +162,39 @@ def calculate_master_pattern( size = int(2 * half_size + 1) - # Which hemisphere(s) to calculate - if hemisphere == "both": - poles = [-1, 1] - elif hemisphere == "upper": - poles = [-1] - elif hemisphere == "lower": - poles = [1] - else: - raise ValueError( - "Unknown `hemisphere`, options are 'upper', 'lower' or 'both'" - ) - - if scaling == "linear": - intensity = abs(self.reflectors.structure_factor) - elif scaling == "square": - factor = self.reflectors.structure_factor - intensity = abs(factor * factor.conjugate()) - elif scaling is None: - intensity = np.ones(self.reflectors.size) - else: - raise ValueError( - "Unknown `scaling`, options are 'linear', 'square' or None" - ) - - # Get Dask arrays of reflector information - intensity = da.from_array(intensity).astype(np.float64) - theta1 = da.from_array((np.pi / 2) - self.reflectors.theta) - theta2 = da.from_array(np.pi / 2) - xyz_ref = da.from_array(Vector3d(self.reflectors).unit.data) - - # Stereographic coordinates (X, Y) for a square encompassing the - # stereographic projection and outside (in the corners) + poles = poles_from_hemisphere(hemisphere) + + match scaling: + case "linear": + intensity = abs(self.reflectors.structure_factor) + case "square": + factor = self.reflectors.structure_factor + intensity = abs(factor * factor.conjugate()) + case None: + intensity = np.ones(self.reflectors.size) + case _: + raise ValueError( + f"Unknown scaling {scaling!r}, options are 'linear', 'square', or " + "None" + ) + + xyz_reflector = Vector3d(self.reflectors).unit.data + theta_reflector = self.reflectors.theta arr = np.linspace(-1, 1, size) - x, y = np.meshgrid(arr, arr) + X, Y = np.meshgrid(arr, arr) + X = X.ravel() + Y = Y.ravel() + n_poles = len(poles) + patterns = np.empty((n_poles, size * size), dtype=np.float64) - # Loop over hemisphere(s) - master_pattern_da = [] - for i in range(len(poles)): - # 3D coordinates (x, y, z) of unit vectors on the - # half-sphere + for i in tqdm(range(n_poles), ncols=80): stereo2sphere = projections.InverseStereographicProjection(poles[i]) - v_hemi = stereo2sphere.xy2vector(x.ravel(), y.ravel()).flatten() - xyz_hemi = da.from_array(v_hemi.data.squeeze(), chunks=(half_size, -1)) - - # Angles between vectors and Kikuchi lines - dp = da.einsum("ij,kj->ik", xyz_hemi, xyz_ref) - angles = da.arccos(da.round(dp, 12)) # Avoid invalid values - - # Exclude Kikuchi bands with: - # 1. Dot products lower than a threshold - # 2. Angles outside a vector - mask1 = da.absolute(dp) <= 1e-7 - mask2 = da.logical_and( - da.logical_and(angles >= theta1, angles <= theta2), ~mask1 + v_hemi = stereo2sphere.xy2vector(X.ravel(), Y.ravel()) + xyz_hemi = v_hemi.data + patterns[i] = get_pattern( + intensity, xyz_hemi, xyz_reflector, theta_reflector ) - - # Generate master pattern on this hemisphere, chunk by chunk - pattern = da.map_blocks( - _get_pattern, intensity, mask1, mask2, drop_axis=1, dtype=np.float64 - ) - master_pattern_da.append(pattern) - - if hemisphere == "both": - master_pattern = np.zeros((2, size * size)) - shape_out = (2, size, size) - else: - master_pattern_da = master_pattern_da[0] - master_pattern = np.zeros((size * size,)) - shape_out = (size, size) - - with ProgressBar(): - da.store(master_pattern_da, master_pattern) - - master_pattern = master_pattern.reshape(shape_out) + patterns = patterns.reshape(-1, size, size).squeeze() if hemisphere == "both": axes = [{"size": 2, "name": "hemisphere"}] @@ -242,7 +205,7 @@ def calculate_master_pattern( axes.append(axis) return EBSDMasterPattern( - master_pattern, + patterns, axes=axes, phase=self.phase, hemisphere=hemisphere, @@ -602,33 +565,6 @@ def _raise_if_no_structure_factor(self): ) -def _get_pattern( - intensity: np.ndarray, mask1: np.ndarray, mask2: np.ndarray -) -> np.ndarray: - """Generate part of a master pattern by summing intensities from - reflectors. - - Used in :meth:`calculate_master_pattern`. - - Parameters - ---------- - intensity - Reflector intensities. - mask1, mask2 - Boolean arrays with ``True`` for reflectors to include the - intensity from in each pattern coordinate. - - Returns - ------- - part - Master pattern part. - """ - intensity_part = np.full(mask1.shape, intensity) - part = 0.5 * np.sum(intensity_part, where=mask1, axis=1) - part += np.sum(intensity_part, where=mask2, axis=1) - return part - - def _plot_spherical( mode: str, ref: ReciprocalLatticeVector, @@ -741,3 +677,31 @@ def _plot_spherical( figure.show() return figure + + +# ------------------- Numba-accelerated functions -------------------- # + + +@nb.njit( + nb.float64[:](nb.float64[:], nb.float64[:, :], nb.float64[:, :], nb.float64[:]), + cache=True, + parallel=True, + fastmath=True, + nogil=True, +) +def get_pattern(intensity, xyz_hemi, xyz_reflector, theta_reflector): + theta2 = np.pi / 2 + theta1 = theta2 - theta_reflector + n = xyz_hemi.shape[0] + m = xyz_reflector.shape[0] + pattern = np.zeros(n, dtype=np.float64) + for i in nb.prange(m): + for j in range(n): + D = vec_dot(xyz_reflector[i], xyz_hemi[j]) + if np.abs(D) <= 1e-7: + pattern[j] = pattern[j] + 0.5 * intensity[i] + else: + angle = np.arccos(D) + if angle <= theta2 and angle >= theta1[i]: + pattern[j] = pattern[j] + intensity[i] + return pattern diff --git a/tests/test_filters/test_window.py b/tests/test_filters/test_window.py index 9d496bf6..8482463a 100644 --- a/tests/test_filters/test_window.py +++ b/tests/test_filters/test_window.py @@ -236,7 +236,7 @@ def test_plot_invalid_window(self): w = Window() w._name = 1 assert not w.is_valid - with pytest.raises(ValueError, match="Window is invalid."): + with pytest.raises(ValueError, match="Window is invalid"): w.plot() @pytest.mark.parametrize( diff --git a/tests/test_io/test_emsoft_master_pattern.py b/tests/test_io/test_emsoft_master_pattern.py index d70c2e9b..c07ae37e 100644 --- a/tests/test_io/test_emsoft_master_pattern.py +++ b/tests/test_io/test_emsoft_master_pattern.py @@ -20,9 +20,9 @@ import pytest from kikuchipy.io.plugins._emsoft_master_pattern import ( - _check_file_format, - _get_data_shape_slices, - _get_datasets, + check_file_format, + get_data_shape_slices, + get_datasets, ) @@ -35,7 +35,7 @@ def test_check_file_format(self, save_path_hdf5): "ProgramName", data=np.array([b"EMEBSDmasterr.f90"], dtype="S17") ) with pytest.raises(IOError, match=".* is not in EMsoft's master "): - _check_file_format(f, "EBSD") + check_file_format(f, "EBSD") @pytest.mark.parametrize( ( @@ -86,7 +86,7 @@ def test_get_data_shape_slices( expected_slices, expected_min_max_energy, ): - data_shape, data_slices = _get_data_shape_slices( + data_shape, data_slices = get_data_shape_slices( npx=npx, energies=energies, energy=energy ) @@ -111,7 +111,7 @@ def test_get_datasets( self, emsoft_ebsd_master_pattern_file, projection, hemisphere, dataset_names ): with File(emsoft_ebsd_master_pattern_file) as f: - datasets = _get_datasets( + datasets = get_datasets( data_group=f["EMData/EBSDmaster"], projection=projection, hemisphere=hemisphere, @@ -121,8 +121,8 @@ def test_get_datasets( @pytest.mark.parametrize( "projection, hemisphere, error_msg", [ - ("stereographicl", "upper", "'projection' value 'stereographicl' "), - ("lambert", "east", "'hemisphere' value 'east' "), + ("stereographicl", "upper", "Unknown projection 'stereographicl',"), + ("lambert", "east", "Unknown hemisphere 'east',"), ], ) def test_get_datasets_raises( @@ -130,7 +130,7 @@ def test_get_datasets_raises( ): with File(emsoft_ebsd_master_pattern_file) as f: with pytest.raises(ValueError, match=error_msg): - _ = _get_datasets( + _ = get_datasets( data_group=f["EMData/EBSDmaster"], projection=projection, hemisphere=hemisphere, diff --git a/tests/test_signals/test_ebsd_master_pattern.py b/tests/test_signals/test_ebsd_master_pattern.py index 37de0b25..b843cac4 100644 --- a/tests/test_signals/test_ebsd_master_pattern.py +++ b/tests/test_signals/test_ebsd_master_pattern.py @@ -168,12 +168,12 @@ def test_properties(self, projection, hemisphere): mp2 = mp.deepcopy() assert mp2.projection == projection - mp2.projection = "gnomonic" - assert mp2.projection != projection + with pytest.raises(ValueError, match="Unknown projection 'gnomonic'"): + mp2.projection = "gnomonic" assert mp2.hemisphere == hemisphere - mp2.hemisphere = "west" - assert mp2.hemisphere != hemisphere + with pytest.raises(ValueError, match="Unknown hemisphere 'west'"): + mp2.hemisphere = "west" assert mp2.phase.point_group.name == mp.phase.point_group.name mp2.phase.space_group = 220 @@ -708,6 +708,7 @@ def test_normalize_intensity_lazy_output(self): mp4 = mp3.normalize_intensity(inplace=False, lazy_output=False) assert isinstance(mp4, kp.signals.EBSDMasterPattern) + @pytest.mark.filterwarnings("ignore:invalid value encountered in cast") def test_adaptive_histogram_equalization(self): mp_sp = kp.data.nickel_ebsd_master_pattern_small() diff --git a/tests/test_signals/test_ecp_master_pattern.py b/tests/test_signals/test_ecp_master_pattern.py index 5ca3292f..45c53380 100644 --- a/tests/test_signals/test_ecp_master_pattern.py +++ b/tests/test_signals/test_ecp_master_pattern.py @@ -45,18 +45,12 @@ def test_init_lazy_ecp_master_pattern(self): def test_set_custom_properties(self, emsoft_ecp_master_pattern_file): s = kp.load(emsoft_ecp_master_pattern_file) - - # phase s.phase = Phase("b") assert s.phase.name == "b" - - # projection - s.projection = "spherical" - assert s.projection == "spherical" - - # hemisphere - s.hemisphere = "east" - assert s.hemisphere == "east" + with pytest.raises(ValueError): + s.projection = "spherical" + with pytest.raises(ValueError): + s.hemisphere = "east" def test_get_master_pattern_arrays_from_energy(self): """Get upper and lower hemisphere of master pattern of the diff --git a/tests/test_simulations/test_kikuchi_pattern_simulator.py b/tests/test_simulations/test_kikuchi_pattern_simulator.py index f2cd4a42..5762be4a 100644 --- a/tests/test_simulations/test_kikuchi_pattern_simulator.py +++ b/tests/test_simulations/test_kikuchi_pattern_simulator.py @@ -106,9 +106,9 @@ def test_default(self): def test_raises(self): """Appropriate error messages are raised.""" simulator = self.simulator - with pytest.raises(ValueError, match="Unknown `hemisphere`, options are"): + with pytest.raises(ValueError, match="Unknown hemisphere 'north', options are"): _ = simulator.calculate_master_pattern(hemisphere="north") - with pytest.raises(ValueError, match="Unknown `scaling`, options are"): + with pytest.raises(ValueError, match="Unknown scaling 'cubic', options are"): _ = simulator.calculate_master_pattern(scaling="cubic") def test_shape(self): @@ -116,7 +116,7 @@ def test_shape(self): simulator = self.simulator mp = simulator.calculate_master_pattern(half_size=100, hemisphere="both") assert mp.data.shape == (2, 201, 201) - assert np.allclose(mp.data[0], mp.data[1]) + assert np.allclose(mp.data[0], mp.data[1], atol=1e-7) axes_names = [a["name"] for a in mp.axes_manager.as_dictionary().values()] assert axes_names == ["hemisphere", "height", "width"] diff --git a/tests/test_rotation/__init__.py b/tests/test_utils/__init__.py similarity index 100% rename from tests/test_rotation/__init__.py rename to tests/test_utils/__init__.py diff --git a/tests/test_util/test_deprecated.py b/tests/test_utils/test_deprecated.py similarity index 98% rename from tests/test_util/test_deprecated.py rename to tests/test_utils/test_deprecated.py index 0cc86bbf..14c1b3de 100644 --- a/tests/test_util/test_deprecated.py +++ b/tests/test_utils/test_deprecated.py @@ -19,7 +19,7 @@ import pytest -from kikuchipy._util import deprecated, deprecated_argument +from kikuchipy._utils import deprecated, deprecated_argument from kikuchipy.constants import VisibleDeprecationWarning diff --git a/tests/test_util/test_logging.py b/tests/test_utils/test_logging.py similarity index 100% rename from tests/test_util/test_logging.py rename to tests/test_utils/test_logging.py diff --git a/tests/test_rotation/test_rotation.py b/tests/test_utils/test_rotation.py similarity index 67% rename from tests/test_rotation/test_rotation.py rename to tests/test_utils/test_rotation.py index f22736a1..fb08e29b 100644 --- a/tests/test_rotation/test_rotation.py +++ b/tests/test_utils/test_rotation.py @@ -20,6 +20,12 @@ from orix.vector import Vector3d import kikuchipy as kp +from kikuchipy._utils.numba import ( + rotate_vector, + rotation_from_euler, + rotation_from_rodrigues, +) +from kikuchipy.signals.util._master_pattern import _get_direction_cosines_for_fixed_pc class TestRotationVectorTools: @@ -29,26 +35,24 @@ def test_rotate_vector(self): """ rot = np.array([0.7071, 0.7071, 0, 0]) sig_shape = (20, 30) - dc = ( - kp.signals.util._master_pattern._get_direction_cosines_for_fixed_pc.py_func( - pcx=0.5, - pcy=0.5, - pcz=0.5, - nrows=sig_shape[0], - ncols=sig_shape[1], - tilt=10, - azimuthal=0, - sample_tilt=70, - signal_mask=np.ones(sig_shape[0] * sig_shape[1], dtype=bool), - ) + dc = _get_direction_cosines_for_fixed_pc.py_func( + pcx=0.5, + pcy=0.5, + pcz=0.5, + nrows=sig_shape[0], + ncols=sig_shape[1], + tilt=10, + azimuthal=0, + sample_tilt=70, + signal_mask=np.ones(sig_shape[0] * sig_shape[1], dtype=bool), ) rot_orix = Rotation(rot) dc_orix = Vector3d(dc) rotated_dc_orix = rot_orix * dc_orix - rotated_dc = kp._rotation._rotate_vector(rot, dc) - rotated_dc_py = kp._rotation._rotate_vector.py_func(rot, dc) + rotated_dc = rotate_vector(rot, dc) + rotated_dc_py = rotate_vector.py_func(rot, dc) assert np.allclose(rotated_dc, rotated_dc_py, atol=1e-3) assert np.allclose(rotated_dc_py, rotated_dc_orix.data, atol=1e-3) @@ -56,8 +60,8 @@ def test_rotate_vector(self): def test_rotation_from_euler(self): euler = np.array([1, 2, 3]) rot_orix = Rotation.from_euler(euler).data - rot_numba = kp._rotation._rotation_from_euler(*euler) - rot_numba_py = kp._rotation._rotation_from_euler.py_func(*euler) + rot_numba = rotation_from_euler(*euler) + rot_numba_py = rotation_from_euler.py_func(*euler) assert np.allclose(rot_numba, rot_numba_py) assert np.allclose(rot_numba_py, rot_orix) @@ -65,8 +69,8 @@ def test_rotation_from_euler(self): def test_rotation_from_rodrigues(self): rod = np.array([1, 2, 3]) rot_orix = Rotation.from_rodrigues(rod).data - rot_numba = kp._rotation._rotation_from_rodrigues(*rod) - rot_numba_py = kp._rotation._rotation_from_rodrigues.py_func(*rod) + rot_numba = rotation_from_rodrigues(*rod) + rot_numba_py = rotation_from_rodrigues.py_func(*rod) assert np.allclose(rot_numba, rot_numba_py) assert np.allclose(rot_numba_py, rot_orix)