diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e98c4988..f721f637 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -2,13 +2,12 @@ name: build
on:
push:
- branches:
- - '*'
+ branches-ignore:
+ - 'pre-commit-ci-update-config'
pull_request:
branches:
- '*'
workflow_dispatch:
- workflow: '*'
jobs:
code:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0676a6fd..b4cd29ca 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.0
+ rev: 24.4.2
hooks:
- id: black
- id: black-jupyter
diff --git a/.zenodo.json b/.zenodo.json
index 9b81059b..783e087a 100644
--- a/.zenodo.json
+++ b/.zenodo.json
@@ -34,6 +34,11 @@
"name": "Anders Christian Mathisen",
"affiliation": "Norwegian University of Science and Technology"
},
+ {
+ "name": "Zhou Xu",
+ "orcid": "0000-0002-7599-1166",
+ "affiliation": "Monash Centre for Electron Microscopy"
+ },
{
"name": "Carter Francis",
"orcid": "0000-0003-2564-1851",
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 5368fb05..ee0fb9b3 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -6,6 +6,30 @@ 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-03 - version 0.13.0
+===========================
+
+Added
+-----
+- We can now read 2D crystal maps from Channel Text Files (CTFs) using ``io.load()``.
+
+Changed
+-------
+- Phase names in crystal maps read from .ang files with ``io.load()`` now prefer to use
+ the abbreviated "Formula" instead of "MaterialName" in the file header.
+
+Removed
+-------
+- Removed deprecated ``from_neo_euler()`` method for ``Quaternion`` and its subclasses.
+- Removed deprecated argument ``convention`` in ``from_euler()`` and ``to_euler()``
+ methods for ``Quaternion`` and its subclasses. Use ``direction`` instead. Passing
+ ``convention`` will now raise an error.
+
+Deprecated
+----------
+- ``loadang()`` and ``loadctf()`` are deprecated and will be removed in the next minor
+ release. Please use ``io.load()`` instead.
+
2024-04-21 - version 0.12.1
===========================
diff --git a/doc/user/related_projects.rst b/doc/user/related_projects.rst
index bb911a94..bdb2f8e0 100644
--- a/doc/user/related_projects.rst
+++ b/doc/user/related_projects.rst
@@ -24,8 +24,8 @@ find useful:
orix depends on numpy-quaternion for quaternion multiplication.
- `texture `_: Python scripts for analysis of
crystallographic texture.
-- `pymicro `_`: Python package to work with material
+- `pymicro `_: Python package to work with material
microstructures and 3D data sets.
-- `DREAM.3D `_`: C++ library to reconstruct, instatiate, quantify,
+- `DREAM.3D `_: C++ library to reconstruct, instatiate, quantify,
mesh, handle and visualize multidimensional (3D), multimodal data (mainly EBSD
orientation data).
\ No newline at end of file
diff --git a/examples/plotting/interactive_xmap.py b/examples/plotting/interactive_xmap.py
new file mode 100644
index 00000000..d3925535
--- /dev/null
+++ b/examples/plotting/interactive_xmap.py
@@ -0,0 +1,98 @@
+"""
+============================
+Interactive crystal map plot
+============================
+
+This example shows how to use
+:doc:`matplotlib event connections ` to
+add an interactive click function to a :class:`~orix.crystal_map.CrystalMap` plot.
+Here, we navigate an inverse pole figure (IPF) map and retreive the phase name and
+corresponding Euler angles from the location of the click.
+
+.. note::
+
+ This example uses the interactive capabilities of Matplotlib, and this will not
+ appear in the static documentation.
+ Please run this code on your machine to see the interactivity.
+
+ You can copy and paste individual parts, or download the entire example using the
+ link at the bottom of the page.
+"""
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+from orix import data, plot
+from orix.crystal_map import CrystalMap
+
+xmap = data.sdss_ferrite_austenite(allow_download=True)
+print(xmap)
+
+pg_laue = xmap.phases[1].point_group.laue
+O_au = xmap["austenite"].orientations
+O_fe = xmap["ferrite"].orientations
+
+# Get IPF colors
+ipf_key = plot.IPFColorKeyTSL(pg_laue)
+rgb_au = ipf_key.orientation2color(O_au)
+rgb_fe = ipf_key.orientation2color(O_fe)
+
+# Combine IPF color arrays
+rgb_all = np.zeros((xmap.size, 3))
+phase_id_au = xmap.phases.id_from_name("austenite")
+phase_id_fe = xmap.phases.id_from_name("ferrite")
+rgb_all[xmap.phase_id == phase_id_au] = rgb_au
+rgb_all[xmap.phase_id == phase_id_fe] = rgb_fe
+
+
+def select_point(xmap: CrystalMap, rgb_all: np.ndarray) -> tuple[int, int]:
+ """Return location of interactive user click on image.
+
+ Interactive function for showing the phase name and Euler angles
+ from the click-position.
+ """
+ fig = xmap.plot(
+ rgb_all,
+ overlay="dp",
+ return_figure=True,
+ figure_kwargs={"figsize": (12, 8)},
+ )
+ ax = fig.axes[0]
+ ax.set_title("Click position")
+
+ # Extract array in the plot with IPF colors + dot product overlay
+ rgb_dp_2d = ax.images[0].get_array()
+
+ x = y = 0
+
+ def on_click(event):
+ x, y = (event.xdata, event.ydata)
+ if x is None:
+ print("Please click inside the IPF map")
+ return
+ print(x, y)
+
+ # Extract location in crystal map and extract phase name and
+ # Euler angles
+ xmap_yx = xmap[int(np.round(y)), int(np.round(x))]
+ phase_name = xmap_yx.phases_in_data[:].name
+ eu = xmap_yx.rotations.to_euler(degrees=True)[0].round(2)
+
+ # Format Euler angles
+ eu_str = "(" + ", ".join(np.array_str(eu)[1:-1].split()) + ")"
+
+ plt.clf()
+ 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}"
+ )
+ plt.draw()
+
+ fig.canvas.mpl_connect("button_press_event", on_click)
+
+ return x, y
+
+
+x, y = select_point(xmap, rgb_all)
+plt.show()
diff --git a/orix/__init__.py b/orix/__init__.py
index 71569379..18ea610e 100644
--- a/orix/__init__.py
+++ b/orix/__init__.py
@@ -1,5 +1,5 @@
__name__ = "orix"
-__version__ = "0.12.1.post0"
+__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."
@@ -13,6 +13,7 @@
"Duncan Johnstone",
"Niels Cautaerts",
"Anders Christian Mathisen",
+ "Zhou Xu",
"Carter Francis",
"Simon Høgås",
"Viljar Johan Femoen",
diff --git a/orix/crystal_map/crystal_map.py b/orix/crystal_map/crystal_map.py
index aae07db9..d74feb82 100644
--- a/orix/crystal_map/crystal_map.py
+++ b/orix/crystal_map/crystal_map.py
@@ -17,7 +17,7 @@
# along with orix. If not, see .
import copy
-from typing import Optional, Tuple, Union
+from typing import Dict, Optional, Tuple, Union
import matplotlib.pyplot as plt
import numpy as np
@@ -350,12 +350,12 @@ def y(self) -> Union[None, np.ndarray]:
@property
def dx(self) -> float:
"""Return the x coordinate step size."""
- return self._step_size_from_coordinates(self._x)
+ return _step_size_from_coordinates(self._x)
@property
def dy(self) -> float:
"""Return the y coordinate step size."""
- return self._step_size_from_coordinates(self._y)
+ return _step_size_from_coordinates(self._y)
@property
def row(self) -> Union[None, np.ndarray]:
@@ -1039,29 +1039,9 @@ def plot(
if return_figure:
return fig
- @staticmethod
- def _step_size_from_coordinates(coordinates: np.ndarray) -> float:
- """Return step size in input ``coordinates`` array.
-
- Parameters
- ----------
- coordinates
- Linear coordinate array.
-
- Returns
- -------
- step_size
- Step size in ``coordinates`` array.
- """
- unique_sorted = np.sort(np.unique(coordinates))
- step_size = 0
- if unique_sorted.size != 1:
- step_size = unique_sorted[1] - unique_sorted[0]
- return step_size
-
def _data_slices_from_coordinates(self, only_is_in_data: bool = True) -> tuple:
- """Return a tuple of slices defining the current data extent in
- all directions.
+ """Return a slices defining the current data extent in all
+ directions.
Parameters
----------
@@ -1072,23 +1052,14 @@ def _data_slices_from_coordinates(self, only_is_in_data: bool = True) -> tuple:
Returns
-------
slices
- Data slice in each existing dimension, in (z, y, x) order.
+ Data slice in each existing direction in (y, x) order.
"""
if only_is_in_data:
coordinates = self._coordinates
else:
coordinates = self._all_coordinates
-
- # Loop over dimension coordinates and step sizes
- slices = []
- for coords, step in zip(coordinates.values(), self._step_sizes.values()):
- if coords is not None and step != 0:
- c_min, c_max = np.min(coords), np.max(coords)
- i_min = int(np.around(c_min / step))
- i_max = int(np.around((c_max / step) + 1))
- slices.append(slice(i_min, i_max))
-
- return tuple(slices)
+ slices = _data_slices_from_coordinates(coordinates, self._step_sizes)
+ return slices
def _data_shape_from_coordinates(self, only_is_in_data: bool = True) -> tuple:
"""Return data shape based upon coordinate arrays.
@@ -1102,7 +1073,7 @@ def _data_shape_from_coordinates(self, only_is_in_data: bool = True) -> tuple:
Returns
-------
data_shape
- Shape of data in all existing dimensions, in (z, y, x) order.
+ Shape of data in each existing direction in (y, x) order.
"""
data_shape = []
for dim_slice in self._data_slices_from_coordinates(only_is_in_data):
@@ -1110,13 +1081,70 @@ def _data_shape_from_coordinates(self, only_is_in_data: bool = True) -> tuple:
return tuple(data_shape)
+def _data_slices_from_coordinates(
+ coords: Dict[str, np.ndarray], steps: Union[Dict[str, float], None] = None
+) -> Tuple[slice]:
+ """Return a list of slices defining the current data extent in all
+ directions.
+
+ Parameters
+ ----------
+ coords
+ Dictionary with coordinate arrays.
+ steps
+ Dictionary with step sizes in each direction. If not given, they
+ are computed from *coords*.
+
+ Returns
+ -------
+ slices
+ Data slice in each direction.
+ """
+ if steps is None:
+ steps = {
+ "x": _step_size_from_coordinates(coords["x"]),
+ "y": _step_size_from_coordinates(coords["y"]),
+ }
+ slices = []
+ for coords, step in zip(coords.values(), steps.values()):
+ if coords is not None and step != 0:
+ c_min, c_max = np.min(coords), np.max(coords)
+ i_min = int(np.around(c_min / step))
+ i_max = int(np.around((c_max / step) + 1))
+ slices.append(slice(i_min, i_max))
+ slices = tuple(slices)
+ return slices
+
+
+def _step_size_from_coordinates(coordinates: np.ndarray) -> float:
+ """Return step size in input *coordinates* array.
+
+ Parameters
+ ----------
+ coordinates
+ Linear coordinate array.
+
+ Returns
+ -------
+ step_size
+ Step size in *coordinates* array.
+ """
+ unique_sorted = np.sort(np.unique(coordinates))
+ if unique_sorted.size != 1:
+ step_size = unique_sorted[1] - unique_sorted[0]
+ else:
+ step_size = 0
+ return step_size
+
+
def create_coordinate_arrays(
shape: Optional[tuple] = None, step_sizes: Optional[tuple] = None
) -> Tuple[dict, int]:
- """Create flattened coordinate arrays from a given map shape and
+ """Return flattened coordinate arrays from a given map shape and
step sizes, suitable for initializing a
- :class:`~orix.crystal_map.CrystalMap`. Arrays for 1D or 2D maps can
- be returned.
+ :class:`~orix.crystal_map.CrystalMap`.
+
+ Arrays for 1D or 2D maps can be returned.
Parameters
----------
@@ -1125,13 +1153,13 @@ def create_coordinate_arrays(
and ten columns.
step_sizes
Map step sizes. If not given, it is set to 1 px in each map
- direction given by ``shape``.
+ direction given by *shape*.
Returns
-------
d
- Dictionary with keys ``"y"`` and ``"x"``, depending on the
- length of ``shape``, with coordinate arrays.
+ Dictionary with keys ``"x"`` and ``"y"``, depending on the
+ length of *shape*, with coordinate arrays.
map_size
Number of map points.
@@ -1145,10 +1173,10 @@ def create_coordinate_arrays(
>>> create_coordinate_arrays((2, 3), (1.5, 1.5))
({'x': array([0. , 1.5, 3. , 0. , 1.5, 3. ]), 'y': array([0. , 0. , 0. , 1.5, 1.5, 1.5])}, 6)
"""
- if shape is None:
+ if not shape:
shape = (5, 10)
ndim = len(shape)
- if step_sizes is None:
+ if not step_sizes:
step_sizes = (1,) * ndim
if ndim == 3 or len(step_sizes) > 2:
diff --git a/orix/io/__init__.py b/orix/io/__init__.py
index d2371bf7..c1434776 100644
--- a/orix/io/__init__.py
+++ b/orix/io/__init__.py
@@ -37,6 +37,7 @@
from h5py import File, is_hdf5
import numpy as np
+from orix._util import deprecated
from orix.crystal_map import CrystalMap
from orix.io.plugins import plugin_list
from orix.io.plugins._h5ebsd import hdf5group2dict
@@ -45,7 +46,6 @@
extensions = [plugin.file_extensions for plugin in plugin_list if plugin.writes]
-# Lists what will be imported when calling "from orix.io import *"
__all__ = [
"loadang",
"loadctf",
@@ -54,6 +54,8 @@
]
+# TODO: Remove after 0.13.0
+@deprecated(since="0.13", removal="0.14", alternative="io.load")
def loadang(file_string: str) -> Rotation:
"""Load ``.ang`` files.
@@ -73,6 +75,8 @@ def loadang(file_string: str) -> Rotation:
return Rotation.from_euler(euler)
+# TODO: Remove after 0.13.0
+@deprecated(since="0.13", removal="0.14", alternative="io.load")
def loadctf(file_string: str) -> Rotation:
"""Load ``.ctf`` files.
diff --git a/orix/io/plugins/__init__.py b/orix/io/plugins/__init__.py
index 0397150e..6485b624 100644
--- a/orix/io/plugins/__init__.py
+++ b/orix/io/plugins/__init__.py
@@ -28,15 +28,17 @@
ang
bruker_h5ebsd
+ ctf
emsoft_h5ebsd
orix_hdf5
"""
-from orix.io.plugins import ang, bruker_h5ebsd, emsoft_h5ebsd, orix_hdf5
+from orix.io.plugins import ang, bruker_h5ebsd, ctf, emsoft_h5ebsd, orix_hdf5
plugin_list = [
ang,
bruker_h5ebsd,
+ ctf,
emsoft_h5ebsd,
orix_hdf5,
]
diff --git a/orix/io/plugins/ang.py b/orix/io/plugins/ang.py
index e82232d5..521b27bb 100644
--- a/orix/io/plugins/ang.py
+++ b/orix/io/plugins/ang.py
@@ -43,10 +43,12 @@
def file_reader(filename: str) -> CrystalMap:
- """Return a crystal map from a file in EDAX TLS's .ang format. The
- map in the input is assumed to be 2D.
+ """Return a crystal map from a file in EDAX TLS's .ang format.
- Many vendors produce an .ang file. Supported vendors are:
+ The map in the input is assumed to be 2D.
+
+ Many vendors/programs produce an .ang file. Files from the following
+ vendors/programs are tested:
* EDAX TSL
* NanoMegas ASTAR Index
@@ -72,20 +74,19 @@ def file_reader(filename: str) -> CrystalMap:
with open(filename) as f:
header = _get_header(f)
- # Get phase names and crystal symmetries from header (potentially empty)
- phase_ids, phase_names, symmetries, lattice_constants = _get_phases_from_header(
- header
- )
- structures = []
- for name, abcABG in zip(phase_names, lattice_constants):
- structures.append(Structure(title=name, lattice=Lattice(*abcABG)))
+ # Phase information, potentially empty
+ phases = _get_phases_from_header(header)
+ phases["structures"] = []
+ lattice_constants = phases.pop("lattice_constants")
+ for name, abcABG in zip(phases["names"], lattice_constants):
+ structure = Structure(title=name, lattice=Lattice(*abcABG))
+ phases["structures"].append(structure)
# Read all file data
file_data = np.loadtxt(filename)
# Get vendor and column names
- n_rows, n_cols = file_data.shape
- vendor, column_names = _get_vendor_columns(header, n_cols)
+ vendor, column_names = _get_vendor_columns(header, file_data.shape[1])
# Data needed to create a CrystalMap object
data_dict = {
@@ -98,18 +99,13 @@ def file_reader(filename: str) -> CrystalMap:
"prop": {},
}
for column, name in enumerate(column_names):
- if name in data_dict.keys():
+ if name in data_dict:
data_dict[name] = file_data[:, column]
else:
data_dict["prop"][name] = file_data[:, column]
# Add phase list to dictionary
- data_dict["phase_list"] = PhaseList(
- names=phase_names,
- point_groups=symmetries,
- structures=structures,
- ids=phase_ids,
- )
+ data_dict["phase_list"] = PhaseList(**phases)
# Set which data points are not indexed
# TODO: Add not-indexed convention for INDEX ASTAR
@@ -149,9 +145,12 @@ def _get_header(file: TextIOWrapper) -> List[str]:
"""
header = []
line = file.readline()
- while line.startswith("#"):
+ i = 0
+ # Prevent endless loop by not reading past 1 000 lines
+ while line.startswith("#") and i < 1_000:
header.append(line.rstrip())
line = file.readline()
+ i += 1
return header
@@ -174,15 +173,13 @@ def _get_vendor_columns(header: List[str], n_cols_file: int) -> Tuple[str, List[
column_names
List of column names.
"""
- # Assume EDAX TSL by default
- vendor = "tsl"
-
- # Determine vendor by searching for the vendor footprint in the header
+ # Determine vendor by searching for vendor footprint in header
vendor_footprint = {
"emsoft": "EMsoft",
"astar": "ACOM",
"orix": "Column names: phi1, Phi, phi2",
}
+ vendor = "tsl" # Default guess
footprint_line = None
for name, footprint in vendor_footprint.items():
for line in header:
@@ -307,9 +304,7 @@ def _get_vendor_columns(header: List[str], n_cols_file: int) -> Tuple[str, List[
return vendor, vendor_column_names
-def _get_phases_from_header(
- header: List[str],
-) -> Tuple[List[int], List[str], List[str], List[List[float]]]:
+def _get_phases_from_header(header: List[str]) -> dict:
"""Return phase names and symmetries detected in an .ang file
header.
@@ -320,43 +315,38 @@ def _get_phases_from_header(
Returns
-------
- ids
- Phase IDs.
- phase_names
- List of names of detected phases.
- phase_point_groups
- List of point groups of detected phase.
- lattice_constants
- List of list of lattice parameters of detected phases.
+ phase_dict
+ Dictionary with the following keys (and types): "ids" (int),
+ "names" (str), "point_groups" (str), "lattice_constants" (list
+ of floats).
Notes
-----
- Regular expressions are used to collect phase name, formula and
- point group. This function have been tested with files from the
- following vendor's formats: EDAX TSL OIM Data Collection v7, ASTAR
- Index, and EMsoft v4/v5.
+ This function has been tested with files from the following vendor's
+ formats: EDAX TSL OIM Data Collection v7, ASTAR Index, and EMsoft
+ v4/v5.
"""
- regexps = {
- "id": "# Phase([ \t]+)([0-9 ]+)",
- "name": "# MaterialName([ \t]+)([A-z0-9 ]+)",
- "formula": "# Formula([ \t]+)([A-z0-9 ]+)",
- "point_group": "# Symmetry([ \t]+)([A-z0-9 ]+)",
+ str_patterns = {
+ "ids": "# Phase([ \t]+)([0-9 ]+)",
+ "names": "# MaterialName([ \t]+)([A-z0-9 ]+)",
+ "formulas": "# Formula([ \t]+)([A-z0-9 ]+)",
+ "point_groups": "# Symmetry([ \t]+)([A-z0-9 ]+)",
"lattice_constants": r"# LatticeConstants([ \t+])(.*)",
}
phases = {
- "name": [],
- "formula": [],
- "point_group": [],
+ "ids": [],
+ "names": [],
+ "formulas": [],
+ "point_groups": [],
"lattice_constants": [],
- "id": [],
}
for line in header:
- for key, exp in regexps.items():
+ for key, exp in str_patterns.items():
match = re.search(exp, line)
if match:
group = re.split("[ \t]", line.lstrip("# ").rstrip(" "))
group = list(filter(None, group))
- if key == "name":
+ if key == "names":
group = " ".join(group[1:]) # Drop "MaterialName"
elif key == "lattice_constants":
group = [float(i) for i in group[1:]]
@@ -364,22 +354,24 @@ def _get_phases_from_header(
group = group[-1]
phases[key].append(group)
- # Check if formula is empty (sometimes the case for ASTAR Index)
- names = phases["formula"]
- if len(names) == 0 or any([i != "" for i in names]):
- names = phases["name"]
+ n_phases = len(phases["names"])
+
+ # Use formulas in place of material names if they are all valid
+ formulas = phases.pop("formulas")
+ if len(formulas) == n_phases and all([len(name) for name in formulas]):
+ phases["names"] = formulas
# Ensure each phase has an ID (hopefully found in the header)
- phase_ids = [int(i) for i in phases["id"]]
- n_phases = len(phases["name"])
- if len(phase_ids) == 0:
+ phase_ids = [int(i) for i in phases["ids"]]
+ if not len(phase_ids):
phase_ids += [i for i in range(n_phases)]
elif n_phases - len(phase_ids) > 0 and len(phase_ids) != 0:
next_id = max(phase_ids) + 1
n_left = n_phases - len(phase_ids)
phase_ids += [i for i in range(next_id, next_id + n_left)]
+ phases["ids"] = phase_ids
- return phase_ids, names, phases["point_group"], phases["lattice_constants"]
+ return phases
def file_writer(
@@ -424,31 +416,31 @@ def file_writer(
Which map property to use as the image quality. If not given
(default), ``"iq"`` or ``"imagequality"``, if present, is used,
otherwise just zeros. If the property has more than one value
- per point and ``index`` is not given, only the first value is
+ per point and *index* is not given, only the first value is
used.
confidence_index_prop
Which map property to use as the confidence index. If not given
(default), ``"ci"``, ``"confidenceindex"``, ``"scores"``, or
``"correlation"``, if present, is used, otherwise just zeros. If
- the property has more than one value per point and ``index`` is
+ the property has more than one value per point and *index* is
not given, only the first value is used.
detector_signal_prop
Which map property to use as the detector signal. If not given
(default), ``"ds"``, or ``"detector_signal"``, if present, is
used, otherwise just zeros. If the property has more than one
- value per point and ``index`` is not given, only the first value
+ value per point and *index* is not given, only the first value
is used.
pattern_fit_prop
Which map property to use as the pattern fit. If not given
(default), ``"fit"`` or ``"patternfit"``, if present, is used,
otherwise just zeros. If the property has more than one value
- per point and ``index`` is not given, only the first value is
+ per point and *index* is not given, only the first value is
used.
extra_prop
One or multiple properties to add as extra columns in the .ang
file, as a string or a list of strings. If not given (default),
no extra properties are added. If a property has more than one
- value per point and ``index`` is not given, only the first value
+ value per point and *index* is not given, only the first value
is used.
"""
header = _get_header_from_phases(xmap)
@@ -598,7 +590,7 @@ def _get_header_from_phases(xmap: CrystalMap) -> str:
phase_name = phase.name
if phase_name == "":
phase_name = f"phase{phase_id}"
- if phase.point_group is None:
+ if not phase.point_group:
point_group_name = "1"
else:
proper_point_group = phase.point_group.proper_subgroup
@@ -649,7 +641,7 @@ def _get_nrows_ncols_step_sizes(xmap: CrystalMap) -> Tuple[int, int, float, floa
dy
dx
"""
- nrows, ncols = (1, 1)
+ nrows = ncols = 1
dy, dx = xmap.dy, xmap.dx
if xmap.ndim == 1:
ncols = xmap.shape[0]
@@ -680,7 +672,7 @@ def _get_prop_arrays(
prop_names: List[str],
desired_prop_names: List[str],
map_size: int,
- index: Union[int, None],
+ index: Optional[int],
decimals: int = 5,
) -> np.ndarray:
"""Return a 2D array (n_points, n_properties) with desired property
@@ -736,7 +728,7 @@ def _get_prop_array(
expected_prop_names: List[str],
prop_names: List[str],
prop_names_lower_arr: np.ndarray,
- index: Union[int, None],
+ index: Optional[int],
decimals: int = 5,
fill_value: Union[int, float, bool] = 0,
) -> Union[np.ndarray, None]:
@@ -745,9 +737,8 @@ def _get_prop_array(
Reasons for why the property cannot be read:
- * Property name isn't among the crystal map properties
- * Property has only one value per point, but ``index`` is not
- ``None``
+ * Property name isn't among the crystal map properties
+ * Property has only one value per point, but *index* is not ``None``
Parameters
----------
@@ -766,10 +757,10 @@ def _get_prop_array(
Property array or none if none found.
"""
kwargs = dict(decimals=decimals, fill_value=fill_value)
- if len(prop_names_lower_arr) == 0 and prop_name is None:
+ if not len(prop_names_lower_arr) and not prop_name:
return
else:
- if prop_name is None:
+ if not prop_name:
# Search for a suitable property
for k in expected_prop_names:
is_equal = k == prop_names_lower_arr
@@ -783,6 +774,6 @@ def _get_prop_array(
# Return the single array even if `index` is given
return xmap.get_map_data(prop_name, **kwargs)
else:
- if index is None:
+ if not index:
index = 0
return xmap.get_map_data(xmap.prop[prop_name][:, index], **kwargs)
diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py
new file mode 100644
index 00000000..e271a5e7
--- /dev/null
+++ b/orix/io/plugins/ctf.py
@@ -0,0 +1,330 @@
+# -*- coding: utf-8 -*-
+# 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 .
+
+"""Reader of a crystal map from a file in the Channel Text File (CTF)
+format.
+"""
+
+from io import TextIOWrapper
+import re
+from typing import Dict, List, Tuple
+
+from diffpy.structure import Lattice, Structure
+import numpy as np
+
+from orix.crystal_map import CrystalMap, PhaseList
+from orix.crystal_map.crystal_map import _data_slices_from_coordinates
+from orix.quaternion import Rotation
+
+__all__ = ["file_reader"]
+
+# Plugin description
+format_name = "ctf"
+file_extensions = ["ctf"]
+writes = False
+writes_this = None
+
+
+def file_reader(filename: str) -> CrystalMap:
+ """Return a crystal map from a file in the Channel Text File (CTF)
+ format.
+
+ The map in the input is assumed to be 2D.
+
+ Many vendors/programs produce a .ctf files. Files from the following
+ vendors/programs are tested:
+
+ * Oxford Instruments AZtec
+ * Bruker Esprit
+ * NanoMegas ASTAR Index
+ * EMsoft (from program `EMdpmerge`)
+ * MTEX
+
+ All points with a phase of 0 are classified as not indexed.
+
+ Parameters
+ ----------
+ filename
+ Path to file.
+
+ Returns
+ -------
+ xmap
+ Crystal map.
+
+ Notes
+ -----
+ Files written by MTEX do not contain information of the space group.
+
+ Files written by EMsoft have the column names for mean angular
+ deviation (MAD), band contrast (BC), and band slope (BS) renamed to
+ DP (dot product), OSM (orientation similarity metric), and IQ (image
+ quality), respectively.
+ """
+ with open(filename, "r") as f:
+ header, data_starting_row, vendor = _get_header(f)
+
+ # Phase information, potentially empty
+ phases = _get_phases_from_header(header)
+ phases["structures"] = []
+ lattice_constants = phases.pop("lattice_constants")
+ for name, abcABG in zip(phases["names"], lattice_constants):
+ structure = Structure(title=name, lattice=Lattice(*abcABG))
+ phases["structures"].append(structure)
+
+ file_data = np.loadtxt(filename, skiprows=data_starting_row)
+
+ # Data needed to create a crystal map
+ data_dict = {
+ "euler1": None,
+ "euler2": None,
+ "euler3": None,
+ "x": None,
+ "y": None,
+ "phase_id": None,
+ "prop": {},
+ }
+ column_names = [
+ "phase_id",
+ "x",
+ "y",
+ "bands",
+ "error",
+ "euler1",
+ "euler2",
+ "euler3",
+ "MAD", # Mean angular deviation
+ "BC", # Band contrast
+ "BS", # Band slope
+ ]
+ emsoft_mapping = {"MAD": "DP", "BC": "OSM", "BS": "IQ"}
+ for column, name in enumerate(column_names):
+ if name in data_dict:
+ data_dict[name] = file_data[:, column]
+ else:
+ if vendor == "emsoft" and name in emsoft_mapping:
+ name = emsoft_mapping[name]
+ data_dict["prop"][name] = file_data[:, column]
+
+ if vendor == "astar":
+ data_dict = _fix_astar_coords(header, data_dict)
+
+ data_dict["phase_list"] = PhaseList(**phases)
+
+ # Set which data points are not indexed
+ not_indexed = data_dict["phase_id"] == 0
+ data_dict["phase_id"][not_indexed] = -1
+
+ # Set scan unit
+ data_dict["scan_unit"] = "um"
+
+ # Create rotations
+ data_dict["rotations"] = Rotation.from_euler(
+ np.column_stack(
+ (data_dict.pop("euler1"), data_dict.pop("euler2"), data_dict.pop("euler3"))
+ ),
+ degrees=True,
+ )
+
+ return CrystalMap(**data_dict)
+
+
+def _get_header(file: TextIOWrapper) -> Tuple[List[str], int, List[str]]:
+ """Return file header, row number of start of data in file, and the
+ detected vendor(s).
+
+ Parameters
+ ----------
+ file
+ File object.
+
+ Returns
+ -------
+ header
+ List with header lines as individual elements.
+ data_starting_row
+ The starting row number for the data lines
+ vendor
+ Vendor detected based on some header pattern. Default is to
+ assume Oxford/Bruker, ``"oxford_or_bruker"`` (assuming no
+ difference between the two vendor's CTF formats). Other options
+ are ``"emsoft"``, ``"astar"``, and ``"mtex"``.
+ """
+ vendor = []
+ vendor_pattern = {
+ "emsoft": re.compile(
+ (
+ r"EMsoft v\. ([A-Za-z0-9]+(_[A-Za-z0-9]+)+); BANDS=pattern index, "
+ r"MAD=CI, BC=OSM, BS=IQ"
+ ),
+ ),
+ "astar": re.compile(r"Author[\t\s]File created from ACOM RES results"),
+ "mtex": re.compile("(?<=)Created from mtex"),
+ }
+
+ header = []
+ line = file.readline()
+ i = 0
+ # Prevent endless loop by not reading past 1 000 lines
+ while not line.startswith("Phase\tX\tY") and i < 1_000:
+ for k, v in vendor_pattern.items():
+ if v.search(line):
+ vendor.append(k)
+ header.append(line.rstrip())
+ i += 1
+ line = file.readline()
+
+ vendor = vendor[0] if len(vendor) == 1 else "oxford_or_bruker"
+
+ return header, i + 1, vendor
+
+
+def _get_phases_from_header(header: List[str]) -> dict:
+ """Return phase names and symmetries detected in a .ctf file header.
+
+ Parameters
+ ----------
+ header
+ List with header lines as individual elements.
+ vendor
+ Vendor of the file.
+
+ Returns
+ -------
+ phase_dict
+ Dictionary with the following keys (and types): "ids" (int),
+ "names" (str), "space_groups" (int), "point_groups" (str),
+ "lattice_constants" (list of floats).
+
+ Notes
+ -----
+ This function has been tested with files from the following vendor's
+ formats: Oxford AZtec HKL v5/v6 and EMsoft v4/v5.
+ """
+ phases = {
+ "ids": [],
+ "names": [],
+ "point_groups": [],
+ "space_groups": [],
+ "lattice_constants": [],
+ }
+ for i, line in enumerate(header):
+ if line.startswith("Phases"):
+ break
+
+ n_phases = int(line.split("\t")[1])
+
+ # Laue classes
+ laue_ids = [
+ "-1",
+ "2/m",
+ "mmm",
+ "4/m",
+ "4/mmm",
+ "-3",
+ "-3m",
+ "6/m",
+ "6/mmm",
+ "m3",
+ "m-3m",
+ ]
+
+ for j in range(n_phases):
+ phase_data = header[i + 1 + j].split("\t")
+ phases["ids"].append(j + 1)
+ abcABG = ";".join(phase_data[:2])
+ abcABG = abcABG.split(";")
+ abcABG = [float(i.replace(",", ".")) for i in abcABG]
+ phases["lattice_constants"].append(abcABG)
+ phases["names"].append(phase_data[2])
+ laue_id = int(phase_data[3])
+ phases["point_groups"].append(laue_ids[laue_id - 1])
+ sg = int(phase_data[4])
+ if sg == 0:
+ sg = None
+ phases["space_groups"].append(sg)
+
+ return phases
+
+
+def _fix_astar_coords(header: List[str], data_dict: dict) -> dict:
+ """Return the data dictionary with coordinate arrays possibly fixed
+ for ASTAR Index files.
+
+ Parameters
+ ----------
+ header
+ List with header lines.
+ data_dict
+ Dictionary for creating a crystal map.
+
+ Returns
+ -------
+ data_dict
+ Dictionary with possibly fixed coordinate arrays.
+
+ Notes
+ -----
+ ASTAR Index files may have fewer decimals in the coordinate columns
+ than in the X/YSteps header values (e.g. X_1 = 0.0019 vs.
+ XStep = 0.00191999995708466). This may cause our crystal map
+ algorithm for finding the map shape to fail. We therefore run this
+ algorithm and compare the found shape to the shape given in the
+ file. If they are different, we use our own coordinate arrays.
+ """
+ coords = {k: data_dict[k] for k in ["x", "y"]}
+ slices = _data_slices_from_coordinates(coords)
+ found_shape = (slices[0].stop + 1, slices[1].stop + 1)
+ cells = _get_xy_cells(header)
+ shape = (cells["y"], cells["x"])
+ if found_shape != shape:
+ steps = _get_xy_step(header)
+ y, x = np.indices(shape, dtype=np.float64)
+ y *= steps["y"]
+ x *= steps["x"]
+ data_dict["y"] = y.ravel()
+ data_dict["x"] = x.ravel()
+ return data_dict
+
+
+def _get_xy_step(header: List[str]) -> Dict[str, float]:
+ pattern_step = re.compile(r"(?<=[XY]Step[\t\s])(.*)")
+ steps = {"x": None, "y": None}
+ for line in header:
+ match = pattern_step.search(line)
+ if match:
+ step = float(match.group(0).replace(",", "."))
+ if line.startswith("XStep"):
+ steps["x"] = step
+ elif line.startswith("YStep"):
+ steps["y"] = step
+ return steps
+
+
+def _get_xy_cells(header: List[str]) -> Dict[str, int]:
+ pattern_cells = re.compile(r"(?<=[XY]Cells[\t\s])(.*)")
+ cells = {"x": None, "y": None}
+ for line in header:
+ match = pattern_cells.search(line)
+ if match:
+ step = int(match.group(0))
+ if line.startswith("XCells"):
+ cells["x"] = step
+ elif line.startswith("YCells"):
+ cells["y"] = step
+ return cells
diff --git a/orix/quaternion/orientation.py b/orix/quaternion/orientation.py
index 215b090d..d654f3d5 100644
--- a/orix/quaternion/orientation.py
+++ b/orix/quaternion/orientation.py
@@ -29,11 +29,10 @@
import numpy as np
from scipy.spatial.transform import Rotation as SciPyRotation
-from orix._util import deprecated
from orix.quaternion.misorientation import Misorientation
from orix.quaternion.rotation import Rotation
from orix.quaternion.symmetry import C1, Symmetry, _get_unique_symmetry_elements
-from orix.vector import Miller, NeoEuler, Vector3d
+from orix.vector import Miller, Vector3d
class Orientation(Misorientation):
@@ -107,7 +106,6 @@ def __sub__(self, other: Orientation) -> Misorientation:
# ------------------------ Class methods ------------------------- #
- # TODO: Remove use of **kwargs in 1.0
@classmethod
def from_euler(
cls,
@@ -115,7 +113,6 @@ def from_euler(
symmetry: Optional[Symmetry] = None,
direction: str = "lab2crystal",
degrees: bool = False,
- **kwargs,
) -> Orientation:
"""Create orientations from sets of Euler angles
:cite:`rowenhorst2015consistent`.
@@ -141,7 +138,7 @@ def from_euler(
O
Orientations.
"""
- O = super().from_euler(euler, direction=direction, degrees=degrees, **kwargs)
+ O = super().from_euler(euler, direction=direction, degrees=degrees)
if symmetry:
O.symmetry = symmetry
return O
@@ -261,32 +258,6 @@ def from_matrix(
O.symmetry = symmetry
return O
- # TODO: Remove before 0.13.0
- @classmethod
- @deprecated(since="0.12", removal="0.13", alternative="from_axes_angles")
- def from_neo_euler(
- cls, neo_euler: NeoEuler, symmetry: Optional[Symmetry] = None
- ) -> Orientation:
- """Create orientations from a neo-euler (vector) representation.
-
- Parameters
- ----------
- neo_euler
- Vector parametrization of orientation(s).
- symmetry
- Symmetry of orientation(s). If not given (default), no
- symmetry is set.
-
- Returns
- -------
- O
- Orientations.
- """
- O = super().from_neo_euler(neo_euler)
- if symmetry:
- O.symmetry = symmetry
- return O
-
@classmethod
def from_axes_angles(
cls,
diff --git a/orix/quaternion/quaternion.py b/orix/quaternion/quaternion.py
index e7fdf5e2..5b77a39c 100644
--- a/orix/quaternion/quaternion.py
+++ b/orix/quaternion/quaternion.py
@@ -28,13 +28,9 @@
from scipy.spatial.transform import Rotation as SciPyRotation
from orix._base import Object3d
-from orix._util import deprecated, deprecated_argument
from orix.quaternion import _conversions
from orix.vector import AxAngle, Homochoric, Miller, Rodrigues, Vector3d
-# Used to round values below 1e-16 to zero
-_FLOAT_EPS = np.finfo(float).eps
-
class Quaternion(Object3d):
r"""Quaternions.
@@ -259,31 +255,6 @@ def __eq__(self, other: Union[Any, Quaternion]) -> bool:
# ------------------------ Class methods ------------------------- #
- # TODO: Remove before 0.13.0
- @classmethod
- @deprecated(since="0.12", removal="0.13", alternative="from_axes_angles")
- def from_neo_euler(cls, neo_euler: "NeoEuler") -> Quaternion:
- """Create unit quaternion(s) from a neo-euler (vector)
- representation.
-
- Parameters
- ----------
- neo_euler
- Vector parametrization of quaternions.
-
- Returns
- -------
- Q
- Unit quaternion(s).
- """
- s = np.sin(neo_euler.angle / 2)
- a = np.cos(neo_euler.angle / 2)
- b = s * neo_euler.axis.x
- c = s * neo_euler.axis.y
- d = s * neo_euler.axis.z
- Q = cls(np.stack([a, b, c, d], axis=-1)).unit
- return Q
-
@classmethod
def from_axes_angles(
cls,
@@ -485,15 +456,12 @@ def from_rodrigues(
return Q
- # TODO: Remove decorator, **kwargs, and use of "convention" in 0.13
@classmethod
- @deprecated_argument("convention", "0.9", "0.13", "direction")
def from_euler(
cls,
euler: Union[np.ndarray, tuple, list],
direction: str = "lab2crystal",
degrees: bool = False,
- **kwargs,
) -> Quaternion:
"""Create unit quaternions from Euler angle sets
:cite:`rowenhorst2015consistent`.
@@ -517,9 +485,7 @@ def from_euler(
Unit quaternions.
"""
direction = direction.lower()
- if direction == "mtex" or (
- "convention" in kwargs and kwargs["convention"] == "mtex"
- ):
+ if direction == "mtex":
# MTEX' rotations are transformations from the crystal to
# the lab reference frames. See
# https://mtex-toolbox.github.io/MTEXvsBungeConvention.html
@@ -804,9 +770,7 @@ def identity(cls, shape: Union[int, tuple] = (1,)) -> Quaternion:
# ---------------------- All "to_*" methods- --------------------- #
- # TODO: Remove decorator and **kwargs in 0.13
- @deprecated_argument("convention", since="0.9", removal="0.13")
- def to_euler(self, degrees: bool = False, **kwargs) -> np.ndarray:
+ def to_euler(self, degrees: bool = False) -> np.ndarray:
r"""Return the unit quaternions as Euler angles in the Bunge
convention :cite:`rowenhorst2015consistent`.
diff --git a/orix/quaternion/rotation.py b/orix/quaternion/rotation.py
index de3f7f2e..3042d3e5 100644
--- a/orix/quaternion/rotation.py
+++ b/orix/quaternion/rotation.py
@@ -28,9 +28,6 @@
from orix.quaternion import Quaternion
from orix.vector import Vector3d
-# Used to round values below 1e-16 to zero
-_FLOAT_EPS = np.finfo(float).eps
-
class Rotation(Quaternion):
r"""Rotations of coordinate systems, leaving objects in place.
diff --git a/orix/tests/conftest.py b/orix/tests/conftest.py
index 22d8a961..da5b7a40 100644
--- a/orix/tests/conftest.py
+++ b/orix/tests/conftest.py
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with orix. If not, see .
-import gc
import os
from tempfile import TemporaryDirectory
@@ -30,6 +29,10 @@
from orix.quaternion import Rotation
+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)])
@@ -40,102 +43,9 @@ def eu():
return np.random.rand(10, 3)
-ANGFILE_TSL_HEADER = (
- "# TEM_PIXperUM 1.000000\n"
- "# x-star 0.413900\n"
- "# y-star 0.729100\n"
- "# z-star 0.514900\n"
- "# WorkingDistance 27.100000\n"
- "#\n"
- "# Phase 2\n"
- "# MaterialName Aluminum\n"
- "# Formula Al\n"
- "# Info \n"
- "# Symmetry 43\n"
- "# LatticeConstants 4.040 4.040 4.040 90.000 90.000 90.000\n"
- "# NumberFamilies 69\n"
- "# hklFamilies 1 -1 -1 1 8.469246 1\n"
- "# ElasticConstants -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000\n"
- "# Categories0 0 0 0 0 \n"
- "# Phase 3\n"
- "# MaterialName Iron Titanium Oxide\n"
- "# Formula FeTiO3\n"
- "# Info \n"
- "# Symmetry 32\n"
- "# LatticeConstants 5.123 5.123 13.760 90.000 90.000 120.000\n"
- "# NumberFamilies 60\n"
- "# hklFamilies 3 0 0 1 100.000000 1\n"
- "# ElasticConstants -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000\n"
- "# Categories0 0 0 0 0\n"
- "#\n"
- "# GRID: SqrGrid\n"
- "# XSTEP: 0.100000\n"
- "# YSTEP: 0.100000\n"
- "# NCOLS_ODD: 42\n"
- "# NCOLS_EVEN: 42\n"
- "# NROWS: 13\n"
- "#\n"
- "# OPERATOR: sem\n"
- "#\n"
- "# SAMPLEID: \n"
- "#\n"
- "# SCANID: \n"
- "#\n"
-)
-
-ANGFILE_ASTAR_HEADER = (
- "# File created from ACOM RES results\n"
- "# ni-dislocations.res\n"
- "# \n"
- "# \n"
- "# MaterialName Nickel\n"
- "# Formula\n"
- "# Symmetry 43\n"
- "# LatticeConstants 3.520 3.520 3.520 90.000 90.000 90.000\n"
- "# NumberFamilies 4\n"
- "# hklFamilies 1 1 1 1 0.000000\n"
- "# hklFamilies 2 0 0 1 0.000000\n"
- "# hklFamilies 2 2 0 1 0.000000\n"
- "# hklFamilies 3 1 1 1 0.000000\n"
- "#\n"
- "# GRID: SqrGrid#\n"
-)
+# ---------------------------- IO fixtures --------------------------- #
-ANGFILE_EMSOFT_HEADER = (
- "# TEM_PIXperUM 1.000000\n"
- "# x-star 0.446667\n"
- "# y-star 0.586875\n"
- "# z-star 0.713450\n"
- "# WorkingDistance 0.000000\n"
- "#\n"
- "# Phase 1\n"
- "# MaterialName austenite\n"
- "# Formula austenite\n"
- "# Info patterns indexed using EMsoft::EMEBSDDI\n"
- "# Symmetry 43\n"
- "# LatticeConstants 3.595 3.595 3.595 90.000 90.000 90.000\n"
- "# NumberFamilies 0\n"
- "# Phase 2\n"
- "# MaterialName ferrite/ferrite\n"
- "# Formula ferrite/ferrite\n"
- "# Info patterns indexed using EMsoft::EMEBSDDI\n"
- "# Symmetry 43\n"
- "# LatticeConstants 2.867 2.867 2.867 90.000 90.000 90.000\n"
- "# NumberFamilies 0\n"
- "# GRID: SqrGrid\n"
- "# XSTEP: 1.500000\n"
- "# YSTEP: 1.500000\n"
- "# NCOLS_ODD: 13\n"
- "# NCOLS_EVEN: 13\n"
- "# NROWS: 42\n"
- "#\n"
- "# OPERATOR: Håkon Wiik Ånes\n"
- "#\n"
- "# SAMPLEID:\n"
- "#\n"
- "# SCANID:\n"
- "#\n"
-)
+# ----------------------------- .ang file ---------------------------- #
@pytest.fixture()
@@ -143,7 +53,6 @@ def temp_ang_file():
with TemporaryDirectory() as tempdir:
f = open(os.path.join(tempdir, "temp_ang_file.ang"), mode="w+")
yield f
- gc.collect() # Garbage collection so that file can be used by multiple tests
@pytest.fixture(params=["h5"])
@@ -155,7 +64,48 @@ def temp_file_path(request):
with TemporaryDirectory() as tmp:
file_path = os.path.join(tmp, "data_temp." + ext)
yield file_path
- gc.collect()
+
+
+ANGFILE_TSL_HEADER = r"""# TEM_PIXperUM 1.000000
+# x-star 0.413900
+# y-star 0.729100
+# z-star 0.514900
+# WorkingDistance 27.100000
+#
+# Phase 2
+# MaterialName Aluminum
+# Formula Al
+# Info
+# Symmetry 43
+# LatticeConstants 4.040 4.040 4.040 90.000 90.000 90.000
+# NumberFamilies 69
+# hklFamilies 1 -1 -1 1 8.469246 1
+# ElasticConstants -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000
+# Categories0 0 0 0 0
+# Phase 3
+# MaterialName Iron Titanium Oxide
+# Formula FeTiO3
+# Info
+# Symmetry 32
+# LatticeConstants 5.123 5.123 13.760 90.000 90.000 120.000
+# NumberFamilies 60
+# hklFamilies 3 0 0 1 100.000000 1
+# ElasticConstants -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000
+# Categories0 0 0 0 0
+#
+# GRID: SqrGrid
+# XSTEP: 0.100000
+# YSTEP: 0.100000
+# NCOLS_ODD: 42
+# NCOLS_EVEN: 42
+# NROWS: 13
+#
+# OPERATOR: sem
+#
+# SAMPLEID:
+#
+# SCANID:
+#"""
@pytest.fixture(
@@ -237,7 +187,23 @@ def angfile_tsl(tmpdir, request):
)
yield f
- gc.collect()
+
+
+ANGFILE_ASTAR_HEADER = r"""# File created from ACOM RES results
+# ni-dislocations.res
+#
+#
+# MaterialName Nickel
+# Formula
+# Symmetry 43
+# LatticeConstants 3.520 3.520 3.520 90.000 90.000 90.000
+# NumberFamilies 4
+# hklFamilies 1 1 1 1 0.000000
+# hklFamilies 2 0 0 1 0.000000
+# hklFamilies 2 2 0 1 0.000000
+# hklFamilies 3 1 1 1 0.000000
+#
+# GRID: SqrGrid#"""
@pytest.fixture(
@@ -302,7 +268,41 @@ def angfile_astar(tmpdir, request):
)
yield f
- gc.collect()
+
+
+ANGFILE_EMSOFT_HEADER = r"""# TEM_PIXperUM 1.000000
+# x-star 0.446667
+# y-star 0.586875
+# z-star 0.713450
+# WorkingDistance 0.000000
+#
+# Phase 1
+# MaterialName austenite
+# Formula austenite
+# Info patterns indexed using EMsoft::EMEBSDDI
+# Symmetry 43
+# LatticeConstants 3.595 3.595 3.595 90.000 90.000 90.000
+# NumberFamilies 0
+# Phase 2
+# MaterialName ferrite/ferrite
+# Formula ferrite/ferrite
+# Info patterns indexed using EMsoft::EMEBSDDI
+# Symmetry 43
+# LatticeConstants 2.867 2.867 2.867 90.000 90.000 90.000
+# NumberFamilies 0
+# GRID: SqrGrid
+# XSTEP: 1.500000
+# YSTEP: 1.500000
+# NCOLS_ODD: 13
+# NCOLS_EVEN: 13
+# NROWS: 42
+#
+# OPERATOR: Håkon Wiik Ånes
+#
+# SAMPLEID:
+#
+# SCANID:
+#"""
@pytest.fixture(
@@ -359,7 +359,445 @@ def angfile_emsoft(tmpdir, request):
)
yield f
- gc.collect()
+
+
+# ----------------------------- .ctf file ---------------------------- #
+
+# Variable map shape and step sizes
+CTF_OXFORD_HEADER = r"""Channel Text File
+Prj standard steel sample
+Author
+JobMode Grid
+XCells %i
+YCells %i
+XStep %.4f
+YStep %.4f
+AcqE1 0.0000
+AcqE2 0.0000
+AcqE3 0.0000
+Euler angles refer to Sample Coordinate system (CS0)! Mag 180.0000 Coverage 97 Device 0 KV 20.0000 TiltAngle 70.0010 TiltAxis 0 DetectorOrientationE1 0.9743 DetectorOrientationE2 89.4698 DetectorOrientationE3 2.7906 WorkingDistance 14.9080 InsertionDistance 185.0
+Phases 2
+3.660;3.660;3.660 90.000;90.000;90.000 Iron fcc 11 225 Some reference
+2.867;2.867;2.867 90.000;90.000;90.000 Iron bcc 11 229 Some other reference
+Phase X Y Bands Error Euler1 Euler2 Euler3 MAD BC BS"""
+
+
+@pytest.fixture(
+ params=[
+ (
+ (7, 13), # map_shape
+ (0.1, 0.1), # step_sizes
+ np.random.choice([1, 2], 7 * 13), # phase_id
+ np.array([[4.48549, 0.95242, 0.79150], [1.34390, 0.27611, 0.82589]]), # R
+ )
+ ]
+)
+def ctf_oxford(tmpdir, request):
+ """Create a dummy CTF file in Oxford Instrument's format from input.
+
+ 10% of points are non-indexed (phase ID of 0 and MAD = 0).
+
+ Parameters expected in `request`
+ --------------------------------
+ map_shape : tuple of ints
+ Map shape to create.
+ step_sizes : tuple of floats
+ Step sizes in x and y coordinates in microns.
+ phase_id : numpy.ndarray
+ Array of map size with phase IDs in header.
+ rotations : numpy.ndarray
+ A sample, smaller than the map size, of Euler angle triplets.
+ """
+ # Unpack parameters
+ (ny, nx), (dy, dx), phase_id, R_example = request.param
+
+ # File columns
+ d, map_size = create_coordinate_arrays((ny, nx), (dy, dx))
+ x, y = d["x"], d["y"]
+ rng = np.random.default_rng()
+ bands = rng.integers(8, size=map_size, dtype=np.uint8)
+ err = np.zeros(map_size, dtype=np.uint8)
+ mad = rng.random(map_size)
+ bc = rng.integers(150, 200, map_size)
+ bs = rng.integers(190, 255, map_size)
+ R_idx = np.random.choice(np.arange(len(R_example)), map_size)
+ R = R_example[R_idx]
+ R = np.rad2deg(R)
+
+ # Insert 10% non-indexed points
+ non_indexed_points = np.random.choice(
+ np.arange(map_size), replace=False, size=int(map_size * 0.1)
+ )
+ phase_id[non_indexed_points] = 0
+ R[non_indexed_points] = 0.0
+ bands[non_indexed_points] = 0
+ err[non_indexed_points] = 3
+ mad[non_indexed_points] = 0.0
+ bc[non_indexed_points] = 0
+ bs[non_indexed_points] = 0
+
+ CTF_OXFORD_HEADER2 = CTF_OXFORD_HEADER % (nx, ny, dx, dy)
+
+ f = tmpdir.join("oxford.ctf")
+ np.savetxt(
+ fname=f,
+ X=np.column_stack(
+ (phase_id, x, y, bands, err, R[:, 0], R[:, 1], R[:, 2], mad, bc, bs)
+ ),
+ fmt="%-4i%-8.4f%-8.4f%-4i%-4i%-11.4f%-11.4f%-11.4f%-8.4f%-4i%-i",
+ header=CTF_OXFORD_HEADER2,
+ comments="",
+ )
+
+ yield f
+
+
+# Variable map shape, comma as decimal separator in fixed step size
+CTF_BRUKER_HEADER = r"""Channel Text File
+Prj unnamed
+Author [Unknown]
+JobMode Grid
+XCells %i
+YCells %i
+XStep 0,001998
+YStep 0,001998
+AcqE1 0
+AcqE2 0
+AcqE3 0
+Euler angles refer to Sample Coordinate system (CS0)! Mag 150000,000000 Coverage 100 Device 0 KV 30,000000 TiltAngle 0 TiltAxis 0
+Phases 1
+4,079000;4,079000;4,079000 90,000000;90,000000;90,000000 Gold 11 225
+Phase X Y Bands Error Euler1 Euler2 Euler3 MAD BC BS"""
+
+
+@pytest.fixture(
+ params=[
+ (
+ (7, 13), # map_shape
+ np.array([[4.48549, 0.95242, 0.79150], [1.34390, 0.27611, 0.82589]]), # R
+ )
+ ]
+)
+def ctf_bruker(tmpdir, request):
+ """Create a dummy CTF file in Bruker's format from input.
+
+ Identical to Oxford files except for the following:
+
+ * All band slopes (BS) may be set to 255
+ * Decimal separators in header may be with comma
+
+ Parameters expected in `request`
+ --------------------------------
+ map_shape : tuple of ints
+ Map shape to create.
+ rotations : numpy.ndarray
+ A sample, smaller than the map size, of Euler angle triplets.
+ """
+ # Unpack parameters
+ (ny, nx), R_example = request.param
+ dy = dx = 0.001998
+
+ # File columns
+ d, map_size = create_coordinate_arrays((ny, nx), (dy, dx))
+ x, y = d["x"], d["y"]
+ rng = np.random.default_rng()
+ bands = rng.integers(8, size=map_size, dtype=np.uint8)
+ err = np.zeros(map_size, dtype=np.uint8)
+ mad = rng.random(map_size)
+ bc = rng.integers(50, 105, map_size)
+ bs = np.full(map_size, 255, dtype=np.uint8)
+ R_idx = np.random.choice(np.arange(len(R_example)), map_size)
+ R = R_example[R_idx]
+ R = np.rad2deg(R)
+
+ # Insert 10% non-indexed points
+ phase_id = np.ones(map_size, dtype=np.uint8)
+ non_indexed_points = np.random.choice(
+ np.arange(map_size), replace=False, size=int(map_size * 0.1)
+ )
+ phase_id[non_indexed_points] = 0
+ R[non_indexed_points] = 0.0
+ bands[non_indexed_points] = 0
+ err[non_indexed_points] = 3
+ mad[non_indexed_points] = 0.0
+ bc[non_indexed_points] = 0
+ bs[non_indexed_points] = 0
+
+ CTF_BRUKER_HEADER2 = CTF_BRUKER_HEADER % (nx, ny)
+
+ f = tmpdir.join("bruker.ctf")
+ np.savetxt(
+ fname=f,
+ X=np.column_stack(
+ (phase_id, x, y, bands, err, R[:, 0], R[:, 1], R[:, 2], mad, bc, bs)
+ ),
+ fmt="%-4i%-8.4f%-8.4f%-4i%-4i%-11.4f%-11.4f%-11.4f%-8.4f%-4i%-i",
+ header=CTF_BRUKER_HEADER2,
+ comments="",
+ )
+
+ yield f
+
+
+# Variable map shape, small fixed step size
+CTF_ASTAR_HEADER = r"""Channel Text File
+Prj C:\some\where\scan.res
+Author File created from ACOM RES results
+JobMode Grid
+XCells %i
+YCells %i
+XStep 0.00191999995708466
+YStep 0.00191999995708466
+AcqE1 0
+AcqE2 0
+AcqE3 0
+Euler angles refer to Sample Coordinate system (CS0)! Mag 200 Coverage 100 Device 0 KV 20 TiltAngle 70 TiltAxis 0
+Phases 1
+4.0780;4.0780;4.0780 90;90;90 _mineral 'Gold' 'Gold' 11 225
+Phase X Y Bands Error Euler1 Euler2 Euler3 MAD BC BS"""
+
+
+@pytest.fixture(
+ params=[
+ (
+ (7, 13), # map_shape
+ np.array([[4.48549, 0.95242, 0.79150], [1.34390, 0.27611, 0.82589]]), # R
+ )
+ ]
+)
+def ctf_astar(tmpdir, request):
+ """Create a dummy CTF file in NanoMegas ASTAR's format from input.
+
+ Identical to Oxford files except for the following:
+
+ * Bands = 6 (always?)
+ * Error = 0 (always?)
+ * Only two decimals in Euler angles
+
+ Parameters expected in `request`
+ --------------------------------
+ map_shape : tuple of ints
+ Map shape to create.
+ rotations : numpy.ndarray
+ A sample, smaller than the map size, of Euler angle triplets.
+ """
+ # Unpack parameters
+ (ny, nx), R_example = request.param
+ dy = dx = 0.00191999995708466
+
+ # File columns
+ d, map_size = create_coordinate_arrays((ny, nx), (dy, dx))
+ x, y = d["x"], d["y"]
+ rng = np.random.default_rng()
+ bands = np.full(map_size, 6, dtype=np.uint8)
+ err = np.zeros(map_size, dtype=np.uint8)
+ mad = rng.random(map_size)
+ bc = rng.integers(0, 60, map_size)
+ bs = rng.integers(35, 42, map_size)
+ R_idx = np.random.choice(np.arange(len(R_example)), map_size)
+ R = R_example[R_idx]
+ R = np.rad2deg(R)
+
+ # Insert 10% non-indexed points
+ phase_id = np.ones(map_size, dtype=np.uint8)
+ non_indexed_points = np.random.choice(
+ np.arange(map_size), replace=False, size=int(map_size * 0.1)
+ )
+ phase_id[non_indexed_points] = 0
+ R[non_indexed_points] = 0.0
+ bands[non_indexed_points] = 0
+ err[non_indexed_points] = 3
+ mad[non_indexed_points] = 0.0
+ bc[non_indexed_points] = 0
+ bs[non_indexed_points] = 0
+
+ CTF_ASTAR_HEADER2 = CTF_ASTAR_HEADER % (nx, ny)
+
+ f = tmpdir.join("astar.ctf")
+ np.savetxt(
+ fname=f,
+ X=np.column_stack(
+ (phase_id, x, y, bands, err, R[:, 0], R[:, 1], R[:, 2], mad, bc, bs)
+ ),
+ fmt="%-4i%-8.4f%-8.4f%-4i%-4i%-9.2f%-9.2f%-9.2f%-8.4f%-4i%-i",
+ header=CTF_ASTAR_HEADER2,
+ comments="",
+ )
+
+ yield f
+
+
+# Variable map shape and step sizes
+CTF_EMSOFT_HEADER = r"""Channel Text File
+EMsoft v. 4_1_1_9d5269a; BANDS=pattern index, MAD=CI, BC=OSM, BS=IQ
+Author Me
+JobMode Grid
+XCells %i
+YCells %i
+XStep %.2f
+YStep %.2f
+AcqE1 0
+AcqE2 0
+AcqE3 0
+Euler angles refer to Sample Coordinate system (CS0)! Mag 30 Coverage 100 Device 0 KV 0.0 TiltAngle 0.00 TiltAxis 0
+Phases 1
+3.524;3.524;3.524 90.000;90.000;90.000 Ni 11 225
+Phase X Y Bands Error Euler1 Euler2 Euler3 MAD BC BS"""
+
+
+@pytest.fixture(
+ params=[
+ (
+ (7, 13), # map_shape
+ (1, 2), # step_sizes
+ np.array([[4.48549, 0.95242, 0.79150], [1.34390, 0.27611, 0.82589]]), # R
+ )
+ ]
+)
+def ctf_emsoft(tmpdir, request):
+ """Create a dummy CTF file in EMsoft's format from input.
+
+ Identical to Oxford files except for the following:
+
+ * Bands = dictionary index
+ * Error = 0
+ * Only three decimals in Euler angles
+
+ Parameters expected in `request`
+ --------------------------------
+ map_shape : tuple of ints
+ Map shape to create.
+ step_sizes : tuple of floats
+ Step sizes in x and y coordinates in microns.
+ rotations : numpy.ndarray
+ A sample, smaller than the map size, of Euler angle triplets.
+ """
+ # Unpack parameters
+ (ny, nx), (dy, dx), R_example = request.param
+
+ # File columns
+ d, map_size = create_coordinate_arrays((ny, nx), (dy, dx))
+ x, y = d["x"], d["y"]
+ rng = np.random.default_rng()
+ bands = rng.integers(0, 333_000, map_size)
+ err = np.zeros(map_size, dtype=np.uint8)
+ mad = rng.random(map_size)
+ bc = rng.integers(60, 140, map_size)
+ bs = rng.integers(60, 120, map_size)
+ R_idx = np.random.choice(np.arange(len(R_example)), map_size)
+ R = R_example[R_idx]
+ R = np.rad2deg(R)
+
+ # Insert 10% non-indexed points
+ phase_id = np.ones(map_size, dtype=np.uint8)
+ non_indexed_points = np.random.choice(
+ np.arange(map_size), replace=False, size=int(map_size * 0.1)
+ )
+ phase_id[non_indexed_points] = 0
+ R[non_indexed_points] = 0.0
+ bands[non_indexed_points] = 0
+ mad[non_indexed_points] = 0.0
+ bc[non_indexed_points] = 0
+ bs[non_indexed_points] = 0
+
+ CTF_EMSOFT_HEADER2 = CTF_EMSOFT_HEADER % (nx, ny, dx, dy)
+
+ f = tmpdir.join("emsoft.ctf")
+ np.savetxt(
+ fname=f,
+ X=np.column_stack(
+ (phase_id, x, y, bands, err, R[:, 0], R[:, 1], R[:, 2], mad, bc, bs)
+ ),
+ fmt="%-4i%-8.4f%-8.4f%-7i%-4i%-10.3f%-10.3f%-10.3f%-8.4f%-4i%-i",
+ header=CTF_EMSOFT_HEADER2,
+ comments="",
+ )
+
+ yield f
+
+
+# Variable map shape and step sizes
+CTF_MTEX_HEADER = r"""Channel Text File
+Prj /some/where/mtex.ctf
+Author Me Again
+JobMode Grid
+XCells %i
+YCells %i
+XStep %.4f
+YStep %.4f
+AcqE1 0.0000
+AcqE2 0.0000
+AcqE3 0.0000
+Euler angles refer to Sample Coordinate system (CS0)! Mag 0.0000 Coverage 0 Device 0 KV 0.0000 TiltAngle 0.0000 TiltAxis 0 DetectorOrientationE1 0.0000 DetectorOrientationE2 0.0000 DetectorOrientationE3 0.0000 WorkingDistance 0.0000 InsertionDistance 0.0000
+Phases 1
+4.079;4.079;4.079 90.000;90.000;90.000 Gold 11 0 Created from mtex
+Phase X Y Bands Error Euler1 Euler2 Euler3 MAD BC BS"""
+
+
+@pytest.fixture(
+ params=[
+ (
+ (7, 13), # map_shape
+ (1, 2), # step_sizes
+ np.array([[4.48549, 0.95242, 0.79150], [1.34390, 0.27611, 0.82589]]), # R
+ )
+ ]
+)
+def ctf_mtex(tmpdir, request):
+ """Create a dummy CTF file in MTEX's format from input.
+
+ Identical to Oxford files except for the properties Bands, Error,
+ MAD, BC, and BS are all equal to 0.
+
+ Parameters expected in `request`
+ --------------------------------
+ map_shape : tuple of ints
+ Map shape to create.
+ step_sizes : tuple of floats
+ Step sizes in x and y coordinates in microns.
+ rotations : numpy.ndarray
+ A sample, smaller than the map size, of Euler angle triplets.
+ """
+ # Unpack parameters
+ (ny, nx), (dy, dx), R_example = request.param
+
+ # File columns
+ d, map_size = create_coordinate_arrays((ny, nx), (dy, dx))
+ x, y = d["x"], d["y"]
+ bands = np.zeros(map_size)
+ err = np.zeros(map_size)
+ mad = np.zeros(map_size)
+ bc = np.zeros(map_size)
+ bs = np.zeros(map_size)
+ R_idx = np.random.choice(np.arange(len(R_example)), map_size)
+ R = R_example[R_idx]
+ R = np.rad2deg(R)
+
+ # Insert 10% non-indexed points
+ phase_id = np.ones(map_size, dtype=np.uint8)
+ non_indexed_points = np.random.choice(
+ np.arange(map_size), replace=False, size=int(map_size * 0.1)
+ )
+ phase_id[non_indexed_points] = 0
+ R[non_indexed_points] = 0.0
+
+ CTF_MTEX_HEADER2 = CTF_MTEX_HEADER % (nx, ny, dx, dy)
+
+ f = tmpdir.join("mtex.ctf")
+ np.savetxt(
+ fname=f,
+ X=np.column_stack(
+ (phase_id, x, y, bands, err, R[:, 0], R[:, 1], R[:, 2], mad, bc, bs)
+ ),
+ fmt="%-4i%-8.4f%-8.4f%-4i%-4i%-11.4f%-11.4f%-11.4f%-8.4f%-4i%-i",
+ header=CTF_MTEX_HEADER2,
+ comments="",
+ )
+
+ yield f
+
+
+# ---------------------------- HDF5 files ---------------------------- #
@pytest.fixture(
@@ -486,7 +924,6 @@ def temp_emsoft_h5ebsd_file(tmpdir, request):
phase_group.create_dataset(name, data=np.array([data], dtype=np.dtype("S")))
yield f
- gc.collect()
@pytest.fixture(
@@ -602,7 +1039,68 @@ def temp_bruker_h5ebsd_file(tmpdir, request):
data_group.create_dataset("phi2", data=rot[:, 2])
yield f
- gc.collect()
+
+
+# --------------------------- Other files ---------------------------- #
+
+
+@pytest.fixture
+def cif_file(tmpdir):
+ """Actual CIF file of beta double prime phase often seen in Al-Mg-Si
+ alloys.
+ """
+ file_contents = """#======================================================================
+
+# CRYSTAL DATA
+
+#----------------------------------------------------------------------
+
+data_VESTA_phase_1
+
+
+_chemical_name_common ''
+_cell_length_a 15.50000
+_cell_length_b 4.05000
+_cell_length_c 6.74000
+_cell_angle_alpha 90
+_cell_angle_beta 105.30000
+_cell_angle_gamma 90
+_space_group_name_H-M_alt 'C 2/m'
+_space_group_IT_number 12
+
+loop_
+_space_group_symop_operation_xyz
+ 'x, y, z'
+ '-x, -y, -z'
+ '-x, y, -z'
+ 'x, -y, z'
+ 'x+1/2, y+1/2, z'
+ '-x+1/2, -y+1/2, -z'
+ '-x+1/2, y+1/2, -z'
+ 'x+1/2, -y+1/2, z'
+
+loop_
+ _atom_site_label
+ _atom_site_occupancy
+ _atom_site_fract_x
+ _atom_site_fract_y
+ _atom_site_fract_z
+ _atom_site_adp_type
+ _atom_site_B_iso_or_equiv
+ _atom_site_type_symbol
+ Mg(1) 1.0 0.000000 0.000000 0.000000 Biso 1.000000 Mg
+ Mg(2) 1.0 0.347000 0.000000 0.089000 Biso 1.000000 Mg
+ Mg(3) 1.0 0.423000 0.000000 0.652000 Biso 1.000000 Mg
+ Si(1) 1.0 0.054000 0.000000 0.649000 Biso 1.000000 Si
+ Si(2) 1.0 0.190000 0.000000 0.224000 Biso 1.000000 Si
+ Al 1.0 0.211000 0.000000 0.626000 Biso 1.000000 Al"""
+ f = open(tmpdir.join("betapp.cif"), mode="w")
+ f.write(file_contents)
+ f.close()
+ yield f.name
+
+
+# ----------------------- Crystal map fixtures ----------------------- #
@pytest.fixture(
@@ -665,65 +1163,6 @@ def crystal_map(crystal_map_input):
return CrystalMap(**crystal_map_input)
-@pytest.fixture
-def cif_file(tmpdir):
- """Actual CIF file of beta double prime phase often seen in Al-Mg-Si
- alloys.
- """
- file_contents = """
-#======================================================================
-
-# CRYSTAL DATA
-
-#----------------------------------------------------------------------
-
-data_VESTA_phase_1
-
-
-_chemical_name_common ''
-_cell_length_a 15.50000
-_cell_length_b 4.05000
-_cell_length_c 6.74000
-_cell_angle_alpha 90
-_cell_angle_beta 105.30000
-_cell_angle_gamma 90
-_space_group_name_H-M_alt 'C 2/m'
-_space_group_IT_number 12
-
-loop_
-_space_group_symop_operation_xyz
- 'x, y, z'
- '-x, -y, -z'
- '-x, y, -z'
- 'x, -y, z'
- 'x+1/2, y+1/2, z'
- '-x+1/2, -y+1/2, -z'
- '-x+1/2, y+1/2, -z'
- 'x+1/2, -y+1/2, z'
-
-loop_
- _atom_site_label
- _atom_site_occupancy
- _atom_site_fract_x
- _atom_site_fract_y
- _atom_site_fract_z
- _atom_site_adp_type
- _atom_site_B_iso_or_equiv
- _atom_site_type_symbol
- Mg(1) 1.0 0.000000 0.000000 0.000000 Biso 1.000000 Mg
- Mg(2) 1.0 0.347000 0.000000 0.089000 Biso 1.000000 Mg
- Mg(3) 1.0 0.423000 0.000000 0.652000 Biso 1.000000 Mg
- Si(1) 1.0 0.054000 0.000000 0.649000 Biso 1.000000 Si
- Si(2) 1.0 0.190000 0.000000 0.224000 Biso 1.000000 Si
- Al 1.0 0.211000 0.000000 0.626000 Biso 1.000000 Al"
-"""
- f = open(tmpdir.join("betapp.cif"), mode="w")
- f.write(file_contents)
- f.close()
- yield f.name
- gc.collect()
-
-
# ---------- Rotation representations for conversion tests ----------- #
# NOTE to future test writers on unittest data:
# All the data below can be recreated using 3Drotations, which is
diff --git a/orix/tests/io/test_ang.py b/orix/tests/io/test_ang.py
index 082c4f70..4e091f93 100644
--- a/orix/tests/io/test_ang.py
+++ b/orix/tests/io/test_ang.py
@@ -77,7 +77,8 @@
indirect=["angfile_astar"],
)
def test_loadang(angfile_astar, expected_data):
- loaded_data = loadang(angfile_astar)
+ with pytest.warns(np.VisibleDeprecationWarning):
+ loaded_data = loadang(angfile_astar)
assert np.allclose(loaded_data.data, expected_data)
@@ -175,7 +176,7 @@ def test_load_ang_tsl(
assert xmap.phases.size == 2 # Including non-indexed
assert xmap.phases.ids == [-1, 0]
phase = xmap.phases[0]
- assert phase.name == "Aluminum"
+ assert phase.name == "Al"
assert phase.point_group.name == "432"
@pytest.mark.parametrize(
@@ -500,12 +501,12 @@ def test_get_phases_from_header(
"#",
"# GRID: SqrGrid#",
]
- ids, names, point_groups, lattice_constants = _get_phases_from_header(header)
+ phases = _get_phases_from_header(header)
- assert names == expected_names
- assert point_groups == expected_point_groups
- assert np.allclose(lattice_constants, expected_lattice_constants)
- assert np.allclose(ids, expected_phase_id)
+ assert phases["names"] == expected_names
+ assert phases["point_groups"] == expected_point_groups
+ assert np.allclose(phases["lattice_constants"], expected_lattice_constants)
+ assert np.allclose(phases["ids"], expected_phase_id)
class TestAngWriter:
diff --git a/orix/tests/io/test_ctf.py b/orix/tests/io/test_ctf.py
new file mode 100644
index 00000000..62a11239
--- /dev/null
+++ b/orix/tests/io/test_ctf.py
@@ -0,0 +1,445 @@
+# 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
+import numpy as np
+import pytest
+
+from orix import io
+from orix.crystal_map import PhaseList
+
+
+class TestCTFReader:
+ @pytest.mark.parametrize(
+ "ctf_oxford, map_shape, step_sizes, R_example",
+ [
+ (
+ (
+ (5, 3),
+ (0.1, 0.1),
+ np.random.choice([1, 2], 5 * 3),
+ np.array(
+ [[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]
+ ),
+ ),
+ (5, 3),
+ (0.1, 0.1),
+ np.array([[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]),
+ ),
+ (
+ (
+ (8, 8),
+ (1.0, 1.5),
+ np.random.choice([1, 2], 8 * 8),
+ np.array(
+ [[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]
+ ),
+ ),
+ (8, 8),
+ (1.0, 1.5),
+ np.array([[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]),
+ ),
+ ],
+ indirect=["ctf_oxford"],
+ )
+ def test_load_ctf_oxford(
+ self,
+ ctf_oxford,
+ map_shape,
+ step_sizes,
+ R_example,
+ ):
+ xmap = io.load(ctf_oxford)
+
+ # Fraction of non-indexed points
+ non_indexed_fraction = int(np.prod(map_shape) * 0.1)
+ assert non_indexed_fraction == np.sum(~xmap.is_indexed)
+
+ # Properties
+ assert list(xmap.prop.keys()) == ["bands", "error", "MAD", "BC", "BS"]
+
+ # Coordinates
+ ny, nx = map_shape
+ dy, dx = step_sizes
+ assert np.allclose(xmap.x, np.tile(np.arange(nx) * dx, ny))
+ assert np.allclose(xmap.y, np.sort(np.tile(np.arange(ny) * dy, nx)))
+ assert xmap.scan_unit == "um"
+
+ # Map shape and size
+ assert xmap.shape == map_shape
+ assert xmap.size == np.prod(map_shape)
+
+ # Attributes are within expected ranges or have a certain value
+ assert xmap.bands.min() >= 0
+ assert xmap.error.min() >= 0
+ assert np.allclose(xmap["not_indexed"].bands, 0)
+ assert not any(np.isclose(xmap["not_indexed"].error, 0))
+ assert np.allclose(xmap["not_indexed"].MAD, 0)
+ assert np.allclose(xmap["not_indexed"].BC, 0)
+ assert np.allclose(xmap["not_indexed"].BS, 0)
+
+ # Rotations
+ R_unique = np.unique(xmap["indexed"].rotations.to_euler(), axis=0)
+ assert np.allclose(
+ np.sort(R_unique, axis=0), np.sort(R_example, axis=0), atol=1e-5
+ )
+ assert np.allclose(xmap["not_indexed"].rotations.to_euler()[0], 0)
+
+ # Phases
+ phases = PhaseList(
+ names=["Iron fcc", "Iron bcc"],
+ space_groups=[225, 229],
+ structures=[
+ Structure(lattice=Lattice(3.66, 3.66, 3.66, 90, 90, 90)),
+ Structure(lattice=Lattice(2.867, 2.867, 2.867, 90, 90, 90)),
+ ],
+ )
+
+ assert all(np.isin(xmap.phase_id, [-1, 1, 2]))
+ assert np.allclose(xmap["not_indexed"].phase_id, -1)
+ assert xmap.phases.ids == [-1, 1, 2]
+ for (_, phase), (_, phase_test) in zip(xmap["indexed"].phases_in_data, phases):
+ assert phase.name == phase_test.name
+ assert phase.space_group.number == phase_test.space_group.number
+ assert np.allclose(
+ phase.structure.lattice.abcABG(), phase_test.structure.lattice.abcABG()
+ )
+
+ @pytest.mark.parametrize(
+ "ctf_bruker, map_shape, R_example",
+ [
+ (
+ (
+ (5, 3),
+ np.array(
+ [[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]
+ ),
+ ),
+ (5, 3),
+ np.array([[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]),
+ ),
+ (
+ (
+ (8, 8),
+ np.array(
+ [[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]
+ ),
+ ),
+ (8, 8),
+ np.array([[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]),
+ ),
+ ],
+ indirect=["ctf_bruker"],
+ )
+ def test_load_ctf_bruker(self, ctf_bruker, map_shape, R_example):
+ xmap = io.load(ctf_bruker)
+
+ # Fraction of non-indexed points
+ non_indexed_fraction = int(np.prod(map_shape) * 0.1)
+ assert non_indexed_fraction == np.sum(~xmap.is_indexed)
+
+ # Properties
+ assert list(xmap.prop.keys()) == ["bands", "error", "MAD", "BC", "BS"]
+
+ # Coordinates
+ ny, nx = map_shape
+ dy = dx = 0.001998
+ assert np.allclose(xmap.x, np.tile(np.arange(nx) * dx, ny), atol=1e-4)
+ assert np.allclose(xmap.y, np.sort(np.tile(np.arange(ny) * dy, nx)), atol=1e-4)
+ assert xmap.scan_unit == "um"
+
+ # Map shape and size
+ assert xmap.shape == map_shape
+ assert xmap.size == np.prod(map_shape)
+
+ # Attributes are within expected ranges or have a certain value
+ assert xmap.bands.min() >= 0
+ assert xmap.error.min() >= 0
+ assert np.allclose(xmap["not_indexed"].bands, 0)
+ assert not any(np.isclose(xmap["not_indexed"].error, 0))
+ assert np.allclose(xmap["not_indexed"].MAD, 0)
+ assert np.allclose(xmap["not_indexed"].BC, 0)
+ assert np.allclose(xmap["not_indexed"].BS, 0)
+
+ # Rotations
+ R_unique = np.unique(xmap["indexed"].rotations.to_euler(), axis=0)
+ assert np.allclose(
+ np.sort(R_unique, axis=0), np.sort(R_example, axis=0), atol=1e-5
+ )
+ assert np.allclose(xmap["not_indexed"].rotations.to_euler()[0], 0)
+
+ # Phases
+ assert all(np.isin(xmap.phase_id, [-1, 1]))
+ assert np.allclose(xmap["not_indexed"].phase_id, -1)
+ assert xmap.phases.ids == [-1, 1]
+ phase = xmap.phases[1]
+ assert phase.name == "Gold"
+ assert phase.space_group.number == 225
+ assert phase.structure.lattice.abcABG() == (4.079, 4.079, 4.079, 90, 90, 90)
+
+ @pytest.mark.parametrize(
+ "ctf_astar, map_shape, R_example",
+ [
+ (
+ (
+ (5, 3),
+ np.array(
+ [[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]
+ ),
+ ),
+ (5, 3),
+ np.array([[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]),
+ ),
+ (
+ (
+ (8, 8),
+ np.array(
+ [[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]
+ ),
+ ),
+ (8, 8),
+ np.array([[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]),
+ ),
+ ],
+ indirect=["ctf_astar"],
+ )
+ def test_load_ctf_astar(self, ctf_astar, map_shape, R_example):
+ xmap = io.load(ctf_astar)
+
+ # Fraction of non-indexed points
+ non_indexed_fraction = int(np.prod(map_shape) * 0.1)
+ assert non_indexed_fraction == np.sum(~xmap.is_indexed)
+
+ # Properties
+ assert list(xmap.prop.keys()) == ["bands", "error", "MAD", "BC", "BS"]
+
+ # Coordinates
+ ny, nx = map_shape
+ dy = dx = 0.00191999995708466
+ assert np.allclose(xmap.x, np.tile(np.arange(nx) * dx, ny), atol=1e-4)
+ assert np.allclose(xmap.y, np.sort(np.tile(np.arange(ny) * dy, nx)), atol=1e-4)
+ assert xmap.scan_unit == "um"
+
+ # Map shape and size
+ assert xmap.shape == map_shape
+ assert xmap.size == np.prod(map_shape)
+
+ # Attributes are within expected ranges or have a certain value
+ assert np.allclose(xmap["indexed"].bands, 6)
+ assert np.allclose(xmap["indexed"].error, 0)
+ assert np.allclose(xmap["not_indexed"].bands, 0)
+ assert not any(np.isclose(xmap["not_indexed"].error, 0))
+ assert np.allclose(xmap["not_indexed"].MAD, 0)
+ assert np.allclose(xmap["not_indexed"].BC, 0)
+ assert np.allclose(xmap["not_indexed"].BS, 0)
+
+ # Rotations
+ R_unique = np.unique(xmap["indexed"].rotations.to_euler(), axis=0)
+ assert np.allclose(
+ np.sort(R_unique, axis=0), np.sort(R_example, axis=0), atol=1e-5
+ )
+ assert np.allclose(xmap["not_indexed"].rotations.to_euler()[0], 0)
+
+ # Phases
+ assert all(np.isin(xmap.phase_id, [-1, 1]))
+ assert np.allclose(xmap["not_indexed"].phase_id, -1)
+ assert xmap.phases.ids == [-1, 1]
+ phase = xmap.phases[1]
+ assert phase.name == "_mineral 'Gold' 'Gold'"
+ assert phase.space_group.number == 225
+ assert phase.structure.lattice.abcABG() == (4.078, 4.078, 4.078, 90, 90, 90)
+
+ @pytest.mark.parametrize(
+ "ctf_emsoft, map_shape, step_sizes, R_example",
+ [
+ (
+ (
+ (5, 3),
+ (0.1, 0.2),
+ np.array(
+ [[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]
+ ),
+ ),
+ (5, 3),
+ (0.1, 0.2),
+ np.array([[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]),
+ ),
+ (
+ (
+ (8, 8),
+ (1.0, 1.5),
+ np.array(
+ [[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]
+ ),
+ ),
+ (8, 8),
+ (1.0, 1.5),
+ np.array([[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]),
+ ),
+ ],
+ indirect=["ctf_emsoft"],
+ )
+ def test_load_ctf_emsoft(
+ self,
+ ctf_emsoft,
+ map_shape,
+ step_sizes,
+ R_example,
+ ):
+ xmap = io.load(ctf_emsoft)
+
+ # Fraction of non-indexed points
+ non_indexed_fraction = int(np.prod(map_shape) * 0.1)
+ assert non_indexed_fraction == np.sum(~xmap.is_indexed)
+
+ # Properties
+ assert list(xmap.prop.keys()) == ["bands", "error", "DP", "OSM", "IQ"]
+
+ # Coordinates
+ ny, nx = map_shape
+ dy, dx = step_sizes
+ assert np.allclose(xmap.x, np.tile(np.arange(nx) * dx, ny))
+ assert np.allclose(xmap.y, np.sort(np.tile(np.arange(ny) * dy, nx)))
+ assert xmap.scan_unit == "um"
+
+ # Map shape and size
+ assert xmap.shape == map_shape
+ assert xmap.size == np.prod(map_shape)
+
+ # Attributes are within expected ranges or have a certain value
+ assert xmap.bands.max() <= 333_000
+ assert np.allclose(xmap.error, 0)
+ assert np.allclose(xmap["not_indexed"].bands, 0)
+ assert np.allclose(xmap["not_indexed"].DP, 0)
+ assert np.allclose(xmap["not_indexed"].OSM, 0)
+ assert np.allclose(xmap["not_indexed"].IQ, 0)
+
+ # Rotations
+ R_unique = np.unique(xmap["indexed"].rotations.to_euler(), axis=0)
+ assert np.allclose(
+ np.sort(R_unique, axis=0), np.sort(R_example, axis=0), atol=1e-3
+ )
+ assert np.allclose(xmap["not_indexed"].rotations.to_euler()[0], 0)
+
+ # Phases
+ phases = PhaseList(
+ names=["Ni"],
+ space_groups=[225],
+ structures=[
+ Structure(lattice=Lattice(3.524, 3.524, 3.524, 90, 90, 90)),
+ ],
+ )
+
+ assert all(np.isin(xmap.phase_id, [-1, 1]))
+ assert np.allclose(xmap["not_indexed"].phase_id, -1)
+ assert xmap.phases.ids == [-1, 1]
+ for (_, phase), (_, phase_test) in zip(xmap["indexed"].phases_in_data, phases):
+ assert phase.name == phase_test.name
+ assert phase.space_group.number == phase_test.space_group.number
+ assert np.allclose(
+ phase.structure.lattice.abcABG(), phase_test.structure.lattice.abcABG()
+ )
+
+ @pytest.mark.parametrize(
+ "ctf_mtex, map_shape, step_sizes, R_example",
+ [
+ (
+ (
+ (5, 3),
+ (0.1, 0.2),
+ np.array(
+ [[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]
+ ),
+ ),
+ (5, 3),
+ (0.1, 0.2),
+ np.array([[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]),
+ ),
+ (
+ (
+ (8, 8),
+ (1.0, 1.5),
+ np.array(
+ [[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]
+ ),
+ ),
+ (8, 8),
+ (1.0, 1.5),
+ np.array([[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]),
+ ),
+ ],
+ indirect=["ctf_mtex"],
+ )
+ def test_load_ctf_mtex(
+ self,
+ ctf_mtex,
+ map_shape,
+ step_sizes,
+ R_example,
+ ):
+ xmap = io.load(ctf_mtex)
+
+ # Fraction of non-indexed points
+ non_indexed_fraction = int(np.prod(map_shape) * 0.1)
+ assert non_indexed_fraction == np.sum(~xmap.is_indexed)
+
+ # Properties
+ assert list(xmap.prop.keys()) == ["bands", "error", "MAD", "BC", "BS"]
+
+ # Coordinates
+ ny, nx = map_shape
+ dy, dx = step_sizes
+ assert np.allclose(xmap.x, np.tile(np.arange(nx) * dx, ny))
+ assert np.allclose(xmap.y, np.sort(np.tile(np.arange(ny) * dy, nx)))
+ assert xmap.scan_unit == "um"
+
+ # Map shape and size
+ assert xmap.shape == map_shape
+ assert xmap.size == np.prod(map_shape)
+
+ # Attributes are within expected ranges or have a certain value
+ assert np.allclose(xmap.bands, 0)
+ assert np.allclose(xmap.error, 0)
+ assert np.allclose(xmap.MAD, 0)
+ assert np.allclose(xmap.BC, 0)
+ assert np.allclose(xmap.BS, 0)
+
+ # Rotations
+ R_unique = np.unique(xmap["indexed"].rotations.to_euler(), axis=0)
+ assert np.allclose(
+ np.sort(R_unique, axis=0), np.sort(R_example, axis=0), atol=1e-5
+ )
+ assert np.allclose(xmap["not_indexed"].rotations.to_euler()[0], 0)
+
+ # Phases
+ phases = PhaseList(
+ names=["Gold"],
+ point_groups=["m-3m"],
+ structures=[
+ Structure(lattice=Lattice(4.079, 4.079, 4.079, 90, 90, 90)),
+ ],
+ )
+
+ assert all(np.isin(xmap.phase_id, [-1, 1]))
+ assert np.allclose(xmap["not_indexed"].phase_id, -1)
+ assert xmap.phases.ids == [-1, 1]
+ for (_, phase), (_, phase_test) in zip(xmap["indexed"].phases_in_data, phases):
+ assert phase.name == phase_test.name
+ assert phase.point_group.name == phase_test.point_group.name
+ assert np.allclose(
+ phase.structure.lattice.abcABG(), phase_test.structure.lattice.abcABG()
+ )
diff --git a/orix/tests/io/test_io.py b/orix/tests/io/test_io.py
index 50df1567..12719c47 100644
--- a/orix/tests/io/test_io.py
+++ b/orix/tests/io/test_io.py
@@ -73,7 +73,7 @@ def test_load_no_filename_match(self):
with pytest.raises(IOError, match=f"No filename matches '{fname}'."):
_ = load(fname)
- @pytest.mark.parametrize("temp_file_path", ["ctf"], indirect=["temp_file_path"])
+ @pytest.mark.parametrize("temp_file_path", ["ktf"], indirect=["temp_file_path"])
def test_load_unsupported_format(self, temp_file_path):
np.savetxt(temp_file_path, X=np.random.rand(100, 8))
with pytest.raises(IOError, match=f"Could not read "):
@@ -146,11 +146,12 @@ def test_save_overwrite(
assert crystal_map2.phases[0].name == expected_phase_name
+# TODO: Remove after 0.13.0
def test_loadctf():
- """Crude test of the ctf loader"""
z = np.random.rand(100, 8)
fname = "temp.ctf"
np.savetxt(fname, z)
- _ = loadctf(fname)
+ with pytest.warns(np.VisibleDeprecationWarning):
+ _ = loadctf(fname)
os.remove(fname)
diff --git a/orix/tests/plot/test_crystal_map_plot.py b/orix/tests/plot/test_crystal_map_plot.py
index 86cb2f52..3766a32e 100644
--- a/orix/tests/plot/test_crystal_map_plot.py
+++ b/orix/tests/plot/test_crystal_map_plot.py
@@ -375,7 +375,7 @@ 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=0 y=0"
+ assert ax.format_coord(0, 0) == "(x, y) = (0, 0)"
fig = plt.figure()
ax = fig.add_subplot(projection=PLOT_MAP)
diff --git a/orix/tests/plot/test_rotation_plot.py b/orix/tests/plot/test_rotation_plot.py
index 4385a9e9..8fe36e1c 100644
--- a/orix/tests/plot/test_rotation_plot.py
+++ b/orix/tests/plot/test_rotation_plot.py
@@ -25,87 +25,84 @@
from orix.quaternion.symmetry import C1, D6
-def test_init_rodrigues_plot():
- fig = plt.figure()
- ax = fig.add_subplot(projection="rodrigues", auto_add_to_figure=False)
- assert isinstance(ax, RodriguesPlot)
-
-
-def test_init_axangle_plot():
- fig = plt.figure()
- ax = fig.add_subplot(projection="axangle", auto_add_to_figure=False)
- assert isinstance(ax, AxAnglePlot)
-
-
-def test_RotationPlot_methods():
- """This code is lifted from demo-3-v0.1."""
- misori = Misorientation([1, 1, 1, 1]) # any will do
- ori = Orientation.random()
- fig = plt.figure()
- ax = fig.add_subplot(
- projection="axangle", proj_type="ortho", auto_add_to_figure=False
- )
- ax.scatter(misori)
- ax.scatter(ori)
- ax.plot(misori)
- ax.plot(ori)
- ax.plot_wireframe(OrientationRegion.from_symmetry(D6, D6))
- plt.close("all")
-
- # Clear the edge case
- ax.transform(np.asarray([1, 1, 1]))
-
-
-def test_full_region_plot():
- empty = OrientationRegion.from_symmetry(C1, C1)
- _ = empty.get_plot_data()
-
-
-def test_RotationPlot_transform_fundamental_zone_raises():
- fig = plt.figure()
- ax = RotationPlot(fig)
- fig.add_axes(ax)
- with pytest.raises(
- TypeError, match="fundamental_zone is not an OrientationRegion object"
- ):
- ax.transform(Orientation.random(), fundamental_zone=1)
-
-
-def test_RotationPlot_map_into_symmetry_reduced_zone():
- # orientations are (in, out) of D6 fundamental zone
- ori = Orientation(((1, 0, 0, 0), (0.5, 0.5, 0.5, 0.5)))
- ori.symmetry = D6
- fz = OrientationRegion.from_symmetry(ori.symmetry)
- assert np.allclose(ori < fz, (True, False))
- # test map_into_symmetry_reduced_zone in RotationPlot.transform
- fig = ori.scatter(return_figure=True)
- xyz_symmetry = fig.axes[0].collections[1]._offsets3d
- # compute same plot again but with C1 symmetry where both orientations are in C1 FZ
- ori.symmetry = C1
- fig2 = ori.scatter(return_figure=True)
- xyz = fig2.axes[0].collections[1]._offsets3d
- # test that the plotted points are not the same
- assert not np.allclose(xyz_symmetry, xyz)
-
-
-def test_correct_aspect_ratio():
- # Set up figure the "old" way
- fig = plt.figure()
- ax = fig.add_subplot(
- projection="axangle", proj_type="ortho", auto_add_to_figure=False
- )
-
- # Check aspect ratio
- x_old, _, z_old = ax.get_box_aspect()
- assert np.allclose(x_old / z_old, 1.334, atol=1e-3)
-
- fr = OrientationRegion.from_symmetry(D6)
- ax._correct_aspect_ratio(fr, set_limits=False)
-
- x_new, _, z_new = ax.get_box_aspect()
- assert np.allclose(x_new / z_new, 3, atol=1e-3)
-
- # Check data limits
- assert np.allclose(ax.get_xlim(), [0, 1])
- ax._correct_aspect_ratio(fr) # set_limits=True is default
- assert np.allclose(ax.get_xlim(), [-np.pi / 2, np.pi / 2])
+class TestRodriguesPlot:
+ def test_creation(self):
+ fig = plt.figure()
+ ax = fig.add_subplot(projection="rodrigues")
+ assert isinstance(ax, RodriguesPlot)
+
+
+class TestAxisAnglePlot:
+ def test_creation(self):
+ fig = plt.figure()
+ ax = fig.add_subplot(projection="axangle")
+ assert isinstance(ax, AxAnglePlot)
+
+ plt.close("all")
+
+ def test_rotation_plot(self):
+ M = Misorientation.random()
+ O = Orientation.random()
+ fig = plt.figure()
+ ax = fig.add_subplot(projection="axangle", proj_type="ortho")
+ ax.scatter(M)
+ ax.scatter(O)
+ ax.plot(M)
+ ax.plot(O)
+ ax.plot_wireframe(OrientationRegion.from_symmetry(D6, D6))
+
+ ax.transform(np.asarray([1, 1, 1])) # Edge case
+
+ plt.close("all")
+
+ def test_get_plot_data(self):
+ empty = OrientationRegion.from_symmetry(C1, C1)
+ _ = empty.get_plot_data()
+
+ def test_rotation_plot_transform_fundamental_zone_raises(self):
+ fig = plt.figure()
+ ax = RotationPlot(fig)
+ fig.add_axes(ax)
+ with pytest.raises(TypeError, match="fundamental_zone is not an "):
+ ax.transform(Orientation.random(), fundamental_zone=1)
+
+ def test_rotation_plot_map_into_symmetry_reduced_zone(self):
+ # Orientations are (in, out) of D6 fundamental zone
+ O = Orientation(((1, 0, 0, 0), (0.5, 0.5, 0.5, 0.5)))
+ O.symmetry = D6
+ fz = OrientationRegion.from_symmetry(O.symmetry)
+ assert np.allclose(O < fz, (True, False))
+
+ # test map_into_symmetry_reduced_zone in RotationPlot.transform
+ fig = O.scatter(return_figure=True)
+ xyz_symmetry = fig.axes[0].collections[1]._offsets3d
+
+ # compute same plot again but with C1 symmetry where both orientations are in C1 FZ
+ O.symmetry = C1
+ fig2 = O.scatter(return_figure=True)
+ xyz = fig2.axes[0].collections[1]._offsets3d
+
+ # test that the plotted points are not the same
+ assert not np.allclose(xyz_symmetry, xyz)
+
+ plt.close("all")
+
+ def test_correct_aspect_ratio(self):
+ fig = plt.figure()
+ ax = fig.add_subplot(projection="axangle", proj_type="ortho")
+
+ # Check aspect ratio
+ x_old, _, z_old = ax.get_box_aspect()
+ assert np.allclose(x_old / z_old, 1.334, atol=1e-3)
+
+ fr = OrientationRegion.from_symmetry(D6)
+ ax._correct_aspect_ratio(fr, set_limits=False)
+
+ x_new, _, z_new = ax.get_box_aspect()
+ assert np.allclose(x_new / z_new, 3, atol=1e-3)
+
+ assert np.allclose(ax.get_xlim(), [0, 1], atol=0.1)
+ ax._correct_aspect_ratio(fr) # set_limits=True is default
+ assert np.allclose(ax.get_xlim(), [-np.pi / 2, np.pi / 2])
+
+ plt.close("all")
diff --git a/orix/tests/quaternion/test_orientation.py b/orix/tests/quaternion/test_orientation.py
index cefaf8f0..2a87982b 100644
--- a/orix/tests/quaternion/test_orientation.py
+++ b/orix/tests/quaternion/test_orientation.py
@@ -16,8 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with orix. If not, see .
-import warnings
-
from diffpy.structure import Lattice, Structure
import matplotlib.pyplot as plt
import numpy as np
@@ -43,7 +41,7 @@
_groups,
_proper_groups,
)
-from orix.vector import AxAngle, Miller, Vector3d
+from orix.vector import Miller, Vector3d
# isort: on
# fmt: on
@@ -522,22 +520,7 @@ def test_from_align_vectors(self):
):
_ = Orientation.from_align_vectors(a, b)
- def test_from_neo_euler_symmetry(self):
- v = AxAngle.from_axes_angles(axes=Vector3d.zvector(), angles=np.pi / 2)
- with pytest.warns(np.VisibleDeprecationWarning):
- o1 = Orientation.from_neo_euler(v)
- assert np.allclose(o1.data, [0.7071, 0, 0, 0.7071])
- assert o1.symmetry.name == "1"
- with pytest.warns(np.VisibleDeprecationWarning):
- o2 = Orientation.from_neo_euler(v, symmetry=Oh)
- o2 = o2.map_into_symmetry_reduced_zone()
- assert np.allclose(o2.data, [-1, 0, 0, 0])
- assert o2.symmetry.name == "m-3m"
- o3 = Orientation(o1.data, symmetry=Oh)
- o3 = o3.map_into_symmetry_reduced_zone()
- assert np.allclose(o3.data, o2.data)
-
- def test_from_axes_angles(self, rotations):
+ def test_from_axes_angles(self):
axis = Vector3d.xvector() - Vector3d.yvector()
angle = np.pi / 2
o1 = Orientation.from_axes_angles(axis, angle, Oh)
@@ -587,58 +570,6 @@ def test_from_scipy_rotation(self):
with pytest.raises(TypeError, match="Value must be an instance of"):
_ = Orientation.from_scipy_rotation(r_scipy, (Oh, Oh))
- # TODO: Remove in 0.13
- def test_from_euler_warns(self):
- """Orientation.from_euler() warns only once when "convention"
- argument is passed.
- """
- euler = np.random.rand(10, 3)
-
- with warnings.catch_warnings():
- warnings.filterwarnings("error")
- _ = Orientation.from_euler(euler)
-
- msg = (
- r"Argument `convention` is deprecated and will be removed in version 0.13. "
- r"To avoid this warning, please do not use `convention`. "
- r"Use `direction` instead. See the documentation of `from_euler\(\)` for "
- "more details."
- )
- with pytest.warns(np.VisibleDeprecationWarning, match=msg) as record2:
- _ = Orientation.from_euler(euler, convention="whatever")
- assert len(record2) == 1
-
- # TODO: Remove in 0.13
- def test_from_euler_convention_mtex(self):
- """Passing convention="mtex" to Orientation.from_euler() works
- but warns once.
- """
- euler = np.random.rand(10, 3)
- ori1 = Orientation.from_euler(euler, direction="crystal2lab")
- with pytest.warns(np.VisibleDeprecationWarning, match=r"Argument `convention`"):
- ori2 = Orientation.from_euler(euler, convention="mtex")
- assert np.allclose(ori1.data, ori2.data)
-
- # TODO: Remove in 0.13
- def test_to_euler_convention_warns(self):
- """Orientation.to_euler() warns only once when "convention"
- argument is passed.
- """
- ori1 = Orientation.from_euler(np.random.rand(10, 3))
-
- with warnings.catch_warnings():
- warnings.filterwarnings("error")
- ori2 = ori1.to_euler()
-
- msg = (
- r"Argument `convention` is deprecated and will be removed in version 0.13. "
- r"To avoid this warning, please do not use `convention`. "
- r"See the documentation of `to_euler\(\)` for more details."
- )
- with pytest.warns(np.VisibleDeprecationWarning, match=msg):
- ori3 = ori1.to_euler(convention="whatever")
- assert np.allclose(ori2, ori3)
-
class TestOrientation:
@pytest.mark.parametrize("symmetry", [C1, C2, C3, C4, D2, D3, D6, T, O, Oh])
diff --git a/orix/tests/quaternion/test_quaternion.py b/orix/tests/quaternion/test_quaternion.py
index bc69ca94..e3959164 100644
--- a/orix/tests/quaternion/test_quaternion.py
+++ b/orix/tests/quaternion/test_quaternion.py
@@ -16,8 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with orix. If not, see .
-import warnings
-
import dask.array as da
from diffpy.structure.spacegroups import sg225
import numpy as np
@@ -25,7 +23,7 @@
from orix._base import DimensionError
from orix.quaternion import Quaternion
-from orix.vector import AxAngle, Homochoric, Rodrigues, Vector3d
+from orix.vector import AxAngle, Homochoric, Vector3d
@pytest.fixture(
@@ -345,9 +343,6 @@ def test_to_from_euler(self, eu):
eu4 = Quaternion.from_euler(eu).to_euler(degrees=True)
assert np.allclose(np.rad2deg(eu), eu4)
- def test_mtex(self, eu):
- _ = Quaternion.from_euler(eu, direction="mtex")
-
def test_direction_values(self, eu):
q_mtex = Quaternion.from_euler(eu, direction="mtex")
q_c2l = Quaternion.from_euler(eu, direction="crystal2lab")
@@ -357,9 +352,6 @@ def test_direction_values(self, eu):
assert np.allclose(q_mtex.data, q_c2l.data)
assert np.allclose((q_l2c * q_c2l).data, [1, 0, 0, 0])
- def test_direction_kwarg(self, eu):
- _ = Quaternion.from_euler(eu)
-
def test_direction_kwarg_dumb(self, eu):
with pytest.raises(ValueError, match="The chosen direction is not one of "):
_ = Quaternion.from_euler(eu, direction="dumb_direction")
@@ -376,56 +368,6 @@ def test_passing_degrees_warns(self):
q = Quaternion.from_euler([90, 0, 0])
assert np.allclose(q.data, [0.5253, 0, 0, -0.8509], atol=1e-4)
- # TODO: Remove in 0.13
- def test_from_euler_warns(self, eu):
- """Quaternion.from_euler() warns only when "convention" argument
- is passed.
- """
- # No warning is raised
- with warnings.catch_warnings():
- warnings.simplefilter("error")
- _ = Quaternion.from_euler(eu)
-
- msg = (
- r"Argument `convention` is deprecated and will be removed in version 0.13. "
- r"To avoid this warning, please do not use `convention`. "
- r"Use `direction` instead. See the documentation of `from_euler\(\)` for "
- "more details."
- )
- with pytest.warns(np.VisibleDeprecationWarning, match=msg):
- _ = Quaternion.from_euler(eu, convention="whatever")
-
- # TODO: Remove in 0.13
- def test_from_euler_convention_mtex(self, eu):
- """Passing convention="mtex" to Quaternion.from_euler() works but
- warns.
- """
- q1 = Quaternion.from_euler(eu, direction="crystal2lab")
- with pytest.warns(np.VisibleDeprecationWarning, match=r"Argument `convention`"):
- q2 = Quaternion.from_euler(eu, convention="mtex")
- assert np.allclose(q1.data, q2.data)
-
- # TODO: Remove in 0.13
- def test_to_euler_convention_warns(self, eu):
- """Quaternion.to_euler() warns only when "convention" argument is
- passed.
- """
- q1 = Quaternion.from_euler(eu)
-
- # No warning is raised
- with warnings.catch_warnings():
- warnings.simplefilter("error")
- q2 = q1.to_euler()
-
- msg = (
- r"Argument `convention` is deprecated and will be removed in version 0.13. "
- r"To avoid this warning, please do not use `convention`. "
- r"See the documentation of `to_euler\(\)` for more details."
- )
- with pytest.warns(np.VisibleDeprecationWarning, match=msg):
- q3 = q1.to_euler(convention="whatever")
- assert np.allclose(q2, q3)
-
class TestFromToMatrix:
def test_to_matrix(self):
@@ -516,13 +458,9 @@ def test_from_axes_angles(self, rotations, extra_dim):
if extra_dim:
rotations = rotations.__class__(rotations.data[..., np.newaxis, :])
ax = AxAngle.from_rotation(rotations)
- with pytest.warns(np.VisibleDeprecationWarning):
- q2 = Quaternion.from_neo_euler(ax)
- q3 = Quaternion.from_axes_angles(ax.axis.data, ax.angle)
- assert np.allclose(q2.data, q3.data)
-
- q4 = Quaternion.from_axes_angles(ax.axis, np.rad2deg(ax.angle), degrees=True)
- assert np.allclose(q4.data, q3.data)
+ Q1 = Quaternion.from_axes_angles(ax.axis.data, ax.angle)
+ Q2 = Quaternion.from_axes_angles(ax.axis, np.rad2deg(ax.angle), degrees=True)
+ assert np.allclose(Q1.data, Q2.data)
def test_to_axes_angles(self, quaternions_conversions, axis_angle_pairs):
ax = Quaternion(quaternions_conversions).to_axes_angles()
@@ -556,21 +494,6 @@ def test_from_to_rodrigues(self, quaternions_conversions, rodrigues_vectors):
with pytest.raises(ValueError, match="Final dimension of vector array must be"):
Quaternion.from_rodrigues([1, 2, 3, 4])
- def test_backwards_consistency(self, quaternions_conversions, rodrigues_vectors):
- axes = rodrigues_vectors[..., :3]
- angles = rodrigues_vectors[..., 3][..., np.newaxis]
- with pytest.warns(RuntimeWarning):
- ro = Rodrigues(axes * angles)
-
- with pytest.warns(np.VisibleDeprecationWarning):
- q1 = Quaternion.from_neo_euler(ro)
- with pytest.warns(UserWarning, match="Highest angle is greater than 179.999 "):
- q2 = Quaternion.from_rodrigues(axes, angles)
-
- assert np.allclose(q1[2].data, 0)
- match_idx = [0, 1, 3, 4, 5, 6, 7, 8, 9]
- assert np.allclose(q1[match_idx].data, q2[match_idx].data, atol=1e-4)
-
def test_from_rodrigues_empty(self):
q = Quaternion.from_rodrigues([])
assert q.size == 0
diff --git a/orix/tests/test_crystal_map.py b/orix/tests/test_crystal_map.py
index 6acf9f02..a04c65d8 100644
--- a/orix/tests/test_crystal_map.py
+++ b/orix/tests/test_crystal_map.py
@@ -22,6 +22,7 @@
import pytest
from orix.crystal_map import CrystalMap, Phase, PhaseList, create_coordinate_arrays
+from orix.crystal_map.crystal_map import _data_slices_from_coordinates
from orix.plot import CrystalMapPlot
from orix.quaternion import Orientation, Rotation
from orix.quaternion.symmetry import C2, C3, C4, O
@@ -1090,6 +1091,11 @@ def test_coordinate_axes(self, crystal_map_input, expected_coordinate_axes):
xmap = CrystalMap(**crystal_map_input)
assert xmap._coordinate_axes == expected_coordinate_axes
+ def test_data_slices_from_coordinates_no_steps(self):
+ d, _ = create_coordinate_arrays((3, 4), step_sizes=(0.1, 0.2))
+ slices = _data_slices_from_coordinates(d)
+ assert slices == (slice(0, 4, None), slice(0, 3, None))
+
class TestCrystalMapPlotMethod:
def test_plot(self, crystal_map):
diff --git a/orix/tests/test_vector3d.py b/orix/tests/test_vector3d.py
index 3f628822..7ed4c386 100644
--- a/orix/tests/test_vector3d.py
+++ b/orix/tests/test_vector3d.py
@@ -460,6 +460,16 @@ def test_zero_perpendicular():
_ = Vector3d.zero((1,)).perpendicular
+def test_get_nearest():
+ v_ref = Vector3d.zvector()
+ v = Vector3d([[0, 0, 0.9], [0, 0, 0.8], [0, 0, 1.1]])
+ v_nearest = v_ref.get_nearest(v)
+ assert np.allclose(v_nearest.data, [0, 0, 0.9])
+
+ with pytest.raises(AttributeError, match="`get_nearest` only works for "):
+ v.get_nearest(v_ref)
+
+
class TestSpareNotImplemented:
def test_radd_notimplemented(self, vector):
with pytest.raises(TypeError):
diff --git a/orix/vector/miller.py b/orix/vector/miller.py
index f8a573f8..e65e68bd 100644
--- a/orix/vector/miller.py
+++ b/orix/vector/miller.py
@@ -20,13 +20,23 @@
from copy import deepcopy
from itertools import product
-from typing import Optional, Tuple, Union
+from typing import TYPE_CHECKING, Optional, Tuple, Union
+
+try:
+ # New in Python 3.11
+ from typing import Self
+except ImportError: # pragma: no cover
+ from typing_extensions import Self
from diffpy.structure import Lattice
import numpy as np
from orix.vector import Vector3d
+if TYPE_CHECKING: # pragma: no cover
+ from orix.crystal_map import Phase
+ from orix.quaternion import Symmetry
+
class Miller(Vector3d):
r"""Direct crystal lattice vectors (uvw or UVTW) and reciprocal
@@ -48,13 +58,12 @@ class Miller(Vector3d):
Indices of direct lattice vector(s). Default is ``None``.
UVTW
Indices of direct lattice vector(s), often preferred over
- ``uvw`` in trigonal and hexagonal lattices. Default is ``None``.
+ *uvw* in trigonal and hexagonal lattices. Default is ``None``.
hkl
Indices of reciprocal lattice vector(s). Default is ``None``.
hkil
- Indices of reciprocal lattice vector(s), often preferred
- over ``hkl`` in trigonal and hexagonal lattices. Default is
- ``None``.
+ Indices of reciprocal lattice vector(s), often preferred over
+ *hkl* in trigonal and hexagonal lattices. Default is ``None``.
phase
A phase with a crystal lattice and symmetry. Must be passed
whenever direct or reciprocal lattice vectors are created.
@@ -77,13 +86,8 @@ def __init__(
UVTW: Union[np.ndarray, list, tuple, None] = None,
hkl: Union[np.ndarray, list, tuple, None] = None,
hkil: Union[np.ndarray, list, tuple, None] = None,
- phase: Optional["orix.crystal_map.Phase"] = None,
- ):
- """Create a set of direct lattice vectors (uvw or UVTW) or
- reciprocal lattice vectors (hkl or hkil) describing directions
- with respect to a crystal reference frame defined by a phase's
- crystal lattice and symmetry.
- """
+ phase: Optional["Phase"] = None,
+ ) -> None:
n_passed = np.sum([i is not None for i in [xyz, uvw, UVTW, hkl, hkil]])
if n_passed == 0 or n_passed > 1:
raise ValueError(
@@ -134,7 +138,7 @@ def coordinate_format(self) -> str:
return self._coordinate_format
@coordinate_format.setter
- def coordinate_format(self, value: str):
+ def coordinate_format(self, value: str) -> None:
"""Set the vector coordinate format."""
formats = ["xyz", "uvw", "UVTW", "hkl", "hkil"]
if value not in formats:
@@ -162,12 +166,12 @@ def hkl(self) -> np.ndarray:
return _transform_space(self.data, "c", "r", self.phase.structure.lattice)
@hkl.setter
- def hkl(self, value: np.ndarray):
+ def hkl(self, value: np.ndarray) -> None:
"""Set the reciprocal lattice vectors."""
self.data = _transform_space(value, "r", "c", self.phase.structure.lattice)
@property
- def hkil(self):
+ def hkil(self) -> np.ndarray:
r"""Return or set the reciprocal lattice vectors expressed as
4-index Miller-Bravais indices.
@@ -181,7 +185,7 @@ def hkil(self):
return _hkl2hkil(self.hkl)
@hkil.setter
- def hkil(self, value: np.ndarray):
+ def hkil(self, value: np.ndarray) -> None:
"""Set the reciprocal lattice vectors expressed as 4-index
Miller-Bravais indices.
"""
@@ -223,12 +227,12 @@ def uvw(self) -> np.ndarray:
return _transform_space(self.data, "c", "d", self.phase.structure.lattice)
@uvw.setter
- def uvw(self, value: np.ndarray):
+ def uvw(self, value: np.ndarray) -> None:
"""Set the direct lattice vectors."""
self.data = _transform_space(value, "d", "c", self.phase.structure.lattice)
@property
- def UVTW(self):
+ def UVTW(self) -> np.ndarray:
r"""Return or set the direct lattice vectors expressed as
4-index Weber symbols.
@@ -249,7 +253,7 @@ def UVTW(self):
return _uvw2UVTW(self.uvw)
@UVTW.setter
- def UVTW(self, value):
+ def UVTW(self, value: np.ndarray) -> None:
"""Set the direct lattice vectors expressed as 4-index Weber
symbols.
"""
@@ -341,7 +345,7 @@ def is_hexagonal(self) -> bool:
return self.phase.is_hexagonal
@property
- def unit(self) -> Miller:
+ def unit(self) -> Self:
"""Return unit vectors."""
m = self.__class__(xyz=super().unit.data, phase=self.phase)
m.coordinate_format = self.coordinate_format
@@ -360,7 +364,7 @@ def __repr__(self) -> str:
f"{name} {shape}, point group {symmetry}, {coordinate_format}\n" f"{data}"
)
- def __getitem__(self, key) -> Miller:
+ def __getitem__(self, key) -> Self:
"""NumPy fancy indexing of vectors."""
m = self.__class__(xyz=self.data[key], phase=self.phase).deepcopy()
m.coordinate_format = self.coordinate_format
@@ -371,10 +375,11 @@ def __getitem__(self, key) -> Miller:
@classmethod
def from_highest_indices(
cls,
- phase: "orix.crystal_map.Phase",
+ phase: "Phase",
uvw: Union[np.ndarray, list, tuple, None] = None,
hkl: Union[np.ndarray, list, tuple, None] = None,
- ) -> Miller:
+ include_zero_vector: bool = False,
+ ) -> Self:
"""Create a set of unique direct or reciprocal lattice vectors
from three highest indices and a phase (crystal lattice and
symmetry).
@@ -401,9 +406,7 @@ def from_highest_indices(
return cls(**init_kw).unique()
@classmethod
- def from_min_dspacing(
- cls, phase: "orix.crystal_map.Phase", min_dspacing: float = 0.05
- ) -> Miller:
+ def from_min_dspacing(cls, phase: "Phase", min_dspacing: float = 0.05) -> Self:
"""Create a set of unique reciprocal lattice vectors with a
a direct space interplanar spacing greater than a lower
threshold.
@@ -427,10 +430,10 @@ def from_min_dspacing(
@classmethod
def random(
cls,
- phase: "orix.crystal_map.Phase",
+ phase: "Phase",
shape: Union[int, tuple] = 1,
coordinate_format: str = "xyz",
- ) -> Miller:
+ ) -> Self:
"""Create random Miller indices.
Parameters
@@ -464,11 +467,11 @@ def random(
# --------------------- Other public methods --------------------- #
- def deepcopy(self) -> Miller:
+ def deepcopy(self) -> Self:
"""Return a deepcopy of the instance."""
return deepcopy(self)
- def round(self, max_index: int = 20) -> Miller:
+ def round(self, max_index: int = 20) -> Self:
"""Round a set of index triplet (Miller) or quartet
(Miller-Bravais/Weber) to the *closest* smallest integers.
@@ -496,9 +499,7 @@ def symmetrise(
unique: bool = False,
return_multiplicity: bool = False,
return_index: bool = False,
- ) -> Union[
- Miller, Tuple[Miller, np.ndarray], Tuple[Miller, np.ndarray, np.ndarray]
- ]:
+ ) -> Union[Self, Tuple[Self, np.ndarray], Tuple[Self, np.ndarray, np.ndarray]]:
"""Return vectors symmetrically equivalent to the vectors.
Parameters
@@ -585,7 +586,7 @@ def symmetrise(
def angle_with(
self,
- other: Miller,
+ other: Self,
use_symmetry: bool = False,
degrees: bool = False,
) -> np.ndarray:
@@ -630,7 +631,7 @@ def angle_with(
return angles
- def cross(self, other: Miller):
+ def cross(self, other: Self) -> Self:
"""Return the cross products of the vectors with the other
vectors, which is considered the zone axes between the vectors.
@@ -652,7 +653,7 @@ def cross(self, other: Miller):
m.coordinate_format = new_fmt[self.coordinate_format]
return m
- def dot(self, other: Miller) -> np.ndarray:
+ def dot(self, other: Self) -> np.ndarray:
"""Return the dot products of the vectors and the other vectors.
Parameters
@@ -669,7 +670,7 @@ def dot(self, other: Miller) -> np.ndarray:
self._compatible_with(other, raise_error=True)
return super().dot(other)
- def dot_outer(self, other: Miller) -> np.ndarray:
+ def dot_outer(self, other: Self) -> np.ndarray:
"""Return the outer dot products of the vectors and the other
vectors.
@@ -687,7 +688,7 @@ def dot_outer(self, other: Miller) -> np.ndarray:
self._compatible_with(other, raise_error=True)
return super().dot_outer(other)
- def flatten(self) -> Miller:
+ def flatten(self) -> Self:
"""Return the flattened vectors.
Returns
@@ -699,7 +700,7 @@ def flatten(self) -> Miller:
m.coordinate_format = self.coordinate_format
return m
- def transpose(self, *axes: Optional[int]) -> Miller:
+ def transpose(self, *axes: Optional[int]) -> Self:
"""Return a new instance with the data transposed.
The order may be undefined if :attr:`ndim` is originally 2. In
@@ -725,7 +726,7 @@ def get_nearest(self, *args) -> NotImplemented:
"""NotImplemented."""
return NotImplemented
- def mean(self, use_symmetry: bool = False) -> Miller:
+ def mean(self, use_symmetry: bool = False) -> Self:
"""Return the mean vector of the set of vectors.
Parameters
@@ -745,7 +746,7 @@ def mean(self, use_symmetry: bool = False) -> Miller:
m.coordinate_format = self.coordinate_format
return m
- def reshape(self, *shape: Union[int, tuple]) -> Miller:
+ def reshape(self, *shape: Union[int, tuple]) -> Self:
"""Return a new instance with the vectors reshaped.
Parameters
@@ -764,7 +765,7 @@ def reshape(self, *shape: Union[int, tuple]) -> Miller:
def unique(
self, use_symmetry: bool = False, return_index: bool = False
- ) -> Union[Miller, Tuple[Miller, np.ndarray]]:
+ ) -> Union[Self, Tuple[Self, np.ndarray]]:
"""Unique vectors in ``self``.
Parameters
@@ -809,9 +810,7 @@ def unique(
else:
return m
- def in_fundamental_sector(
- self, symmetry: Optional["orix.quaternion.Symmetry"] = None
- ) -> Miller:
+ def in_fundamental_sector(self, symmetry: Optional["Symmetry"] = None) -> Self:
"""Project Miller indices to a symmetry's fundamental sector
(inverse pole figure).
@@ -858,7 +857,7 @@ def in_fundamental_sector(
# -------------------- Other private methods --------------------- #
- def _compatible_with(self, other: Miller, raise_error: bool = False) -> bool:
+ def _compatible_with(self, other: Self, raise_error: bool = False) -> bool:
"""Whether ``self`` and ``other`` are the same (the same crystal
lattice and symmetry) with vectors in the same space.
@@ -977,7 +976,7 @@ def _hkil2hkl(hkil: np.ndarray) -> np.ndarray:
return hkl
-def _check_hkil(hkil: np.ndarray):
+def _check_hkil(hkil: np.ndarray) -> None:
hkil = np.asarray(hkil)
if not np.allclose(np.sum(hkil[..., :3], axis=-1), 0, atol=1e-4):
raise ValueError(
@@ -1016,7 +1015,7 @@ def _UVTW2uvw(UVTW: np.ndarray, convention: Optional[str] = None) -> np.ndarray:
return uvw
-def _check_UVTW(UVTW: np.ndarray):
+def _check_UVTW(UVTW: np.ndarray) -> None:
UVTW = np.asarray(UVTW)
if not np.allclose(np.sum(UVTW[..., :3], axis=-1), 0, atol=1e-4):
raise ValueError(
diff --git a/orix/vector/neo_euler.py b/orix/vector/neo_euler.py
index ea822af1..7aab52b0 100644
--- a/orix/vector/neo_euler.py
+++ b/orix/vector/neo_euler.py
@@ -30,12 +30,15 @@
from __future__ import annotations
import abc
-from typing import Union
+from typing import TYPE_CHECKING, Union
import numpy as np
from orix.vector import Vector3d
+if TYPE_CHECKING: # pragma: no cover
+ from orix.quaternion import Rotation
+
class NeoEuler(Vector3d, abc.ABC):
"""Base class for neo-Eulerian vectors."""
@@ -140,7 +143,7 @@ def from_rotation(cls, rotation: "Rotation") -> Rodrigues:
--------
Quaternion.to_rodrigues
"""
- a = np.float64(rotation.a)
+ a = rotation.a.astype(np.float64)
with np.errstate(divide="ignore", invalid="ignore"):
data = np.stack((rotation.b / a, rotation.c / a, rotation.d / a), axis=-1)
data[np.isnan(data)] = 0
diff --git a/orix/vector/vector3d.py b/orix/vector/vector3d.py
index a565ba71..ea62b36d 100644
--- a/orix/vector/vector3d.py
+++ b/orix/vector/vector3d.py
@@ -676,7 +676,8 @@ def get_nearest(
Vector3d (1,)
[[0.6 0. 0. ]]
"""
- assert self.size == 1, "`get_nearest` only works for single vectors."
+ if self.size != 1:
+ raise AttributeError("`get_nearest` only works for single vectors")
tiebreak = Vector3d.zvector() if tiebreak is None else tiebreak
eps = 1e-9 if inclusive else 0
cosines = x.dot(self)
diff --git a/setup.cfg b/setup.cfg
index 27a67e7c..f59f4c06 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -8,12 +8,17 @@ addopts =
# 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]
@@ -23,7 +28,6 @@ precision = 2
known_excludes =
.*
.*/**
- .git/**
*.code-workspace
**/*.pyc
**/*.nbi
diff --git a/setup.py b/setup.py
index e5a3f54e..dfcc4c09 100644
--- a/setup.py
+++ b/setup.py
@@ -12,9 +12,7 @@
"doc": [
"ipykernel", # Used by nbsphinx to execute notebooks
"memory_profiler",
- # TODO: Remove nbconvert pin once
- # https://github.com/pyxem/orix/issues/494 is resolved
- "nbconvert < 7.14",
+ "nbconvert >= 7.16.4",
"nbsphinx >= 0.7",
"numpydoc",
"pydata-sphinx-theme",
@@ -95,6 +93,8 @@
"numpy",
"numpy-quaternion",
"pooch >= 0.13",
+ # TODO: Remove once https://github.com/diffpy/diffpy.structure/issues/97 is fixed
+ "pycifrw",
"scipy",
"tqdm",
],