diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 11e21eca..e27a0964 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -28,6 +28,8 @@ Removed ------- - Removed support for Python 3.6 and Python 3.7, leaving 3.8 as the oldest supported version. +- ``ReciprocalLatticePoint`` class; Use the ``ReciprocalLatticeVector`` class instead, + which is an improved replacement. Fixed ----- diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 8ad2eb98..30f45633 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -12,6 +12,7 @@ We have a `Code of Conduct `_ that must be honoured by contributors. + Start using diffsims ==================== @@ -42,6 +43,7 @@ install of diffsims! PS: If you choose to develop in Windows/Mac you may find the `Github Desktop `_ useful. + Questions? ========== @@ -56,6 +58,7 @@ scary but it ensures that issues are identified and logged until dealt with. Thi also a good place to make a proposal for some new feature or tool that you want to work on. + Good coding practice ==================== @@ -89,7 +92,6 @@ your newly added and modified files prior to each PR. If this doesn't work for you, you can also use the Pre-commit CI to reformat your code on github by commenting "pre-commit autofix" on your PR. - Run and write tests ------------------- @@ -128,7 +130,6 @@ Useful hints on testing: error-prone. See `pytest documentation for more details `_. - Deprecations ------------ We attempt to adhere to semantic versioning as best we can. This means that as little, @@ -138,16 +139,20 @@ so that users get a heads-up one (minor) release before something is removed or with a possible alternative to be used. -A deprecation decorator should be placed right above the object signature to be deprecated. +A deprecation decorator should be placed right above the object signature to be deprecated:: -.. code-block:: python from diffsims.utils._deprecated import deprecated + @deprecated(since=0.8, removal=0.9, alternative="bar") - def foo(self, n): - return n + 1 - @property - @deprecated(since=0.9, removal=0.10, alternative="another", is_function=True) + def foo(self, n): ... + @property + @deprecated( + since="0.9", + removal="0.10", + alternative="another", + alternative_is_function=True + ): ... Build and write documentation ----------------------------- @@ -173,6 +178,7 @@ in the `reStructuredText (reST) plaintext markup language. They should be accessible in the browser by typing ``file:///your-absolute/path/to/diffsims/doc/build/html/index.html`` in the address bar. + Continuous integration (CI) =========================== @@ -181,6 +187,7 @@ diffsims can be installed on Windows, macOS and Linux. After a successful instal the CI server runs the tests. After the tests return no errors, code coverage is reported to `Coveralls `_. + Learn more ========== diff --git a/MANIFEST.in b/MANIFEST.in index c7068aa3..2153cf7e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,7 @@ +# Info on valid syntax for controlling files in the distribution in this file: +# * https://setuptools.pypa.io/en/latest/userguide/miscellaneous.html +# * https://setuptools.pypa.io/en/latest/userguide/datafiles.html + include CHANGELOG.rst include CONTRIBUTING.rst include LICENSE @@ -6,5 +10,6 @@ include README.rst include readthedocs.yaml include setup.cfg include setup.py +include diffsims/tests/**/*.npy -recursive-include doc Makefile make.bat *.rst *.py *.png +recursive-include doc Makefile make.bat *.rst *.py *.png \ No newline at end of file diff --git a/diffsims/crystallography/__init__.py b/diffsims/crystallography/__init__.py index 736a8e3d..564ac510 100644 --- a/diffsims/crystallography/__init__.py +++ b/diffsims/crystallography/__init__.py @@ -20,8 +20,7 @@ g, hkl) for a crystal structure. """ -from diffsims.crystallography.reciprocal_lattice_point import ( - ReciprocalLatticePoint, +from diffsims.crystallography.get_hkl import ( get_equivalent_hkl, get_highest_hkl, get_hkl, @@ -32,6 +31,5 @@ "get_equivalent_hkl", "get_highest_hkl", "get_hkl", - "ReciprocalLatticePoint", "ReciprocalLatticeVector", ] diff --git a/diffsims/crystallography/get_hkl.py b/diffsims/crystallography/get_hkl.py new file mode 100644 index 00000000..8d54135c --- /dev/null +++ b/diffsims/crystallography/get_hkl.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# Copyright 2017-2023 The diffsims developers +# +# This file is part of diffsims. +# +# diffsims 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. +# +# diffsims 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 diffsims. If not, see . + +from itertools import product + +import numpy as np +from orix.vector import Vector3d + +from diffsims.utils._deprecated import deprecated + + +@deprecated( + since="0.6", + alternative="diffsims.crystallography.ReciprocalLatticeVector.from_min_dspacing", + removal="0.7", +) +def get_highest_hkl(lattice, min_dspacing=0.5): + """Return the highest Miller indices hkl of the plane with a direct + space interplanar spacing (d-spacing) greater than but closest to + *min_dspacing*. + + Parameters + ---------- + lattice : diffpy.structure.Lattice + Crystal lattice. + min_dspacing : float, optional + Smallest interplanar spacing to consider. Default is 0.5 Å. + + Returns + ------- + highest_hkl : np.ndarray + Highest Miller indices. + """ + highest_hkl = np.ones(3, dtype=int) + for i in range(3): + hkl = np.zeros(3) + d = min_dspacing + 1 + while d > min_dspacing: + hkl[i] += 1 + d = 1 / lattice.rnorm(hkl) + highest_hkl[i] = hkl[i] + return highest_hkl + + +@deprecated( + since="0.6", + alternative="diffsims.crystallography.ReciprocalLatticeVector.from_highest_hkl", + removal="0.7", +) +def get_hkl(highest_hkl): + """Return a list of planes from a set of highest Miller indices. + + Parameters + ---------- + highest_hkl : orix.vector.Vector3d, np.ndarray, list, or tuple of int + Highest Miller indices to consider. + + Returns + ------- + hkl : np.ndarray + An array of Miller indices. + """ + index_ranges = [np.arange(-i, i + 1) for i in highest_hkl] + return np.asarray(list(product(*index_ranges))) + + +@deprecated( + since="0.6", + alternative="diffsims.crystallography.ReciprocalLatticeVector.symmetrise", + removal="0.7", +) +def get_equivalent_hkl(hkl, operations, unique=False, return_multiplicity=False): + """Return symmetrically equivalent Miller indices. + + Parameters + ---------- + hkl : orix.vector.Vector3d, np.ndarray, list or tuple of int + Miller indices. + operations : orix.quaternion.symmetry.Symmetry + Point group describing allowed symmetry operations. + unique : bool, optional + Whether to return only unique Miller indices. Default is False. + return_multiplicity : bool, optional + Whether to return the multiplicity of the input indices. Default + is False. + + Returns + ------- + new_hkl : orix.vector.Vector3d + The symmetrically equivalent Miller indices. + multiplicity : np.ndarray + Number of symmetrically equivalent indices. Only returned if + `return_multiplicity` is True. + """ + new_hkl = operations.outer(Vector3d(hkl)) + new_hkl = new_hkl.flatten().reshape(*new_hkl.shape[::-1]) + + multiplicity = None + if unique: + n_families = new_hkl.shape[0] + multiplicity = np.zeros(n_families, dtype=int) + temp_hkl = new_hkl[0].unique().data + multiplicity[0] = temp_hkl.shape[0] + if n_families > 1: + for i, hkl in enumerate(new_hkl[1:]): + temp_hkl2 = hkl.unique() + multiplicity[i + 1] = temp_hkl2.size + temp_hkl = np.append(temp_hkl, temp_hkl2.data, axis=0) + new_hkl = Vector3d(temp_hkl[: multiplicity.sum()]) + + # Remove 1-dimensions + new_hkl = new_hkl.squeeze() + + if unique and return_multiplicity: + return new_hkl, multiplicity + else: + return new_hkl diff --git a/diffsims/crystallography/reciprocal_lattice_point.py b/diffsims/crystallography/reciprocal_lattice_point.py deleted file mode 100644 index c9a9f658..00000000 --- a/diffsims/crystallography/reciprocal_lattice_point.py +++ /dev/null @@ -1,484 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2017-2023 The diffsims developers -# -# This file is part of diffsims. -# -# diffsims 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. -# -# diffsims 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 diffsims. If not, see . - -from collections import defaultdict -from itertools import product -from warnings import warn - -import numpy as np -from orix.vector import Vector3d - -from diffsims.structure_factor.structure_factor import ( - get_kinematical_structure_factor, - get_doyleturner_structure_factor, - get_refraction_corrected_wavelength, -) - -_FLOAT_EPS = np.finfo(float).eps # Used to round values below 1e-16 to zero - - -class ReciprocalLatticePoint: - """*[Deprecated]* Reciprocal lattice point (or crystal plane, - reflector, g, etc.) with Miller indices, length of the reciprocal - lattice vectors and other relevant structure_factor parameters. - - Notes - ----- - Deprecated since version 0.5: Class ``ReciprocalLatticePoint`` is - deprecated and will be removed in version 0.6. Use - :class:`~diffsims.crystallography.ReciprocalLatticeVector` instead. - """ - - def __init__(self, phase, hkl): - """A container for Miller indices, structure factors and related - parameters for crystal planes (reciprocal lattice points, - reflectors, g, etc.). - - Parameters - ---------- - phase : orix.crystal_map.phase_list.Phase - A phase container with a crystal structure and a space and - point group describing the allowed symmetry operations. - hkl : orix.vector.Vector3d, np.ndarray, list, or tuple - Miller indices. - - """ - - warn( - message=( - "Class `ReciprocalLatticePoint` is deprecated and will be removed in " - "version 0.6. Use `ReciprocalLatticeVector` instead." - ), - category=np.VisibleDeprecationWarning, - ) - - self.phase = phase - self._raise_if_no_point_group() - self._hkl = Vector3d(hkl) - self._structure_factor = [None] * self.size - self._theta = [None] * self.size - - def __repr__(self): - return ( - f"{self.__class__.__name__} {self.hkl.shape}\n" - f"Phase: {self.phase.name} ({self.phase.point_group.name})\n" - f"{np.array_str(self.hkl.data, precision=4, suppress_small=True)}" - ) - - def __getitem__(self, key): - new_rlp = self.__class__(self.phase, self.hkl[key]) - if self.structure_factor[0] is None: - new_rlp._structure_factor = [None] * new_rlp.size - else: - new_rlp._structure_factor = self.structure_factor[key] - if self.theta[0] is None: - new_rlp._theta = [None] * new_rlp.size - else: - new_rlp._theta = self.theta[key] - return new_rlp - - @property - def hkl(self): - """Return :class:`~orix.vector.Vector3d` of Miller indices.""" - return Vector3d(self._hkl.data.astype(int)) - - @property - def h(self): - """Return :class:`np.ndarray` of Miller index h.""" - return self.hkl.data[..., 0] - - @property - def k(self): - """Return :class:`np.ndarray` of Miller index k.""" - return self.hkl.data[..., 1] - - @property - def l(self): - """Return :class:`np.ndarray` of Miller index l.""" - return self.hkl.data[..., 2] - - @property - def size(self): - """Return `int`.""" - return self.hkl.size - - @property - def shape(self): - """Return `tuple`.""" - return self.hkl.data.shape - - @property - def multiplicity(self): - """Return either `int` or :class:`np.ndarray` of `int`.""" - return self.symmetrise(antipodal=True, return_multiplicity=True)[1] - - @property - def gspacing(self): - """Return :class:`np.ndarray` of reciprocal lattice point - spacings. - """ - return self.phase.structure.lattice.rnorm(self.hkl.data) - - @property - def dspacing(self): - """Return :class:`np.ndarray` of direct lattice interplanar - spacings. - """ - return 1 / self.gspacing - - @property - def scattering_parameter(self): - """Return :class:`np.ndarray` of scattering parameters s.""" - return 0.5 * self.gspacing - - @property - def structure_factor(self): - """Return :class:`np.ndarray` of structure factors F or None.""" - return self._structure_factor - - @property - def allowed(self): - """Return whether planes diffract according to structure_factor - selection rules assuming kinematical scattering theory. - """ - self._raise_if_no_space_group() - - # Translational symmetry - centering = self.phase.space_group.short_name[0] - - if centering == "P": # Primitive - if self.phase.space_group.crystal_system == "HEXAGONAL": - # TODO: See rules in e.g. - # https://mcl1.ncifcrf.gov/dauter_pubs/284.pdf, Table 4 - # http://xrayweb.chem.ou.edu/notes/symmetry.html, Systematic Absences - raise NotImplementedError - else: # Any hkl - return np.ones(self.size, dtype=bool) - elif centering == "F": # Face-centred, hkl all odd/even - selection = np.sum(np.mod(self.hkl.data, 2), axis=1) - return np.array([i not in [1, 2] for i in selection], dtype=bool) - elif centering == "I": # Body-centred, h + k + l = 2n (even) - return np.mod(np.sum(self.hkl.data, axis=1), 2) == 0 - elif centering == "A": # Centred on A faces only - return np.mod(self.k + self.l, 2) == 0 - elif centering == "B": # Centred on B faces only - return np.mod(self.h + self.l, 2) == 0 - elif centering == "C": # Centred on C faces only - return np.mod(self.h + self.k, 2) == 0 - elif centering in ["R", "H"]: # Rhombohedral - return np.mod(-self.h + self.k + self.l, 3) == 0 - - @property - def theta(self): - """Return :class:`np.ndarray` of twice the Bragg angle.""" - return self._theta - - @classmethod - def from_min_dspacing(cls, phase, min_dspacing=0.5): - """Create a CrystalPlane object populated by unique Miller indices - with a direct space interplanar spacing greater than a lower - threshold. - - Parameters - ---------- - phase : orix.crystal_map.phase_list.Phase - A phase container with a crystal structure and a space and - point group describing the allowed symmetry operations. - min_dspacing : float, optional - Smallest interplanar spacing to consider. Default is 0.5 Å. - """ - highest_hkl = get_highest_hkl( - lattice=phase.structure.lattice, min_dspacing=min_dspacing - ) - hkl = get_hkl(highest_hkl=highest_hkl) - return cls(phase=phase, hkl=hkl).unique() - - @classmethod - def from_highest_hkl(cls, phase, highest_hkl): - """Create a CrystalPlane object populated by unique Miller indices - below, but including, a set of higher indices. - - Parameters - ---------- - phase : orix.crystal_map.phase_list.Phase - A phase container with a crystal structure and a space and - point group describing the allowed symmetry operations. - highest_hkl : np.ndarray, list, or tuple of int - Highest Miller indices to consider (including). - """ - hkl = get_hkl(highest_hkl=highest_hkl) - return cls(phase=phase, hkl=hkl).unique() - - def unique(self, use_symmetry=True): - """Return planes with unique Miller indices. - - Parameters - ---------- - use_symmetry : bool, optional - Whether to use symmetry to remove the planes with indices - symmetrically equivalent to another set of indices. - - Returns - ------- - ReciprocalLatticePoint - """ - if use_symmetry: - all_hkl = self.hkl.data - # Remove [0, 0, 0] points - all_hkl = all_hkl[~np.all(np.isclose(all_hkl, 0), axis=1)] - - families = defaultdict(list) - for this_hkl in all_hkl.tolist(): - for that_hkl in families.keys(): - if _is_equivalent(this_hkl, that_hkl): - families[tuple(that_hkl)].append(this_hkl) - break - else: - families[tuple(this_hkl)].append(this_hkl) - - n_families = len(families) - unique_hkl = np.zeros((n_families, 3)) - for i, all_hkl_in_family in enumerate(families.values()): - unique_hkl[i] = sorted(all_hkl_in_family)[-1] - else: - unique_hkl = self.hkl.unique() - # TODO: Enable inheriting classes pass on their properties in this new object - return self.__class__(phase=self.phase, hkl=unique_hkl) - - def symmetrise( - self, - antipodal=True, - unique=True, - return_multiplicity=False, - ): - """Return planes with symmetrically equivalent Miller indices. - - Parameters - ---------- - antipodal : bool, optional - Whether to include antipodal symmetry operations. Default is - True. - unique : bool, optional - Whether to return only distinct indices. Default is True. - If True, zero-entries, which are assumed to be degenerate, are - removed. - return_multiplicity : bool, optional - Whether to return the multiplicity of indices. This option is - only available if `unique` is True. Default is False. - - Returns - ------- - ReciprocalLatticePoint - Planes with Miller indices symmetrically equivalent to the - original planes. - multiplicity : np.ndarray - Multiplicity of the original Miller indices. Only returned if - `return_multiplicity` is True. - - Notes - ----- - Should be the same as EMsoft's CalcFamily in their symmetry.f90 - module, although not entirely sure. Use with care. - """ - # Get symmetry operations - pg = self.phase.point_group - operations = pg if antipodal else pg[~pg.improper] - - out = get_equivalent_hkl( - hkl=self.hkl, - operations=operations, - unique=unique, - return_multiplicity=return_multiplicity, - ) - - # TODO: Enable inheriting classes pass on their properties in this new object - # Format output and return - if unique and return_multiplicity: - multiplicity = out[1] - if multiplicity.size == 1: - multiplicity = multiplicity[0] - return self.__class__(phase=self.phase, hkl=out[0]), multiplicity - else: - return self.__class__(phase=self.phase, hkl=out) - - def calculate_structure_factor(self, method=None, voltage=None): - """Populate `self.structure_factor` with the structure factor F - for each plane. - - Parameters - ---------- - method : str, optional - Either "kinematical" for kinematical X-ray structure factors - or "doyleturner" for structure factors using Doyle-Turner - atomic scattering factors. If None (default), kinematical - structure factors are calculated. - voltage : float, optional - Beam energy in V used when `method=doyleturner`. - """ - if method is None: - method = "kinematical" - methods = ["kinematical", "doyleturner"] - if method not in methods: - raise ValueError(f"method={method} must be among {methods}") - elif method == "doyleturner" and voltage is None: - raise ValueError( - "'voltage' parameter must be set when method='doyleturner'" - ) - - # TODO: Find a better way to call different methods in the loop - structure_factors = np.zeros(self.size) - for i, (hkl, s) in enumerate(zip(self.hkl.data, self.scattering_parameter)): - if method == "kinematical": - structure_factors[i] = get_kinematical_structure_factor( - phase=self.phase, - hkl=hkl, - scattering_parameter=s, - ) - else: - structure_factors[i] = get_doyleturner_structure_factor( - phase=self.phase, - hkl=hkl, - scattering_parameter=s, - voltage=voltage, - ) - self._structure_factor = np.where( - structure_factors < _FLOAT_EPS, 0, structure_factors - ) - - def calculate_theta(self, voltage): - """Populate `self.theta` with the Bragg angle :math:`theta_B` for - each plane. - - Parameters - ---------- - voltage : float - Beam energy in V. - """ - wavelength = get_refraction_corrected_wavelength(self.phase, voltage) - self._theta = np.arcsin(0.5 * wavelength * self.gspacing) - - def _raise_if_no_point_group(self): - """Raise ValueError if the phase attribute has no point group - set. - """ - if self.phase.point_group is None: - raise ValueError(f"The phase {self.phase} must have a point group set") - - def _raise_if_no_space_group(self): - """Raise ValueError if the phase attribute has no space group - set. - """ - if self.phase.space_group is None: - raise ValueError(f"The phase {self.phase} must have a space group set") - - -def get_highest_hkl(lattice, min_dspacing=0.5): - """Return the highest Miller indices hkl of the plane with a direct - space interplanar spacing greater than but closest to a lower - threshold. - - Parameters - ---------- - lattice : diffpy.structure.Lattice - Crystal lattice. - min_dspacing : float, optional - Smallest interplanar spacing to consider. Default is 0.5 Å. - - Returns - ------- - highest_hkl : np.ndarray - Highest Miller indices. - """ - highest_hkl = np.ones(3, dtype=int) - for i in range(3): - hkl = np.zeros(3) - d = min_dspacing + 1 - while d > min_dspacing: - hkl[i] += 1 - d = 1 / lattice.rnorm(hkl) - highest_hkl[i] = hkl[i] - return highest_hkl - - -def get_hkl(highest_hkl): - """Return a list of planes from a set of highest Miller indices. - - Parameters - ---------- - highest_hkl : orix.vector.Vector3d, np.ndarray, list, or tuple of int - Highest Miller indices to consider. - - Returns - ------- - hkl : np.ndarray - An array of Miller indices. - """ - index_ranges = [np.arange(-i, i + 1) for i in highest_hkl] - return np.asarray(list(product(*index_ranges))) - - -def get_equivalent_hkl(hkl, operations, unique=False, return_multiplicity=False): - """Return symmetrically equivalent Miller indices. - - Parameters - ---------- - hkl : orix.vector.Vector3d, np.ndarray, list or tuple of int - Miller indices. - operations : orix.quaternion.symmetry.Symmetry - Point group describing allowed symmetry operations. - unique : bool, optional - Whether to return only unique Miller indices. Default is False. - return_multiplicity : bool, optional - Whether to return the multiplicity of the input indices. Default - is False. - - Returns - ------- - new_hkl : orix.vector.Vector3d - The symmetrically equivalent Miller indices. - multiplicity : np.ndarray - Number of symmetrically equivalent indices. Only returned if - `return_multiplicity` is True. - """ - new_hkl = operations.outer(Vector3d(hkl)) - new_hkl = new_hkl.flatten().reshape(*new_hkl.shape[::-1]) - - multiplicity = None - if unique: - n_families = new_hkl.shape[0] - multiplicity = np.zeros(n_families, dtype=int) - temp_hkl = new_hkl[0].unique().data - multiplicity[0] = temp_hkl.shape[0] - if n_families > 1: - for i, hkl in enumerate(new_hkl[1:]): - temp_hkl2 = hkl.unique() - multiplicity[i + 1] = temp_hkl2.size - temp_hkl = np.append(temp_hkl, temp_hkl2.data, axis=0) - new_hkl = Vector3d(temp_hkl[: multiplicity.sum()]) - - # Remove 1-dimensions - new_hkl = new_hkl.squeeze() - - if unique and return_multiplicity: - return new_hkl, multiplicity - else: - return new_hkl - - -def _is_equivalent(this_hkl: list, that_hkl: list) -> bool: - return sorted(np.abs(this_hkl)) == sorted(np.abs(that_hkl)) diff --git a/diffsims/tests/crystallography/test_get_hkl.py b/diffsims/tests/crystallography/test_get_hkl.py new file mode 100644 index 00000000..8021af28 --- /dev/null +++ b/diffsims/tests/crystallography/test_get_hkl.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# Copyright 2017-2023 The diffsims developers +# +# This file is part of diffsims. +# +# diffsims 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. +# +# diffsims 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 diffsims. If not, see . + +import numpy as np +from orix.crystal_map import Phase +from orix.vector import Vector3d +import pytest + +from diffsims.crystallography import ( + get_equivalent_hkl, + get_highest_hkl, + get_hkl, + ReciprocalLatticeVector, +) + + +class TestGetHKL: + @pytest.mark.parametrize("highest_hkl, n", [([1, 2, 3], 105), ([1, 1, 1], 27)]) + def test_get_hkl(self, highest_hkl, n): + highest_hkl = np.array(highest_hkl) + with pytest.warns(np.VisibleDeprecationWarning): + hkl = get_hkl(highest_hkl) + assert np.allclose(hkl.max(axis=0), highest_hkl) + assert np.allclose(hkl.min(axis=0), -highest_hkl) + assert hkl.shape[0] == n + assert np.allclose(abs(hkl).min(axis=0), [0, 0, 0]) # Contains zero-vector + + # Slightly different implementation (from orix) + phase = Phase(point_group="1") + g = ReciprocalLatticeVector.from_highest_hkl(phase, highest_hkl) + assert np.allclose(g.data.max(axis=0), highest_hkl) + assert np.allclose(g.data.min(axis=0), -highest_hkl) + assert g.size == n - 1 + assert np.allclose(abs(g.data).min(axis=0), [0, 0, 0]) + + @pytest.mark.parametrize( + "d, hkl", [(0.5, [6, 6, 21]), (1, [3, 3, 11]), (1.5, [2, 2, 7])] + ) + def test_get_highest_hkl(self, silicon_carbide_phase, d, hkl): + with pytest.warns(np.VisibleDeprecationWarning): + hkl_out = get_highest_hkl( + silicon_carbide_phase.structure.lattice, min_dspacing=d + ) + assert np.allclose(hkl_out, hkl) + + # Slightly different implementation (from orix) + g = ReciprocalLatticeVector.from_min_dspacing(silicon_carbide_phase, d) + assert np.allclose(g.hkl.max(axis=0) + 1, hkl_out) + + def test_get_equivalent_hkl(self): + phase_225 = Phase(space_group=225) + with pytest.warns(np.VisibleDeprecationWarning): + hkl1 = get_equivalent_hkl( + [1, 1, 1], operations=phase_225.point_group, unique=True + ) + # fmt: off + assert np.allclose( + hkl1.data, + [ + [ 1, 1, 1], + [-1, 1, 1], + [-1, -1, 1], + [ 1, -1, 1], + [ 1, -1, -1], + [ 1, 1, -1], + [-1, 1, -1], + [-1, -1, -1], + ] + ) + # fmt: on + g1 = ReciprocalLatticeVector(phase_225, hkl=[1, 1, 1]) + assert np.allclose(g1.symmetrise().hkl, hkl1.data) + + with pytest.warns(np.VisibleDeprecationWarning): + hkl2 = get_equivalent_hkl([1, 1, 1], operations=phase_225.point_group) + assert hkl2.shape[0] == g1.symmetrise().size * 6 == 48 + + phase_186 = Phase(space_group=186) + with pytest.warns(np.VisibleDeprecationWarning): + hkl3, mult3 = get_equivalent_hkl( + [2, 2, 0], + operations=phase_186.point_group, + return_multiplicity=True, + unique=True, + ) + g3 = ReciprocalLatticeVector(phase_186, hkl=[2, 2, 0]) + assert mult3 == g3.symmetrise().size == 12 + assert np.allclose(hkl3.data[:2], [[2, 2, 0], [-2.7321, 0.7321, 0]], atol=1e-4) + + with pytest.warns(np.VisibleDeprecationWarning): + hkl4, mult4 = get_equivalent_hkl( + Vector3d([[2, 2, 0], [4, 0, 0]]), + phase_186.point_group, + unique=True, + return_multiplicity=True, + ) + g4 = ReciprocalLatticeVector(phase_186, hkl=[[2, 2, 0], [4, 0, 0]]) + g4_sym, mult4_2 = g4.symmetrise(return_multiplicity=True) + assert np.allclose(mult4, [12, 6]) + assert np.allclose(mult4_2, [12, 6]) + assert hkl4.shape[0] == g4_sym.size == 18 diff --git a/diffsims/tests/crystallography/test_reciprocal_lattice_point.py b/diffsims/tests/crystallography/test_reciprocal_lattice_point.py deleted file mode 100644 index d8c82fdb..00000000 --- a/diffsims/tests/crystallography/test_reciprocal_lattice_point.py +++ /dev/null @@ -1,329 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2017-2023 The diffsims developers -# -# This file is part of diffsims. -# -# diffsims 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. -# -# diffsims 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 diffsims. If not, see . - -import numpy as np -from orix.crystal_map import Phase -from orix.vector import Vector3d -import pytest - -from diffsims.crystallography import ReciprocalLatticePoint - - -class TestReciprocalLatticePoint: - @pytest.mark.parametrize( - "hkl", [[[1, 1, 1], [2, 0, 0]], np.array([[1, 1, 1], [2, 0, 0]])] - ) - def test_init_rlp(self, nickel_phase, hkl): - with pytest.warns(np.VisibleDeprecationWarning): - rlp = ReciprocalLatticePoint(phase=nickel_phase, hkl=hkl) - assert rlp.phase.name == nickel_phase.name - assert isinstance(rlp.hkl, Vector3d) - assert rlp.structure_factor[0] is None - assert rlp.theta[0] is None - assert rlp.size == 2 - assert rlp.shape == (2, 3) - assert rlp.hkl[0].shape == (1,) - assert rlp.hkl.data[0].shape == (3,) - assert np.issubdtype(rlp.hkl.data.dtype, int) - - @pytest.mark.parametrize("min_dspacing, desired_size", [(2, 9), (1, 19), (0.5, 83)]) - def test_init_from_min_dspacing(self, ferrite_phase, min_dspacing, desired_size): - with pytest.warns(np.VisibleDeprecationWarning): - assert ( - ReciprocalLatticePoint.from_min_dspacing( - phase=ferrite_phase, min_dspacing=min_dspacing - ).size - == desired_size - ) - - @pytest.mark.parametrize( - "highest_hkl, desired_highest_hkl, desired_lowest_hkl, desired_size", - [ - ([3, 3, 3], [3, 3, 3], [1, 0, 0], 19), - ([3, 4, 0], [3, 4, 0], [0, 4, 0], 13), - ([4, 3, 0], [4, 3, 0], [1, 0, 0], 13), - ], - ) - def test_init_from_highest_hkl( - self, - silicon_carbide_phase, - highest_hkl, - desired_highest_hkl, - desired_lowest_hkl, - desired_size, - ): - with pytest.warns(np.VisibleDeprecationWarning): - rlp = ReciprocalLatticePoint.from_highest_hkl( - phase=silicon_carbide_phase, highest_hkl=highest_hkl - ) - assert np.allclose(rlp[0].hkl.data, desired_highest_hkl) - assert np.allclose(rlp[-1].hkl.data, desired_lowest_hkl) - assert rlp.size == desired_size - - def test_repr(self, ferrite_phase): - with pytest.warns(np.VisibleDeprecationWarning): - rlp = ReciprocalLatticePoint.from_min_dspacing( - ferrite_phase, min_dspacing=2 - ) - assert repr(rlp) == ( - f"ReciprocalLatticePoint (9,)\n" - f"Phase: ferrite (m-3m)\n" - "[[2 2 2]\n [2 2 1]\n [2 2 0]\n [2 1 1]\n [2 1 0]\n [2 0 0]\n [1 1 1]\n" - " [1 1 0]\n [1 0 0]]" - ) - - def test_get_item(self, ferrite_phase): - with pytest.warns(np.VisibleDeprecationWarning): - rlp = ReciprocalLatticePoint.from_min_dspacing( - phase=ferrite_phase, min_dspacing=1.5 - ) - rlp.calculate_structure_factor() - rlp.calculate_theta(voltage=20e3) - - assert rlp[0].size == 1 - assert rlp[:2].size == 2 - assert np.allclose(rlp[5:7].hkl.data, rlp.hkl[5:7].data) - - assert np.allclose(rlp[10:13].structure_factor, rlp.structure_factor[10:13]) - assert np.allclose(rlp[20:23].theta, rlp.theta[20:23]) - - assert rlp.phase.space_group.number == rlp[0].phase.space_group.number - assert rlp.phase.point_group.name == rlp[10:15].phase.point_group.name - assert np.allclose( - rlp.phase.structure.lattice.abcABG(), - rlp[20:23].phase.structure.lattice.abcABG(), - ) - - def test_get_hkl(self, silicon_carbide_phase): - with pytest.warns(np.VisibleDeprecationWarning): - rlp = ReciprocalLatticePoint.from_min_dspacing( - silicon_carbide_phase, min_dspacing=3 - ) - assert np.allclose(rlp.h, [1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0]) - assert np.allclose(rlp.k, [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]) - assert np.allclose(rlp.l, [4, 3, 2, 1, 0, 4, 3, 2, 0, 4, 3, 2]) - - def test_multiplicity(self, nickel_phase, silicon_carbide_phase): - with pytest.warns(np.VisibleDeprecationWarning): - assert np.allclose( - ReciprocalLatticePoint.from_min_dspacing( - phase=nickel_phase, min_dspacing=1 - ).multiplicity, - # fmt: off - np.array([ - 8, 24, 24, 24, 12, 24, 48, 48, 24, 24, 48, 24, 24, 24, 6, 8, 24, - 24, 12, 24, 48, 24, 24, 24, 6, 8, 24, 12, 24, 24, 6, 8, 12, 6 - ]) - # fmt: on - ) - with pytest.warns(np.VisibleDeprecationWarning): - assert np.allclose( - ReciprocalLatticePoint.from_min_dspacing( - phase=silicon_carbide_phase, min_dspacing=1 - ).multiplicity, - # fmt: off - np.array([ - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 6, - 6, 6, 6, 6, 6, 6, 6, 6, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 6, 6, 6, 6, 6, 6, 6, 6, 6, - 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 6, 6, 6, 6, 6, 6, 6, 6, 6, - 1, 1, 1, 1, 1, 1, 1, 1 - ]) - # fmt: on - ) - - def test_gspacing_dspacing_scattering_parameter(self, ferrite_phase): - with pytest.warns(np.VisibleDeprecationWarning): - rlp = ReciprocalLatticePoint.from_min_dspacing( - phase=ferrite_phase, min_dspacing=2 - ) - # fmt: off - assert np.allclose( - rlp.gspacing, - np.array([ - 1.2084778, 1.04657248, 0.98671799, 0.85452285, 0.78006907, 0.69771498, - 0.604238, 0.493359, 0.34885749 - ]) - ) - assert np.allclose( - rlp.dspacing, - np.array([ - 0.82748727, 0.9555, 1.01346079, 1.17024372, 1.28193777, 1.43325, - 1.65497455, 2.02692159, 2.8665 - ]) - ) - assert np.allclose( - rlp.scattering_parameter, - np.array([ - 0.6042389, 0.52328624, 0.493359, 0.42726142, 0.39003453, 0.34885749, - 0.30211945, 0.2466795, 0.17442875 - ]) - ) - # fmt: on - - @pytest.mark.parametrize( - "space_group, hkl, centering, desired_allowed", - [ - (224, [[1, 1, -1], [-2, 2, 2], [3, 1, 0]], "P", [True, True, True]), - (230, [[1, 1, -1], [-2, 2, 2], [3, 1, 0]], "I", [False, True, True]), - (225, [[1, 1, 1], [2, 2, 1], [-3, 3, 3]], "F", [True, False, True]), - (167, [[1, 2, 3], [2, 2, 3], [-3, 2, 4]], "H", [False, True, True]), # R - (68, [[1, 2, 3], [2, 2, 3], [-2, 2, 4]], "C", [False, True, True]), - (41, [[1, 2, 2], [1, 1, -1], [1, 1, 2]], "A", [True, True, False]), - ], - ) - def test_allowed(self, space_group, hkl, centering, desired_allowed): - with pytest.warns(np.VisibleDeprecationWarning): - rlp = ReciprocalLatticePoint(phase=Phase(space_group=space_group), hkl=hkl) - assert rlp.phase.space_group.short_name[0] == centering - assert np.allclose(rlp.allowed, desired_allowed) - - def test_allowed_b_centering(self): - """B centering will never fire since no diffpy.structure space group has - 'B' first in the name. - """ - phase = Phase(space_group=15) - phase.space_group.short_name = "B" - with pytest.warns(np.VisibleDeprecationWarning): - rlp = ReciprocalLatticePoint( - phase=phase, hkl=[[1, 2, 2], [1, 1, -1], [1, 1, 2]] - ) - assert np.allclose(rlp.allowed, [False, True, False]) - - def test_allowed_raises(self, silicon_carbide_phase): - with pytest.raises(NotImplementedError): - with pytest.warns(np.VisibleDeprecationWarning): - _ = ReciprocalLatticePoint.from_min_dspacing( - phase=silicon_carbide_phase, min_dspacing=1 - ).allowed - - def test_unique(self, ferrite_phase): - hkl = [[-1, -1, -1], [1, 1, 1], [1, 0, 0], [0, 0, 1]] - with pytest.warns(np.VisibleDeprecationWarning): - rlp = ReciprocalLatticePoint(phase=ferrite_phase, hkl=hkl) - assert isinstance(rlp.unique(), ReciprocalLatticePoint) - assert np.allclose(rlp.unique(use_symmetry=False).hkl.data, hkl) - assert np.allclose(rlp.unique().hkl.data, [[1, 1, 1], [1, 0, 0]]) - - def test_symmetrise(self): - with pytest.warns(np.VisibleDeprecationWarning): - assert np.allclose( - ReciprocalLatticePoint(phase=Phase(space_group=225), hkl=[1, 1, 1]) - .symmetrise() - .hkl.data, - np.array( - [ - [1, 1, 1], - [-1, 1, 1], - [-1, -1, 1], - [1, -1, 1], - [1, -1, -1], - [1, 1, -1], - [-1, 1, -1], - [-1, -1, -1], - ] - ), - ) - - with pytest.warns(np.VisibleDeprecationWarning): - rlp2, multiplicity = ReciprocalLatticePoint( - phase=Phase(space_group=186), hkl=[2, 2, 0] - ).symmetrise(return_multiplicity=True) - assert multiplicity == 12 - assert np.allclose( - rlp2.hkl.data, - [ - [2, 2, 0], - [-2, 0, 0], - [0, -2, 0], - [-2, -2, 0], - [2, 0, 0], - [0, 2, 0], - [-2, 2, 0], - [0, -2, 0], - [2, 0, 0], - [2, -2, 0], - [0, 2, 0], - [-2, 0, 0], - ], - ) - - with pytest.warns(np.VisibleDeprecationWarning): - rlp3 = ReciprocalLatticePoint( - phase=Phase(space_group=186), hkl=[2, 2, 0] - ).symmetrise(antipodal=False) - assert np.allclose( - rlp3.hkl.data, - [[2, 2, 0], [-2, 0, 0], [0, -2, 0], [-2, -2, 0], [2, 0, 0], [0, 2, 0]], - ) - - @pytest.mark.parametrize( - "method, voltage, hkl, desired_factor", - [ - ("kinematical", None, [1, 1, 0], 35.783295), - (None, None, [1, 1, 0], 35.783295), - ("doyleturner", 20e3, [[2, 0, 0], [1, 1, 0]], [5.581302, 8.096651]), - ], - ) - def test_calculate_structure_factor( - self, ferrite_phase, method, voltage, hkl, desired_factor - ): - with pytest.warns(np.VisibleDeprecationWarning): - rlp = ReciprocalLatticePoint(phase=ferrite_phase, hkl=hkl) - rlp.calculate_structure_factor(method=method, voltage=voltage) - assert np.allclose(rlp.structure_factor, desired_factor) - - def test_calculate_structure_factor_raises(self, ferrite_phase): - with pytest.warns(np.VisibleDeprecationWarning): - rlp = ReciprocalLatticePoint(phase=ferrite_phase, hkl=[1, 0, 0]) - with pytest.raises(ValueError, match="method=man must be among"): - rlp.calculate_structure_factor(method="man") - with pytest.raises(ValueError, match="'voltage' parameter must be set when"): - rlp.calculate_structure_factor(method="doyleturner") - - @pytest.mark.parametrize( - "voltage, hkl, desired_theta", - [(20e3, [1, 1, 1], 0.00259284), (200e3, [2, 0, 0], 0.00087484)], - ) - def test_calculate_theta(self, ferrite_phase, voltage, hkl, desired_theta): - with pytest.warns(np.VisibleDeprecationWarning): - rlp = ReciprocalLatticePoint(phase=ferrite_phase, hkl=hkl) - rlp.calculate_theta(voltage=voltage) - assert np.allclose(rlp.theta, desired_theta) - - def test_one_point(self, ferrite_phase): - with pytest.warns(np.VisibleDeprecationWarning): - rlp = ReciprocalLatticePoint(phase=ferrite_phase, hkl=[1, 1, 0]) - - assert rlp.size == 1 - assert np.allclose(rlp.allowed, True) - - def test_init_without_point_group_raises(self): - phase = Phase() - with pytest.raises(ValueError, match=f"The phase {phase} must have a"): - with pytest.warns(np.VisibleDeprecationWarning): - _ = ReciprocalLatticePoint(phase=phase, hkl=[1, 1, 1]) - - def test_get_allowed_without_space_group_raises(self): - phase = Phase(point_group="432") - with pytest.warns(np.VisibleDeprecationWarning): - rlp = ReciprocalLatticePoint(phase=phase, hkl=[1, 1, 1]) - - with pytest.raises(ValueError, match=f"The phase {phase} must have a"): - _ = rlp.allowed diff --git a/doc/user/installation.rst b/doc/user/installation.rst index 2f170779..32caa725 100644 --- a/doc/user/installation.rst +++ b/doc/user/installation.rst @@ -4,7 +4,8 @@ Installation diffsims can be installed from `Anaconda `_, the `Python Package Index `_ (``pip``), or from source, -and supports Python >= 3.6. +and supports Python >= 3.8. + With pip ======== @@ -22,6 +23,7 @@ To install a specific version of diffsims (say version 0.5.1):: pip install diffsims==0.5.1 + With Anaconda ============= @@ -48,6 +50,7 @@ To install a specific version of diffsims (say version 0.5.1):: conda install diffsims==0.5.1 -c conda-forge + .. _install-from-source: From source diff --git a/setup.cfg b/setup.cfg index b804aa93..304edb0a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,15 +18,15 @@ relative_files = True [coverage:report] precision = 2 +# https://github.com/vidartf/manifix +# https://github.com/vidartf/globmatch [manifix] known_excludes = + doc/build/** + htmlcov/** .* .*/** **/*.nbi **/*.nbc **/*.pyc - .git/** - doc/build/** - htmlcov/** *.code-workspace - **/*.npy \ No newline at end of file