From 1f3f4b1f91551b7846bb36212a60514d15df886b Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Fri, 20 Oct 2023 11:39:39 -0500 Subject: [PATCH 01/48] New Feature: Add Deprecations --- .../reciprocal_lattice_point.py | 1 - diffsims/tests/utils/test_deprecation.py | 115 ++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 diffsims/tests/utils/test_deprecation.py diff --git a/diffsims/crystallography/reciprocal_lattice_point.py b/diffsims/crystallography/reciprocal_lattice_point.py index 656b5b72..c9a9f658 100644 --- a/diffsims/crystallography/reciprocal_lattice_point.py +++ b/diffsims/crystallography/reciprocal_lattice_point.py @@ -29,7 +29,6 @@ get_refraction_corrected_wavelength, ) - _FLOAT_EPS = np.finfo(float).eps # Used to round values below 1e-16 to zero diff --git a/diffsims/tests/utils/test_deprecation.py b/diffsims/tests/utils/test_deprecation.py new file mode 100644 index 00000000..25f7d5ca --- /dev/null +++ b/diffsims/tests/utils/test_deprecation.py @@ -0,0 +1,115 @@ +import warnings + +import numpy as np +import pytest + +from diffsims.utils._deprecated import deprecated, deprecated_argument + + +class TestDeprecationWarning: + def test_deprecation_since(self): + """Ensure functions decorated with the custom deprecated + decorator returns desired output, raises a desired warning, and + gets the desired additions to their docstring. + """ + + @deprecated(since=0.7, alternative="bar", removal=0.8) + def foo(n): + """Some docstring.""" + return n + 1 + + with pytest.warns(np.VisibleDeprecationWarning) as record: + assert foo(4) == 5 + desired_msg = ( + "Function `foo()` is deprecated and will be removed in version 0.8. Use " + "`bar()` instead." + ) + assert str(record[0].message) == desired_msg + assert foo.__doc__ == ( + "[*Deprecated*] Some docstring.\n\n" + "Notes\n-----\n" + ".. deprecated:: 0.7\n" + f" {desired_msg}" + ) + + @deprecated(since=1.9) + def foo2(n): + """Another docstring. + Notes + ----- + Some existing notes. + """ + return n + 2 + + with pytest.warns(np.VisibleDeprecationWarning) as record2: + assert foo2(4) == 6 + desired_msg2 = "Function `foo2()` is deprecated." + assert str(record2[0].message) == desired_msg2 + assert foo2.__doc__ == ( + "[*Deprecated*] Another docstring." + "\nNotes\n-----\n" + "Some existing notes.\n\n" + ".. deprecated:: 1.9\n" + f" {desired_msg2}" + ) + + def test_deprecation_no_old_doc(self): + @deprecated(since=0.7, alternative="bar", removal=0.8) + def foo(n): + return n + 1 + + with pytest.warns(np.VisibleDeprecationWarning) as record: + assert foo(4) == 5 + desired_msg = ( + "Function `foo()` is deprecated and will be removed in version 0.8. Use " + "`bar()` instead." + ) + assert str(record[0].message) == desired_msg + assert foo.__doc__ == ( + "[*Deprecated*] \n" + "\nNotes\n-----\n" + ".. deprecated:: 0.7\n" + f" {desired_msg}" + ) + + +class TestDeprecateArgument: + def test_deprecate_argument(self): + """Functions decorated with the custom `deprecated_argument` + decorator returns desired output and raises a desired warning + only if the argument is passed. + """ + + class Foo: + @deprecated_argument(name="a", since="1.3", removal="1.4") + def bar_arg(self, **kwargs): + return kwargs + + @deprecated_argument(name="a", since="1.3", removal="1.4", alternative="b") + def bar_arg_alt(self, **kwargs): + return kwargs + + my_foo = Foo() + + # Does not warn + with warnings.catch_warnings(): + warnings.simplefilter("error") + assert my_foo.bar_arg(b=1) == {"b": 1} + + # Warns + with pytest.warns(np.VisibleDeprecationWarning) as record2: + assert my_foo.bar_arg(a=2) == {"a": 2} + assert str(record2[0].message) == ( + r"Argument `a` is deprecated and will be removed in version 1.4. " + r"To avoid this warning, please do not use `a`. See the documentation of " + r"`bar_arg()` for more details." + ) + + # Warns with alternative + with pytest.warns(np.VisibleDeprecationWarning) as record3: + assert my_foo.bar_arg_alt(a=3) == {"b": 3} + assert str(record3[0].message) == ( + r"Argument `a` is deprecated and will be removed in version 1.4. " + r"To avoid this warning, please do not use `a`. Use `b` instead. See the " + r"documentation of `bar_arg_alt()` for more details." + ) From 1a9a43940a0daf4a4f8a95a486233c369ad2ccbe Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Fri, 20 Oct 2023 11:40:18 -0500 Subject: [PATCH 02/48] New Feature: Add deprecation wrappers --- diffsims/utils/_deprecated.py | 154 ++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 diffsims/utils/_deprecated.py diff --git a/diffsims/utils/_deprecated.py b/diffsims/utils/_deprecated.py new file mode 100644 index 00000000..a4f1bb18 --- /dev/null +++ b/diffsims/utils/_deprecated.py @@ -0,0 +1,154 @@ +# -*- 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 . + + +"""Helper functions and classes for managing diffsims. +This module and documentation is only relevant for diffsims developers, +not for users. +.. warning: + This module and its submodules are for internal use only. Do not + use them in your own code. We may change the API at any time with no + warning. +""" + +import functools +import inspect +from typing import Callable, Optional, Union +import warnings + +import numpy as np + + +class deprecated: + """Decorator to mark deprecated functions with an informative + warning. + Adapted from + `scikit-image + `_ + and `matplotlib + `_. + """ + + def __init__( + self, + since: Union[str, int, float], + alternative: Optional[str] = None, + alternative_is_function: bool = True, + removal: Union[str, int, float, None] = None, + ): + """Visible deprecation warning. + Parameters + ---------- + since + The release at which this API became deprecated. + alternative + An alternative API that the user may use in place of the + deprecated API. + alternative_is_function + Whether the alternative is a function. Default is ``True``. + removal + The expected removal version. + """ + self.since = since + self.alternative = alternative + self.alternative_is_function = alternative_is_function + self.removal = removal + + def __call__(self, func: Callable): + # Wrap function to raise warning when called, and add warning to + # docstring + if self.alternative is not None: + if self.alternative_is_function: + alt_msg = f" Use `{self.alternative}()` instead." + else: + alt_msg = f" Use `{self.alternative}` instead." + else: + alt_msg = "" + if self.removal is not None: + rm_msg = f" and will be removed in version {self.removal}" + else: + rm_msg = "" + msg = f"Function `{func.__name__}()` is deprecated{rm_msg}.{alt_msg}" + + @functools.wraps(func) + def wrapped(*args, **kwargs): + warnings.simplefilter( + action="always", category=np.VisibleDeprecationWarning, append=True + ) + func_code = func.__code__ + warnings.warn_explicit( + message=msg, + category=np.VisibleDeprecationWarning, + filename=func_code.co_filename, + lineno=func_code.co_firstlineno + 1, + ) + return func(*args, **kwargs) + + # Modify docstring to display deprecation warning + old_doc = inspect.cleandoc(func.__doc__ or "").strip("\n") + notes_header = "\nNotes\n-----" + new_doc = ( + f"[*Deprecated*] {old_doc}\n" + f"{notes_header if notes_header not in old_doc else ''}\n" + f".. deprecated:: {self.since}\n" + f" {msg.strip()}" # Matplotlib uses three spaces + ) + wrapped.__doc__ = new_doc + + return wrapped + + +class deprecated_argument: + """Decorator to remove an argument from a function or method's + signature. + Adapted from `scikit-image + `_. + """ + + def __init__(self, name, since, removal, alternative=None): + self.name = name + self.since = since + self.removal = removal + self.alternative = alternative + + def __call__(self, func): + @functools.wraps(func) + def wrapped(*args, **kwargs): + if self.name in kwargs.keys(): + msg = ( + f"Argument `{self.name}` is deprecated and will be removed in " + f"version {self.removal}. To avoid this warning, please do not use " + f"`{self.name}`. " + ) + if self.alternative is not None: + msg += f"Use `{self.alternative}` instead. " + kwargs[self.alternative] = kwargs.pop(self.name) + msg += f"See the documentation of `{func.__name__}()` for more details." + warnings.simplefilter( + action="always", category=np.VisibleDeprecationWarning + ) + func_code = func.__code__ + warnings.warn_explicit( + message=msg, + category=np.VisibleDeprecationWarning, + filename=func_code.co_filename, + lineno=func_code.co_firstlineno + 1, + ) + return func(*args, **kwargs) + + return wrapped From 151a927ed5efad67336a9f1de69dd0c6e244d8da Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Fri, 20 Oct 2023 11:45:55 -0500 Subject: [PATCH 03/48] Documentation: Added documentation about deprecating code. --- CHANGELOG.rst | 1 + CONTRIBUTING.rst | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f5cc5389..33f520a5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,7 @@ Unreleased Added ----- - Explicit support for Python 3.11. +- Added deprecation tools for deprecating functions and arguments. - Added Pre-Commit for code formatting. Changed diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index a3443541..949ed51e 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -128,6 +128,29 @@ 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, +ideally no, functionality should break between minor releases. Deprecation warnings +are raised whenever possible and feasible for functions/methods/properties/arguments, +so that users get a heads-up one (minor) release before something is removed or changes, +with a possible alternative to be used. + +The decorator should be placed right above the object signature to be deprecated:: + +.. code-block:: python + >>> from diffsims.utils._deprecated import deprecate + >>> @deprecate(since=0.8, removal=0.9, alternative="bar") + >>> def foo(self, n): + >>> return n + 1 + + >>> @property + >>> @deprecate(since=0.9, removal=0.10, alternative="another", is_function=True) + >>> def this_property(self): + >>> return 2 + + Build and write documentation ----------------------------- From 1ecb82473a6be769bbd8dbab12b994deb7e1c763 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Fri, 20 Oct 2023 12:21:20 -0500 Subject: [PATCH 04/48] Testing: Added test for uncovered line. --- diffsims/tests/utils/test_deprecation.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/diffsims/tests/utils/test_deprecation.py b/diffsims/tests/utils/test_deprecation.py index 25f7d5ca..66b03e17 100644 --- a/diffsims/tests/utils/test_deprecation.py +++ b/diffsims/tests/utils/test_deprecation.py @@ -72,6 +72,26 @@ def foo(n): f" {desired_msg}" ) + def test_deprecation_not_function(self): + @deprecated(since=0.7, alternative="bar", removal=0.8, alternative_is_function=False) + def foo(n): + return n + 1 + + with pytest.warns(np.VisibleDeprecationWarning) as record: + assert foo(4) == 5 + desired_msg = ( + "Function `foo()` is deprecated and will be removed in version 0.8. Use " + "`bar` instead." + ) + assert str(record[0].message) == desired_msg + assert foo.__doc__ == ( + "[*Deprecated*] \n" + "\nNotes\n-----\n" + ".. deprecated:: 0.7\n" + f" {desired_msg}" + ) + + class TestDeprecateArgument: def test_deprecate_argument(self): From 825691a29b5612ade5fc6b0a40b382f201f1eabe Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Tue, 24 Oct 2023 15:28:34 -0500 Subject: [PATCH 05/48] Refactor: Added new class for simulating multiple rotations --- diffsims/generators/simulation_generator.py | 295 +++++++++++++ diffsims/libraries/__init__.py | 2 +- diffsims/simulations/__init__.py | 1 + diffsims/simulations/simulation.py | 461 ++++++++++++++++++++ 4 files changed, 758 insertions(+), 1 deletion(-) create mode 100644 diffsims/generators/simulation_generator.py create mode 100644 diffsims/simulations/__init__.py create mode 100644 diffsims/simulations/simulation.py diff --git a/diffsims/generators/simulation_generator.py b/diffsims/generators/simulation_generator.py new file mode 100644 index 00000000..7fe177aa --- /dev/null +++ b/diffsims/generators/simulation_generator.py @@ -0,0 +1,295 @@ +from typing import Sequence, Union +import numpy as np +import matplotlib.pyplot as plt + +from orix.quaternion import Rotation +from orix.crystal_map import Phase + +from diffsims.crystallography import ReciprocalLatticeVector +from diffsims.simulations.simulation import DiffractionSimulation, ProfileSimulation +from diffsims.libraries.diffraction_library import SimulationLibrary +from diffsims.utils.shape_factor_models import ( + linear, + atanc, + lorentzian, + sinc, + sin2c, + lorentzian_precession, + _shape_factor_precession +) + +from diffsims.utils.sim_utils import ( + get_electron_wavelength, + get_kinematical_intensities, + is_lattice_hexagonal, +) + +_shape_factor_model_mapping = { + "linear": linear, + "atanc": atanc, + "sinc": sinc, + "sin2c": sin2c, + "lorentzian": lorentzian, +} + + +class SimulationGenerator: + def __init__( + self, + accelerating_voltage: float = 200, + scattering_params: str = "lobato", + precession_angle: float = 0, + shape_factor_model: str = "lorentzian", + approximate_precession: bool = True, + minimum_intensity: float = 1e-20, + **kwargs, + ): + self.wavelength = get_electron_wavelength(accelerating_voltage) + self.precession_angle = np.abs(precession_angle) + self.approximate_precession = approximate_precession + if isinstance(shape_factor_model, str): + if shape_factor_model in _shape_factor_model_mapping.keys(): + self.shape_factor_model = _shape_factor_model_mapping[ + shape_factor_model + ] + else: + raise NotImplementedError( + f"{shape_factor_model} is not a recognized shape factor " + f"model, choose from: {_shape_factor_model_mapping.keys()} " + f"or provide your own function." + ) + else: + self.shape_factor_model = shape_factor_model + self.minimum_intensity = minimum_intensity + self.shape_factor_kwargs = kwargs + if scattering_params in ["lobato", "xtables", None]: + self.scattering_params = scattering_params + else: + raise NotImplementedError( + "The scattering parameters `{}` is not implemented. " + "See documentation for available " + "implementations.".format(scattering_params) + ) + + def calculate_ed_data( + self, + phase: Phase, + rotation: Rotation = Rotation.from_euler((0, 0, 0), + degrees=True), + reciprocal_radius: float = 1.0, + with_direct_beam: bool = True, + max_excitation_error: float = 1e-2, + shape_factor_width: float = None, + debye_waller_factors: dict = None, + ): + """Calculates the Electron Diffraction data for a structure. + + Parameters + ---------- + phases: + The phase for which to derive the diffraction pattern. + structure : diffpy.structure.structure.Structure + The structure for which to derive the diffraction pattern. + Note that the structure must be rotated to the appropriate + orientation and that testing is conducted on unit cells + (rather than supercells). + reciprocal_radius : float + The maximum radius of the sphere of reciprocal space to + sample, in reciprocal Angstroms. + rotation + The Rotation object to apply to the structure and then + calculate the diffraction pattern. + with_direct_beam : bool + If True, the direct beam is included in the simulated + diffraction pattern. If False, it is not. + max_excitation_error : float + The cut-off for geometric excitation error in the z-direction + in units of reciprocal Angstroms. Spots with a larger distance + from the Ewald sphere are removed from the pattern. + Related to the extinction distance and roungly equal to 1/thickness. + shape_factor_width : float + Determines the width of the reciprocal rel-rod, for fine-grained + control. If not set will be set equal to max_excitation_error. + debye_waller_factors : dict of str:value pairs + Maps element names to their temperature-dependent Debye-Waller factors. + + Returns + ------- + diffsims.sims.diffraction_simulation.DiffractionSimulation + The data associated with this structure and diffraction setup. + """ + if debye_waller_factors is None: + debye_waller_factors = {} + # Specify variables used in calculation + wavelength = self.wavelength + recip = ReciprocalLatticeVector.from_min_dspacing(phase, + min_dspacing=1 / reciprocal_radius, + include_zero_beam=with_direct_beam) + + # Rotate using all the rotations in the list + vectors = [] + for rot in rotation.to_matrix(): + rotated_vectors = recip.rotate_from_matrix(rot) + # Identify the excitation errors of all points (distance from point to Ewald sphere) + r_sphere = 1 / wavelength + r_spot = np.sqrt(np.sum(np.square(rotated_vectors.data[:, :2]), axis=1)) + z_spot = rotated_vectors.data[:, 2] + + z_sphere = -np.sqrt(r_sphere ** 2 - r_spot ** 2) + r_sphere + excitation_error = z_sphere - z_spot + + # determine the pre-selection reflections + if self.precession_angle == 0: + intersection = np.abs(excitation_error) < max_excitation_error + else: + # only consider points that intersect the ewald sphere at some point + # the center point of the sphere + P_z = r_sphere * np.cos(np.deg2rad(self.precession_angle)) + P_t = r_sphere * np.sin(np.deg2rad(self.precession_angle)) + # the extremes of the ewald sphere + z_surf_up = P_z - np.sqrt(r_sphere ** 2 - (r_spot + P_t) ** 2) + z_surf_do = P_z - np.sqrt(r_sphere ** 2 - (r_spot - P_t) ** 2) + intersection = (z_spot - max_excitation_error <= z_surf_up) & ( + z_spot + max_excitation_error >= z_surf_do) + + # select these reflections + intersected_vectors = rotated_vectors[intersection] + excitation_error = excitation_error[intersection] + r_spot = r_spot[intersection] + + if shape_factor_width is None: + shape_factor_width = max_excitation_error + # select and evaluate shape factor model + if self.precession_angle == 0: + # calculate shape factor + shape_factor = self.shape_factor_model( + excitation_error, shape_factor_width, **self.shape_factor_kwargs + ) + else: + if self.approximate_precession: + shape_factor = lorentzian_precession( + excitation_error, + shape_factor_width, + r_spot, + np.deg2rad(self.precession_angle), + ) + else: + shape_factor = _shape_factor_precession( + excitation_error, + r_spot, + np.deg2rad(self.precession_angle), + self.shape_factor_model, + shape_factor_width, + **self.shape_factor_kwargs, + ) + # Calculate diffracted intensities based on a kinematic model. + intensities = get_kinematical_intensities( + phase.structure, + intersected_vectors.hkl, + intersected_vectors.gspacing, + prefactor=shape_factor, + scattering_params=self.scattering_params, + debye_waller_factors=debye_waller_factors, + ) + + # Threshold peaks included in simulation as factor of maximum intensity. + peak_mask = intensities > np.max(intensities) * self.minimum_intensity + intensities = intensities[peak_mask] + intersected_vectors = intersected_vectors[peak_mask] + intersected_vectors.intensity = intensities + + sim = DiffractionSimulation(coordinates=intersected_vectors, + with_direct_beam=with_direct_beam,) + vectors.append(sim) + + lib = SimulationLibrary(phase=phase, + rotations=rotation, + diffraction_generator=self, + simulations=vectors, + ) + return lib + + + def calculate_profile_data( + self, + phase: Phase, + reciprocal_radius: float = 1.0, + minimum_intensity: float = 1e-3, + debye_waller_factors: dict = None, + ): + """Calculates a one dimensional diffraction profile for a + structure. + + Parameters + ---------- + structure : diffpy.structure.structure.Structure + The structure for which to calculate the diffraction profile. + reciprocal_radius : float + The maximum radius of the sphere of reciprocal space to + sample, in reciprocal angstroms. + minimum_intensity : float + The minimum intensity required for a diffraction peak to be + considered real. Deals with numerical precision issues. + debye_waller_factors : dict of str:value pairs + Maps element names to their temperature-dependent Debye-Waller factors. + + Returns + ------- + diffsims.sims.diffraction_simulation.ProfileSimulation + The diffraction profile corresponding to this structure and + experimental conditions. + """ + latt = phase.structure.lattice + + # Obtain crystallographic reciprocal lattice points within range + vectors = ReciprocalLatticeVector.from_min_dspacing(phase, + min_dspacing=1 / reciprocal_radius, + ) + + unique_vectors = vectors.unique(use_symmetry=True).symmetrise() + + multiplicity = unique_vectors.multiplicity + g_indices = unique_vectors.hkl + g_hkls = unique_vectors.gspacing + + + i_hkl = get_kinematical_intensities( + phase.structure, + g_indices, + np.asarray(g_hkls), + prefactor=multiplicity, + scattering_params=self.scattering_params, + debye_waller_factors=debye_waller_factors, + ) + + if is_lattice_hexagonal(latt): + # Use Miller-Bravais indices for hexagonal lattices. + g_indices = ( + g_indices[0], + g_indices[1], + -g_indices[0] - g_indices[1], + g_indices[2], + ) + + hkls_labels = ["".join([str(int(x)) for x in xs]) for xs in g_indices] + + peaks = {} + for l, i, g in zip(hkls_labels, i_hkl, g_hkls): + peaks[l] = [i, g] + + # Scale intensities so that the max intensity is 100. + + max_intensity = max([v[0] for v in peaks.values()]) + x = [] + y = [] + hkls = [] + for k in peaks.keys(): + v = peaks[k] + if v[0] / max_intensity * 100 > minimum_intensity and (k != "000"): + x.append(v[1]) + y.append(v[0]) + hkls.append(k) + + y = np.asarray(y) / max(y) * 100 + + return ProfileSimulation(x, y, hkls) \ No newline at end of file diff --git a/diffsims/libraries/__init__.py b/diffsims/libraries/__init__.py index 1db3a521..a08b1968 100644 --- a/diffsims/libraries/__init__.py +++ b/diffsims/libraries/__init__.py @@ -21,7 +21,7 @@ from diffsims.libraries import ( diffraction_library, structure_library, - vector_library, + vector_library ) __all__ = [ diff --git a/diffsims/simulations/__init__.py b/diffsims/simulations/__init__.py new file mode 100644 index 00000000..ec23448b --- /dev/null +++ b/diffsims/simulations/__init__.py @@ -0,0 +1 @@ +from diffsims.simulations.simulation import DiffractionSimulation \ No newline at end of file diff --git a/diffsims/simulations/simulation.py b/diffsims/simulations/simulation.py new file mode 100644 index 00000000..d828b1a6 --- /dev/null +++ b/diffsims/simulations/simulation.py @@ -0,0 +1,461 @@ +import copy + +import numpy as np +import matplotlib.pyplot as plt + +from diffsims.crystallography.reciprocal_lattice_vector import ReciprocalLatticeVector +from diffsims.pattern.detector_functions import add_shot_and_point_spread +from diffsims.utils import mask_utils + + +class DiffractionSimulation: + """Holds the result of a kinematic diffraction pattern simulation. + + Parameters + ---------- + coordinates : array-like, shape [n_points, 2] + The x-y coordinates of points in reciprocal space. + indices : array-like, shape [n_points, 3] + The indices of the reciprocal lattice points that intersect the + Ewald sphere. + intensities : array-like, shape [n_points, ] + The intensity of the reciprocal lattice points. + calibration : float or tuple of float, optional + The x- and y-scales of the pattern, with respect to the original + reciprocal angstrom coordinates. + offset : tuple of float, optional + The x-y offset of the pattern in reciprocal angstroms. Defaults to + zero in each direction. + """ + + def __init__( + self, + coordinates: ReciprocalLatticeVector, + calibration=None, + offset=(0.0, 0.0), + with_direct_beam=False, + shape=(512, 512), + ): + """Initializes the DiffractionSimulation object with data values for + the coordinates, indices, intensities, calibration and offset. + """ + self._coordinates = coordinates + self.shape = shape + self.calibration = calibration + self.offset = np.array(offset) + self.with_direct_beam = with_direct_beam + + def __len__(self): + return self.coordinates.shape[0] + + @property + def size(self): + return self.__len__() + + def __getitem__(self, sliced): + """Sliced is any valid numpy slice that does not change the number of + dimensions or number of columns""" + coords = self.coordinates[sliced] + return DiffractionSimulation( + coords, + calibration=self.calibration, + offset=self.offset, + with_direct_beam=self.with_direct_beam, + ) + + def deepcopy(self): + return copy.deepcopy(self) + + def append(self, + vectors: ReciprocalLatticeVector): + new_data = copy.deepcopy(self) + new_coords = np.concatenate((new_data._coordinates.data, vectors._coordinates.data), axis=0) + new_data._coordinates = ReciprocalLatticeVector(phase=self._coordinates.phase, xyz=new_coords) + new_data._coordinates.intensity = np.concatenate((self._coordinates.intensity, + vectors._coordinates.intensity)) + return new_data + + @property + def calibrated_coordinates(self): + """ndarray : Coordinates converted into pixel space.""" + if self.calibration is not None: + return (self.coordinates.data[:, :2] + self.offset) / self.calibration + else: + raise Exception("Pixel calibration is not set!") + + @property + def pixel_coordinates(self): + half_shape = np.array(self.shape) / 2 + pixel_coordinates = np.rint(self.calibrated_coordinates[:, :2] + half_shape).astype(int) + return pixel_coordinates + + @property + def calibration(self): + """tuple of float : The x- and y-scales of the pattern, with respect to + the original reciprocal angstrom coordinates.""" + return self._calibration + + @calibration.setter + def calibration(self, calibration): + if calibration is None: + pass + elif np.all(np.equal(calibration, 0)): + raise ValueError("`calibration` cannot be zero.") + elif isinstance(calibration, float) or isinstance(calibration, int): + calibration = np.array((calibration, calibration)) + elif len(calibration) == 2: + calibration = np.array(calibration) + else: + raise ValueError( + "`calibration` must be a float or length-2" "tuple of floats." + ) + self._calibration = calibration + + @property + def direct_beam_mask(self): + """ndarray : If `with_direct_beam` is True, returns a True array for all + points. If `with_direct_beam` is False, returns a True array with False + in the position of the direct beam.""" + if self.with_direct_beam: + return np.ones_like(self._coordinates.intensity, dtype=bool) + else: + mask = np.any(self._coordinates.data!=0, axis=1) + return mask + + @property + def coordinates(self): + """ndarray : The coordinates of all unmasked points.""" + return self._coordinates[self.direct_beam_mask] + + @coordinates.setter + def coordinates(self, coordinates): + self._coordinates = coordinates + + @property + def intensities(self): + return self.coordinates.intensity + + @intensities.setter + def intensities(self, intensities): + self._coordinates.intensity = intensities + print(self.coordinates.intensity) + + def _get_transformed_coordinates( + self, angle, center=(0, 0), mirrored=False, units="real" + ): + """Translate, rotate or mirror the pattern spot coordinates""" + if units != "real": + center = np.array(center) / self.calibration + new_sim = self.deepcopy() + transformed_coords = new_sim.coordinates + cx, cy = center + x = transformed_coords.data[:, 0] + y = transformed_coords.data[:, 1] + mirrored_factor = -1 if mirrored else 1 + theta = mirrored_factor * np.arctan2(y, x) + np.deg2rad(angle) + rd = np.sqrt(x ** 2 + y ** 2) + transformed_coords[:, 0] = rd * np.cos(theta) + cx + transformed_coords[:, 1] = rd * np.sin(theta) + cy + new_sim._coordinates = transformed_coords + return new_sim + + def rotate_shift_coordinates(self, angle, center=(0, 0), mirrored=False): + """ + Rotate, flip or shift patterns in-plane + + Parameters + ---------- + angle: float + In plane rotation angle in degrees + center: 2-tuple of floats + Center coordinate of the patterns + mirrored: bool + Mirror across the x axis + """ + coords_new = self._get_transformed_coordinates( + angle, center, mirrored, units="real" + ) + return coords_new + + def get_as_mask( + self, + shape, + radius=6.0, + negative=True, + radius_function=None, + direct_beam_position=None, + in_plane_angle=0, + mirrored=False, + *args, + **kwargs, + ): + """ + Return the diffraction pattern as a binary mask of type + bool + + Parameters + ---------- + shape: 2-tuple of ints + Shape of the output mask (width, height) + radius: float or array, optional + Radii of the spots in pixels. An array may be supplied + of the same length as the number of spots. + negative: bool, optional + Whether the spots are masked (True) or everything + else is masked (False) + radius_function: Callable, optional + Calculate the radius as a function of the spot intensity, + for example np.sqrt. args and kwargs supplied to this method + are passed to this function. Will override radius. + direct_beam_position: 2-tuple of ints, optional + The (x,y) coordinate in pixels of the direct beam. Defaults to + the center of the image. + in_plane_angle: float, optional + In plane rotation of the pattern in degrees + mirrored: bool, optional + Whether the pattern should be flipped over the x-axis, + corresponding to the inverted orientation + + Returns + ------- + mask: numpy.ndarray + Boolean mask of the diffraction pattern + """ + r = radius + if direct_beam_position is None: + direct_beam_position = (shape[1] // 2, shape[0] // 2) + point_coordinates_shifted = self._get_transformed_coordinates( + in_plane_angle, + center=direct_beam_position, + mirrored=mirrored, + units="pixels", + ) + if radius_function is not None: + r = radius_function(self.intensities, *args, **kwargs) + mask = mask_utils.create_mask(shape, fill=negative) + mask_utils.add_circles_to_mask( + mask, point_coordinates_shifted.coordinates.data, r, fill=not negative + ) + return mask + + def get_diffraction_pattern( + self, + shape=None, + sigma=10, + direct_beam_position=None, + in_plane_angle=0, + mirrored=False, + ): + """Returns the diffraction data as a numpy array with + two-dimensional Gaussians representing each diffracted peak. Should only + be used for qualitative work. + + Parameters + ---------- + shape : tuple of ints + The size of a side length (in pixels) + sigma : float + Standard deviation of the Gaussian function to be plotted (in pixels). + direct_beam_position: 2-tuple of ints, optional + The (x,y) coordinate in pixels of the direct beam. Defaults to + the center of the image. + in_plane_angle: float, optional + In plane rotation of the pattern in degrees + mirrored: bool, optional + Whether the pattern should be flipped over the x-axis, + corresponding to the inverted orientation + + Returns + ------- + diffraction-pattern : numpy.array + The simulated electron diffraction pattern, normalised. + + Notes + ----- + If don't know the exact calibration of your diffraction signal using 1e-2 + produces reasonably good patterns when the lattice parameters are on + the order of 0.5nm and a the default size and sigma are used. + """ + if shape is None: + shape = self.shape + if direct_beam_position is None: + direct_beam_position = (shape[1] // 2, shape[0] // 2) + tranformed = self._get_transformed_coordinates( + in_plane_angle, direct_beam_position, mirrored, units="pixel" + ) + in_frame = ( + (tranformed.coordinates.data[:, 0] >= 0) + & (tranformed.coordinates.data[:, 0] < shape[1]) + & (tranformed.coordinates.data[:, 1] >= 0) + & (tranformed.coordinates.data[:, 1] < shape[0]) + ) + spot_coords = tranformed.coordinates.data[in_frame].astype(int) + spot_intens = self.intensities[in_frame] + pattern = np.zeros(shape) + # checks that we have some spots + if spot_intens.shape[0] == 0: + return pattern + else: + pattern[spot_coords[:, 0], spot_coords[:, 1]] = spot_intens + pattern = add_shot_and_point_spread(pattern.T, sigma, shot_noise=False) + return np.divide(pattern, np.max(pattern)) + + def plot( + self, + size_factor=1, + direct_beam_position=None, + in_plane_angle=0, + mirrored=False, + units="real", + show_labels=False, + label_offset=(0, 0), + label_formatting={}, + min_label_intensity=.1, + ax=None, + **kwargs, + ): + """A quick-plot function for a simulation of spots + + Parameters + ---------- + size_factor : float, optional + linear spot size scaling, default to 1 + direct_beam_position: 2-tuple of ints, optional + The (x,y) coordinate in pixels of the direct beam. Defaults to + the center of the image. + in_plane_angle: float, optional + In plane rotation of the pattern in degrees + mirrored: bool, optional + Whether the pattern should be flipped over the x-axis, + corresponding to the inverted orientation + units : str, optional + 'real' or 'pixel', only changes scalebars, falls back on 'real', the default + show_labels : bool, optional + draw the miller indices near the spots + label_offset : 2-tuple, optional + the relative location of the spot labels. Does nothing if `show_labels` + is False. + label_formatting : dict, optional + keyword arguments passed to `ax.text` for drawing the labels. Does + nothing if `show_labels` is False. + ax : matplotlib Axes, optional + axes on which to draw the pattern. If `None`, a new axis is created + **kwargs : + passed to ax.scatter() method + + Returns + ------- + ax,sp + + Notes + ----- + spot size scales with the square root of the intensity. + """ + if direct_beam_position is None: + direct_beam_position = (0, 0) + if ax is None: + _, ax = plt.subplots() + ax.set_aspect("equal") + coords = self._get_transformed_coordinates( + in_plane_angle, direct_beam_position, mirrored, units=units + ) + sp = ax.scatter( + coords.coordinates.data[:, 0], + coords.coordinates.data[:, 1], + s=size_factor * np.sqrt(self.intensities), + **kwargs, + ) + + if show_labels: + millers = self.coordinates.hkl.astype(np.int16) + # only label the points inside the axes + xlim = ax.get_xlim() + ylim = ax.get_ylim() + condition = ( + (coords.coordinates.data[:, 0] > min(xlim)) + & (coords.coordinates.data[:, 0] < max(xlim)) + & (coords.coordinates.data[:, 1] > min(ylim)) + & (coords.coordinates.data[:, 1] < max(ylim)) + ) + millers = millers[condition] + coords = coords.coordinates.data[condition] + # default alignment options + if ( + "ha" not in label_offset + and "horizontalalignment" not in label_formatting + ): + label_formatting["ha"] = "center" + if "va" not in label_offset and "verticalalignment" not in label_formatting: + label_formatting["va"] = "center" + for miller, coordinate, inten in zip(millers, coords, self.intensities): + if inten > min_label_intensity: + + label = "(" + for index in miller: + if index < 0: + label += r"$\bar{" + str(abs(index)) + r"}$" + else: + label += str(abs(index)) + label += " " + label = label[:-1] + ")" + ax.text( + coordinate[0] + label_offset[0], + coordinate[1] + label_offset[1], + label, + **label_formatting, + ) + if units =="real": + ax.set_xlabel(r"$\AA^{-1}$") + ax.set_ylabel(r"$\AA^{-1}$") + else: + ax.set_xlabel("pixels") + ax.set_ylabel("pixels") + return ax, sp + + +class ProfileSimulation: + """Holds the result of a given kinematic simulation of a diffraction + profile. + + Parameters + ---------- + magnitudes : array-like, shape [n_peaks, 1] + Magnitudes of scattering vectors. + intensities : array-like, shape [n_peaks, 1] + The kinematic intensity of the diffraction peaks. + hkls : [{(h, k, l): mult}] {(h, k, l): mult} is a dict of Miller + indices for all diffracted lattice facets contributing to each + intensity. + """ + + def __init__(self, magnitudes, intensities, hkls): + self.magnitudes = magnitudes + self.intensities = intensities + self.hkls = hkls + + def plot(self, annotate_peaks=True, with_labels=True, fontsize=12): + """Plots the diffraction profile simulation for the + calculate_profile_data method in DiffractionGenerator. + + Parameters + ---------- + annotate_peaks : boolean + If True, peaks are annotaed with hkl information. + with_labels : boolean + If True, xlabels and ylabels are added to the plot. + fontsize : integer + Fontsize for peak labels. + """ + + ax = plt.gca() + for g, i, hkls in zip(self.magnitudes, self.intensities, self.hkls): + label = hkls + ax.plot([g, g], [0, i], color="k", linewidth=3, label=label) + if annotate_peaks: + ax.annotate(label, xy=[g, i], xytext=[g, i], fontsize=fontsize) + + if with_labels: + ax.set_xlabel("A ($^{-1}$)") + ax.set_ylabel("Intensities (scaled)") + + return plt \ No newline at end of file From 9ff5fe0e600117404c8964593cc7e36c01928d99 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Tue, 24 Oct 2023 15:28:52 -0500 Subject: [PATCH 06/48] Refactor: Add tests for new simulations --- .../reciprocal_lattice_vector.py | 31 +- diffsims/libraries/diffraction_library.py | 68 +++- .../generators/test_simulation_generator.py | 256 +++++++++++++++ .../tests/simulations/test_simulations.py | 297 ++++++++++++++++++ diffsims/utils/shape_factor_models.py | 61 +++- 5 files changed, 703 insertions(+), 10 deletions(-) create mode 100644 diffsims/tests/generators/test_simulation_generator.py create mode 100644 diffsims/tests/simulations/test_simulations.py diff --git a/diffsims/crystallography/reciprocal_lattice_vector.py b/diffsims/crystallography/reciprocal_lattice_vector.py index 8ce16ba7..b3178d49 100644 --- a/diffsims/crystallography/reciprocal_lattice_vector.py +++ b/diffsims/crystallography/reciprocal_lattice_vector.py @@ -122,6 +122,7 @@ def __init__(self, phase, xyz=None, hkl=None, hkil=None): self._theta = np.full(self.shape, np.nan) self._structure_factor = np.full(self.shape, np.nan, dtype="complex128") + self._intensity = np.full(self.shape, np.nan) def __getitem__(self, key): miller_new = self.to_miller().__getitem__(key) @@ -139,6 +140,15 @@ def __getitem__(self, key): else: rlv_new._theta = self.theta[key] + if np.isnan(self.intensity).all(): + rlv_new._intensity = np.full(rlv_new.shape, np.nan) + else: + slic = self.intensity[key] + if not hasattr(slic, "__len__"): + slic = np.array([slic, ]) + rlv_new._intensity = slic + + return rlv_new def __repr__(self): @@ -502,6 +512,23 @@ def scattering_parameter(self): return 0.5 * self.gspacing + @property + def intensity(self): + return self._intensity + + @intensity.setter + def intensity(self, value): + if not hasattr(value, "__len__"): + value = np.array([value, ] * self.size) + if len(value) != self.size: + raise ValueError("Length of intensity array must match number of vectors") + self._intensity = np.array(value) + + def rotate_from_matrix(self, rotation_matrix): + return ReciprocalLatticeVector(phase=self.phase, + xyz=np.matmul(rotation_matrix, + self.data.T).T) + @property def structure_factor(self): r"""Kinematical structure factors :math:`F`. @@ -1070,7 +1097,7 @@ def from_highest_hkl(cls, phase, hkl): return cls(phase, hkl=idx).unique() @classmethod - def from_min_dspacing(cls, phase, min_dspacing=0.7): + def from_min_dspacing(cls, phase, min_dspacing=0.7, include_zero_beam=False): """Create a set of unique reciprocal lattice vectors with a a direct space interplanar spacing greater than a lower threshold. @@ -1128,6 +1155,8 @@ def from_min_dspacing(cls, phase, min_dspacing=0.7): dspacing = 1 / phase.structure.lattice.rnorm(hkl) idx = dspacing >= min_dspacing hkl = hkl[idx] + if include_zero_beam: + hkl = np.vstack((hkl, np.zeros(3, dtype=int))) return cls(phase, hkl=hkl).unique() @classmethod diff --git a/diffsims/libraries/diffraction_library.py b/diffsims/libraries/diffraction_library.py index b0c084e1..77beff9a 100644 --- a/diffsims/libraries/diffraction_library.py +++ b/diffsims/libraries/diffraction_library.py @@ -17,13 +17,20 @@ # along with diffsims. If not, see . import pickle +from typing import NamedTuple, Sequence, Set import numpy as np +from orix.quaternion import Rotation +from orix.crystal_map import Phase +from diffsims.sims.diffraction_simulation import DiffractionSimulation +from diffsims.generators.diffraction_generator import DiffractionGenerator __all__ = [ "DiffractionLibrary", "load_DiffractionLibrary", + "SimulationLibrary", + "SimulationLibraries", ] @@ -84,7 +91,8 @@ def _get_library_entry_from_angles(library, phase, angles): """ phase_entry = library[phase] - for orientation_index, orientation in enumerate(phase_entry["orientations"]): + orientations = phase_entry["orientations"].to_euler(degrees=True) + for orientation_index, orientation in enumerate(orientations): if np.sum(np.abs(np.subtract(orientation, angles))) < 1e-2: return orientation_index @@ -94,6 +102,57 @@ def _get_library_entry_from_angles(library, phase, angles): ) +class SimulationLibrary(NamedTuple): + phase: Phase + rotations: Rotation + diffraction_generator: DiffractionGenerator + simulations: Sequence[DiffractionSimulation] + str_rotations: Sequence[str] = None + + def __repr__(self): + return (f"DiffractionPhaseLibrary(phase={self.phase.name}," + f" No. Rotations={self.__len__()})") + + def __post_init__(self): + if len(self.rotations) != len(self.simulations): + raise ValueError("Number of rotations and simulations must be the same") + if self.str_rotations is not None and len(self.rotations) != len(self.str_rotations): + raise ValueError("Number of rotations and str_rotations must be the same") + + def __len__(self): + return len(self.rotations) + + def __getitem__(self, item): + if isinstance(item, str): + item = self.str_rotations.index(item) + return SimulationLibrary(self.phase, + self.rotations[item], + self.simulations[item] + ) + + def get_library_entry(self, + rotation: Rotation, + angle_cutoff: float = 1e-2) -> 'DiffractionPhaseLibrary': + angles = self.rotations.angle_with(rotation) + is_in_range = np.sum(np.abs(angles), axis=1) < angle_cutoff + return self[is_in_range] + + +class SimulationLibraries(dict): + """ + A dictionary containing all the structures and their associated rotations + """ + + def __init__(self, libraries: Sequence[SimulationLibrary]): + super().__init__() + for library in libraries: + self[library.phase.name] = library + + def __repr__(self): + return f"DiffractionLibrary)" + + + class DiffractionLibrary(dict): """Maps crystal structure (phase) and orientation to simulated diffraction data. @@ -127,11 +186,11 @@ def get_library_entry(self, phase=None, angle=None): Parameters ---------- - phase : str + phase : str or int Key for the phase of interest. If unspecified the choice is random. angle : tuple The orientation of interest as a tuple of Euler angles following the - Bunge convention [z, x, z] in degrees. If unspecified the choise is + Bunge convention [z, x, z] in degrees. If unspecified the choice is random (the first hit). Returns @@ -141,6 +200,8 @@ def get_library_entry(self, phase=None, angle=None): phase and orientation with associated properties. """ + if isinstance(phase, int): + phase = list(self.keys())[phase] if phase is not None: phase_entry = self[phase] if angle is not None: @@ -156,7 +217,6 @@ def get_library_entry(self, phase=None, angle=None): return { "Sim": phase_entry["simulations"][orientation_index], "intensities": phase_entry["intensities"][orientation_index], - "pixel_coords": phase_entry["pixel_coords"][orientation_index], "pattern_norm": np.linalg.norm( phase_entry["intensities"][orientation_index] ), diff --git a/diffsims/tests/generators/test_simulation_generator.py b/diffsims/tests/generators/test_simulation_generator.py new file mode 100644 index 00000000..00b9d904 --- /dev/null +++ b/diffsims/tests/generators/test_simulation_generator.py @@ -0,0 +1,256 @@ +# -*- 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 +import pytest + +import diffpy.structure +from orix.crystal_map import Phase + +from diffsims.generators.simulation_generator import SimulationGenerator +from diffsims.simulations.simulation import ProfileSimulation +from diffsims.utils.shape_factor_models import ( + linear, + binary, + sin2c, + atanc, + lorentzian, + _shape_factor_precession, +) + + +@pytest.fixture(params=[(300)]) +def diffraction_calculator(request): + return SimulationGenerator(request.param) + + +@pytest.fixture(scope="module") +def diffraction_calculator_precession_full(): + return SimulationGenerator(300, precession_angle=0.5, approximate_precession=False) + + +@pytest.fixture(scope="module") +def diffraction_calculator_precession_simple(): + return SimulationGenerator(300, precession_angle=0.5, approximate_precession=True) + + +def local_excite(excitation_error, maximum_excitation_error, t): + return (np.sin(t) * excitation_error) / maximum_excitation_error + + +@pytest.fixture(scope="module") +def diffraction_calculator_custom(): + return SimulationGenerator(300, shape_factor_model=local_excite, t=0.2) + + +@pytest.fixture(params=[(1, 3), (1,), (False,)]) +def precessed(request): + var = request.param + return var if len(var) - 1 else var[0] + + +def make_phase(lattice_parameter=None): + """ + We construct an Fd-3m silicon (with lattice parameter 5.431 as a default) + """ + if lattice_parameter is not None: + a = lattice_parameter + else: + a = 5.431 + latt = diffpy.structure.lattice.Lattice(a, a, a, 90, 90, 90) + # TODO - Make this construction with internal diffpy syntax + atom_list = [] + for coords in [[0, 0, 0], [0.5, 0, 0.5], [0, 0.5, 0.5], [0.5, 0.5, 0]]: + x, y, z = coords[0], coords[1], coords[2] + atom_list.append( + diffpy.structure.atom.Atom(atype="Si", xyz=[x, y, z], lattice=latt) + ) # Motif part A + atom_list.append( + diffpy.structure.atom.Atom( + atype="Si", xyz=[x + 0.25, y + 0.25, z + 0.25], lattice=latt + ) + ) # Motif part B + struct = diffpy.structure.Structure(atoms=atom_list, lattice=latt) + p = Phase(structure=struct, space_group=227) + return p + + +@pytest.fixture() +def local_structure(): + return make_phase() + + +def probe(x, out=None, scale=None): + if hasattr(x, "shape"): + return (abs(x[..., 0]) < 6) * (abs(x[..., 1]) < 6) + else: + v = abs(x[0].reshape(-1, 1, 1)) < 6 + v = v * abs(x[1].reshape(1, -1, 1)) < 6 + return v + 0 * x[2].reshape(1, 1, -1) + + +@pytest.mark.parametrize("model", [binary, linear, atanc, sin2c, lorentzian]) +def test_shape_factor_precession(model): + excitation = np.array([-0.1, 0.1]) + r = np.array([1, 5]) + _ = _shape_factor_precession(excitation, r, 0.5, model, 0.1) + + +def test_linear_shape_factor(): + excitation = np.array([-2, -1, -0.5, 0, 0.5, 1, 2]) + totest = linear(excitation, 1) + np.testing.assert_allclose(totest, np.array([0, 0, 0.5, 1, 0.5, 0, 0])) + np.testing.assert_allclose(linear(0.5, 1), 0.5) + + +@pytest.mark.parametrize( + "model, expected", + [("linear", linear), ("lorentzian", lorentzian), (binary, binary)], +) +def test_diffraction_generator_init(model, expected): + generator = SimulationGenerator(300, shape_factor_model=model) + assert generator.shape_factor_model == expected + + +class TestDiffractionCalculator: + def test_init(self, diffraction_calculator: SimulationGenerator): + assert diffraction_calculator.scattering_params == "lobato" + assert diffraction_calculator.precession_angle == 0 + assert diffraction_calculator.shape_factor_model == lorentzian + assert diffraction_calculator.approximate_precession == True + assert diffraction_calculator.minimum_intensity == 1e-20 + + def test_matching_results( + self, diffraction_calculator: SimulationGenerator, local_structure + ): + diffraction = diffraction_calculator.calculate_ed_data( + local_structure, reciprocal_radius=5.0 + ) + assert diffraction.simulations[0].coordinates.size == 72 + + def test_precession_simple( + self, diffraction_calculator_precession_simple, local_structure + ): + diffraction = diffraction_calculator_precession_simple.calculate_ed_data( + local_structure, + reciprocal_radius=5.0, + ) + assert diffraction.simulations[0].coordinates.size == 252 + + def test_precession_full( + self, diffraction_calculator_precession_full, local_structure + ): + diffraction = diffraction_calculator_precession_full.calculate_ed_data( + local_structure, + reciprocal_radius=5.0, + ) + assert diffraction.simulations[0].coordinates.size == 252 + + def test_custom_shape_func(self, diffraction_calculator_custom, local_structure): + diffraction = diffraction_calculator_custom.calculate_ed_data( + local_structure, + reciprocal_radius=5.0, + ) + assert diffraction.simulations[0].coordinates.size == 52 + + def test_appropriate_scaling(self, diffraction_calculator: SimulationGenerator): + """Tests that doubling the unit cell halves the pattern spacing.""" + silicon = make_phase(5) + big_silicon = make_phase(10) + diffraction = diffraction_calculator.calculate_ed_data( + phase=silicon, reciprocal_radius=5.0 + ) + big_diffraction = diffraction_calculator.calculate_ed_data( + phase=big_silicon, reciprocal_radius=5.0 + ) + indices = [tuple(i) for i in diffraction.simulations[0].coordinates.hkl] + big_indices = [tuple(i) for i in big_diffraction.simulations[0].coordinates.hkl] + assert (2, 2, 0) in indices + assert (2, 2, 0) in big_indices + coordinates = diffraction.simulations[0].coordinates[indices.index((2, 2, 0))] + big_coordinates = big_diffraction.simulations[0].coordinates[ + big_indices.index((2, 2, 0)) + ] + assert np.allclose(coordinates.data, big_coordinates.data * 2) + + def test_appropriate_intensities(self, diffraction_calculator, local_structure): + """Tests the central beam is strongest.""" + diffraction = diffraction_calculator.calculate_ed_data( + local_structure, reciprocal_radius=0.5, with_direct_beam=False + ) # direct beam doesn't work + indices = [ + tuple(np.round(i).astype(int)) + for i in diffraction.simulations[0].coordinates.hkl + ] + central_beam = indices.index((0, 1, 0)) + + smaller = np.greater_equal( + diffraction.simulations[0].intensities[central_beam], + diffraction.simulations[0].intensities, + ) + assert np.all(smaller) + + def test_shape_factor_strings(self, diffraction_calculator, local_structure): + _ = diffraction_calculator.calculate_ed_data( + local_structure, + ) + + def test_shape_factor_custom(self, diffraction_calculator, local_structure): + t1 = diffraction_calculator.calculate_ed_data( + local_structure, max_excitation_error=0.02 + ) + t2 = diffraction_calculator.calculate_ed_data( + local_structure, max_excitation_error=0.4 + ) + + # softly makes sure the two sims are different + assert np.sum(t1.simulations[0].intensities) != np.sum( + t2.simulations[0].intensities + ) + + def test_calculate_profile_class(self, local_structure, diffraction_calculator): + # tests the non-hexagonal (cubic) case + profile = diffraction_calculator.calculate_profile_data( + local_structure, reciprocal_radius=1.0 + ) + assert isinstance(profile, ProfileSimulation) + + latt = diffpy.structure.lattice.Lattice(3, 3, 5, 90, 90, 120) + atom = diffpy.structure.atom.Atom(atype="Ni", xyz=[0, 0, 0], lattice=latt) + hexagonal_structure = diffpy.structure.Structure(atoms=[atom], lattice=latt) + phase = Phase(structure=hexagonal_structure, space_group=194) + hexagonal_profile = diffraction_calculator.calculate_profile_data( + phase=phase, reciprocal_radius=1.0 + ) + assert isinstance(hexagonal_profile, ProfileSimulation) + + +@pytest.mark.parametrize("scattering_param", ["lobato", "xtables"]) +def test_param_check(scattering_param): + generator = SimulationGenerator(300, scattering_params=scattering_param) + + +@pytest.mark.xfail(raises=NotImplementedError) +def test_invalid_scattering_params(): + scattering_param = "_empty" + generator = SimulationGenerator(300, scattering_params=scattering_param) + + +@pytest.mark.xfail(faises=NotImplementedError) +def test_invalid_shape_model(): + generator = SimulationGenerator(300, shape_factor_model="dracula") diff --git a/diffsims/tests/simulations/test_simulations.py b/diffsims/tests/simulations/test_simulations.py new file mode 100644 index 00000000..3a2f7f0f --- /dev/null +++ b/diffsims/tests/simulations/test_simulations.py @@ -0,0 +1,297 @@ +# -*- 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 +import pytest + +from diffpy.structure import Structure, Atom, Lattice +from orix.crystal_map import Phase + +from diffsims.crystallography import ReciprocalLatticeVector +from diffsims.simulations.simulation import DiffractionSimulation + +@pytest.fixture +def profile_simulation(): + return ProfileSimulation( + magnitudes=[ + 0.31891931643691351, + 0.52079306292509475, + 0.6106839974876449, + 0.73651261277849378, + 0.80259601243613932, + 0.9020400452156796, + 0.95675794931074043, + 1.0415861258501895, + 1.0893168446141808, + 1.1645286909108374, + 1.2074090451670043, + 1.2756772657476541, + ], + intensities=np.array( + [ + 100.0, + 99.34619104, + 64.1846346, + 18.57137199, + 28.84307971, + 41.31084268, + 23.42104951, + 13.996264, + 24.87559364, + 20.85636003, + 9.46737774, + 5.43222307, + ] + ), + hkls=[ + (1, 1, 1), + (2, 2, 0), + (3, 1, 1), + (4, 0, 0), + (3, 3, 1), + (4, 2, 2), + (3, 3, 3), + (4, 4, 0), + (5, 3, 1), + (6, 2, 0), + (5, 3, 3), + (4, 4, 4), + ], + ) + + +def test_plot_profile_simulation(profile_simulation): + profile_simulation.get_plot() + + +class TestDiffractionSimulation: + @pytest.fixture + def al_phase(self): + p = Phase( + name="al", + space_group=225, + structure=Structure( + atoms=[Atom("al", [0, 0, 0])], + lattice=Lattice(0.405, 0.405, 0.405, 90, 90, 90) + ) + ) + return p + + @pytest.fixture + def diffraction_simulation(self, al_phase): + vector = ReciprocalLatticeVector(phase=al_phase, + xyz=np.array([[0, 0, 0], + [1, 2, 3], + [3, 4, 5]])) + + return DiffractionSimulation(vector) + + @pytest.fixture + def diffraction_simulation_calibrated(self, al_phase): + vector = ReciprocalLatticeVector(phase=al_phase, + xyz=np.array([[0, 0, 0], + [1, 2, 3], + [3, 4, 5]])) + + return DiffractionSimulation(vector, calibration=0.5) + + def test_init(self, diffraction_simulation): + assert np.allclose( + diffraction_simulation.coordinates.data, np.array([[1, 2, 3], [3, 4, 5]]) + ) + assert diffraction_simulation.coordinates.hkl.shape == (2, 3) + assert np.isnan(diffraction_simulation.coordinates.intensity).all() + assert diffraction_simulation.coordinates.intensity.shape == (2,) + assert diffraction_simulation.calibration is None + assert len(diffraction_simulation) == 2 + + def test_single_spot(self, al_phase): + rlv = ReciprocalLatticeVector(phase=al_phase, xyz=np.array([[1, 2, 3]])) + assert DiffractionSimulation(rlv).coordinates.data.shape == (1, 3) + + def test_get_item(self, diffraction_simulation): + assert diffraction_simulation[1].coordinates.data.shape == (1, 3) + assert diffraction_simulation[0:2].coordinates.data.shape == (2, 3) + + def test_append(self, diffraction_simulation): + sim = diffraction_simulation.append(diffraction_simulation) + assert np.allclose( + sim.coordinates.data, + np.array( + [[1., 2., 3.], + [3., 4., 5.], + [1., 2., 3.], + [3., 4., 5.]] + ), + ) + assert sim.size == 4 + + def test_indices_setter_getter(self, diffraction_simulation): + indices = np.array([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) + diffraction_simulation.indices = indices[-1] + assert np.allclose(diffraction_simulation.indices, indices[-1]) + diffraction_simulation.with_direct_beam = True + diffraction_simulation.indices = indices + assert np.allclose(diffraction_simulation.indices, indices) + + def test_coordinates_setter(self, diffraction_simulation): + starting_coords = diffraction_simulation.coordinates[-1] + diffraction_simulation.coordinates = diffraction_simulation.coordinates[-1] + assert np.allclose(diffraction_simulation.coordinates.data, starting_coords.data) + diffraction_simulation.with_direct_beam = True + + def test_intensities_setter(self, diffraction_simulation): + ints = np.array([1, 2, 3]) + diffraction_simulation.intensities = ints[-1] + assert np.allclose(diffraction_simulation.intensities, ints[-1]) + diffraction_simulation.with_direct_beam = True + diffraction_simulation.intensities = ints + assert np.allclose(diffraction_simulation.intensities, ints) + + @pytest.mark.parametrize( + "calibration, expected", + [ + (5.0, np.array((5.0, 5.0))), + pytest.param(0, (0, 0), marks=pytest.mark.xfail(raises=ValueError)), + pytest.param((0, 0), (0, 0), marks=pytest.mark.xfail(raises=ValueError)), + ((1.5, 1.5), np.array((1.5, 1.5))), + ((1.3, 1.5), np.array((1.3, 1.5))), + ], + ) + def test_calibration(self, diffraction_simulation, calibration, expected): + diffraction_simulation.calibration = calibration + assert np.allclose(diffraction_simulation.calibration, expected) + + @pytest.mark.parametrize( + "coordinates, with_direct_beam, expected", + [ + ( + np.array([[-1, 0, 0], [0, 0, 0], [1, 0, 0]]), + False, + np.array([True, False, True]), + ), + ( + np.array([[-1, 0, 0], [0, 0, 0], [1, 0, 0]]), + True, + np.array([True, True, True]), + ), + (np.array([[-1, 0, 0], [1, 0, 0]]), False, np.array([True, True])), + ], + ) + def test_direct_beam_mask(self, al_phase, coordinates, with_direct_beam, expected): + vect =ReciprocalLatticeVector(phase=al_phase, xyz=coordinates) + diffraction_simulation = DiffractionSimulation(vect, + with_direct_beam=with_direct_beam) + diffraction_simulation.with_direct_beam = with_direct_beam + mask = diffraction_simulation.direct_beam_mask + assert np.all(mask == expected) + + @pytest.mark.parametrize( + "coordinates, calibration, offset, expected", + [ + ( + np.array([[1.0, 0.0, 0.0], [1.0, 2.0, 0.0]]), + 1.0, + (0.0, 0.0), + np.array([[1.0, 0.0], [1.0, 2.0]]), + ), + ( + np.array([[1.0, 0.0, 0.0], [1.0, 2.0, 0.0]]), + 2.0, + (3.0, 1.0), + np.array([[2.0, 0.5], [2.0, 1.5]]), + ), + pytest.param( + np.array([[1.0, 0.0, 0.0], [1.0, 2.0, 0.0]]), + None, + (0.0, 0.0), + None, + marks=pytest.mark.xfail(raises=Exception), + ), + ], + ) + def test_calibrated_coordinates( + self, + al_phase, + coordinates, + calibration, + offset, + expected, + ): + vect =ReciprocalLatticeVector(phase=al_phase, xyz=coordinates) + diffraction_simulation = DiffractionSimulation(vect) + diffraction_simulation.calibration = calibration + diffraction_simulation.offset = offset + assert np.allclose(diffraction_simulation.calibrated_coordinates, expected) + + @pytest.mark.parametrize( + "units, expected", + [ + ("real", np.array([[-2, 1, 3], [-4, 3, 5]])), + ("pixel", np.array([[-2, 1, 3], [-4, 3, 5]])), + ], + ) + def test_transform_coordinates( + self, diffraction_simulation_calibrated, units, expected + ): + tc = diffraction_simulation_calibrated._get_transformed_coordinates( + 90, units=units + ) + assert np.allclose(tc.coordinates.data, expected) + + def test_rotate_shift_coordinates(self, diffraction_simulation): + rot = diffraction_simulation.rotate_shift_coordinates(90) + assert np.allclose(rot.coordinates.data, np.array([[-2, 1, 3], [-4, 3, 5]]) + ) + + def test_assertion_free_get_diffraction_pattern(self, al_phase): + vect =ReciprocalLatticeVector(phase=al_phase, xyz=np.array([[0.3, 1.2, 0]])) + vect.intensity = np.ones(1) + short_sim = DiffractionSimulation(vect, calibration=[1, 2]) + + z = short_sim.get_diffraction_pattern() + + vect =ReciprocalLatticeVector(phase=al_phase, xyz=np.asarray([[0.3, 1000, 0]])) + vect.intensity = np.ones(1) + empty_sim = DiffractionSimulation(vect, calibration=[1, 2]) + + z = empty_sim.get_diffraction_pattern(shape=(10, 20)) + + def test_get_as_mask(self, al_phase): + vect = ReciprocalLatticeVector(phase=al_phase, xyz=np.asarray([[0.3, 1.2, 0]])) + vect.intensity = np.ones(1) + short_sim = DiffractionSimulation(vect, calibration=[1, 2]) + mask = short_sim.get_as_mask( + (20, 10), + radius_function=np.sqrt, + ) + assert mask.shape[0] == 20 + assert mask.shape[1] == 10 + + @pytest.mark.parametrize("units_in", ["pixel", "real"]) + def test_plot_method(self,al_phase, units_in): + vect = ReciprocalLatticeVector(phase=al_phase, xyz=np.asarray( + [ + [0.3, 1.2, 0], + [-2, 3, 0], + [2.1, 3.4, 0], + ] + ),) + vect.intensity = np.array([3.0, 5.0, 2.0]) + short_sim = DiffractionSimulation(coordinates=vect, calibration=[1, 2]) + ax, sp = short_sim.plot(units=units_in, show_labels=True) \ No newline at end of file diff --git a/diffsims/utils/shape_factor_models.py b/diffsims/utils/shape_factor_models.py index 97015c4a..aa271ec1 100644 --- a/diffsims/utils/shape_factor_models.py +++ b/diffsims/utils/shape_factor_models.py @@ -17,6 +17,7 @@ # along with diffsims. If not, see . import numpy as np +from scipy.integrate import quad __all__ = [ @@ -69,7 +70,7 @@ def linear(excitation_error, max_excitation_error): if isinstance(excitation_error, np.ndarray): sf[sf < 0.0] = 0.0 else: - sf = max(sf, 0.) + sf = max(sf, 0.0) return sf @@ -178,7 +179,7 @@ def lorentzian(excitation_error, max_excitation_error): sigma = np.pi / max_excitation_error fac = ( sigma - / (np.pi * (sigma ** 2 * excitation_error ** 2 + 1)) + / (np.pi * (sigma**2 * excitation_error**2 + 1)) * max_excitation_error ) return fac @@ -217,7 +218,57 @@ def lorentzian_precession( [1] L. Palatinus, P. Brázda, M. Jelínek, J. Hrdá, G. Steciuk, M. Klementová, Specifics of the data processing of precession electron diffraction tomography data and their implementation in the program PETS2.0, Acta Crystallogr. Sect. B Struct. Sci. Cryst. Eng. Mater. 75 (2019) 512–522. doi:10.1107/S2052520619007534. """ sigma = np.pi / max_excitation_error - u = sigma ** 2 * (r_spot ** 2 * precession_angle ** 2 - excitation_error ** 2) + 1 - z = np.sqrt(u ** 2 + 4 * sigma ** 2 * excitation_error ** 2) - fac = (sigma / np.pi) * np.sqrt(2 * (u + z) / z ** 2) + u = sigma**2 * (r_spot**2 * precession_angle**2 - excitation_error**2) + 1 + z = np.sqrt(u**2 + 4 * sigma**2 * excitation_error**2) + fac = (sigma / np.pi) * np.sqrt(2 * (u + z) / z**2) return fac + + +def _shape_factor_precession( + excitation_error, r_spot, phi, shape_function, max_excitation, **kwargs +): + """ + The rel-rod shape factors for reflections taking into account + precession + + Parameters + ---------- + excitation_error : np.ndarray (N,) + An array of excitation errors + r_spot : np.ndarray (N,) + An array representing the distance of spots from the z-axis in A^-1 + phi : float + The precession angle in radians + shape_function : callable + A function that describes the influence from the rel-rods. Should be + in the form func(excitation_error: np.ndarray, max_excitation: float, + **kwargs) + max_excitation : float + Parameter to describe the "extent" of the rel-rods. + + Other parameters + ---------------- + ** kwargs: passed directly to shape_function + + Notes + ----- + * We calculate excitation_error as z_spot - z_sphere so that it is + negative when the spot is outside the ewald sphere and positive when inside + conform W&C chapter 12, section 12.6 + * We assume that the sample is a thin infinitely wide slab perpendicular + to the optical axis, so that the shape factor function only depends on the + distance from each spot to the Ewald sphere parallel to the optical axis. + """ + shf = np.zeros(excitation_error.shape) + # loop over all spots + for i, (excitation_error_i, r_spot_i) in enumerate(zip(excitation_error, r_spot)): + + def integrand(theta): + # Equation 8 in L.Palatinus et al. Acta Cryst. (2019) B75, 512-522 + S_zero = excitation_error_i + variable_term = r_spot_i * (phi) * np.cos(theta) + return shape_function(S_zero + variable_term, max_excitation, **kwargs) + + # average factor integrated over the full revolution of the beam + shf[i] = (1 / (2 * np.pi)) * quad(integrand, 0, 2 * np.pi)[0] + return shf From 022716da035c8d76c11e4f0c128eccf2a4916e77 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Tue, 24 Oct 2023 15:29:19 -0500 Subject: [PATCH 07/48] Documentation: Add example for simulating one diffraction pattern --- .../simulating_one_diffraction_pattern.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py diff --git a/examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py b/examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py new file mode 100644 index 00000000..703b12e4 --- /dev/null +++ b/examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py @@ -0,0 +1,30 @@ +#################################### +# Simulating One Diffraction Pattern +# ================================== + +from orix.crystal_map import Phase +from orix.quaternion import Rotation +from diffpy.structure import Atom, Lattice, Structure + +from diffsims.generators.simulation_generator import SimulationGenerator + +a = 5.431 +latt = Lattice(a, a, a, 90, 90, 90) +atom_list = [] +for coords in [[0, 0, 0], [0.5, 0, 0.5], [0, 0.5, 0.5], [0.5, 0.5, 0]]: + x, y, z = coords[0], coords[1], coords[2] + atom_list.append(Atom(atype="Si", xyz=[x, y, z], lattice=latt)) # Motif part A + atom_list.append(Atom( + atype="Si", xyz=[x + 0.25, y + 0.25, z + 0.25], lattice=latt + ) + ) # Motif part B +struct = Structure(atoms=atom_list, lattice=latt) +p = Phase(structure=struct, space_group=227) + +gen = SimulationGenerator(accelerating_voltage=200,) +rot = Rotation.from_axes_angles([1, 0, 0], 45, degrees=True) # 45 degree rotation around x-axis +sim = gen.calculate_ed_data(phase=p, rotation=rot) + +sim.simulations[0].plot() # plot the first (and only) diffraction pattern + +#%% From 4630e69bb79dc61f4d8c37d6518e7ba5ea4c00b6 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Tue, 24 Oct 2023 15:34:34 -0500 Subject: [PATCH 08/48] Deprecation: Deprecate old code --- diffsims/generators/diffraction_generator.py | 4 ++++ diffsims/generators/library_generator.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/diffsims/generators/diffraction_generator.py b/diffsims/generators/diffraction_generator.py index 69cc11de..85727903 100644 --- a/diffsims/generators/diffraction_generator.py +++ b/diffsims/generators/diffraction_generator.py @@ -41,6 +41,7 @@ lorentzian_precession, ) +from diffsims.utils._deprecated import deprecated __all__ = [ "AtomicDiffractionGenerator", @@ -153,6 +154,9 @@ class DiffractionGenerator(object): `custom shape_factor_model` is used. """ + @deprecated(since="0.6.0", + removal="0.7.0", + alternative="diffsims.generators.simulation_generator.SimulationGenerator") def __init__( self, accelerating_voltage, diff --git a/diffsims/generators/library_generator.py b/diffsims/generators/library_generator.py index 82cefb11..908ac4ce 100644 --- a/diffsims/generators/library_generator.py +++ b/diffsims/generators/library_generator.py @@ -27,7 +27,7 @@ from diffsims.utils.sim_utils import get_points_in_sphere from diffsims.utils.vector_utils import get_angle_cartesian_vec - +from diffsims.utils._deprecated import deprecated __all__ = [ "DiffractionLibraryGenerator", @@ -39,7 +39,7 @@ class DiffractionLibraryGenerator: """Computes a library of electron diffraction patterns for specified atomic structures and orientations. """ - + @deprecated(since="0.6.0", alternative="Diffsims.generators.SimulationGenerator") def __init__(self, electron_diffraction_calculator): """Initialises the generator with a diffraction calculator. From 0833ed42b5b87d1774265056a6949191a91c21c8 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Wed, 25 Oct 2023 09:57:45 -0500 Subject: [PATCH 09/48] Rollback: Rollback code for finding orientations. --- diffsims/libraries/diffraction_library.py | 3 +-- diffsims/tests/simulations/test_simulations.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/diffsims/libraries/diffraction_library.py b/diffsims/libraries/diffraction_library.py index 77beff9a..e48490ba 100644 --- a/diffsims/libraries/diffraction_library.py +++ b/diffsims/libraries/diffraction_library.py @@ -91,8 +91,7 @@ def _get_library_entry_from_angles(library, phase, angles): """ phase_entry = library[phase] - orientations = phase_entry["orientations"].to_euler(degrees=True) - for orientation_index, orientation in enumerate(orientations): + for orientation_index, orientation in enumerate(phase_entry["orientations"]): if np.sum(np.abs(np.subtract(orientation, angles))) < 1e-2: return orientation_index diff --git a/diffsims/tests/simulations/test_simulations.py b/diffsims/tests/simulations/test_simulations.py index 3a2f7f0f..9be27645 100644 --- a/diffsims/tests/simulations/test_simulations.py +++ b/diffsims/tests/simulations/test_simulations.py @@ -23,7 +23,7 @@ from orix.crystal_map import Phase from diffsims.crystallography import ReciprocalLatticeVector -from diffsims.simulations.simulation import DiffractionSimulation +from diffsims.simulations.simulation import DiffractionSimulation, ProfileSimulation @pytest.fixture def profile_simulation(): @@ -76,7 +76,7 @@ def profile_simulation(): def test_plot_profile_simulation(profile_simulation): - profile_simulation.get_plot() + profile_simulation.plot() class TestDiffractionSimulation: From 5cc74f9ae6e4f857e1e1caf9e6ac5f252e4d79b8 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Wed, 25 Oct 2023 09:59:51 -0500 Subject: [PATCH 10/48] Formatting: Applied `black` formatting --- .../reciprocal_lattice_vector.py | 20 +++++--- diffsims/generators/simulation_generator.py | 50 ++++++++++--------- diffsims/libraries/diffraction_library.py | 27 +++++----- diffsims/simulations/simulation.py | 31 +++++++----- .../simulating_one_diffraction_pattern.py | 15 +++--- 5 files changed, 83 insertions(+), 60 deletions(-) diff --git a/diffsims/crystallography/reciprocal_lattice_vector.py b/diffsims/crystallography/reciprocal_lattice_vector.py index b3178d49..12f4dae7 100644 --- a/diffsims/crystallography/reciprocal_lattice_vector.py +++ b/diffsims/crystallography/reciprocal_lattice_vector.py @@ -145,10 +145,13 @@ def __getitem__(self, key): else: slic = self.intensity[key] if not hasattr(slic, "__len__"): - slic = np.array([slic, ]) + slic = np.array( + [ + slic, + ] + ) rlv_new._intensity = slic - return rlv_new def __repr__(self): @@ -519,15 +522,20 @@ def intensity(self): @intensity.setter def intensity(self, value): if not hasattr(value, "__len__"): - value = np.array([value, ] * self.size) + value = np.array( + [ + value, + ] + * self.size + ) if len(value) != self.size: raise ValueError("Length of intensity array must match number of vectors") self._intensity = np.array(value) def rotate_from_matrix(self, rotation_matrix): - return ReciprocalLatticeVector(phase=self.phase, - xyz=np.matmul(rotation_matrix, - self.data.T).T) + return ReciprocalLatticeVector( + phase=self.phase, xyz=np.matmul(rotation_matrix, self.data.T).T + ) @property def structure_factor(self): diff --git a/diffsims/generators/simulation_generator.py b/diffsims/generators/simulation_generator.py index 7fe177aa..43b129bf 100644 --- a/diffsims/generators/simulation_generator.py +++ b/diffsims/generators/simulation_generator.py @@ -15,7 +15,7 @@ sinc, sin2c, lorentzian_precession, - _shape_factor_precession + _shape_factor_precession, ) from diffsims.utils.sim_utils import ( @@ -74,8 +74,7 @@ def __init__( def calculate_ed_data( self, phase: Phase, - rotation: Rotation = Rotation.from_euler((0, 0, 0), - degrees=True), + rotation: Rotation = Rotation.from_euler((0, 0, 0), degrees=True), reciprocal_radius: float = 1.0, with_direct_beam: bool = True, max_excitation_error: float = 1e-2, @@ -122,9 +121,11 @@ def calculate_ed_data( debye_waller_factors = {} # Specify variables used in calculation wavelength = self.wavelength - recip = ReciprocalLatticeVector.from_min_dspacing(phase, - min_dspacing=1 / reciprocal_radius, - include_zero_beam=with_direct_beam) + recip = ReciprocalLatticeVector.from_min_dspacing( + phase, + min_dspacing=1 / reciprocal_radius, + include_zero_beam=with_direct_beam, + ) # Rotate using all the rotations in the list vectors = [] @@ -135,7 +136,7 @@ def calculate_ed_data( r_spot = np.sqrt(np.sum(np.square(rotated_vectors.data[:, :2]), axis=1)) z_spot = rotated_vectors.data[:, 2] - z_sphere = -np.sqrt(r_sphere ** 2 - r_spot ** 2) + r_sphere + z_sphere = -np.sqrt(r_sphere**2 - r_spot**2) + r_sphere excitation_error = z_sphere - z_spot # determine the pre-selection reflections @@ -147,10 +148,11 @@ def calculate_ed_data( P_z = r_sphere * np.cos(np.deg2rad(self.precession_angle)) P_t = r_sphere * np.sin(np.deg2rad(self.precession_angle)) # the extremes of the ewald sphere - z_surf_up = P_z - np.sqrt(r_sphere ** 2 - (r_spot + P_t) ** 2) - z_surf_do = P_z - np.sqrt(r_sphere ** 2 - (r_spot - P_t) ** 2) + z_surf_up = P_z - np.sqrt(r_sphere**2 - (r_spot + P_t) ** 2) + z_surf_do = P_z - np.sqrt(r_sphere**2 - (r_spot - P_t) ** 2) intersection = (z_spot - max_excitation_error <= z_surf_up) & ( - z_spot + max_excitation_error >= z_surf_do) + z_spot + max_excitation_error >= z_surf_do + ) # select these reflections intersected_vectors = rotated_vectors[intersection] @@ -198,18 +200,20 @@ def calculate_ed_data( intersected_vectors = intersected_vectors[peak_mask] intersected_vectors.intensity = intensities - sim = DiffractionSimulation(coordinates=intersected_vectors, - with_direct_beam=with_direct_beam,) + sim = DiffractionSimulation( + coordinates=intersected_vectors, + with_direct_beam=with_direct_beam, + ) vectors.append(sim) - lib = SimulationLibrary(phase=phase, - rotations=rotation, - diffraction_generator=self, - simulations=vectors, - ) + lib = SimulationLibrary( + phase=phase, + rotations=rotation, + diffraction_generator=self, + simulations=vectors, + ) return lib - def calculate_profile_data( self, phase: Phase, @@ -242,9 +246,10 @@ def calculate_profile_data( latt = phase.structure.lattice # Obtain crystallographic reciprocal lattice points within range - vectors = ReciprocalLatticeVector.from_min_dspacing(phase, - min_dspacing=1 / reciprocal_radius, - ) + vectors = ReciprocalLatticeVector.from_min_dspacing( + phase, + min_dspacing=1 / reciprocal_radius, + ) unique_vectors = vectors.unique(use_symmetry=True).symmetrise() @@ -252,7 +257,6 @@ def calculate_profile_data( g_indices = unique_vectors.hkl g_hkls = unique_vectors.gspacing - i_hkl = get_kinematical_intensities( phase.structure, g_indices, @@ -292,4 +296,4 @@ def calculate_profile_data( y = np.asarray(y) / max(y) * 100 - return ProfileSimulation(x, y, hkls) \ No newline at end of file + return ProfileSimulation(x, y, hkls) diff --git a/diffsims/libraries/diffraction_library.py b/diffsims/libraries/diffraction_library.py index e48490ba..90f1cd5b 100644 --- a/diffsims/libraries/diffraction_library.py +++ b/diffsims/libraries/diffraction_library.py @@ -26,6 +26,7 @@ from diffsims.sims.diffraction_simulation import DiffractionSimulation from diffsims.generators.diffraction_generator import DiffractionGenerator + __all__ = [ "DiffractionLibrary", "load_DiffractionLibrary", @@ -109,13 +110,17 @@ class SimulationLibrary(NamedTuple): str_rotations: Sequence[str] = None def __repr__(self): - return (f"DiffractionPhaseLibrary(phase={self.phase.name}," - f" No. Rotations={self.__len__()})") + return ( + f"DiffractionPhaseLibrary(phase={self.phase.name}," + f" No. Rotations={self.__len__()})" + ) def __post_init__(self): if len(self.rotations) != len(self.simulations): raise ValueError("Number of rotations and simulations must be the same") - if self.str_rotations is not None and len(self.rotations) != len(self.str_rotations): + if self.str_rotations is not None and len(self.rotations) != len( + self.str_rotations + ): raise ValueError("Number of rotations and str_rotations must be the same") def __len__(self): @@ -124,14 +129,13 @@ def __len__(self): def __getitem__(self, item): if isinstance(item, str): item = self.str_rotations.index(item) - return SimulationLibrary(self.phase, - self.rotations[item], - self.simulations[item] - ) - - def get_library_entry(self, - rotation: Rotation, - angle_cutoff: float = 1e-2) -> 'DiffractionPhaseLibrary': + return SimulationLibrary( + self.phase, self.rotations[item], self.simulations[item] + ) + + def get_library_entry( + self, rotation: Rotation, angle_cutoff: float = 1e-2 + ) -> "DiffractionPhaseLibrary": angles = self.rotations.angle_with(rotation) is_in_range = np.sum(np.abs(angles), axis=1) < angle_cutoff return self[is_in_range] @@ -151,7 +155,6 @@ def __repr__(self): return f"DiffractionLibrary)" - class DiffractionLibrary(dict): """Maps crystal structure (phase) and orientation to simulated diffraction data. diff --git a/diffsims/simulations/simulation.py b/diffsims/simulations/simulation.py index d828b1a6..4a4f63e1 100644 --- a/diffsims/simulations/simulation.py +++ b/diffsims/simulations/simulation.py @@ -66,13 +66,17 @@ def __getitem__(self, sliced): def deepcopy(self): return copy.deepcopy(self) - def append(self, - vectors: ReciprocalLatticeVector): + def append(self, vectors: ReciprocalLatticeVector): new_data = copy.deepcopy(self) - new_coords = np.concatenate((new_data._coordinates.data, vectors._coordinates.data), axis=0) - new_data._coordinates = ReciprocalLatticeVector(phase=self._coordinates.phase, xyz=new_coords) - new_data._coordinates.intensity = np.concatenate((self._coordinates.intensity, - vectors._coordinates.intensity)) + new_coords = np.concatenate( + (new_data._coordinates.data, vectors._coordinates.data), axis=0 + ) + new_data._coordinates = ReciprocalLatticeVector( + phase=self._coordinates.phase, xyz=new_coords + ) + new_data._coordinates.intensity = np.concatenate( + (self._coordinates.intensity, vectors._coordinates.intensity) + ) return new_data @property @@ -86,7 +90,9 @@ def calibrated_coordinates(self): @property def pixel_coordinates(self): half_shape = np.array(self.shape) / 2 - pixel_coordinates = np.rint(self.calibrated_coordinates[:, :2] + half_shape).astype(int) + pixel_coordinates = np.rint( + self.calibrated_coordinates[:, :2] + half_shape + ).astype(int) return pixel_coordinates @property @@ -119,7 +125,7 @@ def direct_beam_mask(self): if self.with_direct_beam: return np.ones_like(self._coordinates.intensity, dtype=bool) else: - mask = np.any(self._coordinates.data!=0, axis=1) + mask = np.any(self._coordinates.data != 0, axis=1) return mask @property @@ -153,7 +159,7 @@ def _get_transformed_coordinates( y = transformed_coords.data[:, 1] mirrored_factor = -1 if mirrored else 1 theta = mirrored_factor * np.arctan2(y, x) + np.deg2rad(angle) - rd = np.sqrt(x ** 2 + y ** 2) + rd = np.sqrt(x**2 + y**2) transformed_coords[:, 0] = rd * np.cos(theta) + cx transformed_coords[:, 1] = rd * np.sin(theta) + cy new_sim._coordinates = transformed_coords @@ -310,7 +316,7 @@ def plot( show_labels=False, label_offset=(0, 0), label_formatting={}, - min_label_intensity=.1, + min_label_intensity=0.1, ax=None, **kwargs, ): @@ -389,7 +395,6 @@ def plot( label_formatting["va"] = "center" for miller, coordinate, inten in zip(millers, coords, self.intensities): if inten > min_label_intensity: - label = "(" for index in miller: if index < 0: @@ -404,7 +409,7 @@ def plot( label, **label_formatting, ) - if units =="real": + if units == "real": ax.set_xlabel(r"$\AA^{-1}$") ax.set_ylabel(r"$\AA^{-1}$") else: @@ -458,4 +463,4 @@ def plot(self, annotate_peaks=True, with_labels=True, fontsize=12): ax.set_xlabel("A ($^{-1}$)") ax.set_ylabel("Intensities (scaled)") - return plt \ No newline at end of file + return plt diff --git a/examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py b/examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py index 703b12e4..aeda8afb 100644 --- a/examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py +++ b/examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py @@ -14,17 +14,20 @@ for coords in [[0, 0, 0], [0.5, 0, 0.5], [0, 0.5, 0.5], [0.5, 0.5, 0]]: x, y, z = coords[0], coords[1], coords[2] atom_list.append(Atom(atype="Si", xyz=[x, y, z], lattice=latt)) # Motif part A - atom_list.append(Atom( - atype="Si", xyz=[x + 0.25, y + 0.25, z + 0.25], lattice=latt - ) + atom_list.append( + Atom(atype="Si", xyz=[x + 0.25, y + 0.25, z + 0.25], lattice=latt) ) # Motif part B struct = Structure(atoms=atom_list, lattice=latt) p = Phase(structure=struct, space_group=227) -gen = SimulationGenerator(accelerating_voltage=200,) -rot = Rotation.from_axes_angles([1, 0, 0], 45, degrees=True) # 45 degree rotation around x-axis +gen = SimulationGenerator( + accelerating_voltage=200, +) +rot = Rotation.from_axes_angles( + [1, 0, 0], 45, degrees=True +) # 45 degree rotation around x-axis sim = gen.calculate_ed_data(phase=p, rotation=rot) sim.simulations[0].plot() # plot the first (and only) diffraction pattern -#%% +# %% From 02933f8c0ecec2f7fc501eac9017b7ea1b13fd6c Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Wed, 25 Oct 2023 10:11:26 -0500 Subject: [PATCH 11/48] NewFeature: Added ``get_polar_coordinates`` function to ``DiffractionSimulation`` --- diffsims/simulations/simulation.py | 13 +++++++++++++ diffsims/tests/simulations/test_simulations.py | 12 ++++++++++++ 2 files changed, 25 insertions(+) diff --git a/diffsims/simulations/simulation.py b/diffsims/simulations/simulation.py index 4a4f63e1..c91dff1e 100644 --- a/diffsims/simulations/simulation.py +++ b/diffsims/simulations/simulation.py @@ -117,6 +117,18 @@ def calibration(self, calibration): ) self._calibration = calibration + def get_polar_coordinates(self, real=True): + """Returns the polar coordinates of the diffraction pattern + """ + x = self.coordinates.data[:, 0] + y = self.coordinates.data[:, 1] + if not real: + x = x / self.calibration[0] + y = y / self.calibration[1] + r = np.sqrt(x**2 + y**2) + theta = np.arctan2(y, x) + return r, theta + @property def direct_beam_mask(self): """ndarray : If `with_direct_beam` is True, returns a True array for all @@ -418,6 +430,7 @@ def plot( return ax, sp + class ProfileSimulation: """Holds the result of a given kinematic simulation of a diffraction profile. diff --git a/diffsims/tests/simulations/test_simulations.py b/diffsims/tests/simulations/test_simulations.py index 9be27645..649aa93d 100644 --- a/diffsims/tests/simulations/test_simulations.py +++ b/diffsims/tests/simulations/test_simulations.py @@ -283,6 +283,18 @@ def test_get_as_mask(self, al_phase): assert mask.shape[0] == 20 assert mask.shape[1] == 10 + def test_polar_coordinates(self, al_phase): + vect = ReciprocalLatticeVector(phase=al_phase, + xyz=np.asarray([[1, 1, 0]])) + vect.intensity = np.ones(1) + short_sim = DiffractionSimulation(vect, calibration=[0.5, 0.5]) + r, t = short_sim.get_polar_coordinates(real=True) + assert r == [1.4142135623730951, ] + assert t == [0.7853981633974483, ] + r, t = short_sim.get_polar_coordinates(real=False) + assert r == [np.sqrt(8), ] + assert t == [0.7853981633974483, ] + @pytest.mark.parametrize("units_in", ["pixel", "real"]) def test_plot_method(self,al_phase, units_in): vect = ReciprocalLatticeVector(phase=al_phase, xyz=np.asarray( From c47bbe508a9be8c240f83e976db229b62d39525f Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Thu, 26 Oct 2023 14:07:46 -0500 Subject: [PATCH 12/48] Refactor: Sped up library Simulation --- diffsims/crystallography/reciprocal_lattice_vector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diffsims/crystallography/reciprocal_lattice_vector.py b/diffsims/crystallography/reciprocal_lattice_vector.py index 12f4dae7..88c2ed8b 100644 --- a/diffsims/crystallography/reciprocal_lattice_vector.py +++ b/diffsims/crystallography/reciprocal_lattice_vector.py @@ -125,8 +125,8 @@ def __init__(self, phase, xyz=None, hkl=None, hkil=None): self._intensity = np.full(self.shape, np.nan) def __getitem__(self, key): - miller_new = self.to_miller().__getitem__(key) - rlv_new = self.from_miller(miller_new) + new_data = self.data[key] + rlv_new = self.__class__(self.phase, xyz=new_data) if np.isnan(self.structure_factor).all(): rlv_new._structure_factor = np.full( From 9305fe4329799fadbc49cc4ba7387206dc96cf88 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Thu, 26 Oct 2023 14:08:12 -0500 Subject: [PATCH 13/48] Refactor: Moved SimulationLibraries --- diffsims/generators/simulation_generator.py | 4 +- diffsims/libraries/diffraction_library.py | 60 -------------- diffsims/libraries/simulation_library.py | 90 +++++++++++++++++++++ 3 files changed, 91 insertions(+), 63 deletions(-) create mode 100644 diffsims/libraries/simulation_library.py diff --git a/diffsims/generators/simulation_generator.py b/diffsims/generators/simulation_generator.py index 43b129bf..1f23edef 100644 --- a/diffsims/generators/simulation_generator.py +++ b/diffsims/generators/simulation_generator.py @@ -1,13 +1,11 @@ -from typing import Sequence, Union import numpy as np -import matplotlib.pyplot as plt from orix.quaternion import Rotation from orix.crystal_map import Phase from diffsims.crystallography import ReciprocalLatticeVector from diffsims.simulations.simulation import DiffractionSimulation, ProfileSimulation -from diffsims.libraries.diffraction_library import SimulationLibrary +from diffsims.libraries.simulation_library import SimulationLibrary from diffsims.utils.shape_factor_models import ( linear, atanc, diff --git a/diffsims/libraries/diffraction_library.py b/diffsims/libraries/diffraction_library.py index 90f1cd5b..e1cbf540 100644 --- a/diffsims/libraries/diffraction_library.py +++ b/diffsims/libraries/diffraction_library.py @@ -17,21 +17,14 @@ # along with diffsims. If not, see . import pickle -from typing import NamedTuple, Sequence, Set import numpy as np -from orix.quaternion import Rotation -from orix.crystal_map import Phase - -from diffsims.sims.diffraction_simulation import DiffractionSimulation from diffsims.generators.diffraction_generator import DiffractionGenerator __all__ = [ "DiffractionLibrary", "load_DiffractionLibrary", - "SimulationLibrary", - "SimulationLibraries", ] @@ -102,59 +95,6 @@ def _get_library_entry_from_angles(library, phase, angles): ) -class SimulationLibrary(NamedTuple): - phase: Phase - rotations: Rotation - diffraction_generator: DiffractionGenerator - simulations: Sequence[DiffractionSimulation] - str_rotations: Sequence[str] = None - - def __repr__(self): - return ( - f"DiffractionPhaseLibrary(phase={self.phase.name}," - f" No. Rotations={self.__len__()})" - ) - - def __post_init__(self): - if len(self.rotations) != len(self.simulations): - raise ValueError("Number of rotations and simulations must be the same") - if self.str_rotations is not None and len(self.rotations) != len( - self.str_rotations - ): - raise ValueError("Number of rotations and str_rotations must be the same") - - def __len__(self): - return len(self.rotations) - - def __getitem__(self, item): - if isinstance(item, str): - item = self.str_rotations.index(item) - return SimulationLibrary( - self.phase, self.rotations[item], self.simulations[item] - ) - - def get_library_entry( - self, rotation: Rotation, angle_cutoff: float = 1e-2 - ) -> "DiffractionPhaseLibrary": - angles = self.rotations.angle_with(rotation) - is_in_range = np.sum(np.abs(angles), axis=1) < angle_cutoff - return self[is_in_range] - - -class SimulationLibraries(dict): - """ - A dictionary containing all the structures and their associated rotations - """ - - def __init__(self, libraries: Sequence[SimulationLibrary]): - super().__init__() - for library in libraries: - self[library.phase.name] = library - - def __repr__(self): - return f"DiffractionLibrary)" - - class DiffractionLibrary(dict): """Maps crystal structure (phase) and orientation to simulated diffraction data. diff --git a/diffsims/libraries/simulation_library.py b/diffsims/libraries/simulation_library.py new file mode 100644 index 00000000..6eb951ea --- /dev/null +++ b/diffsims/libraries/simulation_library.py @@ -0,0 +1,90 @@ +from typing import NamedTuple, Sequence + +from orix.quaternion import Rotation +from orix.crystal_map import Phase +from orix.vector import Vector3d +import numpy as np + +from diffsims.sims.diffraction_simulation import DiffractionSimulation +from diffsims.generators.diffraction_generator import DiffractionGenerator + + +class SimulationLibrary(NamedTuple): + phase: Phase + rotations: Rotation + diffraction_generator: DiffractionGenerator + simulations: Sequence[DiffractionSimulation] + str_rotations: Sequence[str] = None + + def __repr__(self): + return ( + f"DiffractionPhaseLibrary(phase={self.phase.name}," + f" No. Rotations={self.__len__()})" + ) + + def __post_init__(self): + if len(self.rotations) != len(self.simulations): + raise ValueError("Number of rotations and simulations must be the same") + if self.str_rotations is not None and len(self.rotations) != len( + self.str_rotations + ): + raise ValueError("Number of rotations and str_rotations must be the same") + self.simulation_index = 0 # for interactive plotting + + def __len__(self): + return len(self.rotations) + + def __getitem__(self, item): + if isinstance(item, str): + item = self.str_rotations.index(item) + return SimulationLibrary( + self.phase, self.rotations[item], self.simulations[item] + ) + + def get_library_entry( + self, rotation: Rotation, angle_cutoff: float = 1e-2 + ) -> "DiffractionPhaseLibrary": + angles = self.rotations.angle_with(rotation) + is_in_range = np.sum(np.abs(angles), axis=1) < angle_cutoff + return self[is_in_range] + + def rotations_to_vectors(self, + beam_direction: Vector3d = None) -> Vector3d: + """Converts the rotations to vectors + + Parameters + ---------- + beam_direction + The beam direction used to determine the vectors based on the rotations + """ + if beam_direction is None: + beam_direction = Vector3d.zvector() + vectors = self.rotations * beam_direction + return vectors + + def plot_rotations(self, + beam_direction: Vector3d = None, + **kwargs): + """Plots all the diffraction patterns in the library + + Parameters + ---------- + beam_direction + The beam direction used to determine the vectors based on the rotations + """ + vectors = self.rotations_to_vectors(beam_direction) + vectors.scatter(**kwargs) + + +class SimulationLibraries(dict): + """ + A dictionary containing all the structures and their associated rotations + """ + + def __init__(self, libraries: Sequence[SimulationLibrary]): + super().__init__() + for library in libraries: + self[library.phase.name] = library + + def __repr__(self): + return f"DiffractionLibrary)" From 9436ef11e831a5eb681f0171616b0cf7db626789 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Sat, 28 Oct 2023 08:04:16 -0500 Subject: [PATCH 14/48] New Feature: Added Simulation class --- diffsims/generators/simulation_generator.py | 7 +- diffsims/libraries/simulation_library.py | 44 +++- diffsims/sims/diffraction_simulation.py | 2 +- diffsims/simulations/__init__.py | 2 +- diffsims/simulations/simulation.py | 225 ++++++++++++++---- diffsims/tests/simulations/test_simulation.py | 173 ++++++++++++++ .../tests/simulations/test_simulations.py | 76 +++--- .../simulating_one_diffraction_pattern.py | 6 +- 8 files changed, 442 insertions(+), 93 deletions(-) create mode 100644 diffsims/tests/simulations/test_simulation.py diff --git a/diffsims/generators/simulation_generator.py b/diffsims/generators/simulation_generator.py index 1f23edef..45614c5d 100644 --- a/diffsims/generators/simulation_generator.py +++ b/diffsims/generators/simulation_generator.py @@ -1,10 +1,11 @@ import numpy as np +import matplotlib.pyplot as plt from orix.quaternion import Rotation from orix.crystal_map import Phase from diffsims.crystallography import ReciprocalLatticeVector -from diffsims.simulations.simulation import DiffractionSimulation, ProfileSimulation +from diffsims.simulations.simulation import Simulation, ProfileSimulation from diffsims.libraries.simulation_library import SimulationLibrary from diffsims.utils.shape_factor_models import ( linear, @@ -69,6 +70,10 @@ def __init__( "implementations.".format(scattering_params) ) + def plot_ewald_sphere(self, ax=None): + if ax is None: + fig, ax = plt.subplots() + def calculate_ed_data( self, phase: Phase, diff --git a/diffsims/libraries/simulation_library.py b/diffsims/libraries/simulation_library.py index 6eb951ea..e7f6357d 100644 --- a/diffsims/libraries/simulation_library.py +++ b/diffsims/libraries/simulation_library.py @@ -1,20 +1,23 @@ -from typing import NamedTuple, Sequence +from typing import NamedTuple, Sequence, TYPE_CHECKING from orix.quaternion import Rotation from orix.crystal_map import Phase from orix.vector import Vector3d import numpy as np -from diffsims.sims.diffraction_simulation import DiffractionSimulation -from diffsims.generators.diffraction_generator import DiffractionGenerator +from diffsims.simulations.simulation import Simulation as DiffractionSimulation + +if TYPE_CHECKING: + from diffsims.generators.simulation_generator import SimulationGenerator class SimulationLibrary(NamedTuple): - phase: Phase - rotations: Rotation - diffraction_generator: DiffractionGenerator + phase: Sequence[Phase] + rotations: Sequence[Rotation] + diffraction_generator: "SimulationGenerator" simulations: Sequence[DiffractionSimulation] str_rotations: Sequence[str] = None + calibration: float = None def __repr__(self): return ( @@ -48,8 +51,7 @@ def get_library_entry( is_in_range = np.sum(np.abs(angles), axis=1) < angle_cutoff return self[is_in_range] - def rotations_to_vectors(self, - beam_direction: Vector3d = None) -> Vector3d: + def rotations_to_vectors(self, beam_direction: Vector3d = None) -> Vector3d: """Converts the rotations to vectors Parameters @@ -62,9 +64,11 @@ def rotations_to_vectors(self, vectors = self.rotations * beam_direction return vectors - def plot_rotations(self, - beam_direction: Vector3d = None, - **kwargs): + def max_num_spots(self): + """Returns the maximum number of spots in the library""" + return max([i.intensities.shape[0] for i in self.simulations]) + + def plot_rotations(self, beam_direction: Vector3d = None, **kwargs): """Plots all the diffraction patterns in the library Parameters @@ -75,6 +79,24 @@ def plot_rotations(self, vectors = self.rotations_to_vectors(beam_direction) vectors.scatter(**kwargs) + def polar_flatten_simulations(self): + """Flatten the simulations into arrays of shape (n_simulations, max(num_diffraction_spots)) + for the polar coordinates (r,theta, intensity) of the diffraction spots + """ + max_num_spots = self.max_num_spots() + + r = np.zeros((len(self), max_num_spots)) + theta = np.zeros((len(self), max_num_spots)) + intensity = np.zeros((len(self), max_num_spots)) + + for i, sim in enumerate(self.simulations): + ( + r[i, : sim.intensities.shape[0]], + theta[i, : sim.intensities.shape[0]], + ) = sim.get_polar_coordinates() + intensity[i, : sim.intensities.shape[0]] = sim.intensities + return r, theta, intensity + class SimulationLibraries(dict): """ diff --git a/diffsims/sims/diffraction_simulation.py b/diffsims/sims/diffraction_simulation.py index 8f9fdb83..1df7745f 100644 --- a/diffsims/sims/diffraction_simulation.py +++ b/diffsims/sims/diffraction_simulation.py @@ -210,7 +210,7 @@ def _get_transformed_coordinates( y = coords_transformed[:, 1] mirrored_factor = -1 if mirrored else 1 theta = mirrored_factor * np.arctan2(y, x) + np.deg2rad(angle) - rd = np.sqrt(x ** 2 + y ** 2) + rd = np.sqrt(x**2 + y**2) coords_transformed[:, 0] = rd * np.cos(theta) + cx coords_transformed[:, 1] = rd * np.sin(theta) + cy return coords_transformed diff --git a/diffsims/simulations/__init__.py b/diffsims/simulations/__init__.py index ec23448b..8c1ee119 100644 --- a/diffsims/simulations/__init__.py +++ b/diffsims/simulations/__init__.py @@ -1 +1 @@ -from diffsims.simulations.simulation import DiffractionSimulation \ No newline at end of file +from diffsims.simulations.simulation import Simulation diff --git a/diffsims/simulations/simulation.py b/diffsims/simulations/simulation.py index c91dff1e..7350c435 100644 --- a/diffsims/simulations/simulation.py +++ b/diffsims/simulations/simulation.py @@ -1,25 +1,101 @@ +from typing import Union, Sequence, TYPE_CHECKING import copy import numpy as np import matplotlib.pyplot as plt +from orix.crystal_map import Phase +from orix.quaternion import Rotation from diffsims.crystallography.reciprocal_lattice_vector import ReciprocalLatticeVector from diffsims.pattern.detector_functions import add_shot_and_point_spread from diffsims.utils import mask_utils +if TYPE_CHECKING: + from diffsims.generators.simulation_generator import SimulationGenerator -class DiffractionSimulation: - """Holds the result of a kinematic diffraction pattern simulation. + +class PhaseGetter: + """A class for getting the phases of a simulation library. + + Parameters + ---------- + phases : Sequence[Phase] + The phases in the library. + """ + + def __init__(self, simulation): + self.simulation = simulation + + def __getitem__(self, item): + all_phases = self.simulation.phases + if isinstance(all_phases, Phase): + raise ValueError("Only one phase in the simulation") + elif isinstance(item, str): + ind = [phase.name for phase in all_phases].index(item) + elif isinstance(item, (int, slice)): + ind = item + else: + raise ValueError("Item must be a string or integer") + new_coords = self.simulation.coordinates[ind] + new_rotations = self.simulation.rotations[ind] + new_phases = all_phases[ind] + return Simulation( + phases=new_phases, + coordinates=new_coords, + rotations=new_rotations, + simulation_generator=self.simulation.simulation_generator, + calibration=self.simulation.calibration, + offset=self.simulation.offset, + with_direct_beam=self.simulation.with_direct_beam, + shape=self.simulation.shape, + ) + + +class RotationGetter: + """A class for getting a Rotation of a simulation library. + + Parameters + ---------- + phases : Sequence[Phase] + The phases in the library. + """ + + def __init__(self, simulation): + self.simulation = simulation + + def __getitem__(self, item): + all_phases = self.simulation.phases + if isinstance(self.simulation.coordinates, ReciprocalLatticeVector): + raise ValueError("Only one rotation in the simulation") + elif isinstance(all_phases, Phase): # only one phase in the simulation + coords = self.simulation.coordinates[item] + phases = self.simulation.phases + rotations = self.simulation.rotations[item] + else: # multiple phases in the simulation + coords = [c[item] for c in self.simulation.coordinates] + phases = self.simulation.phases + rotations = [rot[item] for rot in self.simulation.rotations] + return Simulation( + phases=phases, + coordinates=coords, + rotations=rotations, + simulation_generator=self.simulation.simulation_generator, + calibration=self.simulation.calibration, + offset=self.simulation.offset, + with_direct_beam=self.simulation.with_direct_beam, + shape=self.simulation.shape, + ) + + +class Simulation: + """Holds the result of a kinematic diffraction simulation for some phase + and rotation. Parameters ---------- - coordinates : array-like, shape [n_points, 2] + coordinates : ragged ndarray, shape [n_points] The x-y coordinates of points in reciprocal space. - indices : array-like, shape [n_points, 3] - The indices of the reciprocal lattice points that intersect the - Ewald sphere. - intensities : array-like, shape [n_points, ] - The intensity of the reciprocal lattice points. + calibration : float or tuple of float, optional The x- and y-scales of the pattern, with respect to the original reciprocal angstrom coordinates. @@ -30,21 +106,59 @@ class DiffractionSimulation: def __init__( self, - coordinates: ReciprocalLatticeVector, - calibration=None, - offset=(0.0, 0.0), - with_direct_beam=False, - shape=(512, 512), + phases: Sequence[Phase], + coordinates: Union[ + Sequence[ReciprocalLatticeVector], + Sequence[Sequence[ReciprocalLatticeVector]], + ], + rotations: Union[Rotation, Sequence[Rotation]], + simulation_generator: "SimulationGenerator", + calibration: Sequence[float] = (0.1, 0.1), + offset: Sequence[float] = (0.0, 0.0), + with_direct_beam: bool = False, + shape: Sequence[int] = (512, 512), ): """Initializes the DiffractionSimulation object with data values for the coordinates, indices, intensities, calibration and offset. """ - self._coordinates = coordinates - self.shape = shape + # Basic data + if not isinstance(phases, Phase): + phases = np.array(phases) + if not isinstance(rotations, Rotation): + rotations = np.array(rotations) + if not isinstance(coordinates, ReciprocalLatticeVector): + coordinates = np.array(coordinates, dtype=object) + self.phases = phases + self.rotations = rotations + self.coordinates = coordinates + self.simulation_generator = simulation_generator + + # Data for integrating with real data from a detector self.calibration = calibration + self.shape = shape self.offset = np.array(offset) self.with_direct_beam = with_direct_beam + # for interactive plotting and iterating through the Simulations + self.phase_index = 0 + self.rotation_index = 0 + + # for slicing a simulation + self.iphase = PhaseGetter(self) + self.irot = RotationGetter(self) + + def __iter__(self): + return self + + def __next__(self): + if self.rotation_index <= len(self.coordinates[self.phase_index]) - 1: + self.rotation_index += 1 + elif self.phase_index <= len(self.phase) - 1: + self.phase_index += 1 + else: + raise StopIteration + return self.coordinates[self.phase_index][self.rotation_index] + def __len__(self): return self.coordinates.shape[0] @@ -118,8 +232,7 @@ def calibration(self, calibration): self._calibration = calibration def get_polar_coordinates(self, real=True): - """Returns the polar coordinates of the diffraction pattern - """ + """Returns the polar coordinates of the diffraction pattern""" x = self.coordinates.data[:, 0] y = self.coordinates.data[:, 1] if not real: @@ -140,32 +253,16 @@ def direct_beam_mask(self): mask = np.any(self._coordinates.data != 0, axis=1) return mask - @property - def coordinates(self): - """ndarray : The coordinates of all unmasked points.""" - return self._coordinates[self.direct_beam_mask] - - @coordinates.setter - def coordinates(self, coordinates): - self._coordinates = coordinates - - @property - def intensities(self): - return self.coordinates.intensity - - @intensities.setter - def intensities(self, intensities): - self._coordinates.intensity = intensities - print(self.coordinates.intensity) - def _get_transformed_coordinates( self, angle, center=(0, 0), mirrored=False, units="real" ): """Translate, rotate or mirror the pattern spot coordinates""" + + coords = self.get_current_coordinates() + if units != "real": center = np.array(center) / self.calibration - new_sim = self.deepcopy() - transformed_coords = new_sim.coordinates + transformed_coords = coords cx, cy = center x = transformed_coords.data[:, 0] y = transformed_coords.data[:, 1] @@ -174,8 +271,7 @@ def _get_transformed_coordinates( rd = np.sqrt(x**2 + y**2) transformed_coords[:, 0] = rd * np.cos(theta) + cx transformed_coords[:, 1] = rd * np.sin(theta) + cy - new_sim._coordinates = transformed_coords - return new_sim + return transformed_coords def rotate_shift_coordinates(self, angle, center=(0, 0), mirrored=False): """ @@ -318,6 +414,48 @@ def get_diffraction_pattern( pattern = add_shot_and_point_spread(pattern.T, sigma, shot_noise=False) return np.divide(pattern, np.max(pattern)) + @property + def num_phases(self): + """Returns the number of phases in the simulation""" + if hasattr(self.phases, "__len__"): + return len(self.phases) + else: + return 0 + + @property + def num_vectors(self): + num_phases = self.num_phases + if isinstance(self.coordinates, ReciprocalLatticeVector): + return 0 + elif hasattr(self.coordinates, "__len__") and num_phases == 0: + return (len(self.coordinates),) + else: # hasattr(self.coordinates, "__len__") and num_phases>0: + return tuple( + [len(c) if hasattr(c, "__len__") else 0 for c in self.coordinates] + ) + + @property + def has_multiple_phases(self): + """Returns True if the simulation has multiple phases""" + return self.num_phases > 1 + + @property + def has_multiple_vectors(self): + """Returns True if the simulation has multiple vectors""" + if isinstance(self.coordinates, ReciprocalLatticeVector): + return False + else: + return True + + def get_current_coordinates(self): + """Returns the coordinates of the current phase and rotation""" + if self.has_multiple_phases: + return self.coordinates[self.phase_index][self.rotation_index] + elif not self.has_multiple_phases and self.has_multiple_vectors: + return self.coordinates[self.rotation_index] + else: + return self.coordinates + def plot( self, size_factor=1, @@ -378,14 +516,14 @@ def plot( in_plane_angle, direct_beam_position, mirrored, units=units ) sp = ax.scatter( - coords.coordinates.data[:, 0], - coords.coordinates.data[:, 1], - s=size_factor * np.sqrt(self.intensities), + coords.data[:, 0], + coords.data[:, 1], + s=size_factor * np.sqrt(coords.intensity), **kwargs, ) if show_labels: - millers = self.coordinates.hkl.astype(np.int16) + millers = coords.hkl.astype(np.int16) # only label the points inside the axes xlim = ax.get_xlim() ylim = ax.get_ylim() @@ -430,7 +568,6 @@ def plot( return ax, sp - class ProfileSimulation: """Holds the result of a given kinematic simulation of a diffraction profile. diff --git a/diffsims/tests/simulations/test_simulation.py b/diffsims/tests/simulations/test_simulation.py new file mode 100644 index 00000000..a9c33780 --- /dev/null +++ b/diffsims/tests/simulations/test_simulation.py @@ -0,0 +1,173 @@ +import numpy as np +import pytest + +from diffpy.structure import Structure, Atom, Lattice +from orix.crystal_map import Phase +from orix.quaternion import Rotation + +from diffsims.simulations.simulation import Simulation +from diffsims.generators.simulation_generator import SimulationGenerator +from diffsims.crystallography.reciprocal_lattice_vector import ReciprocalLatticeVector + + +class TestSingleSimulation: + @pytest.fixture + def al_phase(self): + p = Phase( + name="al", + space_group=225, + structure=Structure( + atoms=[Atom("al", [0, 0, 0])], + lattice=Lattice(0.405, 0.405, 0.405, 90, 90, 90), + ), + ) + return p + + @pytest.fixture + def single_simulation(self, al_phase): + gen = SimulationGenerator(accelerating_voltage=200) + rot = Rotation.from_axes_angles([1, 0, 0], 45, degrees=True) + coords = ReciprocalLatticeVector( + phase=al_phase, xyz=[[1, 0, 0], [0, 1, 0], [1, 1, 0]] + ) + sim = Simulation( + phases=al_phase, simulation_generator=gen, coordinates=coords, rotations=rot + ) + return sim + + def test_init(self, single_simulation): + assert isinstance(single_simulation, Simulation) + assert isinstance(single_simulation.phases, Phase) + assert isinstance(single_simulation.simulation_generator, SimulationGenerator) + assert isinstance(single_simulation.rotations, Rotation) + + def test_iphase(self, single_simulation): + with pytest.raises(ValueError): + single_simulation.iphase[0] + + def test_plot(self, single_simulation): + single_simulation.plot() + + +class TestSinglePhaseMultiSimulation: + @pytest.fixture + def al_phase(self): + p = Phase( + name="al", + space_group=225, + structure=Structure( + atoms=[Atom("al", [0, 0, 0])], + lattice=Lattice(0.405, 0.405, 0.405, 90, 90, 90), + ), + ) + return p + + @pytest.fixture + def multi_simulation(self, al_phase): + gen = SimulationGenerator(accelerating_voltage=200) + rot = Rotation.from_axes_angles([1, 0, 0], (0, 15, 30, 45), degrees=True) + coords = ReciprocalLatticeVector( + phase=al_phase, xyz=[[1, 0, 0], [0, 1, 0], [1, 1, 0]] + ) + coords = [ + coords, + ] * 4 + sim = Simulation( + phases=al_phase, simulation_generator=gen, coordinates=coords, rotations=rot + ) + return sim + + def test_init(self, multi_simulation): + assert isinstance(multi_simulation, Simulation) + assert isinstance(multi_simulation.phases, Phase) + assert isinstance(multi_simulation.simulation_generator, SimulationGenerator) + assert isinstance(multi_simulation.rotations, Rotation) + assert isinstance(multi_simulation.coordinates, np.ndarray) + + def test_iphase(self, multi_simulation): + with pytest.raises(ValueError): + multi_simulation.iphase[0] + + def test_irot(self, multi_simulation): + sliced_sim = multi_simulation.irot[0] + assert isinstance(sliced_sim, Simulation) + assert isinstance(sliced_sim.phases, Phase) + assert sliced_sim.rotations.size == 1 + assert sliced_sim.num_vectors == 0 + + def test_irot_slice(self, multi_simulation): + sliced_sim = multi_simulation.irot[0:2] + assert isinstance(sliced_sim, Simulation) + assert isinstance(sliced_sim.phases, Phase) + assert sliced_sim.rotations.size == 2 + assert sliced_sim.num_vectors == (2,) + + def test_plot(self, multi_simulation): + multi_simulation.plot() + + +class TestMultiPhaseMultiSimulation: + @pytest.fixture + def al_phase(self): + p = Phase( + name="al", + space_group=225, + structure=Structure( + atoms=[Atom("al", [0, 0, 0])], + lattice=Lattice(0.405, 0.405, 0.405, 90, 90, 90), + ), + ) + return p + + @pytest.fixture + def multi_simulation(self, al_phase): + gen = SimulationGenerator(accelerating_voltage=200) + rot = Rotation.from_axes_angles([1, 0, 0], (0, 15, 30, 45), degrees=True) + rot2 = rot + coords = ReciprocalLatticeVector( + phase=al_phase, xyz=[[1, 0, 0], [0, 1, 0], [1, 1, 0]] + ) + + coords = [ + coords, + ] * 4 + coords2 = coords + al_phase2 = al_phase.deepcopy() + al_phase2.name = "al2" + sim = Simulation( + phases=[al_phase, al_phase2], + simulation_generator=gen, + coordinates=[coords, coords2], + rotations=[rot, rot2], + ) + return sim + + def test_init(self, multi_simulation): + assert isinstance(multi_simulation, Simulation) + assert isinstance(multi_simulation.phases, np.ndarray) + assert isinstance(multi_simulation.simulation_generator, SimulationGenerator) + assert isinstance(multi_simulation.rotations, np.ndarray) + assert isinstance(multi_simulation.coordinates, np.ndarray) + + def test_iphase(self, multi_simulation): + phase_slic = multi_simulation.iphase[0] + assert isinstance(phase_slic, Simulation) + assert isinstance(phase_slic.phases, Phase) + assert phase_slic.rotations.size == 4 + + def test_irot(self, multi_simulation): + sliced_sim = multi_simulation.irot[0] + assert isinstance(sliced_sim, Simulation) + assert isinstance(sliced_sim.phases, np.ndarray) + assert sliced_sim.rotations.size == 2 + assert sliced_sim.num_vectors == (0, 0) + + def test_irot_slice(self, multi_simulation): + sliced_sim = multi_simulation.irot[0:2] + assert isinstance(sliced_sim, Simulation) + assert isinstance(sliced_sim.phases, np.ndarray) + assert sliced_sim.rotations.size == 2 + assert sliced_sim.num_vectors == (2, 2) + + def test_plot(self, multi_simulation): + multi_simulation.plot() diff --git a/diffsims/tests/simulations/test_simulations.py b/diffsims/tests/simulations/test_simulations.py index 649aa93d..0cf899e5 100644 --- a/diffsims/tests/simulations/test_simulations.py +++ b/diffsims/tests/simulations/test_simulations.py @@ -23,7 +23,8 @@ from orix.crystal_map import Phase from diffsims.crystallography import ReciprocalLatticeVector -from diffsims.simulations.simulation import DiffractionSimulation, ProfileSimulation +from diffsims.simulations.simulation import Simulation, ProfileSimulation + @pytest.fixture def profile_simulation(): @@ -87,26 +88,24 @@ def al_phase(self): space_group=225, structure=Structure( atoms=[Atom("al", [0, 0, 0])], - lattice=Lattice(0.405, 0.405, 0.405, 90, 90, 90) - ) + lattice=Lattice(0.405, 0.405, 0.405, 90, 90, 90), + ), ) return p @pytest.fixture def diffraction_simulation(self, al_phase): - vector = ReciprocalLatticeVector(phase=al_phase, - xyz=np.array([[0, 0, 0], - [1, 2, 3], - [3, 4, 5]])) + vector = ReciprocalLatticeVector( + phase=al_phase, xyz=np.array([[0, 0, 0], [1, 2, 3], [3, 4, 5]]) + ) return DiffractionSimulation(vector) @pytest.fixture def diffraction_simulation_calibrated(self, al_phase): - vector = ReciprocalLatticeVector(phase=al_phase, - xyz=np.array([[0, 0, 0], - [1, 2, 3], - [3, 4, 5]])) + vector = ReciprocalLatticeVector( + phase=al_phase, xyz=np.array([[0, 0, 0], [1, 2, 3], [3, 4, 5]]) + ) return DiffractionSimulation(vector, calibration=0.5) @@ -133,10 +132,7 @@ def test_append(self, diffraction_simulation): assert np.allclose( sim.coordinates.data, np.array( - [[1., 2., 3.], - [3., 4., 5.], - [1., 2., 3.], - [3., 4., 5.]] + [[1.0, 2.0, 3.0], [3.0, 4.0, 5.0], [1.0, 2.0, 3.0], [3.0, 4.0, 5.0]] ), ) assert sim.size == 4 @@ -152,7 +148,9 @@ def test_indices_setter_getter(self, diffraction_simulation): def test_coordinates_setter(self, diffraction_simulation): starting_coords = diffraction_simulation.coordinates[-1] diffraction_simulation.coordinates = diffraction_simulation.coordinates[-1] - assert np.allclose(diffraction_simulation.coordinates.data, starting_coords.data) + assert np.allclose( + diffraction_simulation.coordinates.data, starting_coords.data + ) diffraction_simulation.with_direct_beam = True def test_intensities_setter(self, diffraction_simulation): @@ -194,9 +192,10 @@ def test_calibration(self, diffraction_simulation, calibration, expected): ], ) def test_direct_beam_mask(self, al_phase, coordinates, with_direct_beam, expected): - vect =ReciprocalLatticeVector(phase=al_phase, xyz=coordinates) - diffraction_simulation = DiffractionSimulation(vect, - with_direct_beam=with_direct_beam) + vect = ReciprocalLatticeVector(phase=al_phase, xyz=coordinates) + diffraction_simulation = DiffractionSimulation( + vect, with_direct_beam=with_direct_beam + ) diffraction_simulation.with_direct_beam = with_direct_beam mask = diffraction_simulation.direct_beam_mask assert np.all(mask == expected) @@ -233,7 +232,7 @@ def test_calibrated_coordinates( offset, expected, ): - vect =ReciprocalLatticeVector(phase=al_phase, xyz=coordinates) + vect = ReciprocalLatticeVector(phase=al_phase, xyz=coordinates) diffraction_simulation = DiffractionSimulation(vect) diffraction_simulation.calibration = calibration diffraction_simulation.offset = offset @@ -256,17 +255,16 @@ def test_transform_coordinates( def test_rotate_shift_coordinates(self, diffraction_simulation): rot = diffraction_simulation.rotate_shift_coordinates(90) - assert np.allclose(rot.coordinates.data, np.array([[-2, 1, 3], [-4, 3, 5]]) - ) + assert np.allclose(rot.coordinates.data, np.array([[-2, 1, 3], [-4, 3, 5]])) def test_assertion_free_get_diffraction_pattern(self, al_phase): - vect =ReciprocalLatticeVector(phase=al_phase, xyz=np.array([[0.3, 1.2, 0]])) + vect = ReciprocalLatticeVector(phase=al_phase, xyz=np.array([[0.3, 1.2, 0]])) vect.intensity = np.ones(1) short_sim = DiffractionSimulation(vect, calibration=[1, 2]) z = short_sim.get_diffraction_pattern() - vect =ReciprocalLatticeVector(phase=al_phase, xyz=np.asarray([[0.3, 1000, 0]])) + vect = ReciprocalLatticeVector(phase=al_phase, xyz=np.asarray([[0.3, 1000, 0]])) vect.intensity = np.ones(1) empty_sim = DiffractionSimulation(vect, calibration=[1, 2]) @@ -284,26 +282,36 @@ def test_get_as_mask(self, al_phase): assert mask.shape[1] == 10 def test_polar_coordinates(self, al_phase): - vect = ReciprocalLatticeVector(phase=al_phase, - xyz=np.asarray([[1, 1, 0]])) + vect = ReciprocalLatticeVector(phase=al_phase, xyz=np.asarray([[1, 1, 0]])) vect.intensity = np.ones(1) short_sim = DiffractionSimulation(vect, calibration=[0.5, 0.5]) r, t = short_sim.get_polar_coordinates(real=True) - assert r == [1.4142135623730951, ] - assert t == [0.7853981633974483, ] + assert r == [ + 1.4142135623730951, + ] + assert t == [ + 0.7853981633974483, + ] r, t = short_sim.get_polar_coordinates(real=False) - assert r == [np.sqrt(8), ] - assert t == [0.7853981633974483, ] + assert r == [ + np.sqrt(8), + ] + assert t == [ + 0.7853981633974483, + ] @pytest.mark.parametrize("units_in", ["pixel", "real"]) - def test_plot_method(self,al_phase, units_in): - vect = ReciprocalLatticeVector(phase=al_phase, xyz=np.asarray( + def test_plot_method(self, al_phase, units_in): + vect = ReciprocalLatticeVector( + phase=al_phase, + xyz=np.asarray( [ [0.3, 1.2, 0], [-2, 3, 0], [2.1, 3.4, 0], ] - ),) + ), + ) vect.intensity = np.array([3.0, 5.0, 2.0]) short_sim = DiffractionSimulation(coordinates=vect, calibration=[1, 2]) - ax, sp = short_sim.plot(units=units_in, show_labels=True) \ No newline at end of file + ax, sp = short_sim.plot(units=units_in, show_labels=True) diff --git a/examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py b/examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py index aeda8afb..a7717fef 100644 --- a/examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py +++ b/examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py @@ -28,6 +28,10 @@ ) # 45 degree rotation around x-axis sim = gen.calculate_ed_data(phase=p, rotation=rot) -sim.simulations[0].plot() # plot the first (and only) diffraction pattern +sim.plot() # plot the first (and only) diffraction pattern + +# %% + +sim.coordinates # coordinates of the first (and only) diffraction pattern # %% From f040ded07001c0d82e6e61009dcb92fbc5919e6c Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Sat, 28 Oct 2023 08:55:27 -0500 Subject: [PATCH 15/48] Refactor: Cleaned SimulationGenerator tests with changed API --- diffsims/generators/simulation_generator.py | 198 ++++++++++-------- .../generators/test_simulation_generator.py | 31 ++- diffsims/utils/sim_utils.py | 20 +- 3 files changed, 133 insertions(+), 116 deletions(-) diff --git a/diffsims/generators/simulation_generator.py b/diffsims/generators/simulation_generator.py index 45614c5d..211a36e5 100644 --- a/diffsims/generators/simulation_generator.py +++ b/diffsims/generators/simulation_generator.py @@ -70,10 +70,6 @@ def __init__( "implementations.".format(scattering_params) ) - def plot_ewald_sphere(self, ax=None): - if ax is None: - fig, ax = plt.subplots() - def calculate_ed_data( self, phase: Phase, @@ -120,102 +116,132 @@ def calculate_ed_data( diffsims.sims.diffraction_simulation.DiffractionSimulation The data associated with this structure and diffraction setup. """ + if isinstance(phase, Phase): + phase = [phase] + if isinstance(rotation, Rotation): + rotation = [rotation] if debye_waller_factors is None: debye_waller_factors = {} # Specify variables used in calculation wavelength = self.wavelength - recip = ReciprocalLatticeVector.from_min_dspacing( - phase, - min_dspacing=1 / reciprocal_radius, - include_zero_beam=with_direct_beam, - ) # Rotate using all the rotations in the list vectors = [] - for rot in rotation.to_matrix(): - rotated_vectors = recip.rotate_from_matrix(rot) - # Identify the excitation errors of all points (distance from point to Ewald sphere) - r_sphere = 1 / wavelength - r_spot = np.sqrt(np.sum(np.square(rotated_vectors.data[:, :2]), axis=1)) - z_spot = rotated_vectors.data[:, 2] - - z_sphere = -np.sqrt(r_sphere**2 - r_spot**2) + r_sphere - excitation_error = z_sphere - z_spot - - # determine the pre-selection reflections - if self.precession_angle == 0: - intersection = np.abs(excitation_error) < max_excitation_error - else: - # only consider points that intersect the ewald sphere at some point - # the center point of the sphere - P_z = r_sphere * np.cos(np.deg2rad(self.precession_angle)) - P_t = r_sphere * np.sin(np.deg2rad(self.precession_angle)) - # the extremes of the ewald sphere - z_surf_up = P_z - np.sqrt(r_sphere**2 - (r_spot + P_t) ** 2) - z_surf_do = P_z - np.sqrt(r_sphere**2 - (r_spot - P_t) ** 2) - intersection = (z_spot - max_excitation_error <= z_surf_up) & ( - z_spot + max_excitation_error >= z_surf_do + for p, rotate in zip(phase, rotation): + recip = ReciprocalLatticeVector.from_min_dspacing( + p, + min_dspacing=1 / reciprocal_radius, + include_zero_beam=with_direct_beam, + ) + phase_vectors = [] + for rot in rotate.to_matrix(): + # Calculate the reciprocal lattice vectors that intersect the Ewald sphere. + intersected_vectors, shape_factor = self.get_intersecting_reflections( + recip, + rot, + wavelength, + max_excitation_error, + shape_factor_width=shape_factor_width, ) - # select these reflections - intersected_vectors = rotated_vectors[intersection] - excitation_error = excitation_error[intersection] - r_spot = r_spot[intersection] - - if shape_factor_width is None: - shape_factor_width = max_excitation_error - # select and evaluate shape factor model - if self.precession_angle == 0: - # calculate shape factor - shape_factor = self.shape_factor_model( - excitation_error, shape_factor_width, **self.shape_factor_kwargs + # Calculate diffracted intensities based on a kinematic model. + intensities = get_kinematical_intensities( + p.structure, + intersected_vectors.hkl, + intersected_vectors.gspacing, + prefactor=shape_factor, + scattering_params=self.scattering_params, + debye_waller_factors=debye_waller_factors, ) - else: - if self.approximate_precession: - shape_factor = lorentzian_precession( - excitation_error, - shape_factor_width, - r_spot, - np.deg2rad(self.precession_angle), - ) - else: - shape_factor = _shape_factor_precession( - excitation_error, - r_spot, - np.deg2rad(self.precession_angle), - self.shape_factor_model, - shape_factor_width, - **self.shape_factor_kwargs, - ) - # Calculate diffracted intensities based on a kinematic model. - intensities = get_kinematical_intensities( - phase.structure, - intersected_vectors.hkl, - intersected_vectors.gspacing, - prefactor=shape_factor, - scattering_params=self.scattering_params, - debye_waller_factors=debye_waller_factors, - ) - # Threshold peaks included in simulation as factor of maximum intensity. - peak_mask = intensities > np.max(intensities) * self.minimum_intensity - intensities = intensities[peak_mask] - intersected_vectors = intersected_vectors[peak_mask] - intersected_vectors.intensity = intensities + # Threshold peaks included in simulation as factor of maximum intensity. + peak_mask = intensities > np.max(intensities) * self.minimum_intensity + intensities = intensities[peak_mask] + intersected_vectors = intersected_vectors[peak_mask] + intersected_vectors.intensity = intensities + phase_vectors.append(intersected_vectors) + vectors.append(phase_vectors) + + if len(phase) == 1: + vectors = vectors[0] + phase = phase[0] + rotation = rotation[0] + if rotation.size == 1: + vectors = vectors[0] + + # Create a simulation object + sim = Simulation( + phases=phase, + coordinates=vectors, + rotations=rotation, + simulation_generator=self, + ) + return sim - sim = DiffractionSimulation( - coordinates=intersected_vectors, - with_direct_beam=with_direct_beam, + def get_intersecting_reflections( + self, + recip: ReciprocalLatticeVector, + rot: np.ndarray, + wavelength: float, + max_excitation_error: float, + shape_factor_width: float = None, + ): + """Calculates the reciprocal lattice vectors that intersect the Ewald sphere.""" + rotated_vectors = recip.rotate_from_matrix(rot) + # Identify the excitation errors of all points (distance from point to Ewald sphere) + r_sphere = 1 / wavelength + r_spot = np.sqrt(np.sum(np.square(rotated_vectors.data[:, :2]), axis=1)) + z_spot = rotated_vectors.data[:, 2] + + z_sphere = -np.sqrt(r_sphere**2 - r_spot**2) + r_sphere + excitation_error = z_sphere - z_spot + + # determine the pre-selection reflections + if self.precession_angle == 0: + intersection = np.abs(excitation_error) < max_excitation_error + else: + # only consider points that intersect the ewald sphere at some point + # the center point of the sphere + P_z = r_sphere * np.cos(np.deg2rad(self.precession_angle)) + P_t = r_sphere * np.sin(np.deg2rad(self.precession_angle)) + # the extremes of the ewald sphere + z_surf_up = P_z - np.sqrt(r_sphere**2 - (r_spot + P_t) ** 2) + z_surf_do = P_z - np.sqrt(r_sphere**2 - (r_spot - P_t) ** 2) + intersection = (z_spot - max_excitation_error <= z_surf_up) & ( + z_spot + max_excitation_error >= z_surf_do ) - vectors.append(sim) - lib = SimulationLibrary( - phase=phase, - rotations=rotation, - diffraction_generator=self, - simulations=vectors, - ) - return lib + # select these reflections + intersected_vectors = rotated_vectors[intersection] + excitation_error = excitation_error[intersection] + r_spot = r_spot[intersection] + + if shape_factor_width is None: + shape_factor_width = max_excitation_error + # select and evaluate shape factor model + if self.precession_angle == 0: + # calculate shape factor + shape_factor = self.shape_factor_model( + excitation_error, shape_factor_width, **self.shape_factor_kwargs + ) + else: + if self.approximate_precession: + shape_factor = lorentzian_precession( + excitation_error, + shape_factor_width, + r_spot, + np.deg2rad(self.precession_angle), + ) + else: + shape_factor = _shape_factor_precession( + excitation_error, + r_spot, + np.deg2rad(self.precession_angle), + self.shape_factor_model, + shape_factor_width, + **self.shape_factor_kwargs, + ) + return intersected_vectors, shape_factor def calculate_profile_data( self, diff --git a/diffsims/tests/generators/test_simulation_generator.py b/diffsims/tests/generators/test_simulation_generator.py index 00b9d904..2ca3af4e 100644 --- a/diffsims/tests/generators/test_simulation_generator.py +++ b/diffsims/tests/generators/test_simulation_generator.py @@ -141,7 +141,7 @@ def test_matching_results( diffraction = diffraction_calculator.calculate_ed_data( local_structure, reciprocal_radius=5.0 ) - assert diffraction.simulations[0].coordinates.size == 72 + assert diffraction.coordinates.size == 72 def test_precession_simple( self, diffraction_calculator_precession_simple, local_structure @@ -150,7 +150,7 @@ def test_precession_simple( local_structure, reciprocal_radius=5.0, ) - assert diffraction.simulations[0].coordinates.size == 252 + assert diffraction.coordinates.size == 252 def test_precession_full( self, diffraction_calculator_precession_full, local_structure @@ -159,14 +159,14 @@ def test_precession_full( local_structure, reciprocal_radius=5.0, ) - assert diffraction.simulations[0].coordinates.size == 252 + assert diffraction.coordinates.size == 252 def test_custom_shape_func(self, diffraction_calculator_custom, local_structure): diffraction = diffraction_calculator_custom.calculate_ed_data( local_structure, reciprocal_radius=5.0, ) - assert diffraction.simulations[0].coordinates.size == 52 + assert diffraction.coordinates.size == 52 def test_appropriate_scaling(self, diffraction_calculator: SimulationGenerator): """Tests that doubling the unit cell halves the pattern spacing.""" @@ -178,14 +178,12 @@ def test_appropriate_scaling(self, diffraction_calculator: SimulationGenerator): big_diffraction = diffraction_calculator.calculate_ed_data( phase=big_silicon, reciprocal_radius=5.0 ) - indices = [tuple(i) for i in diffraction.simulations[0].coordinates.hkl] - big_indices = [tuple(i) for i in big_diffraction.simulations[0].coordinates.hkl] + indices = [tuple(i) for i in diffraction.coordinates.hkl] + big_indices = [tuple(i) for i in big_diffraction.coordinates.hkl] assert (2, 2, 0) in indices assert (2, 2, 0) in big_indices - coordinates = diffraction.simulations[0].coordinates[indices.index((2, 2, 0))] - big_coordinates = big_diffraction.simulations[0].coordinates[ - big_indices.index((2, 2, 0)) - ] + coordinates = diffraction.coordinates[indices.index((2, 2, 0))] + big_coordinates = big_diffraction.coordinates[big_indices.index((2, 2, 0))] assert np.allclose(coordinates.data, big_coordinates.data * 2) def test_appropriate_intensities(self, diffraction_calculator, local_structure): @@ -193,15 +191,12 @@ def test_appropriate_intensities(self, diffraction_calculator, local_structure): diffraction = diffraction_calculator.calculate_ed_data( local_structure, reciprocal_radius=0.5, with_direct_beam=False ) # direct beam doesn't work - indices = [ - tuple(np.round(i).astype(int)) - for i in diffraction.simulations[0].coordinates.hkl - ] + indices = [tuple(np.round(i).astype(int)) for i in diffraction.coordinates.hkl] central_beam = indices.index((0, 1, 0)) smaller = np.greater_equal( - diffraction.simulations[0].intensities[central_beam], - diffraction.simulations[0].intensities, + diffraction.coordinates.intensity[central_beam], + diffraction.coordinates.intensity, ) assert np.all(smaller) @@ -219,9 +214,7 @@ def test_shape_factor_custom(self, diffraction_calculator, local_structure): ) # softly makes sure the two sims are different - assert np.sum(t1.simulations[0].intensities) != np.sum( - t2.simulations[0].intensities - ) + assert np.sum(t1.coordinates.intensity) != np.sum(t2.coordinates.intensity) def test_calculate_profile_class(self, local_structure, diffraction_calculator): # tests the non-hexagonal (cubic) case diff --git a/diffsims/utils/sim_utils.py b/diffsims/utils/sim_utils.py index 09af2e80..145e0d71 100644 --- a/diffsims/utils/sim_utils.py +++ b/diffsims/utils/sim_utils.py @@ -15,7 +15,6 @@ # # You should have received a copy of the GNU General Public License # along with diffsims. If not, see . - import collections import math @@ -267,7 +266,7 @@ def _get_kinematical_structure_factor( scattering_params=scattering_params, ) - gspacing_squared = g_hkls_array ** 2 + gspacing_squared = g_hkls_array**2 if scattering_params is not None: atomic_scattering_factor = get_atomic_scattering_factors( @@ -395,7 +394,7 @@ def simulate_kinematic_scattering( kx, ky = np.meshgrid(l, l) # Convert 2D k-vectors into 3D k-vectors accounting for Ewald sphere. - k = np.array((kx, ky, (wavelength / 2) * (kx ** 2 + ky ** 2))) + k = np.array((kx, ky, (wavelength / 2) * (kx**2 + ky**2))) # Calculate scattering vector squared for each k-vector. gs_sq = np.linalg.norm(k, axis=0) ** 2 @@ -411,7 +410,7 @@ def simulate_kinematic_scattering( elif illumination == "gaussian_probe": for r in atomic_coordinates: probe = (1 / (np.sqrt(2 * np.pi) * sigma)) * np.exp( - (-np.abs(((r[0] ** 2) - (r[1] ** 2)))) / (4 * sigma ** 2) + (-np.abs(((r[0] ** 2) - (r[1] ** 2)))) / (4 * sigma**2) ) scattering = scattering + (probe * fs * np.exp(np.dot(k.T, r) * np.pi * 2j)) else: @@ -503,7 +502,6 @@ def uvtw_to_uvw(uvtw): def get_intensities_params(reciprocal_lattice, reciprocal_radius): - """Calculates the variables needed for get_kinematical_intensities Parameters @@ -574,7 +572,7 @@ def get_holz_angle(electron_wavelength, lattice_parameter): k0 = 1.0 / electron_wavelength kz = 1.0 / lattice_parameter in_root = kz * ((2 * k0) - kz) - sin_angle = (in_root ** 0.5) / k0 + sin_angle = (in_root**0.5) / k0 angle = np.arcsin(sin_angle) return angle @@ -716,7 +714,7 @@ def acceleration_voltage_to_velocity(acceleration_voltage): """ - part1 = (1 + (acceleration_voltage * e) / (m_e * c ** 2)) ** 2 + part1 = (1 + (acceleration_voltage * e) / (m_e * c**2)) ** 2 v = c * (1 - (1 / part1)) ** 0.5 return v @@ -741,7 +739,7 @@ def acceleration_voltage_to_relativistic_mass(acceleration_voltage): """ v = acceleration_voltage_to_velocity(acceleration_voltage) - part1 = 1 - (v ** 2) / (c ** 2) + part1 = 1 - (v**2) / (c**2) mr = m_e / (part1) ** 0.5 return mr @@ -773,7 +771,7 @@ def et_to_beta(et, acceleration_voltage): wavelength = acceleration_voltage_to_wavelength(acceleration_voltage) m = acceleration_voltage_to_relativistic_mass(acceleration_voltage) - beta = e * (wavelength ** 2) * m * et / (h ** 2) + beta = e * (wavelength**2) * m * et / (h**2) return beta @@ -792,7 +790,7 @@ def acceleration_voltage_to_wavelength(acceleration_voltage): """ energy = acceleration_voltage * e - wavelength = h / (2 * m_e * energy * (1 + (energy / (2 * m_e * c ** 2)))) ** 0.5 + wavelength = h / (2 * m_e * energy * (1 + (energy / (2 * m_e * c**2)))) ** 0.5 return wavelength @@ -825,6 +823,6 @@ def diffraction_scattering_angle(acceleration_voltage, lattice_size, miller_inde wavelength = acceleration_voltage_to_wavelength(acceleration_voltage) h, k, l = miller_index a = lattice_size - d = a / (h ** 2 + k ** 2 + l ** 2) ** 0.5 + d = a / (h**2 + k**2 + l**2) ** 0.5 scattering_angle = 2 * np.arcsin(wavelength / (2 * d)) return scattering_angle From b646d331aaf7470a6817f232d2767fbc055fab92 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Sat, 28 Oct 2023 09:20:17 -0500 Subject: [PATCH 16/48] Refactor: Cleaned up examples for simulating diffraction --- diffsims/generators/simulation_generator.py | 20 ++--- .../simulating_diffraction_patterns.py | 76 +++++++++++++++++++ .../simulating_one_diffraction_pattern.py | 37 --------- 3 files changed, 84 insertions(+), 49 deletions(-) create mode 100644 examples/creating_a_simulation_library/simulating_diffraction_patterns.py delete mode 100644 examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py diff --git a/diffsims/generators/simulation_generator.py b/diffsims/generators/simulation_generator.py index 211a36e5..e44f1912 100644 --- a/diffsims/generators/simulation_generator.py +++ b/diffsims/generators/simulation_generator.py @@ -1,12 +1,11 @@ +from typing import Union, Sequence import numpy as np -import matplotlib.pyplot as plt from orix.quaternion import Rotation from orix.crystal_map import Phase from diffsims.crystallography import ReciprocalLatticeVector from diffsims.simulations.simulation import Simulation, ProfileSimulation -from diffsims.libraries.simulation_library import SimulationLibrary from diffsims.utils.shape_factor_models import ( linear, atanc, @@ -72,8 +71,10 @@ def __init__( def calculate_ed_data( self, - phase: Phase, - rotation: Rotation = Rotation.from_euler((0, 0, 0), degrees=True), + phase: Union[Phase, Sequence[Phase]], + rotation: Union[Rotation, Sequence[Rotation]] = Rotation.from_euler( + (0, 0, 0), degrees=True + ), reciprocal_radius: float = 1.0, with_direct_beam: bool = True, max_excitation_error: float = 1e-2, @@ -84,18 +85,13 @@ def calculate_ed_data( Parameters ---------- - phases: - The phase for which to derive the diffraction pattern. - structure : diffpy.structure.structure.Structure - The structure for which to derive the diffraction pattern. - Note that the structure must be rotated to the appropriate - orientation and that testing is conducted on unit cells - (rather than supercells). + phase: + The phase(s) for which to derive the diffraction pattern. reciprocal_radius : float The maximum radius of the sphere of reciprocal space to sample, in reciprocal Angstroms. rotation - The Rotation object to apply to the structure and then + The Rotation object(s) to apply to the structure and then calculate the diffraction pattern. with_direct_beam : bool If True, the direct beam is included in the simulated diff --git a/examples/creating_a_simulation_library/simulating_diffraction_patterns.py b/examples/creating_a_simulation_library/simulating_diffraction_patterns.py new file mode 100644 index 00000000..aa30dd29 --- /dev/null +++ b/examples/creating_a_simulation_library/simulating_diffraction_patterns.py @@ -0,0 +1,76 @@ +# ===================================================== +# Simulating One Diffraction Pattern for a Single Phase +# ===================================================== + +from orix.crystal_map import Phase +from orix.quaternion import Rotation +from diffpy.structure import Atom, Lattice, Structure + +from diffsims.generators.simulation_generator import SimulationGenerator + +a = 5.431 +latt = Lattice(a, a, a, 90, 90, 90) +atom_list = [] +for coords in [[0, 0, 0], [0.5, 0, 0.5], [0, 0.5, 0.5], [0.5, 0.5, 0]]: + x, y, z = coords[0], coords[1], coords[2] + atom_list.append(Atom(atype="Si", xyz=[x, y, z], lattice=latt)) # Motif part A + atom_list.append( + Atom(atype="Si", xyz=[x + 0.25, y + 0.25, z + 0.25], lattice=latt) + ) # Motif part B +struct = Structure(atoms=atom_list, lattice=latt) +p = Phase(structure=struct, space_group=227) + +gen = SimulationGenerator( + accelerating_voltage=200, +) +rot = Rotation.from_axes_angles( + [1, 0, 0], 45, degrees=True +) # 45 degree rotation around x-axis +sim = gen.calculate_ed_data(phase=p, rotation=rot) + +sim.plot() # plot the first (and only) diffraction pattern + +# %% + +sim.coordinates # coordinates of the first (and only) diffraction pattern + +# %% + + +# =========================================================== +# Simulating Multiple Rotations for a Single Phase +# =========================================================== + +rot = Rotation.from_axes_angles( + [1, 0, 0], (0, 15, 30, 45, 60, 75, 90), degrees=True +) # 45 degree rotation around x-axis +sim = gen.calculate_ed_data(phase=p, rotation=rot) + +sim.plot() # plot the first diffraction pattern + +# %% + +sim.irot[3].plot() # plot the fourth(45 degrees) diffraction pattern +# %% + +sim.coordinates # coordinates of all the diffraction patterns + +# ============================================================ +# Simulating Multiple Rotations for Multiple Phases +# ============================================================ +p2 = p.deepcopy() + +p2.name = "al_2" + +rot = Rotation.from_axes_angles( + [1, 0, 0], (0, 15, 30, 45, 60, 75, 90), degrees=True +) # 45 degree rotation around x-axis +sim = gen.calculate_ed_data(phase=[p, p2], rotation=[rot, rot]) + +sim.plot() # plot the first diffraction pattern + +# %% + +sim.iphase["al_2"].irot[3].plot() # plot the fourth(45 degrees) diffraction pattern + +sim.coordinates # coordinates of all the diffraction patterns diff --git a/examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py b/examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py deleted file mode 100644 index a7717fef..00000000 --- a/examples/creating_a_simulation_library/simulating_one_diffraction_pattern.py +++ /dev/null @@ -1,37 +0,0 @@ -#################################### -# Simulating One Diffraction Pattern -# ================================== - -from orix.crystal_map import Phase -from orix.quaternion import Rotation -from diffpy.structure import Atom, Lattice, Structure - -from diffsims.generators.simulation_generator import SimulationGenerator - -a = 5.431 -latt = Lattice(a, a, a, 90, 90, 90) -atom_list = [] -for coords in [[0, 0, 0], [0.5, 0, 0.5], [0, 0.5, 0.5], [0.5, 0.5, 0]]: - x, y, z = coords[0], coords[1], coords[2] - atom_list.append(Atom(atype="Si", xyz=[x, y, z], lattice=latt)) # Motif part A - atom_list.append( - Atom(atype="Si", xyz=[x + 0.25, y + 0.25, z + 0.25], lattice=latt) - ) # Motif part B -struct = Structure(atoms=atom_list, lattice=latt) -p = Phase(structure=struct, space_group=227) - -gen = SimulationGenerator( - accelerating_voltage=200, -) -rot = Rotation.from_axes_angles( - [1, 0, 0], 45, degrees=True -) # 45 degree rotation around x-axis -sim = gen.calculate_ed_data(phase=p, rotation=rot) - -sim.plot() # plot the first (and only) diffraction pattern - -# %% - -sim.coordinates # coordinates of the first (and only) diffraction pattern - -# %% From 130f7416fb4043cfdb521c6ec8755bdce31b8446 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Sat, 28 Oct 2023 13:13:50 -0500 Subject: [PATCH 17/48] Refactor: Added new plotting feature for plotting rotations --- diffsims/generators/simulation_generator.py | 1 + diffsims/simulations/simulation.py | 39 +++++++- diffsims/tests/simulations/test_simulation.py | 3 + .../tests/simulations/test_simulations.py | 95 ++++--------------- 4 files changed, 63 insertions(+), 75 deletions(-) diff --git a/diffsims/generators/simulation_generator.py b/diffsims/generators/simulation_generator.py index e44f1912..f6efbd82 100644 --- a/diffsims/generators/simulation_generator.py +++ b/diffsims/generators/simulation_generator.py @@ -171,6 +171,7 @@ def calculate_ed_data( coordinates=vectors, rotations=rotation, simulation_generator=self, + reciporical_radius=reciprocal_radius, ) return sim diff --git a/diffsims/simulations/simulation.py b/diffsims/simulations/simulation.py index 7350c435..40bf9fb2 100644 --- a/diffsims/simulations/simulation.py +++ b/diffsims/simulations/simulation.py @@ -5,6 +5,7 @@ import matplotlib.pyplot as plt from orix.crystal_map import Phase from orix.quaternion import Rotation +from orix.vector import Vector3d from diffsims.crystallography.reciprocal_lattice_vector import ReciprocalLatticeVector from diffsims.pattern.detector_functions import add_shot_and_point_spread @@ -117,6 +118,7 @@ def __init__( offset: Sequence[float] = (0.0, 0.0), with_direct_beam: bool = False, shape: Sequence[int] = (512, 512), + reciporical_radius: float = 1.0, ): """Initializes the DiffractionSimulation object with data values for the coordinates, indices, intensities, calibration and offset. @@ -128,10 +130,12 @@ def __init__( rotations = np.array(rotations) if not isinstance(coordinates, ReciprocalLatticeVector): coordinates = np.array(coordinates, dtype=object) + self.phases = phases self.rotations = rotations self.coordinates = coordinates self.simulation_generator = simulation_generator + self.reciporical_radius = reciporical_radius # Data for integrating with real data from a detector self.calibration = calibration @@ -142,6 +146,8 @@ def __init__( # for interactive plotting and iterating through the Simulations self.phase_index = 0 self.rotation_index = 0 + self._rot_plot = None + self._diff_plot = None # for slicing a simulation self.iphase = PhaseGetter(self) @@ -273,6 +279,13 @@ def _get_transformed_coordinates( transformed_coords[:, 1] = rd * np.sin(theta) + cy return transformed_coords + @property + def current_phase(self): + if self.has_multiple_phases: + return self.phases[self.phase_index] + else: + return self.phases + def rotate_shift_coordinates(self, angle, center=(0, 0), mirrored=False): """ Rotate, flip or shift patterns in-plane @@ -456,6 +469,26 @@ def get_current_coordinates(self): else: return self.coordinates + def plot_rotations(self, beam_direction: Vector3d = Vector3d.zvector()): + """Plots the rotations of the current phase in stereographic projection""" + if self.has_multiple_phases: + rots = self.rotations[self.phase_index] + else: + rots = self.rotations + vect_rot = rots * beam_direction + facecolor = ["k"] * rots.size + facecolor[self.rotation_index] = "r" # highlight the current rotation + fig = vect_rot.scatter( + grid=True, + facecolor=facecolor, + return_figure=True, + ) + pointer = vect_rot[self.rotation_index] + _plot = fig.axes[0] + _plot.scatter(pointer.data[0][0], pointer.data[0][1], color="r") + _plot = fig.axes[0] + _plot.set_title("Rotations" + self.current_phase.name) + def plot( self, size_factor=1, @@ -465,7 +498,7 @@ def plot( units="real", show_labels=False, label_offset=(0, 0), - label_formatting={}, + label_formatting=None, min_label_intensity=0.1, ax=None, **kwargs, @@ -507,6 +540,8 @@ def plot( ----- spot size scales with the square root of the intensity. """ + if label_formatting is None: + label_formatting = {} if direct_beam_position is None: direct_beam_position = (0, 0) if ax is None: @@ -521,6 +556,8 @@ def plot( s=size_factor * np.sqrt(coords.intensity), **kwargs, ) + ax.set_xlim(-self.reciporical_radius, self.reciporical_radius) + ax.set_ylim(-self.reciporical_radius, self.reciporical_radius) if show_labels: millers = coords.hkl.astype(np.int16) diff --git a/diffsims/tests/simulations/test_simulation.py b/diffsims/tests/simulations/test_simulation.py index a9c33780..5248c135 100644 --- a/diffsims/tests/simulations/test_simulation.py +++ b/diffsims/tests/simulations/test_simulation.py @@ -171,3 +171,6 @@ def test_irot_slice(self, multi_simulation): def test_plot(self, multi_simulation): multi_simulation.plot() + + def test_plot_rotation(self, multi_simulation): + multi_simulation.plot_rotations() diff --git a/diffsims/tests/simulations/test_simulations.py b/diffsims/tests/simulations/test_simulations.py index 0cf899e5..02c97f7c 100644 --- a/diffsims/tests/simulations/test_simulations.py +++ b/diffsims/tests/simulations/test_simulations.py @@ -21,9 +21,11 @@ from diffpy.structure import Structure, Atom, Lattice from orix.crystal_map import Phase +from orix.quaternion import Rotation from diffsims.crystallography import ReciprocalLatticeVector from diffsims.simulations.simulation import Simulation, ProfileSimulation +from diffsims.generators.simulation_generator import SimulationGenerator @pytest.fixture @@ -99,7 +101,12 @@ def diffraction_simulation(self, al_phase): phase=al_phase, xyz=np.array([[0, 0, 0], [1, 2, 3], [3, 4, 5]]) ) - return DiffractionSimulation(vector) + return Simulation( + phases=al_phase, + rotations=Rotation.from_axes_angles([0, 0, 1], angles=0), + coordinates=vector, + simulation_generator=SimulationGenerator(300), + ) @pytest.fixture def diffraction_simulation_calibrated(self, al_phase): @@ -107,59 +114,24 @@ def diffraction_simulation_calibrated(self, al_phase): phase=al_phase, xyz=np.array([[0, 0, 0], [1, 2, 3], [3, 4, 5]]) ) - return DiffractionSimulation(vector, calibration=0.5) + return Simulation( + phases=al_phase, + rotations=Rotation.from_axes_angles([0, 0, 1], angles=0), + coordinates=vector, + simulation_generator=SimulationGenerator(300), + calibration=0.5, + ) def test_init(self, diffraction_simulation): assert np.allclose( - diffraction_simulation.coordinates.data, np.array([[1, 2, 3], [3, 4, 5]]) + diffraction_simulation.coordinates.data, + np.array([[0, 0, 0], [1, 2, 3], [3, 4, 5]]), ) - assert diffraction_simulation.coordinates.hkl.shape == (2, 3) + assert diffraction_simulation.coordinates.hkl.shape == (3, 3) assert np.isnan(diffraction_simulation.coordinates.intensity).all() - assert diffraction_simulation.coordinates.intensity.shape == (2,) - assert diffraction_simulation.calibration is None - assert len(diffraction_simulation) == 2 - - def test_single_spot(self, al_phase): - rlv = ReciprocalLatticeVector(phase=al_phase, xyz=np.array([[1, 2, 3]])) - assert DiffractionSimulation(rlv).coordinates.data.shape == (1, 3) - - def test_get_item(self, diffraction_simulation): - assert diffraction_simulation[1].coordinates.data.shape == (1, 3) - assert diffraction_simulation[0:2].coordinates.data.shape == (2, 3) - - def test_append(self, diffraction_simulation): - sim = diffraction_simulation.append(diffraction_simulation) - assert np.allclose( - sim.coordinates.data, - np.array( - [[1.0, 2.0, 3.0], [3.0, 4.0, 5.0], [1.0, 2.0, 3.0], [3.0, 4.0, 5.0]] - ), - ) - assert sim.size == 4 - - def test_indices_setter_getter(self, diffraction_simulation): - indices = np.array([[1, 2, 3], [2, 3, 4], [3, 4, 5]]) - diffraction_simulation.indices = indices[-1] - assert np.allclose(diffraction_simulation.indices, indices[-1]) - diffraction_simulation.with_direct_beam = True - diffraction_simulation.indices = indices - assert np.allclose(diffraction_simulation.indices, indices) - - def test_coordinates_setter(self, diffraction_simulation): - starting_coords = diffraction_simulation.coordinates[-1] - diffraction_simulation.coordinates = diffraction_simulation.coordinates[-1] - assert np.allclose( - diffraction_simulation.coordinates.data, starting_coords.data - ) - diffraction_simulation.with_direct_beam = True - - def test_intensities_setter(self, diffraction_simulation): - ints = np.array([1, 2, 3]) - diffraction_simulation.intensities = ints[-1] - assert np.allclose(diffraction_simulation.intensities, ints[-1]) - diffraction_simulation.with_direct_beam = True - diffraction_simulation.intensities = ints - assert np.allclose(diffraction_simulation.intensities, ints) + assert diffraction_simulation.coordinates.intensity.shape == (3,) + assert np.allclose(diffraction_simulation.calibration, np.array([0.1, 0.1])) + assert len(diffraction_simulation) == 3 @pytest.mark.parametrize( "calibration, expected", @@ -175,31 +147,6 @@ def test_calibration(self, diffraction_simulation, calibration, expected): diffraction_simulation.calibration = calibration assert np.allclose(diffraction_simulation.calibration, expected) - @pytest.mark.parametrize( - "coordinates, with_direct_beam, expected", - [ - ( - np.array([[-1, 0, 0], [0, 0, 0], [1, 0, 0]]), - False, - np.array([True, False, True]), - ), - ( - np.array([[-1, 0, 0], [0, 0, 0], [1, 0, 0]]), - True, - np.array([True, True, True]), - ), - (np.array([[-1, 0, 0], [1, 0, 0]]), False, np.array([True, True])), - ], - ) - def test_direct_beam_mask(self, al_phase, coordinates, with_direct_beam, expected): - vect = ReciprocalLatticeVector(phase=al_phase, xyz=coordinates) - diffraction_simulation = DiffractionSimulation( - vect, with_direct_beam=with_direct_beam - ) - diffraction_simulation.with_direct_beam = with_direct_beam - mask = diffraction_simulation.direct_beam_mask - assert np.all(mask == expected) - @pytest.mark.parametrize( "coordinates, calibration, offset, expected", [ From eea91c6eaa223d5b212c1a82a0c4f83dad7ee5c5 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Mon, 6 Nov 2023 15:56:13 -0600 Subject: [PATCH 18/48] New Feature: Added polar flatten to simulation to get an optimized representation for temp matching --- .../reciprocal_lattice_vector.py | 50 ++++++++++++++++++- diffsims/simulations/simulation.py | 35 +++++++++++-- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/diffsims/crystallography/reciprocal_lattice_vector.py b/diffsims/crystallography/reciprocal_lattice_vector.py index 88c2ed8b..f7ac2de1 100644 --- a/diffsims/crystallography/reciprocal_lattice_vector.py +++ b/diffsims/crystallography/reciprocal_lattice_vector.py @@ -15,7 +15,7 @@ # # You should have received a copy of the GNU General Public License # along with diffsims. If not, see . - +from typing import Tuple from collections import defaultdict from copy import deepcopy @@ -1217,6 +1217,54 @@ def from_miller(cls, miller): ) return cls(miller.phase, **{miller.coordinate_format: miller.coordinates}) + def to_polar( + self, degrees: bool = False + ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + """Convert the vectors to polar coordinates. + + Parameters + ---------- + degrees : bool, optional + Whether to return angles in degrees (default is False). + + Returns + ------- + r : numpy.ndarray + Length of the vectors. + theta : numpy.ndarray + Polar angle of the vectors. + phi : numpy.ndarray + Azimuthal angle of the vectors. + + Examples + -------- + See :class:`ReciprocalLatticeVector` for the creation of ``rlv`` + + >>> rlv + ReciprocalLatticeVector (2,), al (m-3m) + [[1. 1. 1.] + [2. 0. 0.]] + >>> r, theta, phi = rlv.to_polar() + >>> r + array([1.73205081, 2. ]) + >>> theta + array([0.95531662, 0. ]) + >>> phi + array([0., 0.]) + + """ + + x = self.data[:, 0] + y = self.data[:, 1] + z = self.data[:, 2] + + r = np.sqrt(x**2 + y**2) + theta = np.arctan2(y, x) + + if degrees: + theta = np.rad2deg(theta) + return r, theta, z + def to_miller(self): """Return the vectors as a ``Miller`` instance. diff --git a/diffsims/simulations/simulation.py b/diffsims/simulations/simulation.py index 40bf9fb2..e82ed144 100644 --- a/diffsims/simulations/simulation.py +++ b/diffsims/simulations/simulation.py @@ -365,6 +365,35 @@ def get_as_mask( ) return mask + def polar_flatten_simulations(self): + """Flattens the simulations into polar coordinates for use in template matching. + + The resulting arrays are of shape (n_simulations, n_spots) where n_spots is the + maximum number of spots in any simulation. + + + Returns + ------- + r_templates, theta_templates, intensities_templates + """ + + flattened_vectors = [sim.data for sim in self] + flattened_intensity = [sim.intensity for sim in self] + max_num_spots = max([len(v) for v in flattened_vectors]) + + r_templates = np.zeros((len(flattened_vectors), max_num_spots)) + theta_templates = np.zeros((len(flattened_vectors), max_num_spots)) + intensities_templates = np.zeros((len(flattened_vectors), max_num_spots)) + + for i, (v, i_v) in enumerate(zip(flattened_vectors, flattened_intensity)): + r, t, _ = v.to_polar() + r_templates[i, : len(r)] = r + theta_templates[i, : len(t)] = t + intensities_templates[i, : len(i_v)] = i_v + + r, t = self.get_polar_coordinates() + return r, t, intensities_templates + def get_diffraction_pattern( self, shape=None, @@ -625,7 +654,7 @@ def __init__(self, magnitudes, intensities, hkls): self.intensities = intensities self.hkls = hkls - def plot(self, annotate_peaks=True, with_labels=True, fontsize=12): + def plot(self, annotate_peaks=True, with_labels=True, fontsize=12, ax=None): """Plots the diffraction profile simulation for the calculate_profile_data method in DiffractionGenerator. @@ -638,8 +667,8 @@ def plot(self, annotate_peaks=True, with_labels=True, fontsize=12): fontsize : integer Fontsize for peak labels. """ - - ax = plt.gca() + if ax is None: + fig, ax = plt.subplots() for g, i, hkls in zip(self.magnitudes, self.intensities, self.hkls): label = hkls ax.plot([g, g], [0, i], color="k", linewidth=3, label=label) From aa96b37232c55b98fd1b60dd637663dc92b0b342 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Mon, 6 Nov 2023 15:56:31 -0600 Subject: [PATCH 19/48] Testing: Removed extra tests --- .../tests/simulations/test_simulations.py | 85 ++----------------- 1 file changed, 6 insertions(+), 79 deletions(-) diff --git a/diffsims/tests/simulations/test_simulations.py b/diffsims/tests/simulations/test_simulations.py index 02c97f7c..50c38328 100644 --- a/diffsims/tests/simulations/test_simulations.py +++ b/diffsims/tests/simulations/test_simulations.py @@ -180,85 +180,12 @@ def test_calibrated_coordinates( expected, ): vect = ReciprocalLatticeVector(phase=al_phase, xyz=coordinates) - diffraction_simulation = DiffractionSimulation(vect) + diffraction_simulation = Simulation( + phases=al_phase, + coordinates=vect, + rotations=[0, 0, 1], + simulation_generator=SimulationGenerator(300), + ) diffraction_simulation.calibration = calibration diffraction_simulation.offset = offset assert np.allclose(diffraction_simulation.calibrated_coordinates, expected) - - @pytest.mark.parametrize( - "units, expected", - [ - ("real", np.array([[-2, 1, 3], [-4, 3, 5]])), - ("pixel", np.array([[-2, 1, 3], [-4, 3, 5]])), - ], - ) - def test_transform_coordinates( - self, diffraction_simulation_calibrated, units, expected - ): - tc = diffraction_simulation_calibrated._get_transformed_coordinates( - 90, units=units - ) - assert np.allclose(tc.coordinates.data, expected) - - def test_rotate_shift_coordinates(self, diffraction_simulation): - rot = diffraction_simulation.rotate_shift_coordinates(90) - assert np.allclose(rot.coordinates.data, np.array([[-2, 1, 3], [-4, 3, 5]])) - - def test_assertion_free_get_diffraction_pattern(self, al_phase): - vect = ReciprocalLatticeVector(phase=al_phase, xyz=np.array([[0.3, 1.2, 0]])) - vect.intensity = np.ones(1) - short_sim = DiffractionSimulation(vect, calibration=[1, 2]) - - z = short_sim.get_diffraction_pattern() - - vect = ReciprocalLatticeVector(phase=al_phase, xyz=np.asarray([[0.3, 1000, 0]])) - vect.intensity = np.ones(1) - empty_sim = DiffractionSimulation(vect, calibration=[1, 2]) - - z = empty_sim.get_diffraction_pattern(shape=(10, 20)) - - def test_get_as_mask(self, al_phase): - vect = ReciprocalLatticeVector(phase=al_phase, xyz=np.asarray([[0.3, 1.2, 0]])) - vect.intensity = np.ones(1) - short_sim = DiffractionSimulation(vect, calibration=[1, 2]) - mask = short_sim.get_as_mask( - (20, 10), - radius_function=np.sqrt, - ) - assert mask.shape[0] == 20 - assert mask.shape[1] == 10 - - def test_polar_coordinates(self, al_phase): - vect = ReciprocalLatticeVector(phase=al_phase, xyz=np.asarray([[1, 1, 0]])) - vect.intensity = np.ones(1) - short_sim = DiffractionSimulation(vect, calibration=[0.5, 0.5]) - r, t = short_sim.get_polar_coordinates(real=True) - assert r == [ - 1.4142135623730951, - ] - assert t == [ - 0.7853981633974483, - ] - r, t = short_sim.get_polar_coordinates(real=False) - assert r == [ - np.sqrt(8), - ] - assert t == [ - 0.7853981633974483, - ] - - @pytest.mark.parametrize("units_in", ["pixel", "real"]) - def test_plot_method(self, al_phase, units_in): - vect = ReciprocalLatticeVector( - phase=al_phase, - xyz=np.asarray( - [ - [0.3, 1.2, 0], - [-2, 3, 0], - [2.1, 3.4, 0], - ] - ), - ) - vect.intensity = np.array([3.0, 5.0, 2.0]) - short_sim = DiffractionSimulation(coordinates=vect, calibration=[1, 2]) - ax, sp = short_sim.plot(units=units_in, show_labels=True) From 6141b9fb5496dd0d018f8b8e1ad3ceb6bdd4a170 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Wed, 15 Nov 2023 07:41:08 -0600 Subject: [PATCH 20/48] Manifest: Fixed failing manifest --- diffsims/tests/simulations/__init__.py | 0 setup.cfg | 1 + 2 files changed, 1 insertion(+) create mode 100644 diffsims/tests/simulations/__init__.py diff --git a/diffsims/tests/simulations/__init__.py b/diffsims/tests/simulations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/setup.cfg b/setup.cfg index 46204e6d..f9ab2afb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,6 +20,7 @@ precision = 2 [manifix] known_excludes = + .examples/** .* .*/** **/*.nbi From 8060b7aa363103ee9c6f5c35775d4a315556e54b Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Wed, 15 Nov 2023 07:42:29 -0600 Subject: [PATCH 21/48] New Feature: Added direct beam to plotting --- diffsims/simulations/simulation.py | 78 +++++++++---------- diffsims/tests/simulations/test_simulation.py | 1 + 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/diffsims/simulations/simulation.py b/diffsims/simulations/simulation.py index e82ed144..48a3f1b0 100644 --- a/diffsims/simulations/simulation.py +++ b/diffsims/simulations/simulation.py @@ -1,4 +1,4 @@ -from typing import Union, Sequence, TYPE_CHECKING +from typing import Union, Sequence, TYPE_CHECKING, Any import copy import numpy as np @@ -172,11 +172,11 @@ def __len__(self): def size(self): return self.__len__() - def __getitem__(self, sliced): + def __getitem__(self, sliced: Any): """Sliced is any valid numpy slice that does not change the number of dimensions or number of columns""" coords = self.coordinates[sliced] - return DiffractionSimulation( + return Simulation( coords, calibration=self.calibration, offset=self.offset, @@ -222,7 +222,7 @@ def calibration(self): return self._calibration @calibration.setter - def calibration(self, calibration): + def calibration(self, calibration: Union[float, Sequence[float]]): if calibration is None: pass elif np.all(np.equal(calibration, 0)): @@ -237,30 +237,12 @@ def calibration(self, calibration): ) self._calibration = calibration - def get_polar_coordinates(self, real=True): - """Returns the polar coordinates of the diffraction pattern""" - x = self.coordinates.data[:, 0] - y = self.coordinates.data[:, 1] - if not real: - x = x / self.calibration[0] - y = y / self.calibration[1] - r = np.sqrt(x**2 + y**2) - theta = np.arctan2(y, x) - return r, theta - - @property - def direct_beam_mask(self): - """ndarray : If `with_direct_beam` is True, returns a True array for all - points. If `with_direct_beam` is False, returns a True array with False - in the position of the direct beam.""" - if self.with_direct_beam: - return np.ones_like(self._coordinates.intensity, dtype=bool) - else: - mask = np.any(self._coordinates.data != 0, axis=1) - return mask - def _get_transformed_coordinates( - self, angle, center=(0, 0), mirrored=False, units="real" + self, + angle: float, + center: Sequence = (0, 0), + mirrored: bool = False, + units: str = "real", ): """Translate, rotate or mirror the pattern spot coordinates""" @@ -286,18 +268,19 @@ def current_phase(self): else: return self.phases - def rotate_shift_coordinates(self, angle, center=(0, 0), mirrored=False): - """ - Rotate, flip or shift patterns in-plane + def rotate_shift_coordinates( + self, angle: float, center: Sequence = (0, 0), mirrored: bool = False + ): + """Rotate, flip or shift patterns in-plane Parameters ---------- - angle: float + angle In plane rotation angle in degrees - center: 2-tuple of floats + center Center coordinate of the patterns - mirrored: bool - Mirror across the x axis + mirrored + Mirror across the x-axis """ coords_new = self._get_transformed_coordinates( angle, center, mirrored, units="real" @@ -306,9 +289,9 @@ def rotate_shift_coordinates(self, angle, center=(0, 0), mirrored=False): def get_as_mask( self, - shape, - radius=6.0, - negative=True, + shape: Sequence[int], + radius: float = 6.0, + negative: bool = False, radius_function=None, direct_beam_position=None, in_plane_angle=0, @@ -391,8 +374,7 @@ def polar_flatten_simulations(self): theta_templates[i, : len(t)] = t intensities_templates[i, : len(i_v)] = i_v - r, t = self.get_polar_coordinates() - return r, t, intensities_templates + return r_templates, theta_templates, intensities_templates def get_diffraction_pattern( self, @@ -529,6 +511,7 @@ def plot( label_offset=(0, 0), label_formatting=None, min_label_intensity=0.1, + include_direct_beam=True, ax=None, **kwargs, ): @@ -556,6 +539,10 @@ def plot( label_formatting : dict, optional keyword arguments passed to `ax.text` for drawing the labels. Does nothing if `show_labels` is False. + min_label_intensity : float, optional + minimum intensity for a spot to be labelled + include_direct_beam : bool, optional + whether to include the direct beam in the plot ax : matplotlib Axes, optional axes on which to draw the pattern. If `None`, a new axis is created **kwargs : @@ -579,10 +566,17 @@ def plot( coords = self._get_transformed_coordinates( in_plane_angle, direct_beam_position, mirrored, units=units ) + if include_direct_beam: + spots = coords.data[:, :2] + spots = np.concatenate((spots, np.array([direct_beam_position]))) + intensity = np.concatenate((coords.intensity, np.array([1]))) + else: + spots = coords.data.data[:, :2] + intensity = coords.intensity sp = ax.scatter( - coords.data[:, 0], - coords.data[:, 1], - s=size_factor * np.sqrt(coords.intensity), + spots[:, 0], + spots[:, 1], + s=size_factor * np.sqrt(intensity), **kwargs, ) ax.set_xlim(-self.reciporical_radius, self.reciporical_radius) diff --git a/diffsims/tests/simulations/test_simulation.py b/diffsims/tests/simulations/test_simulation.py index 5248c135..af626136 100644 --- a/diffsims/tests/simulations/test_simulation.py +++ b/diffsims/tests/simulations/test_simulation.py @@ -168,6 +168,7 @@ def test_irot_slice(self, multi_simulation): assert isinstance(sliced_sim.phases, np.ndarray) assert sliced_sim.rotations.size == 2 assert sliced_sim.num_vectors == (2, 2) + sliced_sim.plot() def test_plot(self, multi_simulation): multi_simulation.plot() From 759cbfc49afe109562f58e97005d4b3ea67c261f Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Wed, 15 Nov 2023 07:42:48 -0600 Subject: [PATCH 22/48] Deprecation: Deprecate old simulation methods --- diffsims/libraries/diffraction_library.py | 7 +++++++ diffsims/sims/diffraction_simulation.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/diffsims/libraries/diffraction_library.py b/diffsims/libraries/diffraction_library.py index e1cbf540..2d3c1c6d 100644 --- a/diffsims/libraries/diffraction_library.py +++ b/diffsims/libraries/diffraction_library.py @@ -21,6 +21,7 @@ import numpy as np from diffsims.generators.diffraction_generator import DiffractionGenerator +from diffsims.utils._deprecated import deprecated __all__ = [ "DiffractionLibrary", @@ -115,6 +116,12 @@ class DiffractionLibrary(dict): """ + @deprecated( + since="0.6.0", + alternative="diffsims.generators.simulation_generator.SimulationGenerator", + alternative_is_function=False, + removal="0.8.0", + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.identifiers = None diff --git a/diffsims/sims/diffraction_simulation.py b/diffsims/sims/diffraction_simulation.py index 1df7745f..ebc5b5d9 100644 --- a/diffsims/sims/diffraction_simulation.py +++ b/diffsims/sims/diffraction_simulation.py @@ -22,6 +22,7 @@ from diffsims.pattern.detector_functions import add_shot_and_point_spread from diffsims.utils import mask_utils +from diffsims.utils._deprecated import deprecated __all__ = [ @@ -50,6 +51,12 @@ class DiffractionSimulation: zero in each direction. """ + @deprecated( + since="0.6.0", + alternative="diffsims.simulation.Simulation", + alternative_is_function=False, + removal="0.8.0", + ) def __init__( self, coordinates, From 68e78ad6a34bf8fe765479e6c1747100f129259c Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Wed, 15 Nov 2023 08:08:34 -0600 Subject: [PATCH 23/48] Upped min Orix version 0.9.0 -->0.11.0 --- setup.cfg | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index f9ab2afb..5db726e9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ precision = 2 [manifix] known_excludes = - .examples/** + ./examples/** .* .*/** **/*.nbi diff --git a/setup.py b/setup.py index b830d2dd..a24ae2c9 100644 --- a/setup.py +++ b/setup.py @@ -79,7 +79,7 @@ "matplotlib >= 3.3", "numba", "numpy >= 1.17", - "orix >= 0.9", + "orix >= 0.11", "psutil", "scipy >= 1.1", "tqdm >= 4.9", From fdde53c81092511de2b3024cd259db25c2fbb4dc Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Wed, 15 Nov 2023 08:28:30 -0600 Subject: [PATCH 24/48] Fix test failures Fix test failures --- .github/workflows/build.yml | 2 +- setup.cfg | 2 +- setup.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e127d820..620a3784 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,7 @@ jobs: - os: ubuntu-latest python-version: 3.7 OLDEST_SUPPORTED_VERSION: true - DEPENDENCIES: diffpy.structure==3.0.0 matplotlib==3.3 numpy==1.17 orix==0.9.0 scipy==1.1 tqdm==4.9 + DEPENDENCIES: diffpy.structure==3.0.2 matplotlib==3.3 numpy==1.17 orix==0.11.0 scipy==1.2 tqdm==4.9 LABEL: -oldest steps: - uses: actions/checkout@v3 diff --git a/setup.cfg b/setup.cfg index 5db726e9..16c923af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ precision = 2 [manifix] known_excludes = - ./examples/** + examples/** .* .*/** **/*.nbi diff --git a/setup.py b/setup.py index a24ae2c9..eeacbcb6 100644 --- a/setup.py +++ b/setup.py @@ -75,13 +75,13 @@ packages=find_packages(), extras_require=extra_feature_requirements, install_requires=[ - "diffpy.structure >= 3.0.0", # First Python 3 support + "diffpy.structure >= 3.0.2", # First Python 3 support "matplotlib >= 3.3", "numba", "numpy >= 1.17", "orix >= 0.11", "psutil", - "scipy >= 1.1", + "scipy >= 1.2", "tqdm >= 4.9", "transforms3d", ], From dda06939d38d0cda894ef45b589766cf186940bf Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Tue, 21 Nov 2023 15:13:41 -0600 Subject: [PATCH 25/48] Refactor: Clean up tests and improve coverage for Simualtions --- diffsims/libraries/diffraction_library.py | 3 +- diffsims/libraries/simulation_library.py | 112 ------------------ diffsims/simulations/simulation.py | 53 +++------ .../test_reciprocal_lattice_vector.py | 19 +++ .../generators/test_simulation_generator.py | 15 --- .../tests/simulations/test_simulations.py | 78 +++++++----- 6 files changed, 88 insertions(+), 192 deletions(-) delete mode 100644 diffsims/libraries/simulation_library.py diff --git a/diffsims/libraries/diffraction_library.py b/diffsims/libraries/diffraction_library.py index 2d3c1c6d..1929724b 100644 --- a/diffsims/libraries/diffraction_library.py +++ b/diffsims/libraries/diffraction_library.py @@ -149,8 +149,6 @@ def get_library_entry(self, phase=None, angle=None): phase and orientation with associated properties. """ - if isinstance(phase, int): - phase = list(self.keys())[phase] if phase is not None: phase_entry = self[phase] if angle is not None: @@ -166,6 +164,7 @@ def get_library_entry(self, phase=None, angle=None): return { "Sim": phase_entry["simulations"][orientation_index], "intensities": phase_entry["intensities"][orientation_index], + "pixel_coords": phase_entry["pixel_coords"][orientation_index], "pattern_norm": np.linalg.norm( phase_entry["intensities"][orientation_index] ), diff --git a/diffsims/libraries/simulation_library.py b/diffsims/libraries/simulation_library.py deleted file mode 100644 index e7f6357d..00000000 --- a/diffsims/libraries/simulation_library.py +++ /dev/null @@ -1,112 +0,0 @@ -from typing import NamedTuple, Sequence, TYPE_CHECKING - -from orix.quaternion import Rotation -from orix.crystal_map import Phase -from orix.vector import Vector3d -import numpy as np - -from diffsims.simulations.simulation import Simulation as DiffractionSimulation - -if TYPE_CHECKING: - from diffsims.generators.simulation_generator import SimulationGenerator - - -class SimulationLibrary(NamedTuple): - phase: Sequence[Phase] - rotations: Sequence[Rotation] - diffraction_generator: "SimulationGenerator" - simulations: Sequence[DiffractionSimulation] - str_rotations: Sequence[str] = None - calibration: float = None - - def __repr__(self): - return ( - f"DiffractionPhaseLibrary(phase={self.phase.name}," - f" No. Rotations={self.__len__()})" - ) - - def __post_init__(self): - if len(self.rotations) != len(self.simulations): - raise ValueError("Number of rotations and simulations must be the same") - if self.str_rotations is not None and len(self.rotations) != len( - self.str_rotations - ): - raise ValueError("Number of rotations and str_rotations must be the same") - self.simulation_index = 0 # for interactive plotting - - def __len__(self): - return len(self.rotations) - - def __getitem__(self, item): - if isinstance(item, str): - item = self.str_rotations.index(item) - return SimulationLibrary( - self.phase, self.rotations[item], self.simulations[item] - ) - - def get_library_entry( - self, rotation: Rotation, angle_cutoff: float = 1e-2 - ) -> "DiffractionPhaseLibrary": - angles = self.rotations.angle_with(rotation) - is_in_range = np.sum(np.abs(angles), axis=1) < angle_cutoff - return self[is_in_range] - - def rotations_to_vectors(self, beam_direction: Vector3d = None) -> Vector3d: - """Converts the rotations to vectors - - Parameters - ---------- - beam_direction - The beam direction used to determine the vectors based on the rotations - """ - if beam_direction is None: - beam_direction = Vector3d.zvector() - vectors = self.rotations * beam_direction - return vectors - - def max_num_spots(self): - """Returns the maximum number of spots in the library""" - return max([i.intensities.shape[0] for i in self.simulations]) - - def plot_rotations(self, beam_direction: Vector3d = None, **kwargs): - """Plots all the diffraction patterns in the library - - Parameters - ---------- - beam_direction - The beam direction used to determine the vectors based on the rotations - """ - vectors = self.rotations_to_vectors(beam_direction) - vectors.scatter(**kwargs) - - def polar_flatten_simulations(self): - """Flatten the simulations into arrays of shape (n_simulations, max(num_diffraction_spots)) - for the polar coordinates (r,theta, intensity) of the diffraction spots - """ - max_num_spots = self.max_num_spots() - - r = np.zeros((len(self), max_num_spots)) - theta = np.zeros((len(self), max_num_spots)) - intensity = np.zeros((len(self), max_num_spots)) - - for i, sim in enumerate(self.simulations): - ( - r[i, : sim.intensities.shape[0]], - theta[i, : sim.intensities.shape[0]], - ) = sim.get_polar_coordinates() - intensity[i, : sim.intensities.shape[0]] = sim.intensities - return r, theta, intensity - - -class SimulationLibraries(dict): - """ - A dictionary containing all the structures and their associated rotations - """ - - def __init__(self, libraries: Sequence[SimulationLibrary]): - super().__init__() - for library in libraries: - self[library.phase.name] = library - - def __repr__(self): - return f"DiffractionLibrary)" diff --git a/diffsims/simulations/simulation.py b/diffsims/simulations/simulation.py index 48a3f1b0..d176a573 100644 --- a/diffsims/simulations/simulation.py +++ b/diffsims/simulations/simulation.py @@ -11,7 +11,7 @@ from diffsims.pattern.detector_functions import add_shot_and_point_spread from diffsims.utils import mask_utils -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from diffsims.generators.simulation_generator import SimulationGenerator @@ -157,48 +157,29 @@ def __iter__(self): return self def __next__(self): - if self.rotation_index <= len(self.coordinates[self.phase_index]) - 1: + if self.has_multiple_phases: + coords = self.coordinates[self.phase_index] + else: + coords = self.coordinates + if self.has_multiple_vectors: + coords = coords[self.rotation_index] + else: + coords = coords + if self.has_multiple_vectors: self.rotation_index += 1 - elif self.phase_index <= len(self.phase) - 1: - self.phase_index += 1 + if self.rotation_index >= coords.size: + self.rotation_index = 0 + self.phase_index += 1 else: + self.phase_index += 1 + if self.phase_index >= self.num_phases: + self.phase_index = 0 raise StopIteration - return self.coordinates[self.phase_index][self.rotation_index] - - def __len__(self): - return self.coordinates.shape[0] - - @property - def size(self): - return self.__len__() - - def __getitem__(self, sliced: Any): - """Sliced is any valid numpy slice that does not change the number of - dimensions or number of columns""" - coords = self.coordinates[sliced] - return Simulation( - coords, - calibration=self.calibration, - offset=self.offset, - with_direct_beam=self.with_direct_beam, - ) + return coords def deepcopy(self): return copy.deepcopy(self) - def append(self, vectors: ReciprocalLatticeVector): - new_data = copy.deepcopy(self) - new_coords = np.concatenate( - (new_data._coordinates.data, vectors._coordinates.data), axis=0 - ) - new_data._coordinates = ReciprocalLatticeVector( - phase=self._coordinates.phase, xyz=new_coords - ) - new_data._coordinates.intensity = np.concatenate( - (self._coordinates.intensity, vectors._coordinates.intensity) - ) - return new_data - @property def calibrated_coordinates(self): """ndarray : Coordinates converted into pixel space.""" diff --git a/diffsims/tests/crystallography/test_reciprocal_lattice_vector.py b/diffsims/tests/crystallography/test_reciprocal_lattice_vector.py index d8717107..41f29fb3 100644 --- a/diffsims/tests/crystallography/test_reciprocal_lattice_vector.py +++ b/diffsims/tests/crystallography/test_reciprocal_lattice_vector.py @@ -108,6 +108,25 @@ def test_repr(self, ferrite_phase): "[[ 1. 1. 0.]", ] + def test_add_intensity(self, ferrite_phase): + rlv = ReciprocalLatticeVector.from_min_dspacing(ferrite_phase, 1.5) + rlv.intensity = 1 + assert isinstance(rlv.intensity, np.ndarray) + assert np.allclose(rlv.intensity, np.ones(rlv.size)) + + def test_add_intensity_error(self, ferrite_phase): + rlv = ReciprocalLatticeVector.from_min_dspacing(ferrite_phase, 1.5) + with pytest.raises(ValueError): + rlv.intensity = [0, 1] + + @pytest.mark.parametrize("degrees", [True, False]) + def test_to_polar(self, ferrite_phase, degrees): + rlv = ReciprocalLatticeVector.from_min_dspacing(ferrite_phase, 1.5) + r, theta, z = rlv.to_polar(degrees=degrees) + assert r.shape == (rlv.size,) + assert theta.shape == (rlv.size,) + assert z.shape == (rlv.size,) + def test_get_item(self, ferrite_phase): """Indexing gives desired vectors and properties carry over.""" rlv = ReciprocalLatticeVector.from_min_dspacing(ferrite_phase, 1.5) diff --git a/diffsims/tests/generators/test_simulation_generator.py b/diffsims/tests/generators/test_simulation_generator.py index 2ca3af4e..ec05b990 100644 --- a/diffsims/tests/generators/test_simulation_generator.py +++ b/diffsims/tests/generators/test_simulation_generator.py @@ -58,12 +58,6 @@ def diffraction_calculator_custom(): return SimulationGenerator(300, shape_factor_model=local_excite, t=0.2) -@pytest.fixture(params=[(1, 3), (1,), (False,)]) -def precessed(request): - var = request.param - return var if len(var) - 1 else var[0] - - def make_phase(lattice_parameter=None): """ We construct an Fd-3m silicon (with lattice parameter 5.431 as a default) @@ -95,15 +89,6 @@ def local_structure(): return make_phase() -def probe(x, out=None, scale=None): - if hasattr(x, "shape"): - return (abs(x[..., 0]) < 6) * (abs(x[..., 1]) < 6) - else: - v = abs(x[0].reshape(-1, 1, 1)) < 6 - v = v * abs(x[1].reshape(1, -1, 1)) < 6 - return v + 0 * x[2].reshape(1, 1, -1) - - @pytest.mark.parametrize("model", [binary, linear, atanc, sin2c, lorentzian]) def test_shape_factor_precession(model): excitation = np.array([-0.1, 0.1]) diff --git a/diffsims/tests/simulations/test_simulations.py b/diffsims/tests/simulations/test_simulations.py index 50c38328..3b1fedd4 100644 --- a/diffsims/tests/simulations/test_simulations.py +++ b/diffsims/tests/simulations/test_simulations.py @@ -82,34 +82,22 @@ def test_plot_profile_simulation(profile_simulation): profile_simulation.plot() -class TestDiffractionSimulation: - @pytest.fixture - def al_phase(self): - p = Phase( - name="al", - space_group=225, - structure=Structure( - atoms=[Atom("al", [0, 0, 0])], - lattice=Lattice(0.405, 0.405, 0.405, 90, 90, 90), - ), - ) - return p - - @pytest.fixture - def diffraction_simulation(self, al_phase): - vector = ReciprocalLatticeVector( - phase=al_phase, xyz=np.array([[0, 0, 0], [1, 2, 3], [3, 4, 5]]) - ) +@pytest.fixture(scope="module") +def al_phase(): + p = Phase( + name="al", + space_group=225, + structure=Structure( + atoms=[Atom("al", [0, 0, 0])], + lattice=Lattice(0.405, 0.405, 0.405, 90, 90, 90), + ), + ) + return p - return Simulation( - phases=al_phase, - rotations=Rotation.from_axes_angles([0, 0, 1], angles=0), - coordinates=vector, - simulation_generator=SimulationGenerator(300), - ) +class TestDiffractionSimulation: @pytest.fixture - def diffraction_simulation_calibrated(self, al_phase): + def diffraction_simulation(self, al_phase): vector = ReciprocalLatticeVector( phase=al_phase, xyz=np.array([[0, 0, 0], [1, 2, 3], [3, 4, 5]]) ) @@ -119,7 +107,6 @@ def diffraction_simulation_calibrated(self, al_phase): rotations=Rotation.from_axes_angles([0, 0, 1], angles=0), coordinates=vector, simulation_generator=SimulationGenerator(300), - calibration=0.5, ) def test_init(self, diffraction_simulation): @@ -131,7 +118,7 @@ def test_init(self, diffraction_simulation): assert np.isnan(diffraction_simulation.coordinates.intensity).all() assert diffraction_simulation.coordinates.intensity.shape == (3,) assert np.allclose(diffraction_simulation.calibration, np.array([0.1, 0.1])) - assert len(diffraction_simulation) == 3 + assert diffraction_simulation.coordinates.size == 3 @pytest.mark.parametrize( "calibration, expected", @@ -189,3 +176,40 @@ def test_calibrated_coordinates( diffraction_simulation.calibration = calibration diffraction_simulation.offset = offset assert np.allclose(diffraction_simulation.calibrated_coordinates, expected) + + def test_irot(self, diffraction_simulation): + with pytest.raises(ValueError): + diffraction_simulation.irot[0] + + def test_iphase(self, diffraction_simulation): + with pytest.raises(ValueError): + diffraction_simulation.iphase[0] + + +class TestMultiRotationSimulation: + @pytest.fixture(scope="class") + def diffraction_simulation(self, al_phase): + vector = ReciprocalLatticeVector( + phase=al_phase, xyz=np.array([[0, 0, 0], [1, 2, 3], [3, 4, 5]]) + ) + + return Simulation( + phases=al_phase, + rotations=Rotation.from_axes_angles([0, 0, 1], angles=[0, 45]), + coordinates=[vector, vector], + simulation_generator=SimulationGenerator(300), + ) + + def test_init(self, diffraction_simulation): + for sim in diffraction_simulation: + assert isinstance(sim, Simulation) + + def test_irot(self, diffraction_simulation): + assert isinstance(diffraction_simulation.irot[0], Simulation) + assert diffraction_simulation.irot[0].rotations == Rotation.from_axes_angles( + [0, 0, 1], angles=0 + ) + assert isinstance(diffraction_simulation.irot[1], Simulation) + assert diffraction_simulation.irot[1].rotations == Rotation.from_axes_angles( + [0, 0, 1], angles=45 + ) From 1701287fa0affe67c67e2387976558fa72a9b79a Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Sat, 25 Nov 2023 20:27:44 -0600 Subject: [PATCH 26/48] Refactor: Fix iter for simulations --- diffsims/simulations/simulation.py | 47 +++++----- .../tests/simulations/test_simulations.py | 93 ++++++++++++++++--- 2 files changed, 107 insertions(+), 33 deletions(-) diff --git a/diffsims/simulations/simulation.py b/diffsims/simulations/simulation.py index d176a573..c2d3948d 100644 --- a/diffsims/simulations/simulation.py +++ b/diffsims/simulations/simulation.py @@ -66,7 +66,7 @@ def __init__(self, simulation): def __getitem__(self, item): all_phases = self.simulation.phases - if isinstance(self.simulation.coordinates, ReciprocalLatticeVector): + if self.simulation.current_size == 1: raise ValueError("Only one rotation in the simulation") elif isinstance(all_phases, Phase): # only one phase in the simulation coords = self.simulation.coordinates[item] @@ -157,25 +157,31 @@ def __iter__(self): return self def __next__(self): - if self.has_multiple_phases: - coords = self.coordinates[self.phase_index] - else: - coords = self.coordinates - if self.has_multiple_vectors: - coords = coords[self.rotation_index] + if self.phase_index == self.num_phases: + self.phase_index = 0 + raise StopIteration else: - coords = coords - if self.has_multiple_vectors: - self.rotation_index += 1 - if self.rotation_index >= coords.size: + if self.has_multiple_phases: + coords = self.coordinates[self.phase_index] + else: + coords = self.coordinates + if self.has_multiple_vectors: + coords = coords[self.rotation_index] + else: + coords = coords + if self.rotation_index + 1 == self.current_size: self.rotation_index = 0 self.phase_index += 1 + else: + self.rotation_index += 1 + return coords + + @property + def current_size(self): + if self.has_multiple_phases: + return self.coordinates[self.phase_index].size else: - self.phase_index += 1 - if self.phase_index >= self.num_phases: - self.phase_index = 0 - raise StopIteration - return coords + return self.coordinates.size def deepcopy(self): return copy.deepcopy(self) @@ -425,13 +431,13 @@ def num_phases(self): if hasattr(self.phases, "__len__"): return len(self.phases) else: - return 0 + return 1 @property def num_vectors(self): num_phases = self.num_phases if isinstance(self.coordinates, ReciprocalLatticeVector): - return 0 + return len(self.coordinates) elif hasattr(self.coordinates, "__len__") and num_phases == 0: return (len(self.coordinates),) else: # hasattr(self.coordinates, "__len__") and num_phases>0: @@ -447,10 +453,7 @@ def has_multiple_phases(self): @property def has_multiple_vectors(self): """Returns True if the simulation has multiple vectors""" - if isinstance(self.coordinates, ReciprocalLatticeVector): - return False - else: - return True + return self.coordinates.size > 1 def get_current_coordinates(self): """Returns the coordinates of the current phase and rotation""" diff --git a/diffsims/tests/simulations/test_simulations.py b/diffsims/tests/simulations/test_simulations.py index 3b1fedd4..77c3f79d 100644 --- a/diffsims/tests/simulations/test_simulations.py +++ b/diffsims/tests/simulations/test_simulations.py @@ -99,7 +99,12 @@ class TestDiffractionSimulation: @pytest.fixture def diffraction_simulation(self, al_phase): vector = ReciprocalLatticeVector( - phase=al_phase, xyz=np.array([[0, 0, 0], [1, 2, 3], [3, 4, 5]]) + phase=al_phase, + xyz=np.array( + [ + [0, 0, 0], + ] + ), ) return Simulation( @@ -112,13 +117,17 @@ def diffraction_simulation(self, al_phase): def test_init(self, diffraction_simulation): assert np.allclose( diffraction_simulation.coordinates.data, - np.array([[0, 0, 0], [1, 2, 3], [3, 4, 5]]), + np.array( + [ + [0, 0, 0], + ] + ), ) - assert diffraction_simulation.coordinates.hkl.shape == (3, 3) + assert diffraction_simulation.coordinates.hkl.shape == (1, 3) assert np.isnan(diffraction_simulation.coordinates.intensity).all() - assert diffraction_simulation.coordinates.intensity.shape == (3,) + assert diffraction_simulation.coordinates.intensity.shape == (1,) assert np.allclose(diffraction_simulation.calibration, np.array([0.1, 0.1])) - assert diffraction_simulation.coordinates.size == 3 + assert diffraction_simulation.coordinates.size == 1 @pytest.mark.parametrize( "calibration, expected", @@ -185,6 +194,13 @@ def test_iphase(self, diffraction_simulation): with pytest.raises(ValueError): diffraction_simulation.iphase[0] + def test_iter(self, diffraction_simulation): + count = 0 + for sim in diffraction_simulation: + count += 1 + assert isinstance(sim, ReciprocalLatticeVector) + assert count == 1 + class TestMultiRotationSimulation: @pytest.fixture(scope="class") @@ -195,15 +211,11 @@ def diffraction_simulation(self, al_phase): return Simulation( phases=al_phase, - rotations=Rotation.from_axes_angles([0, 0, 1], angles=[0, 45]), - coordinates=[vector, vector], + rotations=Rotation.from_axes_angles([0, 0, 1], angles=[0, 45, 60]), + coordinates=vector, simulation_generator=SimulationGenerator(300), ) - def test_init(self, diffraction_simulation): - for sim in diffraction_simulation: - assert isinstance(sim, Simulation) - def test_irot(self, diffraction_simulation): assert isinstance(diffraction_simulation.irot[0], Simulation) assert diffraction_simulation.irot[0].rotations == Rotation.from_axes_angles( @@ -213,3 +225,62 @@ def test_irot(self, diffraction_simulation): assert diffraction_simulation.irot[1].rotations == Rotation.from_axes_angles( [0, 0, 1], angles=45 ) + + def test_iter(self, diffraction_simulation): + diffraction_simulation.phase_index = 0 + diffraction_simulation.rotation_index = 0 + count = 0 + for sim in diffraction_simulation: + count += 1 + assert isinstance(sim, ReciprocalLatticeVector) + assert count == 3 + + +class TestMultiPhaseMultiRotationSimulation: + @pytest.fixture(scope="class") + def diffraction_simulation(self, al_phase): + vector = ReciprocalLatticeVector( + phase=al_phase, xyz=np.array([[0, 0, 0], [1, 2, 3], [3, 4, 5]]) + ) + al_phase2 = al_phase.deepcopy() + al_phase2.name = "al2" + al_phase.name = "al1" + + return Simulation( + phases=[al_phase, al_phase2], + rotations=[ + Rotation.from_axes_angles([0, 0, 1], angles=[0, 45, 60]), + Rotation.from_axes_angles([0, 0, 1], angles=[0, 45, 60]), + ], + coordinates=[vector, vector], + simulation_generator=SimulationGenerator(300), + ) + + def test_iphase(self, diffraction_simulation): + assert isinstance(diffraction_simulation.iphase[0], Simulation) + assert diffraction_simulation.iphase[0].current_phase.name == "al1" + assert diffraction_simulation.iphase["al1"].current_phase.name == "al1" + assert isinstance(diffraction_simulation.iphase[0].phases, Phase) + + assert isinstance(diffraction_simulation.iphase[1], Simulation) + assert diffraction_simulation.iphase[1].current_phase.name == "al2" + assert diffraction_simulation.iphase["al2"].current_phase.name == "al2" + + def test_irot(self, diffraction_simulation): + assert isinstance(diffraction_simulation.irot[0], Simulation) + assert diffraction_simulation.iphase[0].irot[ + 0 + ].rotations == Rotation.from_axes_angles([0, 0, 1], angles=0) + assert isinstance(diffraction_simulation.irot[1], Simulation) + assert diffraction_simulation.iphase[0].irot[ + 1 + ].rotations == Rotation.from_axes_angles([0, 0, 1], angles=45) + + def test_iter(self, diffraction_simulation): + diffraction_simulation.phase_index = 0 + diffraction_simulation.rotation_index = 0 + count = 0 + for sim in diffraction_simulation: + count += 1 + assert isinstance(sim, ReciprocalLatticeVector) + assert count == 6 From 11eb81358f93b8c629e2259f1bd8ef18551755b7 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Mon, 27 Nov 2023 15:20:35 -0600 Subject: [PATCH 27/48] Refactor: Fixed test coverage and removed some extra code --- diffsims/generators/simulation_generator.py | 92 +----- diffsims/simulations/simulation.py | 308 ++++++------------ .../generators/test_simulation_generator.py | 17 - diffsims/tests/simulations/test_simulation.py | 226 +++++++++++-- .../tests/simulations/test_simulations.py | 286 ---------------- .../simulating_diffraction_patterns.py | 27 +- 6 files changed, 309 insertions(+), 647 deletions(-) delete mode 100644 diffsims/tests/simulations/test_simulations.py diff --git a/diffsims/generators/simulation_generator.py b/diffsims/generators/simulation_generator.py index f6efbd82..6625e314 100644 --- a/diffsims/generators/simulation_generator.py +++ b/diffsims/generators/simulation_generator.py @@ -5,7 +5,7 @@ from orix.crystal_map import Phase from diffsims.crystallography import ReciprocalLatticeVector -from diffsims.simulations.simulation import Simulation, ProfileSimulation +from diffsims.simulations.simulation import Simulation from diffsims.utils.shape_factor_models import ( linear, atanc, @@ -162,8 +162,8 @@ def calculate_ed_data( vectors = vectors[0] phase = phase[0] rotation = rotation[0] - if rotation.size == 1: - vectors = vectors[0] + if rotation.size == 1: + vectors = vectors[0] # Create a simulation object sim = Simulation( @@ -171,7 +171,7 @@ def calculate_ed_data( coordinates=vectors, rotations=rotation, simulation_generator=self, - reciporical_radius=reciprocal_radius, + reciprocal_radius=reciprocal_radius, ) return sim @@ -239,87 +239,3 @@ def get_intersecting_reflections( **self.shape_factor_kwargs, ) return intersected_vectors, shape_factor - - def calculate_profile_data( - self, - phase: Phase, - reciprocal_radius: float = 1.0, - minimum_intensity: float = 1e-3, - debye_waller_factors: dict = None, - ): - """Calculates a one dimensional diffraction profile for a - structure. - - Parameters - ---------- - structure : diffpy.structure.structure.Structure - The structure for which to calculate the diffraction profile. - reciprocal_radius : float - The maximum radius of the sphere of reciprocal space to - sample, in reciprocal angstroms. - minimum_intensity : float - The minimum intensity required for a diffraction peak to be - considered real. Deals with numerical precision issues. - debye_waller_factors : dict of str:value pairs - Maps element names to their temperature-dependent Debye-Waller factors. - - Returns - ------- - diffsims.sims.diffraction_simulation.ProfileSimulation - The diffraction profile corresponding to this structure and - experimental conditions. - """ - latt = phase.structure.lattice - - # Obtain crystallographic reciprocal lattice points within range - vectors = ReciprocalLatticeVector.from_min_dspacing( - phase, - min_dspacing=1 / reciprocal_radius, - ) - - unique_vectors = vectors.unique(use_symmetry=True).symmetrise() - - multiplicity = unique_vectors.multiplicity - g_indices = unique_vectors.hkl - g_hkls = unique_vectors.gspacing - - i_hkl = get_kinematical_intensities( - phase.structure, - g_indices, - np.asarray(g_hkls), - prefactor=multiplicity, - scattering_params=self.scattering_params, - debye_waller_factors=debye_waller_factors, - ) - - if is_lattice_hexagonal(latt): - # Use Miller-Bravais indices for hexagonal lattices. - g_indices = ( - g_indices[0], - g_indices[1], - -g_indices[0] - g_indices[1], - g_indices[2], - ) - - hkls_labels = ["".join([str(int(x)) for x in xs]) for xs in g_indices] - - peaks = {} - for l, i, g in zip(hkls_labels, i_hkl, g_hkls): - peaks[l] = [i, g] - - # Scale intensities so that the max intensity is 100. - - max_intensity = max([v[0] for v in peaks.values()]) - x = [] - y = [] - hkls = [] - for k in peaks.keys(): - v = peaks[k] - if v[0] / max_intensity * 100 > minimum_intensity and (k != "000"): - x.append(v[1]) - y.append(v[0]) - hkls.append(k) - - y = np.asarray(y) / max(y) * 100 - - return ProfileSimulation(x, y, hkls) diff --git a/diffsims/simulations/simulation.py b/diffsims/simulations/simulation.py index c2d3948d..01185eb4 100644 --- a/diffsims/simulations/simulation.py +++ b/diffsims/simulations/simulation.py @@ -45,10 +45,6 @@ def __getitem__(self, item): coordinates=new_coords, rotations=new_rotations, simulation_generator=self.simulation.simulation_generator, - calibration=self.simulation.calibration, - offset=self.simulation.offset, - with_direct_beam=self.simulation.with_direct_beam, - shape=self.simulation.shape, ) @@ -81,10 +77,6 @@ def __getitem__(self, item): coordinates=coords, rotations=rotations, simulation_generator=self.simulation.simulation_generator, - calibration=self.simulation.calibration, - offset=self.simulation.offset, - with_direct_beam=self.simulation.with_direct_beam, - shape=self.simulation.shape, ) @@ -114,40 +106,70 @@ def __init__( ], rotations: Union[Rotation, Sequence[Rotation]], simulation_generator: "SimulationGenerator", - calibration: Sequence[float] = (0.1, 0.1), - offset: Sequence[float] = (0.0, 0.0), - with_direct_beam: bool = False, - shape: Sequence[int] = (512, 512), - reciporical_radius: float = 1.0, + reciprocal_radius: float = 1.0, ): """Initializes the DiffractionSimulation object with data values for the coordinates, indices, intensities, calibration and offset. + + Parameters + ---------- + coordinates + The list of ReciprocalLatticeVector objects for each phase and rotation. If there + are multiple phases, then this should be a list of lists of ReciprocalLatticeVector objects. + If there is only one phase, then this should be a list of ReciprocalLatticeVector objects. + rotations + The list of Rotation objects for each phase. If there are multiple phases, then this should + be a list of Rotation objects. If there is only one phase, then this should be a single + Rotation object. + phases + The list of Phase objects for each phase. If there is only one phase, then this should be + a single Phase object. + simulation_generator + The SimulationGenerator object used to generate the diffraction patterns. + """ # Basic data - if not isinstance(phases, Phase): - phases = np.array(phases) - if not isinstance(rotations, Rotation): - rotations = np.array(rotations) - if not isinstance(coordinates, ReciprocalLatticeVector): + if isinstance(rotations, Rotation) and rotations.size == 1: + if not isinstance(coordinates, ReciprocalLatticeVector): + raise ValueError( + "If there is only one rotation, then the coordinates must be a ReciprocalLatticeVector object" + ) + elif isinstance(rotations, Rotation): coordinates = np.array(coordinates, dtype=object) - + if coordinates.size != rotations.size: + raise ValueError( + f"The number of rotations: {rotations.size} must match the number of " + f"coordinates {coordinates.size}" + ) + else: # iterable of Rotation + rotations = np.array(rotations, dtype=object) + coordinates = np.array(coordinates, dtype=object) + if len(coordinates.shape) != 2: + coordinates = coordinates[:, np.newaxis] + phases = np.array(phases) + if rotations.size != phases.size: + raise ValueError( + f"The number of rotations: {rotations.size} must match the number of " + f"phases {phases.size}" + ) + + for r, c in zip(rotations, coordinates): + if r.size != c.size: + raise ValueError( + f"The number of rotations: {r.size} must match the number of " + f"coordinates {c.shape[0]}" + ) self.phases = phases self.rotations = rotations self.coordinates = coordinates self.simulation_generator = simulation_generator - self.reciporical_radius = reciporical_radius - - # Data for integrating with real data from a detector - self.calibration = calibration - self.shape = shape - self.offset = np.array(offset) - self.with_direct_beam = with_direct_beam # for interactive plotting and iterating through the Simulations self.phase_index = 0 self.rotation_index = 0 self._rot_plot = None self._diff_plot = None + self.reciporical_radius = reciprocal_radius # for slicing a simulation self.iphase = PhaseGetter(self) @@ -179,64 +201,28 @@ def __next__(self): @property def current_size(self): if self.has_multiple_phases: - return self.coordinates[self.phase_index].size + return self.rotations[self.phase_index].size else: - return self.coordinates.size + return self.rotations.size def deepcopy(self): return copy.deepcopy(self) - @property - def calibrated_coordinates(self): - """ndarray : Coordinates converted into pixel space.""" - if self.calibration is not None: - return (self.coordinates.data[:, :2] + self.offset) / self.calibration - else: - raise Exception("Pixel calibration is not set!") - - @property - def pixel_coordinates(self): - half_shape = np.array(self.shape) / 2 - pixel_coordinates = np.rint( - self.calibrated_coordinates[:, :2] + half_shape - ).astype(int) - return pixel_coordinates - - @property - def calibration(self): - """tuple of float : The x- and y-scales of the pattern, with respect to - the original reciprocal angstrom coordinates.""" - return self._calibration - - @calibration.setter - def calibration(self, calibration: Union[float, Sequence[float]]): - if calibration is None: - pass - elif np.all(np.equal(calibration, 0)): - raise ValueError("`calibration` cannot be zero.") - elif isinstance(calibration, float) or isinstance(calibration, int): - calibration = np.array((calibration, calibration)) - elif len(calibration) == 2: - calibration = np.array(calibration) - else: - raise ValueError( - "`calibration` must be a float or length-2" "tuple of floats." - ) - self._calibration = calibration - def _get_transformed_coordinates( self, angle: float, center: Sequence = (0, 0), mirrored: bool = False, units: str = "real", + calibration: float = None, ): """Translate, rotate or mirror the pattern spot coordinates""" coords = self.get_current_coordinates() if units != "real": - center = np.array(center) / self.calibration + center = np.array(center) + coords.data = coords.data / calibration transformed_coords = coords cx, cy = center x = transformed_coords.data[:, 0] @@ -274,70 +260,8 @@ def rotate_shift_coordinates( ) return coords_new - def get_as_mask( - self, - shape: Sequence[int], - radius: float = 6.0, - negative: bool = False, - radius_function=None, - direct_beam_position=None, - in_plane_angle=0, - mirrored=False, - *args, - **kwargs, - ): - """ - Return the diffraction pattern as a binary mask of type - bool - - Parameters - ---------- - shape: 2-tuple of ints - Shape of the output mask (width, height) - radius: float or array, optional - Radii of the spots in pixels. An array may be supplied - of the same length as the number of spots. - negative: bool, optional - Whether the spots are masked (True) or everything - else is masked (False) - radius_function: Callable, optional - Calculate the radius as a function of the spot intensity, - for example np.sqrt. args and kwargs supplied to this method - are passed to this function. Will override radius. - direct_beam_position: 2-tuple of ints, optional - The (x,y) coordinate in pixels of the direct beam. Defaults to - the center of the image. - in_plane_angle: float, optional - In plane rotation of the pattern in degrees - mirrored: bool, optional - Whether the pattern should be flipped over the x-axis, - corresponding to the inverted orientation - - Returns - ------- - mask: numpy.ndarray - Boolean mask of the diffraction pattern - """ - r = radius - if direct_beam_position is None: - direct_beam_position = (shape[1] // 2, shape[0] // 2) - point_coordinates_shifted = self._get_transformed_coordinates( - in_plane_angle, - center=direct_beam_position, - mirrored=mirrored, - units="pixels", - ) - if radius_function is not None: - r = radius_function(self.intensities, *args, **kwargs) - mask = mask_utils.create_mask(shape, fill=negative) - mask_utils.add_circles_to_mask( - mask, point_coordinates_shifted.coordinates.data, r, fill=not negative - ) - return mask - def polar_flatten_simulations(self): """Flattens the simulations into polar coordinates for use in template matching. - The resulting arrays are of shape (n_simulations, n_spots) where n_spots is the maximum number of spots in any simulation. @@ -347,19 +271,18 @@ def polar_flatten_simulations(self): r_templates, theta_templates, intensities_templates """ - flattened_vectors = [sim.data for sim in self] - flattened_intensity = [sim.intensity for sim in self] - max_num_spots = max([len(v) for v in flattened_vectors]) + flattened_vectors = [sim for sim in self] + max_num_spots = max([v.size for v in flattened_vectors]) r_templates = np.zeros((len(flattened_vectors), max_num_spots)) theta_templates = np.zeros((len(flattened_vectors), max_num_spots)) intensities_templates = np.zeros((len(flattened_vectors), max_num_spots)) - for i, (v, i_v) in enumerate(zip(flattened_vectors, flattened_intensity)): + for i, v in enumerate(flattened_vectors): r, t, _ = v.to_polar() r_templates[i, : len(r)] = r theta_templates[i, : len(t)] = t - intensities_templates[i, : len(i_v)] = i_v + intensities_templates[i, : len(v.intensity)] = v.intensity return r_templates, theta_templates, intensities_templates @@ -369,6 +292,7 @@ def get_diffraction_pattern( sigma=10, direct_beam_position=None, in_plane_angle=0, + calibration=0.01, mirrored=False, ): """Returns the diffraction data as a numpy array with @@ -399,23 +323,26 @@ def get_diffraction_pattern( ----- If don't know the exact calibration of your diffraction signal using 1e-2 produces reasonably good patterns when the lattice parameters are on - the order of 0.5nm and a the default size and sigma are used. + the order of 0.5nm and the default size and sigma are used. """ - if shape is None: - shape = self.shape if direct_beam_position is None: direct_beam_position = (shape[1] // 2, shape[0] // 2) - tranformed = self._get_transformed_coordinates( - in_plane_angle, direct_beam_position, mirrored, units="pixel" + transformed = self._get_transformed_coordinates( + in_plane_angle, + direct_beam_position, + mirrored, + units="pixel", + calibration=calibration, ) in_frame = ( - (tranformed.coordinates.data[:, 0] >= 0) - & (tranformed.coordinates.data[:, 0] < shape[1]) - & (tranformed.coordinates.data[:, 1] >= 0) - & (tranformed.coordinates.data[:, 1] < shape[0]) + (transformed.data[:, 0] >= 0) + & (transformed.data[:, 0] < shape[1]) + & (transformed.data[:, 1] >= 0) + & (transformed.data[:, 1] < shape[0]) ) - spot_coords = tranformed.coordinates.data[in_frame].astype(int) - spot_intens = self.intensities[in_frame] + spot_coords = transformed.data[in_frame].astype(int) + + spot_intens = transformed.intensity[in_frame] pattern = np.zeros(shape) # checks that we have some spots if spot_intens.shape[0] == 0: @@ -433,18 +360,6 @@ def num_phases(self): else: return 1 - @property - def num_vectors(self): - num_phases = self.num_phases - if isinstance(self.coordinates, ReciprocalLatticeVector): - return len(self.coordinates) - elif hasattr(self.coordinates, "__len__") and num_phases == 0: - return (len(self.coordinates),) - else: # hasattr(self.coordinates, "__len__") and num_phases>0: - return tuple( - [len(c) if hasattr(c, "__len__") else 0 for c in self.coordinates] - ) - @property def has_multiple_phases(self): """Returns True if the simulation has multiple phases""" @@ -458,11 +373,13 @@ def has_multiple_vectors(self): def get_current_coordinates(self): """Returns the coordinates of the current phase and rotation""" if self.has_multiple_phases: - return self.coordinates[self.phase_index][self.rotation_index] + return copy.deepcopy( + self.coordinates[self.phase_index][self.rotation_index] + ) elif not self.has_multiple_phases and self.has_multiple_vectors: - return self.coordinates[self.rotation_index] + return copy.deepcopy(self.coordinates[self.rotation_index]) else: - return self.coordinates + return copy.deepcopy(self.coordinates) def plot_rotations(self, beam_direction: Vector3d = Vector3d.zvector()): """Plots the rotations of the current phase in stereographic projection""" @@ -496,6 +413,7 @@ def plot( label_formatting=None, min_label_intensity=0.1, include_direct_beam=True, + calibration=0.1, ax=None, **kwargs, ): @@ -548,14 +466,18 @@ def plot( _, ax = plt.subplots() ax.set_aspect("equal") coords = self._get_transformed_coordinates( - in_plane_angle, direct_beam_position, mirrored, units=units + in_plane_angle, + direct_beam_position, + mirrored, + units=units, + calibration=calibration, ) if include_direct_beam: spots = coords.data[:, :2] spots = np.concatenate((spots, np.array([direct_beam_position]))) intensity = np.concatenate((coords.intensity, np.array([1]))) else: - spots = coords.data.data[:, :2] + spots = coords.data[:, :2] intensity = coords.intensity sp = ax.scatter( spots[:, 0], @@ -572,13 +494,13 @@ def plot( xlim = ax.get_xlim() ylim = ax.get_ylim() condition = ( - (coords.coordinates.data[:, 0] > min(xlim)) - & (coords.coordinates.data[:, 0] < max(xlim)) - & (coords.coordinates.data[:, 1] > min(ylim)) - & (coords.coordinates.data[:, 1] < max(ylim)) + (coords.data[:, 0] > min(xlim)) + & (coords.data[:, 0] < max(xlim)) + & (coords.data[:, 1] > min(ylim)) + & (coords.data[:, 1] < max(ylim)) ) millers = millers[condition] - coords = coords.coordinates.data[condition] + coords = coords.data[condition] # default alignment options if ( "ha" not in label_offset @@ -587,8 +509,8 @@ def plot( label_formatting["ha"] = "center" if "va" not in label_offset and "verticalalignment" not in label_formatting: label_formatting["va"] = "center" - for miller, coordinate, inten in zip(millers, coords, self.intensities): - if inten > min_label_intensity: + for miller, coordinate, inten in zip(millers, coords, intensity): + if np.isnan(inten) or inten > min_label_intensity: label = "(" for index in miller: if index < 0: @@ -610,51 +532,3 @@ def plot( ax.set_xlabel("pixels") ax.set_ylabel("pixels") return ax, sp - - -class ProfileSimulation: - """Holds the result of a given kinematic simulation of a diffraction - profile. - - Parameters - ---------- - magnitudes : array-like, shape [n_peaks, 1] - Magnitudes of scattering vectors. - intensities : array-like, shape [n_peaks, 1] - The kinematic intensity of the diffraction peaks. - hkls : [{(h, k, l): mult}] {(h, k, l): mult} is a dict of Miller - indices for all diffracted lattice facets contributing to each - intensity. - """ - - def __init__(self, magnitudes, intensities, hkls): - self.magnitudes = magnitudes - self.intensities = intensities - self.hkls = hkls - - def plot(self, annotate_peaks=True, with_labels=True, fontsize=12, ax=None): - """Plots the diffraction profile simulation for the - calculate_profile_data method in DiffractionGenerator. - - Parameters - ---------- - annotate_peaks : boolean - If True, peaks are annotaed with hkl information. - with_labels : boolean - If True, xlabels and ylabels are added to the plot. - fontsize : integer - Fontsize for peak labels. - """ - if ax is None: - fig, ax = plt.subplots() - for g, i, hkls in zip(self.magnitudes, self.intensities, self.hkls): - label = hkls - ax.plot([g, g], [0, i], color="k", linewidth=3, label=label) - if annotate_peaks: - ax.annotate(label, xy=[g, i], xytext=[g, i], fontsize=fontsize) - - if with_labels: - ax.set_xlabel("A ($^{-1}$)") - ax.set_ylabel("Intensities (scaled)") - - return plt diff --git a/diffsims/tests/generators/test_simulation_generator.py b/diffsims/tests/generators/test_simulation_generator.py index ec05b990..5c00f926 100644 --- a/diffsims/tests/generators/test_simulation_generator.py +++ b/diffsims/tests/generators/test_simulation_generator.py @@ -23,7 +23,6 @@ from orix.crystal_map import Phase from diffsims.generators.simulation_generator import SimulationGenerator -from diffsims.simulations.simulation import ProfileSimulation from diffsims.utils.shape_factor_models import ( linear, binary, @@ -201,22 +200,6 @@ def test_shape_factor_custom(self, diffraction_calculator, local_structure): # softly makes sure the two sims are different assert np.sum(t1.coordinates.intensity) != np.sum(t2.coordinates.intensity) - def test_calculate_profile_class(self, local_structure, diffraction_calculator): - # tests the non-hexagonal (cubic) case - profile = diffraction_calculator.calculate_profile_data( - local_structure, reciprocal_radius=1.0 - ) - assert isinstance(profile, ProfileSimulation) - - latt = diffpy.structure.lattice.Lattice(3, 3, 5, 90, 90, 120) - atom = diffpy.structure.atom.Atom(atype="Ni", xyz=[0, 0, 0], lattice=latt) - hexagonal_structure = diffpy.structure.Structure(atoms=[atom], lattice=latt) - phase = Phase(structure=hexagonal_structure, space_group=194) - hexagonal_profile = diffraction_calculator.calculate_profile_data( - phase=phase, reciprocal_radius=1.0 - ) - assert isinstance(hexagonal_profile, ProfileSimulation) - @pytest.mark.parametrize("scattering_param", ["lobato", "xtables"]) def test_param_check(scattering_param): diff --git a/diffsims/tests/simulations/test_simulation.py b/diffsims/tests/simulations/test_simulation.py index af626136..809beec1 100644 --- a/diffsims/tests/simulations/test_simulation.py +++ b/diffsims/tests/simulations/test_simulation.py @@ -10,26 +10,25 @@ from diffsims.crystallography.reciprocal_lattice_vector import ReciprocalLatticeVector -class TestSingleSimulation: - @pytest.fixture - def al_phase(self): - p = Phase( - name="al", - space_group=225, - structure=Structure( - atoms=[Atom("al", [0, 0, 0])], - lattice=Lattice(0.405, 0.405, 0.405, 90, 90, 90), - ), - ) - return p +@pytest.fixture(scope="module") +def al_phase(): + p = Phase( + name="al", + space_group=225, + structure=Structure( + atoms=[Atom("al", [0, 0, 0])], + lattice=Lattice(0.405, 0.405, 0.405, 90, 90, 90), + ), + ) + return p + +class TestSingleSimulation: @pytest.fixture def single_simulation(self, al_phase): gen = SimulationGenerator(accelerating_voltage=200) rot = Rotation.from_axes_angles([1, 0, 0], 45, degrees=True) - coords = ReciprocalLatticeVector( - phase=al_phase, xyz=[[1, 0, 0], [0, 1, 0], [1, 1, 0]] - ) + coords = ReciprocalLatticeVector(phase=al_phase, xyz=[[1, 0, 0]]) sim = Simulation( phases=al_phase, simulation_generator=gen, coordinates=coords, rotations=rot ) @@ -45,9 +44,96 @@ def test_iphase(self, single_simulation): with pytest.raises(ValueError): single_simulation.iphase[0] + def test_irot(self, single_simulation): + with pytest.raises(ValueError): + single_simulation.irot[0] + + def test_iter(self, single_simulation): + count = 0 + for sim in single_simulation: + count += 1 + assert isinstance(sim, ReciprocalLatticeVector) + assert count == 1 + def test_plot(self, single_simulation): single_simulation.plot() + def test_polar_flatten(self, single_simulation): + ( + r_templates, + theta_templates, + intensities_templates, + ) = single_simulation.polar_flatten_simulations() + assert r_templates.shape == (1, 1) + assert theta_templates.shape == (1, 1) + assert intensities_templates.shape == (1, 1) + + def test_deepcopy(self, single_simulation): + copied = single_simulation.deepcopy() + assert copied is not single_simulation + + +class TestSimulationInitFailures: + def test_different_size(self, al_phase): + gen = SimulationGenerator(accelerating_voltage=200) + rot = Rotation.from_axes_angles([1, 0, 0], 45, degrees=True) + coords = ReciprocalLatticeVector(phase=al_phase, xyz=[[1, 0, 0], [1, 1, 1]]) + with pytest.raises(ValueError): + sim = Simulation( + phases=al_phase, + simulation_generator=gen, + coordinates=[coords, coords], + rotations=rot, + ) + + def test_different_size2(self, al_phase): + gen = SimulationGenerator(accelerating_voltage=200) + rot = Rotation.from_axes_angles([1, 0, 0], (0, 45), degrees=True) + coords = ReciprocalLatticeVector(phase=al_phase, xyz=[[1, 0, 0], [1, 1, 1]]) + with pytest.raises(ValueError): + sim = Simulation( + phases=al_phase, + simulation_generator=gen, + coordinates=[coords, coords, coords], + rotations=rot, + ) + + def test_different_size_multiphase(self, al_phase): + gen = SimulationGenerator(accelerating_voltage=200) + rot = Rotation.from_axes_angles([1, 0, 0], 45, degrees=True) + coords = ReciprocalLatticeVector(phase=al_phase, xyz=[[1, 0, 0], [1, 1, 1]]) + with pytest.raises(ValueError): + sim = Simulation( + phases=[al_phase, al_phase], + simulation_generator=gen, + coordinates=[[coords, coords], [coords, coords]], + rotations=[rot, rot], + ) + + def test_different_num_phase(self, al_phase): + gen = SimulationGenerator(accelerating_voltage=200) + rot = Rotation.from_axes_angles([1, 0, 0], 45, degrees=True) + coords = ReciprocalLatticeVector(phase=al_phase, xyz=[[1, 0, 0], [1, 1, 1]]) + with pytest.raises(ValueError): + sim = Simulation( + phases=[al_phase, al_phase], + simulation_generator=gen, + coordinates=[[coords, coords], [coords, coords], [coords, coords]], + rotations=[rot, rot], + ) + + def test_different_num_phase_and_rot(self, al_phase): + gen = SimulationGenerator(accelerating_voltage=200) + rot = Rotation.from_axes_angles([1, 0, 0], 45, degrees=True) + coords = ReciprocalLatticeVector(phase=al_phase, xyz=[[1, 0, 0], [1, 1, 1]]) + with pytest.raises(ValueError): + sim = Simulation( + phases=[al_phase, al_phase], + simulation_generator=gen, + coordinates=[[coords, coords], [coords, coords], [coords, coords]], + rotations=[rot, rot, rot], + ) + class TestSinglePhaseMultiSimulation: @pytest.fixture @@ -67,13 +153,16 @@ def multi_simulation(self, al_phase): gen = SimulationGenerator(accelerating_voltage=200) rot = Rotation.from_axes_angles([1, 0, 0], (0, 15, 30, 45), degrees=True) coords = ReciprocalLatticeVector( - phase=al_phase, xyz=[[1, 0, 0], [0, 1, 0], [1, 1, 0]] + phase=al_phase, xyz=[[1, 0, 0], [0, 1, 0], [1, 1, 0], [1, 1, 1]] ) - coords = [ - coords, - ] * 4 + + vectors = [coords, coords, coords, coords] + sim = Simulation( - phases=al_phase, simulation_generator=gen, coordinates=coords, rotations=rot + phases=al_phase, + simulation_generator=gen, + coordinates=vectors, + rotations=rot, ) return sim @@ -93,18 +182,40 @@ def test_irot(self, multi_simulation): assert isinstance(sliced_sim, Simulation) assert isinstance(sliced_sim.phases, Phase) assert sliced_sim.rotations.size == 1 - assert sliced_sim.num_vectors == 0 + assert sliced_sim.coordinates.size == 4 def test_irot_slice(self, multi_simulation): sliced_sim = multi_simulation.irot[0:2] assert isinstance(sliced_sim, Simulation) assert isinstance(sliced_sim.phases, Phase) assert sliced_sim.rotations.size == 2 - assert sliced_sim.num_vectors == (2,) + assert sliced_sim.coordinates.size == 2 def test_plot(self, multi_simulation): multi_simulation.plot() + def test_plot_rotation(self, multi_simulation): + multi_simulation.plot_rotations() + + def test_iter(self, multi_simulation): + multi_simulation.phase_index = 0 + multi_simulation.rotation_index = 0 + count = 0 + for sim in multi_simulation: + count += 1 + assert isinstance(sim, ReciprocalLatticeVector) + assert count == 4 + + def test_polar_flatten(self, multi_simulation): + ( + r_templates, + theta_templates, + intensities_templates, + ) = multi_simulation.polar_flatten_simulations() + assert r_templates.shape == (4, 4) + assert theta_templates.shape == (4, 4) + assert intensities_templates.shape == (4, 4) + class TestMultiPhaseMultiSimulation: @pytest.fixture @@ -125,19 +236,16 @@ def multi_simulation(self, al_phase): rot = Rotation.from_axes_angles([1, 0, 0], (0, 15, 30, 45), degrees=True) rot2 = rot coords = ReciprocalLatticeVector( - phase=al_phase, xyz=[[1, 0, 0], [0, 1, 0], [1, 1, 0]] + phase=al_phase, xyz=[[1, 0, 0], [0, -1, 0], [1, -1, 0], [1, -1, -1]] ) - - coords = [ - coords, - ] * 4 - coords2 = coords + coords.intensity = 1 + vectors = [coords, coords, coords, coords] al_phase2 = al_phase.deepcopy() al_phase2.name = "al2" sim = Simulation( phases=[al_phase, al_phase2], simulation_generator=gen, - coordinates=[coords, coords2], + coordinates=[vectors, vectors], rotations=[rot, rot2], ) return sim @@ -155,23 +263,75 @@ def test_iphase(self, multi_simulation): assert isinstance(phase_slic.phases, Phase) assert phase_slic.rotations.size == 4 + def test_iphase_error(self, multi_simulation): + with pytest.raises(ValueError): + phase_slic = multi_simulation.iphase[3.1] + def test_irot(self, multi_simulation): sliced_sim = multi_simulation.irot[0] assert isinstance(sliced_sim, Simulation) assert isinstance(sliced_sim.phases, np.ndarray) assert sliced_sim.rotations.size == 2 - assert sliced_sim.num_vectors == (0, 0) def test_irot_slice(self, multi_simulation): sliced_sim = multi_simulation.irot[0:2] assert isinstance(sliced_sim, Simulation) assert isinstance(sliced_sim.phases, np.ndarray) assert sliced_sim.rotations.size == 2 - assert sliced_sim.num_vectors == (2, 2) sliced_sim.plot() - def test_plot(self, multi_simulation): - multi_simulation.plot() + @pytest.mark.parametrize("show_labels", [True, False]) + @pytest.mark.parametrize("units", ["real", "pixel"]) + @pytest.mark.parametrize("include_zero_beam", [True, False]) + def test_plot(self, multi_simulation, show_labels, units, include_zero_beam): + multi_simulation.phase_index = 0 + multi_simulation.rotation_index = 0 + multi_simulation.reciporical_radius = 2 + multi_simulation.coordinates[0][0].intensity = np.nan + multi_simulation.plot( + show_labels=show_labels, + units=units, + min_label_intensity=0.0, + include_direct_beam=include_zero_beam, + calibration=0.1, + ) def test_plot_rotation(self, multi_simulation): multi_simulation.plot_rotations() + + def test_iter(self, multi_simulation): + multi_simulation.phase_index = 0 + multi_simulation.rotation_index = 0 + count = 0 + for sim in multi_simulation: + count += 1 + assert isinstance(sim, ReciprocalLatticeVector) + assert count == 8 + + def test_get_diffraction_pattern(self, multi_simulation): + pat = multi_simulation.get_diffraction_pattern( + shape=(100, 100), calibration=0.01 + ) + assert pat.shape == (100, 100) + assert np.max(pat.data) == 0 + + def test_get_diffraction_pattern2(self, multi_simulation): + pat = multi_simulation.get_diffraction_pattern( + shape=(512, 512), calibration=0.01 + ) + assert pat.shape == (512, 512) + assert np.max(pat.data) == 1 + + def test_polar_flatten(self, multi_simulation): + ( + r_templates, + theta_templates, + intensities_templates, + ) = multi_simulation.polar_flatten_simulations() + assert r_templates.shape == (8, 4) + assert theta_templates.shape == (8, 4) + assert intensities_templates.shape == (8, 4) + + def test_rotate_shift_coords(self, multi_simulation): + rot = multi_simulation.rotate_shift_coordinates(angle=0.1) + assert isinstance(rot, ReciprocalLatticeVector) diff --git a/diffsims/tests/simulations/test_simulations.py b/diffsims/tests/simulations/test_simulations.py deleted file mode 100644 index 77c3f79d..00000000 --- a/diffsims/tests/simulations/test_simulations.py +++ /dev/null @@ -1,286 +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 -import pytest - -from diffpy.structure import Structure, Atom, Lattice -from orix.crystal_map import Phase -from orix.quaternion import Rotation - -from diffsims.crystallography import ReciprocalLatticeVector -from diffsims.simulations.simulation import Simulation, ProfileSimulation -from diffsims.generators.simulation_generator import SimulationGenerator - - -@pytest.fixture -def profile_simulation(): - return ProfileSimulation( - magnitudes=[ - 0.31891931643691351, - 0.52079306292509475, - 0.6106839974876449, - 0.73651261277849378, - 0.80259601243613932, - 0.9020400452156796, - 0.95675794931074043, - 1.0415861258501895, - 1.0893168446141808, - 1.1645286909108374, - 1.2074090451670043, - 1.2756772657476541, - ], - intensities=np.array( - [ - 100.0, - 99.34619104, - 64.1846346, - 18.57137199, - 28.84307971, - 41.31084268, - 23.42104951, - 13.996264, - 24.87559364, - 20.85636003, - 9.46737774, - 5.43222307, - ] - ), - hkls=[ - (1, 1, 1), - (2, 2, 0), - (3, 1, 1), - (4, 0, 0), - (3, 3, 1), - (4, 2, 2), - (3, 3, 3), - (4, 4, 0), - (5, 3, 1), - (6, 2, 0), - (5, 3, 3), - (4, 4, 4), - ], - ) - - -def test_plot_profile_simulation(profile_simulation): - profile_simulation.plot() - - -@pytest.fixture(scope="module") -def al_phase(): - p = Phase( - name="al", - space_group=225, - structure=Structure( - atoms=[Atom("al", [0, 0, 0])], - lattice=Lattice(0.405, 0.405, 0.405, 90, 90, 90), - ), - ) - return p - - -class TestDiffractionSimulation: - @pytest.fixture - def diffraction_simulation(self, al_phase): - vector = ReciprocalLatticeVector( - phase=al_phase, - xyz=np.array( - [ - [0, 0, 0], - ] - ), - ) - - return Simulation( - phases=al_phase, - rotations=Rotation.from_axes_angles([0, 0, 1], angles=0), - coordinates=vector, - simulation_generator=SimulationGenerator(300), - ) - - def test_init(self, diffraction_simulation): - assert np.allclose( - diffraction_simulation.coordinates.data, - np.array( - [ - [0, 0, 0], - ] - ), - ) - assert diffraction_simulation.coordinates.hkl.shape == (1, 3) - assert np.isnan(diffraction_simulation.coordinates.intensity).all() - assert diffraction_simulation.coordinates.intensity.shape == (1,) - assert np.allclose(diffraction_simulation.calibration, np.array([0.1, 0.1])) - assert diffraction_simulation.coordinates.size == 1 - - @pytest.mark.parametrize( - "calibration, expected", - [ - (5.0, np.array((5.0, 5.0))), - pytest.param(0, (0, 0), marks=pytest.mark.xfail(raises=ValueError)), - pytest.param((0, 0), (0, 0), marks=pytest.mark.xfail(raises=ValueError)), - ((1.5, 1.5), np.array((1.5, 1.5))), - ((1.3, 1.5), np.array((1.3, 1.5))), - ], - ) - def test_calibration(self, diffraction_simulation, calibration, expected): - diffraction_simulation.calibration = calibration - assert np.allclose(diffraction_simulation.calibration, expected) - - @pytest.mark.parametrize( - "coordinates, calibration, offset, expected", - [ - ( - np.array([[1.0, 0.0, 0.0], [1.0, 2.0, 0.0]]), - 1.0, - (0.0, 0.0), - np.array([[1.0, 0.0], [1.0, 2.0]]), - ), - ( - np.array([[1.0, 0.0, 0.0], [1.0, 2.0, 0.0]]), - 2.0, - (3.0, 1.0), - np.array([[2.0, 0.5], [2.0, 1.5]]), - ), - pytest.param( - np.array([[1.0, 0.0, 0.0], [1.0, 2.0, 0.0]]), - None, - (0.0, 0.0), - None, - marks=pytest.mark.xfail(raises=Exception), - ), - ], - ) - def test_calibrated_coordinates( - self, - al_phase, - coordinates, - calibration, - offset, - expected, - ): - vect = ReciprocalLatticeVector(phase=al_phase, xyz=coordinates) - diffraction_simulation = Simulation( - phases=al_phase, - coordinates=vect, - rotations=[0, 0, 1], - simulation_generator=SimulationGenerator(300), - ) - diffraction_simulation.calibration = calibration - diffraction_simulation.offset = offset - assert np.allclose(diffraction_simulation.calibrated_coordinates, expected) - - def test_irot(self, diffraction_simulation): - with pytest.raises(ValueError): - diffraction_simulation.irot[0] - - def test_iphase(self, diffraction_simulation): - with pytest.raises(ValueError): - diffraction_simulation.iphase[0] - - def test_iter(self, diffraction_simulation): - count = 0 - for sim in diffraction_simulation: - count += 1 - assert isinstance(sim, ReciprocalLatticeVector) - assert count == 1 - - -class TestMultiRotationSimulation: - @pytest.fixture(scope="class") - def diffraction_simulation(self, al_phase): - vector = ReciprocalLatticeVector( - phase=al_phase, xyz=np.array([[0, 0, 0], [1, 2, 3], [3, 4, 5]]) - ) - - return Simulation( - phases=al_phase, - rotations=Rotation.from_axes_angles([0, 0, 1], angles=[0, 45, 60]), - coordinates=vector, - simulation_generator=SimulationGenerator(300), - ) - - def test_irot(self, diffraction_simulation): - assert isinstance(diffraction_simulation.irot[0], Simulation) - assert diffraction_simulation.irot[0].rotations == Rotation.from_axes_angles( - [0, 0, 1], angles=0 - ) - assert isinstance(diffraction_simulation.irot[1], Simulation) - assert diffraction_simulation.irot[1].rotations == Rotation.from_axes_angles( - [0, 0, 1], angles=45 - ) - - def test_iter(self, diffraction_simulation): - diffraction_simulation.phase_index = 0 - diffraction_simulation.rotation_index = 0 - count = 0 - for sim in diffraction_simulation: - count += 1 - assert isinstance(sim, ReciprocalLatticeVector) - assert count == 3 - - -class TestMultiPhaseMultiRotationSimulation: - @pytest.fixture(scope="class") - def diffraction_simulation(self, al_phase): - vector = ReciprocalLatticeVector( - phase=al_phase, xyz=np.array([[0, 0, 0], [1, 2, 3], [3, 4, 5]]) - ) - al_phase2 = al_phase.deepcopy() - al_phase2.name = "al2" - al_phase.name = "al1" - - return Simulation( - phases=[al_phase, al_phase2], - rotations=[ - Rotation.from_axes_angles([0, 0, 1], angles=[0, 45, 60]), - Rotation.from_axes_angles([0, 0, 1], angles=[0, 45, 60]), - ], - coordinates=[vector, vector], - simulation_generator=SimulationGenerator(300), - ) - - def test_iphase(self, diffraction_simulation): - assert isinstance(diffraction_simulation.iphase[0], Simulation) - assert diffraction_simulation.iphase[0].current_phase.name == "al1" - assert diffraction_simulation.iphase["al1"].current_phase.name == "al1" - assert isinstance(diffraction_simulation.iphase[0].phases, Phase) - - assert isinstance(diffraction_simulation.iphase[1], Simulation) - assert diffraction_simulation.iphase[1].current_phase.name == "al2" - assert diffraction_simulation.iphase["al2"].current_phase.name == "al2" - - def test_irot(self, diffraction_simulation): - assert isinstance(diffraction_simulation.irot[0], Simulation) - assert diffraction_simulation.iphase[0].irot[ - 0 - ].rotations == Rotation.from_axes_angles([0, 0, 1], angles=0) - assert isinstance(diffraction_simulation.irot[1], Simulation) - assert diffraction_simulation.iphase[0].irot[ - 1 - ].rotations == Rotation.from_axes_angles([0, 0, 1], angles=45) - - def test_iter(self, diffraction_simulation): - diffraction_simulation.phase_index = 0 - diffraction_simulation.rotation_index = 0 - count = 0 - for sim in diffraction_simulation: - count += 1 - assert isinstance(sim, ReciprocalLatticeVector) - assert count == 6 diff --git a/examples/creating_a_simulation_library/simulating_diffraction_patterns.py b/examples/creating_a_simulation_library/simulating_diffraction_patterns.py index aa30dd29..845bb1bf 100644 --- a/examples/creating_a_simulation_library/simulating_diffraction_patterns.py +++ b/examples/creating_a_simulation_library/simulating_diffraction_patterns.py @@ -5,6 +5,7 @@ from orix.crystal_map import Phase from orix.quaternion import Rotation from diffpy.structure import Atom, Lattice, Structure +import matplotlib.pyplot as plt from diffsims.generators.simulation_generator import SimulationGenerator @@ -28,7 +29,7 @@ ) # 45 degree rotation around x-axis sim = gen.calculate_ed_data(phase=p, rotation=rot) -sim.plot() # plot the first (and only) diffraction pattern +sim.plot(show_labels=True) # plot the first (and only) diffraction pattern # %% @@ -46,11 +47,11 @@ ) # 45 degree rotation around x-axis sim = gen.calculate_ed_data(phase=p, rotation=rot) -sim.plot() # plot the first diffraction pattern +sim.plot(show_labels=True) # plot the first diffraction pattern # %% -sim.irot[3].plot() # plot the fourth(45 degrees) diffraction pattern +sim.irot[3].plot(show_labels=True) # plot the fourth(45 degrees) diffraction pattern # %% sim.coordinates # coordinates of all the diffraction patterns @@ -67,10 +68,24 @@ ) # 45 degree rotation around x-axis sim = gen.calculate_ed_data(phase=[p, p2], rotation=[rot, rot]) -sim.plot() # plot the first diffraction pattern +sim.plot( + include_direct_beam=True, show_labels=True, min_label_intensity=0.1 +) # plot the first diffraction pattern # %% -sim.iphase["al_2"].irot[3].plot() # plot the fourth(45 degrees) diffraction pattern +sim.iphase["al_2"].irot[3].plot( + show_labels=True, min_label_intensity=0.1 +) # plot the fourth(45 degrees) diffraction pattern -sim.coordinates # coordinates of all the diffraction patterns + +# =================================== +# Plotting a Real Diffraction Pattern +# =================================== +dp = sim.get_diffraction_pattern( + shape=(512, 512), + calibration=0.01, +) +plt.figure() +plt.imshow(dp) +# %% From a82b067d8dcbf1e428a8851877f5ef8a53153a69 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Tue, 28 Nov 2023 07:41:27 -0600 Subject: [PATCH 28/48] Testing: Testing to 100% --- .../reciprocal_lattice_vector.py | 1 - diffsims/tests/simulations/test_simulation.py | 21 +++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/diffsims/crystallography/reciprocal_lattice_vector.py b/diffsims/crystallography/reciprocal_lattice_vector.py index f7ac2de1..ef54c9f8 100644 --- a/diffsims/crystallography/reciprocal_lattice_vector.py +++ b/diffsims/crystallography/reciprocal_lattice_vector.py @@ -182,7 +182,6 @@ def hkl(self): >>> rlv.hkl array([[1., 1., 1.], [2., 0., 0.]]) - """ return _transform_space(self.data, "c", "r", self.phase.structure.lattice) diff --git a/diffsims/tests/simulations/test_simulation.py b/diffsims/tests/simulations/test_simulation.py index 809beec1..91ad98f1 100644 --- a/diffsims/tests/simulations/test_simulation.py +++ b/diffsims/tests/simulations/test_simulation.py @@ -236,7 +236,13 @@ def multi_simulation(self, al_phase): rot = Rotation.from_axes_angles([1, 0, 0], (0, 15, 30, 45), degrees=True) rot2 = rot coords = ReciprocalLatticeVector( - phase=al_phase, xyz=[[1, 0, 0], [0, -1, 0], [1, -1, 0], [1, -1, -1]] + phase=al_phase, + xyz=[ + [1, 0, 0], + [0, -0.3, 0], + [1 / 0.405, 1 / -0.405, 0], + [0.1, -0.1, -0.3], + ], ) coords.intensity = 1 vectors = [coords, coords, coords, coords] @@ -263,6 +269,13 @@ def test_iphase(self, multi_simulation): assert isinstance(phase_slic.phases, Phase) assert phase_slic.rotations.size == 4 + def test_iphase_str(self, multi_simulation): + phase_slic = multi_simulation.iphase["al"] + assert isinstance(phase_slic, Simulation) + assert isinstance(phase_slic.phases, Phase) + assert phase_slic.rotations.size == 4 + assert phase_slic.phases.name == "al" + def test_iphase_error(self, multi_simulation): with pytest.raises(ValueError): phase_slic = multi_simulation.iphase[3.1] @@ -278,7 +291,6 @@ def test_irot_slice(self, multi_simulation): assert isinstance(sliced_sim, Simulation) assert isinstance(sliced_sim.phases, np.ndarray) assert sliced_sim.rotations.size == 2 - sliced_sim.plot() @pytest.mark.parametrize("show_labels", [True, False]) @pytest.mark.parametrize("units", ["real", "pixel"]) @@ -309,10 +321,11 @@ def test_iter(self, multi_simulation): assert count == 8 def test_get_diffraction_pattern(self, multi_simulation): + # No diffraction spots in this pattern pat = multi_simulation.get_diffraction_pattern( - shape=(100, 100), calibration=0.01 + shape=(50, 50), calibration=0.001 ) - assert pat.shape == (100, 100) + assert pat.shape == (50, 50) assert np.max(pat.data) == 0 def test_get_diffraction_pattern2(self, multi_simulation): From d8de6532c69dcdedebe485b92df4d75a53c8af4d Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Thu, 14 Dec 2023 13:30:17 -0600 Subject: [PATCH 29/48] Documentation: Added Copyright statements --- diffsims/generators/simulation_generator.py | 18 +++++++++++++++ diffsims/simulations/__init__.py | 18 +++++++++++++++ diffsims/simulations/simulation.py | 20 +++++++++++++++- diffsims/tests/simulations/test_simulation.py | 18 +++++++++++++++ diffsims/tests/utils/test_deprecation.py | 23 +++++++++++++++++-- .../simulating_diffraction_patterns.py | 3 ++- 6 files changed, 96 insertions(+), 4 deletions(-) diff --git a/diffsims/generators/simulation_generator.py b/diffsims/generators/simulation_generator.py index 6625e314..5857479a 100644 --- a/diffsims/generators/simulation_generator.py +++ b/diffsims/generators/simulation_generator.py @@ -1,3 +1,21 @@ +# -*- 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 typing import Union, Sequence import numpy as np diff --git a/diffsims/simulations/__init__.py b/diffsims/simulations/__init__.py index 8c1ee119..d38e6b50 100644 --- a/diffsims/simulations/__init__.py +++ b/diffsims/simulations/__init__.py @@ -1 +1,19 @@ +# -*- 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 diffsims.simulations.simulation import Simulation diff --git a/diffsims/simulations/simulation.py b/diffsims/simulations/simulation.py index 01185eb4..eb964c03 100644 --- a/diffsims/simulations/simulation.py +++ b/diffsims/simulations/simulation.py @@ -1,3 +1,21 @@ +# -*- 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 typing import Union, Sequence, TYPE_CHECKING, Any import copy @@ -9,8 +27,8 @@ from diffsims.crystallography.reciprocal_lattice_vector import ReciprocalLatticeVector from diffsims.pattern.detector_functions import add_shot_and_point_spread -from diffsims.utils import mask_utils +# to avoid circular imports if TYPE_CHECKING: # pragma: no cover from diffsims.generators.simulation_generator import SimulationGenerator diff --git a/diffsims/tests/simulations/test_simulation.py b/diffsims/tests/simulations/test_simulation.py index 91ad98f1..f586a80f 100644 --- a/diffsims/tests/simulations/test_simulation.py +++ b/diffsims/tests/simulations/test_simulation.py @@ -1,3 +1,21 @@ +# -*- 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 import pytest diff --git a/diffsims/tests/utils/test_deprecation.py b/diffsims/tests/utils/test_deprecation.py index 66b03e17..8bfdfa53 100644 --- a/diffsims/tests/utils/test_deprecation.py +++ b/diffsims/tests/utils/test_deprecation.py @@ -1,3 +1,21 @@ +# -*- 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 warnings import numpy as np @@ -73,7 +91,9 @@ def foo(n): ) def test_deprecation_not_function(self): - @deprecated(since=0.7, alternative="bar", removal=0.8, alternative_is_function=False) + @deprecated( + since=0.7, alternative="bar", removal=0.8, alternative_is_function=False + ) def foo(n): return n + 1 @@ -92,7 +112,6 @@ def foo(n): ) - class TestDeprecateArgument: def test_deprecate_argument(self): """Functions decorated with the custom `deprecated_argument` diff --git a/examples/creating_a_simulation_library/simulating_diffraction_patterns.py b/examples/creating_a_simulation_library/simulating_diffraction_patterns.py index 845bb1bf..022e7972 100644 --- a/examples/creating_a_simulation_library/simulating_diffraction_patterns.py +++ b/examples/creating_a_simulation_library/simulating_diffraction_patterns.py @@ -59,7 +59,8 @@ # ============================================================ # Simulating Multiple Rotations for Multiple Phases # ============================================================ -p2 = p.deepcopy() + +p2 = p.deepcopy() # copy the phase p2.name = "al_2" From 96f0b281f3adf60ef31694e5cbd10da23497e867 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Thu, 14 Dec 2023 17:13:04 -0600 Subject: [PATCH 30/48] Documentation: Improve documentation for simulations --- diffsims/generators/simulation_generator.py | 37 +++++++++++++++++---- diffsims/simulations/simulation.py | 16 ++------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/diffsims/generators/simulation_generator.py b/diffsims/generators/simulation_generator.py index 5857479a..a7d5882c 100644 --- a/diffsims/generators/simulation_generator.py +++ b/diffsims/generators/simulation_generator.py @@ -50,6 +50,10 @@ class SimulationGenerator: + """ + A class for generating kinematic diffraction simulations. + """ + def __init__( self, accelerating_voltage: float = 200, @@ -99,30 +103,31 @@ def calculate_ed_data( shape_factor_width: float = None, debye_waller_factors: dict = None, ): - """Calculates the Electron Diffraction data for a structure. + """Calculates the diffraction pattern for one or more phases given a list + of rotations for each phase. Parameters ---------- phase: The phase(s) for which to derive the diffraction pattern. - reciprocal_radius : float + reciprocal_radius The maximum radius of the sphere of reciprocal space to sample, in reciprocal Angstroms. rotation The Rotation object(s) to apply to the structure and then calculate the diffraction pattern. - with_direct_beam : bool + with_direct_beam If True, the direct beam is included in the simulated diffraction pattern. If False, it is not. - max_excitation_error : float + max_excitation_error The cut-off for geometric excitation error in the z-direction in units of reciprocal Angstroms. Spots with a larger distance from the Ewald sphere are removed from the pattern. Related to the extinction distance and roungly equal to 1/thickness. - shape_factor_width : float + shape_factor_width Determines the width of the reciprocal rel-rod, for fine-grained control. If not set will be set equal to max_excitation_error. - debye_waller_factors : dict of str:value pairs + debye_waller_factors Maps element names to their temperature-dependent Debye-Waller factors. Returns @@ -201,7 +206,25 @@ def get_intersecting_reflections( max_excitation_error: float, shape_factor_width: float = None, ): - """Calculates the reciprocal lattice vectors that intersect the Ewald sphere.""" + """Calculates the reciprocal lattice vectors that intersect the Ewald sphere. + + Parameters + ---------- + recip + The reciprocal lattice vectors to rotate. + rot + The rotation matrix to apply to the reciprocal lattice vectors. + wavelength + The wavelength of the electrons in Angstroms. + max_excitation_error + The cut-off for geometric excitation error in the z-direction + in units of reciprocal Angstroms. Spots with a larger distance + from the Ewald sphere are removed from the pattern. + Related to the extinction distance and roungly equal to 1/thickness. + shape_factor_width + Determines the width of the reciprocal rel-rod, for fine-grained + control. If not set will be set equal to max_excitation_error. + """ rotated_vectors = recip.rotate_from_matrix(rot) # Identify the excitation errors of all points (distance from point to Ewald sphere) r_sphere = 1 / wavelength diff --git a/diffsims/simulations/simulation.py b/diffsims/simulations/simulation.py index eb964c03..b91122dd 100644 --- a/diffsims/simulations/simulation.py +++ b/diffsims/simulations/simulation.py @@ -100,19 +100,8 @@ def __getitem__(self, item): class Simulation: """Holds the result of a kinematic diffraction simulation for some phase - and rotation. - - Parameters - ---------- - coordinates : ragged ndarray, shape [n_points] - The x-y coordinates of points in reciprocal space. - - calibration : float or tuple of float, optional - The x- and y-scales of the pattern, with respect to the original - reciprocal angstrom coordinates. - offset : tuple of float, optional - The x-y offset of the pattern in reciprocal angstroms. Defaults to - zero in each direction. + and rotation. This class is iterable and can be used to iterate through + simulations of different phases and rotations. """ def __init__( @@ -218,6 +207,7 @@ def __next__(self): @property def current_size(self): + """Returns the number of rotations in the current phase""" if self.has_multiple_phases: return self.rotations[self.phase_index].size else: From 69a2efd7cb49a6080542f75a9117d931e7411919 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Thu, 14 Dec 2023 20:47:45 -0600 Subject: [PATCH 31/48] Documentation: Add to CHANGELOG.rst --- CHANGELOG.rst | 10 ++++++++++ diffsims/generators/__init__.py | 2 ++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 33f520a5..78a90f7f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,10 @@ Added ----- - Explicit support for Python 3.11. - Added deprecation tools for deprecating functions and arguments. +- Added new class :class:`diffsims.generators.simulation_generator.SimulationGenerator` for + generating kinemetic diffraction from a phase and a set of reflections. +- Added new class :class:`diffsims.generators.simulations.Simulation` for storing results + from a :class:`diffsims.generators.simulation_generator.SimulationGenerator`. - Added Pre-Commit for code formatting. Changed @@ -22,6 +26,12 @@ Changed Deprecated ---------- +- Deprecated :class:`diffsims.generators.diffraction_generator.DiffractionGenerator` in + favor of :class:`diffsims.generators.simulation_generator.SimulationGenerator`. +- Deprecated :class:`diffsims.generators.library_generator.DiffractionLibraryGenerator` in + favor of :class:`diffsims.generators.simulation_generator.SimulationGenerator`. +- Deprecated :class:`diffsims.diffraction_library.DiffractionLibrary` in favor of + :class:`diffsims.generators.simulations.Simulation`. Removed ------- diff --git a/diffsims/generators/__init__.py b/diffsims/generators/__init__.py index 56b17d09..ab61bf8a 100644 --- a/diffsims/generators/__init__.py +++ b/diffsims/generators/__init__.py @@ -26,6 +26,7 @@ rotation_list_generators, sphere_mesh_generators, zap_map_generator, + simulation_generator, ) __all__ = [ @@ -33,5 +34,6 @@ "library_generator", "rotation_list_generators", "sphere_mesh_generators", + "simulation_generator", "zap_map_generator", ] From 94085d05a6c043ea1b55da6a61c5913bd58a0d41 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Mon, 18 Dec 2023 21:30:27 -0600 Subject: [PATCH 32/48] NewFeature: Add simulate1d --- diffsims/generators/simulation_generator.py | 88 ++++++++++++++++++- diffsims/simulations/__init__.py | 1 + diffsims/simulations/simulation1d.py | 82 +++++++++++++++++ .../generators/test_simulation_generator.py | 8 +- .../tests/simulations/test_simulation1d.py | 59 +++++++++++++ 5 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 diffsims/simulations/simulation1d.py create mode 100644 diffsims/tests/simulations/test_simulation1d.py diff --git a/diffsims/generators/simulation_generator.py b/diffsims/generators/simulation_generator.py index a7d5882c..7d5e57c0 100644 --- a/diffsims/generators/simulation_generator.py +++ b/diffsims/generators/simulation_generator.py @@ -23,7 +23,6 @@ from orix.crystal_map import Phase from diffsims.crystallography import ReciprocalLatticeVector -from diffsims.simulations.simulation import Simulation from diffsims.utils.shape_factor_models import ( linear, atanc, @@ -38,6 +37,8 @@ get_electron_wavelength, get_kinematical_intensities, is_lattice_hexagonal, + get_points_in_sphere, + get_intensities_params, ) _shape_factor_model_mapping = { @@ -48,6 +49,8 @@ "lorentzian": lorentzian, } +from diffsims.simulations import Simulation1D, Simulation + class SimulationGenerator: """ @@ -198,6 +201,89 @@ def calculate_ed_data( ) return sim + def calculate_diffraction1d( + self, + phase: Phase, + reciprocal_radius: float = 1.0, + minimum_intensity: float = 1e-3, + debye_waller_factors: dict = None, + ): + """Calculates the 1-D profile of the diffraction pattern for one phases. + + This is useful for plotting the diffracting reflections for some phases. + + Parameters + ---------- + phase: + The phase for which to derive the diffraction pattern. + reciprocal_radius + The maximum radius of the sphere of reciprocal space to + sample, in reciprocal Angstroms. + minimum_intensity + The minimum intensity of a reflection to be included in the profile. + debye_waller_factors + Maps element names to their temperature-dependent Debye-Waller factors. + """ + latt = phase.structure.lattice + + # Obtain crystallographic reciprocal lattice points within range + recip_latt = latt.reciprocal() + spot_indices, _, spot_distances = get_points_in_sphere( + recip_latt, reciprocal_radius + ) + + ##spot_indicies is a numpy.array of the hkls allowd in the recip radius + g_indices, multiplicities, g_hkls = get_intensities_params( + recip_latt, reciprocal_radius + ) + + i_hkl = get_kinematical_intensities( + phase.structure, + g_indices, + np.asarray(g_hkls), + prefactor=multiplicities, + scattering_params=self.scattering_params, + debye_waller_factors=debye_waller_factors, + ) + + if is_lattice_hexagonal(latt): + # Use Miller-Bravais indices for hexagonal lattices. + g_indices = ( + g_indices[0], + g_indices[1], + -g_indices[0] - g_indices[1], + g_indices[2], + ) + + hkls_labels = ["".join([str(int(x)) for x in xs]) for xs in g_indices] + + peaks = {} + for l, i, g in zip(hkls_labels, i_hkl, g_hkls): + peaks[l] = [i, g] + + # Scale intensities so that the max intensity is 100. + + max_intensity = max([v[0] for v in peaks.values()]) + reciporical_spacing = [] + intensities = [] + hkls = [] + for k in peaks.keys(): + v = peaks[k] + if v[0] / max_intensity * 100 > minimum_intensity and (k != "000"): + reciporical_spacing.append(v[1]) + intensities.append(v[0]) + hkls.append(k) + + intensities = np.asarray(intensities) / max(intensities) * 100 + + return Simulation1D( + phase=phase, + reciprocal_spacing=reciporical_spacing, + intensities=intensities, + hkl=hkls, + reciprocal_radius=reciprocal_radius, + ) + def get_intersecting_reflections( self, recip: ReciprocalLatticeVector, diff --git a/diffsims/simulations/__init__.py b/diffsims/simulations/__init__.py index d38e6b50..b677d307 100644 --- a/diffsims/simulations/__init__.py +++ b/diffsims/simulations/__init__.py @@ -17,3 +17,4 @@ # along with diffsims. If not, see . from diffsims.simulations.simulation import Simulation +from diffsims.simulations.simulation1d import Simulation1D diff --git a/diffsims/simulations/simulation1d.py b/diffsims/simulations/simulation1d.py new file mode 100644 index 00000000..05694552 --- /dev/null +++ b/diffsims/simulations/simulation1d.py @@ -0,0 +1,82 @@ +# -*- 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 typing import Union, Sequence, TYPE_CHECKING, Any +import copy + +import numpy as np +import matplotlib.pyplot as plt +from orix.crystal_map import Phase +from orix.quaternion import Rotation +from orix.vector import Vector3d + +from diffsims.crystallography.reciprocal_lattice_vector import ReciprocalLatticeVector +from diffsims.pattern.detector_functions import add_shot_and_point_spread + +# to avoid circular imports +if TYPE_CHECKING: # pragma: no cover + from diffsims.generators.simulation_generator import SimulationGenerator + + +class Simulation1D: + """Holds the result of a 1D simulation for some phase""" + + def __init__( + self, + phase: Phase, + reciprocal_spacing: np.ndarray, + intensities: np.ndarray, + hkl: np.ndarray, + reciprocal_radius: float, + ): + """Initializes the DiffractionSimulation object with data values for + the coordinates, indices, intensities, calibration and offset. + + Parameters + ---------- + phase + The phase of the simulation + reciprocal_spacing + The spacing of the reciprocal lattice vectors + intensities + The intensities of the diffraction spots + hkl + The hkl indices of the diffraction spots + reciprocal_radius + The radius which the reciprocal lattice spacings are plotted out to + """ + self.phase = phase + self.reciprocal_spacing = reciprocal_spacing + self.intensities = intensities + self.hkl = hkl + self.reciprocal_radius = reciprocal_radius + + def plot(self, ax=None, annotate_peaks=False, fontsize=12, with_labels=True): + """Plots the 1D diffraction pattern,""" + if ax is None: + fig, ax = plt.subplots(1, 1) + for g, i, hkls in zip(self.reciprocal_spacing, self.intensities, self.hkl): + label = hkls + ax.plot([g, g], [0, i], color="k", linewidth=3, label=label) + if annotate_peaks: + ax.annotate(label, xy=[g, i], xytext=[g, i], fontsize=fontsize) + + if with_labels: + ax.set_xlabel("A ($^{-1}$)") + ax.set_ylabel("Intensities (scaled)") + return ax diff --git a/diffsims/tests/generators/test_simulation_generator.py b/diffsims/tests/generators/test_simulation_generator.py index 5c00f926..244bd01a 100644 --- a/diffsims/tests/generators/test_simulation_generator.py +++ b/diffsims/tests/generators/test_simulation_generator.py @@ -31,6 +31,7 @@ lorentzian, _shape_factor_precession, ) +from diffsims.simulations import Simulation1D @pytest.fixture(params=[(300)]) @@ -196,10 +197,15 @@ def test_shape_factor_custom(self, diffraction_calculator, local_structure): t2 = diffraction_calculator.calculate_ed_data( local_structure, max_excitation_error=0.4 ) - # softly makes sure the two sims are different assert np.sum(t1.coordinates.intensity) != np.sum(t2.coordinates.intensity) + def test_simulate_1d(self): + generator = SimulationGenerator(300) + phase = make_phase() + sim = generator.calculate_diffraction1d(phase, 0.5) + assert isinstance(sim, Simulation1D) + @pytest.mark.parametrize("scattering_param", ["lobato", "xtables"]) def test_param_check(scattering_param): diff --git a/diffsims/tests/simulations/test_simulation1d.py b/diffsims/tests/simulations/test_simulation1d.py new file mode 100644 index 00000000..49b8c8d2 --- /dev/null +++ b/diffsims/tests/simulations/test_simulation1d.py @@ -0,0 +1,59 @@ +# -*- 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 matplotlib.pyplot as plt +import pytest + +from orix.crystal_map import Phase +import numpy as np + +from diffsims.tests.generators.test_simulation_generator import make_phase +from diffsims.simulations import Simulation1D + + +class TestSingleSimulation: + @pytest.fixture + def simulation1d(self): + al_phase = make_phase() + hkls = np.array(["100", "110", "111"]) + magnitudes = np.array([1, 2, 3]) + inten = np.array([1, 2, 3]) + recip = 4.0 + + return Simulation1D( + phase=al_phase, + hkl=hkls, + reciprocal_spacing=magnitudes, + intensities=inten, + reciprocal_radius=recip, + ) + + def test_init(self, simulation1d): + assert isinstance(simulation1d, Simulation1D) + assert isinstance(simulation1d.phase, Phase) + assert isinstance(simulation1d.hkl, np.ndarray) + assert isinstance(simulation1d.reciprocal_spacing, np.ndarray) + assert isinstance(simulation1d.intensities, np.ndarray) + assert isinstance(simulation1d.reciprocal_radius, float) + + @pytest.mark.parametrize("annotate", [True, False]) + @pytest.mark.parametrize("ax", [None, "new"]) + @pytest.mark.parametrize("with_labels", [True, False]) + def test_plot(self, simulation1d, annotate, ax, with_labels): + if ax == "new": + fig, ax = plt.subplots() + fig = simulation1d.plot(annotate_peaks=annotate, ax=ax, with_labels=with_labels) From 0916cd32c2008a568398bd3fdf76eeb030ce6611 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Mon, 18 Dec 2023 21:39:38 -0600 Subject: [PATCH 33/48] Refactor: Adjust documentation per @pc494 and Simulation --> Simulation2D --- CHANGELOG.rst | 2 ++ CONTRIBUTING.rst | 8 ++--- diffsims/generators/simulation_generator.py | 4 +-- diffsims/sims/diffraction_simulation.py | 2 +- diffsims/simulations/__init__.py | 2 +- .../{simulation.py => simulation2d.py} | 6 ++-- diffsims/tests/simulations/test_simulation.py | 36 +++++++++---------- diffsims/utils/sim_utils.py | 2 +- 8 files changed, 31 insertions(+), 31 deletions(-) rename diffsims/simulations/{simulation.py => simulation2d.py} (99%) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 78a90f7f..f54632c2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,8 @@ Added - Added new class :class:`diffsims.generators.simulations.Simulation` for storing results from a :class:`diffsims.generators.simulation_generator.SimulationGenerator`. - Added Pre-Commit for code formatting. +- Added new class :class:`diffsims.simulations.Simulation1D` for storing the results of a + 1D simulation. Changed ------- diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 949ed51e..64aebf70 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -131,11 +131,9 @@ Useful hints on testing: Deprecations ------------ -We attempt to adhere to semantic versioning as best we can. This means that as little, -ideally no, functionality should break between minor releases. Deprecation warnings -are raised whenever possible and feasible for functions/methods/properties/arguments, -so that users get a heads-up one (minor) release before something is removed or changes, -with a possible alternative to be used. +'This project attempts to adhere to (pre 1.0.0) semantic versioning and ideally no functionality +should be broken between minor releases. Deprecation warnings are raised to give user a full minor +release cycle to adjust their code before a breaking change is implemented.' The decorator should be placed right above the object signature to be deprecated:: diff --git a/diffsims/generators/simulation_generator.py b/diffsims/generators/simulation_generator.py index 7d5e57c0..3362df67 100644 --- a/diffsims/generators/simulation_generator.py +++ b/diffsims/generators/simulation_generator.py @@ -49,7 +49,7 @@ "lorentzian": lorentzian, } -from diffsims.simulations import Simulation1D, Simulation +from diffsims.simulations import Simulation1D, Simulation2D class SimulationGenerator: @@ -192,7 +192,7 @@ def calculate_ed_data( vectors = vectors[0] # Create a simulation object - sim = Simulation( + sim = Simulation2D( phases=phase, coordinates=vectors, rotations=rotation, diff --git a/diffsims/sims/diffraction_simulation.py b/diffsims/sims/diffraction_simulation.py index ebc5b5d9..04b13445 100644 --- a/diffsims/sims/diffraction_simulation.py +++ b/diffsims/sims/diffraction_simulation.py @@ -53,7 +53,7 @@ class DiffractionSimulation: @deprecated( since="0.6.0", - alternative="diffsims.simulation.Simulation", + alternative="diffsims.simulation.Simulation2D", alternative_is_function=False, removal="0.8.0", ) diff --git a/diffsims/simulations/__init__.py b/diffsims/simulations/__init__.py index b677d307..05864a60 100644 --- a/diffsims/simulations/__init__.py +++ b/diffsims/simulations/__init__.py @@ -16,5 +16,5 @@ # You should have received a copy of the GNU General Public License # along with diffsims. If not, see . -from diffsims.simulations.simulation import Simulation +from diffsims.simulations.simulation2d import Simulation2D from diffsims.simulations.simulation1d import Simulation1D diff --git a/diffsims/simulations/simulation.py b/diffsims/simulations/simulation2d.py similarity index 99% rename from diffsims/simulations/simulation.py rename to diffsims/simulations/simulation2d.py index b91122dd..3937eb4e 100644 --- a/diffsims/simulations/simulation.py +++ b/diffsims/simulations/simulation2d.py @@ -58,7 +58,7 @@ def __getitem__(self, item): new_coords = self.simulation.coordinates[ind] new_rotations = self.simulation.rotations[ind] new_phases = all_phases[ind] - return Simulation( + return Simulation2D( phases=new_phases, coordinates=new_coords, rotations=new_rotations, @@ -90,7 +90,7 @@ def __getitem__(self, item): coords = [c[item] for c in self.simulation.coordinates] phases = self.simulation.phases rotations = [rot[item] for rot in self.simulation.rotations] - return Simulation( + return Simulation2D( phases=phases, coordinates=coords, rotations=rotations, @@ -98,7 +98,7 @@ def __getitem__(self, item): ) -class Simulation: +class Simulation2D: """Holds the result of a kinematic diffraction simulation for some phase and rotation. This class is iterable and can be used to iterate through simulations of different phases and rotations. diff --git a/diffsims/tests/simulations/test_simulation.py b/diffsims/tests/simulations/test_simulation.py index f586a80f..4ddcba31 100644 --- a/diffsims/tests/simulations/test_simulation.py +++ b/diffsims/tests/simulations/test_simulation.py @@ -23,7 +23,7 @@ from orix.crystal_map import Phase from orix.quaternion import Rotation -from diffsims.simulations.simulation import Simulation +from diffsims.simulations import Simulation2D from diffsims.generators.simulation_generator import SimulationGenerator from diffsims.crystallography.reciprocal_lattice_vector import ReciprocalLatticeVector @@ -47,13 +47,13 @@ def single_simulation(self, al_phase): gen = SimulationGenerator(accelerating_voltage=200) rot = Rotation.from_axes_angles([1, 0, 0], 45, degrees=True) coords = ReciprocalLatticeVector(phase=al_phase, xyz=[[1, 0, 0]]) - sim = Simulation( + sim = Simulation2D( phases=al_phase, simulation_generator=gen, coordinates=coords, rotations=rot ) return sim def test_init(self, single_simulation): - assert isinstance(single_simulation, Simulation) + assert isinstance(single_simulation, Simulation2D) assert isinstance(single_simulation.phases, Phase) assert isinstance(single_simulation.simulation_generator, SimulationGenerator) assert isinstance(single_simulation.rotations, Rotation) @@ -97,7 +97,7 @@ def test_different_size(self, al_phase): rot = Rotation.from_axes_angles([1, 0, 0], 45, degrees=True) coords = ReciprocalLatticeVector(phase=al_phase, xyz=[[1, 0, 0], [1, 1, 1]]) with pytest.raises(ValueError): - sim = Simulation( + sim = Simulation2D( phases=al_phase, simulation_generator=gen, coordinates=[coords, coords], @@ -109,7 +109,7 @@ def test_different_size2(self, al_phase): rot = Rotation.from_axes_angles([1, 0, 0], (0, 45), degrees=True) coords = ReciprocalLatticeVector(phase=al_phase, xyz=[[1, 0, 0], [1, 1, 1]]) with pytest.raises(ValueError): - sim = Simulation( + sim = Simulation2D( phases=al_phase, simulation_generator=gen, coordinates=[coords, coords, coords], @@ -121,7 +121,7 @@ def test_different_size_multiphase(self, al_phase): rot = Rotation.from_axes_angles([1, 0, 0], 45, degrees=True) coords = ReciprocalLatticeVector(phase=al_phase, xyz=[[1, 0, 0], [1, 1, 1]]) with pytest.raises(ValueError): - sim = Simulation( + sim = Simulation2D( phases=[al_phase, al_phase], simulation_generator=gen, coordinates=[[coords, coords], [coords, coords]], @@ -133,7 +133,7 @@ def test_different_num_phase(self, al_phase): rot = Rotation.from_axes_angles([1, 0, 0], 45, degrees=True) coords = ReciprocalLatticeVector(phase=al_phase, xyz=[[1, 0, 0], [1, 1, 1]]) with pytest.raises(ValueError): - sim = Simulation( + sim = Simulation2D( phases=[al_phase, al_phase], simulation_generator=gen, coordinates=[[coords, coords], [coords, coords], [coords, coords]], @@ -145,7 +145,7 @@ def test_different_num_phase_and_rot(self, al_phase): rot = Rotation.from_axes_angles([1, 0, 0], 45, degrees=True) coords = ReciprocalLatticeVector(phase=al_phase, xyz=[[1, 0, 0], [1, 1, 1]]) with pytest.raises(ValueError): - sim = Simulation( + sim = Simulation2D( phases=[al_phase, al_phase], simulation_generator=gen, coordinates=[[coords, coords], [coords, coords], [coords, coords]], @@ -176,7 +176,7 @@ def multi_simulation(self, al_phase): vectors = [coords, coords, coords, coords] - sim = Simulation( + sim = Simulation2D( phases=al_phase, simulation_generator=gen, coordinates=vectors, @@ -185,7 +185,7 @@ def multi_simulation(self, al_phase): return sim def test_init(self, multi_simulation): - assert isinstance(multi_simulation, Simulation) + assert isinstance(multi_simulation, Simulation2D) assert isinstance(multi_simulation.phases, Phase) assert isinstance(multi_simulation.simulation_generator, SimulationGenerator) assert isinstance(multi_simulation.rotations, Rotation) @@ -197,14 +197,14 @@ def test_iphase(self, multi_simulation): def test_irot(self, multi_simulation): sliced_sim = multi_simulation.irot[0] - assert isinstance(sliced_sim, Simulation) + assert isinstance(sliced_sim, Simulation2D) assert isinstance(sliced_sim.phases, Phase) assert sliced_sim.rotations.size == 1 assert sliced_sim.coordinates.size == 4 def test_irot_slice(self, multi_simulation): sliced_sim = multi_simulation.irot[0:2] - assert isinstance(sliced_sim, Simulation) + assert isinstance(sliced_sim, Simulation2D) assert isinstance(sliced_sim.phases, Phase) assert sliced_sim.rotations.size == 2 assert sliced_sim.coordinates.size == 2 @@ -266,7 +266,7 @@ def multi_simulation(self, al_phase): vectors = [coords, coords, coords, coords] al_phase2 = al_phase.deepcopy() al_phase2.name = "al2" - sim = Simulation( + sim = Simulation2D( phases=[al_phase, al_phase2], simulation_generator=gen, coordinates=[vectors, vectors], @@ -275,7 +275,7 @@ def multi_simulation(self, al_phase): return sim def test_init(self, multi_simulation): - assert isinstance(multi_simulation, Simulation) + assert isinstance(multi_simulation, Simulation2D) assert isinstance(multi_simulation.phases, np.ndarray) assert isinstance(multi_simulation.simulation_generator, SimulationGenerator) assert isinstance(multi_simulation.rotations, np.ndarray) @@ -283,13 +283,13 @@ def test_init(self, multi_simulation): def test_iphase(self, multi_simulation): phase_slic = multi_simulation.iphase[0] - assert isinstance(phase_slic, Simulation) + assert isinstance(phase_slic, Simulation2D) assert isinstance(phase_slic.phases, Phase) assert phase_slic.rotations.size == 4 def test_iphase_str(self, multi_simulation): phase_slic = multi_simulation.iphase["al"] - assert isinstance(phase_slic, Simulation) + assert isinstance(phase_slic, Simulation2D) assert isinstance(phase_slic.phases, Phase) assert phase_slic.rotations.size == 4 assert phase_slic.phases.name == "al" @@ -300,13 +300,13 @@ def test_iphase_error(self, multi_simulation): def test_irot(self, multi_simulation): sliced_sim = multi_simulation.irot[0] - assert isinstance(sliced_sim, Simulation) + assert isinstance(sliced_sim, Simulation2D) assert isinstance(sliced_sim.phases, np.ndarray) assert sliced_sim.rotations.size == 2 def test_irot_slice(self, multi_simulation): sliced_sim = multi_simulation.irot[0:2] - assert isinstance(sliced_sim, Simulation) + assert isinstance(sliced_sim, Simulation2D) assert isinstance(sliced_sim.phases, np.ndarray) assert sliced_sim.rotations.size == 2 diff --git a/diffsims/utils/sim_utils.py b/diffsims/utils/sim_utils.py index 145e0d71..b6f5c19e 100644 --- a/diffsims/utils/sim_utils.py +++ b/diffsims/utils/sim_utils.py @@ -367,7 +367,7 @@ def simulate_kinematic_scattering( accelerating_voltage : float Accelerating voltage in keV. simulation_size : int - Simulation size, n, specifies the n x n array size for + Simulation2D size, n, specifies the n x n array size for the simulation calculation. max_k : float Maximum scattering vector magnitude in reciprocal angstroms. From c47baffa09f4600f402e3a1c6dd3e681f0fce4dd Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Tue, 19 Dec 2023 12:51:25 -0600 Subject: [PATCH 34/48] Testing: Fix hexagonal Simulation1D --- diffsims/generators/simulation_generator.py | 14 +++++++------ .../generators/test_simulation_generator.py | 21 ++++++++++++++++++- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/diffsims/generators/simulation_generator.py b/diffsims/generators/simulation_generator.py index 3362df67..fb1cb1cd 100644 --- a/diffsims/generators/simulation_generator.py +++ b/diffsims/generators/simulation_generator.py @@ -248,12 +248,14 @@ def calculate_diffraction1d( if is_lattice_hexagonal(latt): # Use Miller-Bravais indices for hexagonal lattices. - g_indices = ( - g_indices[0], - g_indices[1], - -g_indices[0] - g_indices[1], - g_indices[2], - ) + g_indices = np.array( + [ + g_indices[:, 0], + g_indices[:, 1], + g_indices[:, 0] - g_indices[:, 1], + g_indices[:, 2], + ] + ).T hkls_labels = ["".join([str(int(x)) for x in xs]) for xs in g_indices] diff --git a/diffsims/tests/generators/test_simulation_generator.py b/diffsims/tests/generators/test_simulation_generator.py index 244bd01a..0356117a 100644 --- a/diffsims/tests/generators/test_simulation_generator.py +++ b/diffsims/tests/generators/test_simulation_generator.py @@ -32,6 +32,7 @@ _shape_factor_precession, ) from diffsims.simulations import Simulation1D +from diffsims.utils.sim_utils import is_lattice_hexagonal @pytest.fixture(params=[(300)]) @@ -200,12 +201,30 @@ def test_shape_factor_custom(self, diffraction_calculator, local_structure): # softly makes sure the two sims are different assert np.sum(t1.coordinates.intensity) != np.sum(t2.coordinates.intensity) - def test_simulate_1d(self): + @pytest.mark.parametrize("is_hex", [True, False]) + def test_simulate_1d(self, is_hex): generator = SimulationGenerator(300) phase = make_phase() + if is_hex: + phase.structure.lattice.a = phase.structure.lattice.b + phase.structure.lattice.alpha = 90 + phase.structure.lattice.beta = 90 + phase.structure.lattice.gamma = 120 + assert is_lattice_hexagonal(phase.structure.lattice) + else: + assert not is_lattice_hexagonal(phase.structure.lattice) sim = generator.calculate_diffraction1d(phase, 0.5) assert isinstance(sim, Simulation1D) + assert len(sim.intensities) == len(sim.reciprocal_spacing) + assert len(sim.intensities) == len(sim.hkl) + for h in sim.hkl: + h = h.replace("-", "") + if is_hex: + assert len(h) == 4 + else: + assert len(h) == 3 + @pytest.mark.parametrize("scattering_param", ["lobato", "xtables"]) def test_param_check(scattering_param): From 107d27a28ac9a3808552ccd484dc70774cc68113 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Sat, 13 Jan 2024 20:23:41 -0600 Subject: [PATCH 35/48] Bugfix: Fix intensity scaling with zero beam --- diffsims/generators/simulation_generator.py | 47 +++++++++++++++---- diffsims/simulations/simulation1d.py | 22 +++++---- diffsims/simulations/simulation2d.py | 10 ++-- .../generators/test_simulation_generator.py | 6 +-- .../tests/simulations/test_simulation1d.py | 1 + 5 files changed, 61 insertions(+), 25 deletions(-) diff --git a/diffsims/generators/simulation_generator.py b/diffsims/generators/simulation_generator.py index fb1cb1cd..51d8dcfa 100644 --- a/diffsims/generators/simulation_generator.py +++ b/diffsims/generators/simulation_generator.py @@ -57,6 +57,13 @@ class SimulationGenerator: A class for generating kinematic diffraction simulations. """ + def __repr__(self): + return ( + f"SimulationGenerator(accelerating_voltage={self.accelerating_voltage}, " + f"scattering_params={self.scattering_params}, " + f"approximate_precession={self.approximate_precession})" + ) + def __init__( self, accelerating_voltage: float = 200, @@ -67,7 +74,7 @@ def __init__( minimum_intensity: float = 1e-20, **kwargs, ): - self.wavelength = get_electron_wavelength(accelerating_voltage) + self.accelerating_voltage = accelerating_voltage self.precession_angle = np.abs(precession_angle) self.approximate_precession = approximate_precession if isinstance(shape_factor_model, str): @@ -94,6 +101,10 @@ def __init__( "implementations.".format(scattering_params) ) + @property + def wavelength(self): + return get_electron_wavelength(self.accelerating_voltage) + def calculate_ed_data( self, phase: Union[Phase, Sequence[Phase]], @@ -126,7 +137,7 @@ def calculate_ed_data( The cut-off for geometric excitation error in the z-direction in units of reciprocal Angstroms. Spots with a larger distance from the Ewald sphere are removed from the pattern. - Related to the extinction distance and roungly equal to 1/thickness. + Related to the extinction distance and roughly equal to 1/thickness. shape_factor_width Determines the width of the reciprocal rel-rod, for fine-grained control. If not set will be set equal to max_excitation_error. @@ -158,25 +169,30 @@ def calculate_ed_data( phase_vectors = [] for rot in rotate.to_matrix(): # Calculate the reciprocal lattice vectors that intersect the Ewald sphere. - intersected_vectors, shape_factor = self.get_intersecting_reflections( + ( + intersected_vectors, + hkl, + shape_factor, + ) = self.get_intersecting_reflections( recip, rot, wavelength, max_excitation_error, shape_factor_width=shape_factor_width, + with_direct_beam=with_direct_beam, ) # Calculate diffracted intensities based on a kinematic model. intensities = get_kinematical_intensities( p.structure, - intersected_vectors.hkl, + hkl, intersected_vectors.gspacing, prefactor=shape_factor, scattering_params=self.scattering_params, debye_waller_factors=debye_waller_factors, ) - # Threshold peaks included in simulation as factor of maximum intensity. + # Threshold peaks included in simulation as factor of zero beam intensity. peak_mask = intensities > np.max(intensities) * self.minimum_intensity intensities = intensities[peak_mask] intersected_vectors = intersected_vectors[peak_mask] @@ -232,7 +248,7 @@ def calculate_diffraction1d( recip_latt, reciprocal_radius ) - ##spot_indicies is a numpy.array of the hkls allowd in the recip radius + ##spot_indicies is a numpy.array of the hkls allowed in the recip radius g_indices, multiplicities, g_hkls = get_intensities_params( recip_latt, reciprocal_radius ) @@ -284,6 +300,7 @@ def calculate_diffraction1d( intensities=intensities, hkl=hkls, reciprocal_radius=reciprocal_radius, + wavelength=self.wavelength, ) def get_intersecting_reflections( @@ -293,6 +310,7 @@ def get_intersecting_reflections( wavelength: float, max_excitation_error: float, shape_factor_width: float = None, + with_direct_beam: bool = True, ): """Calculates the reciprocal lattice vectors that intersect the Ewald sphere. @@ -313,11 +331,18 @@ def get_intersecting_reflections( Determines the width of the reciprocal rel-rod, for fine-grained control. If not set will be set equal to max_excitation_error. """ + initial_hkl = recip.hkl rotated_vectors = recip.rotate_from_matrix(rot) + + if with_direct_beam: + rotated_vectors = np.vstack([rotated_vectors.data, [0, 0, 0]]) + initial_hkl = np.vstack([initial_hkl, [0, 0, 0]]) + else: + rotated_vectors = rotated_vectors.data # Identify the excitation errors of all points (distance from point to Ewald sphere) r_sphere = 1 / wavelength - r_spot = np.sqrt(np.sum(np.square(rotated_vectors.data[:, :2]), axis=1)) - z_spot = rotated_vectors.data[:, 2] + r_spot = np.sqrt(np.sum(np.square(rotated_vectors[:, :2]), axis=1)) + z_spot = rotated_vectors[:, 2] z_sphere = -np.sqrt(r_sphere**2 - r_spot**2) + r_sphere excitation_error = z_sphere - z_spot @@ -339,8 +364,12 @@ def get_intersecting_reflections( # select these reflections intersected_vectors = rotated_vectors[intersection] + intersected_vectors = ReciprocalLatticeVector( + phase=recip.phase, xyz=intersected_vectors + ) excitation_error = excitation_error[intersection] r_spot = r_spot[intersection] + hkl = initial_hkl[intersection] if shape_factor_width is None: shape_factor_width = max_excitation_error @@ -367,4 +396,4 @@ def get_intersecting_reflections( shape_factor_width, **self.shape_factor_kwargs, ) - return intersected_vectors, shape_factor + return intersected_vectors, hkl, shape_factor diff --git a/diffsims/simulations/simulation1d.py b/diffsims/simulations/simulation1d.py index 05694552..2e36bb17 100644 --- a/diffsims/simulations/simulation1d.py +++ b/diffsims/simulations/simulation1d.py @@ -16,17 +16,12 @@ # You should have received a copy of the GNU General Public License # along with diffsims. If not, see . -from typing import Union, Sequence, TYPE_CHECKING, Any -import copy +from typing import TYPE_CHECKING import numpy as np import matplotlib.pyplot as plt from orix.crystal_map import Phase -from orix.quaternion import Rotation -from orix.vector import Vector3d - -from diffsims.crystallography.reciprocal_lattice_vector import ReciprocalLatticeVector -from diffsims.pattern.detector_functions import add_shot_and_point_spread +from diffsims.utils.sim_utils import get_electron_wavelength # to avoid circular imports if TYPE_CHECKING: # pragma: no cover @@ -43,6 +38,7 @@ def __init__( intensities: np.ndarray, hkl: np.ndarray, reciprocal_radius: float, + wavelength: float, ): """Initializes the DiffractionSimulation object with data values for the coordinates, indices, intensities, calibration and offset. @@ -52,19 +48,29 @@ def __init__( phase The phase of the simulation reciprocal_spacing - The spacing of the reciprocal lattice vectors + The spacing of the reciprocal lattice vectors in A^-1 intensities The intensities of the diffraction spots hkl The hkl indices of the diffraction spots reciprocal_radius The radius which the reciprocal lattice spacings are plotted out to + wavelength + The wavelength of the beam in A^-1 """ self.phase = phase self.reciprocal_spacing = reciprocal_spacing self.intensities = intensities self.hkl = hkl self.reciprocal_radius = reciprocal_radius + self.wavelength = wavelength + + def __repr__(self): + return f"Simulation1D(name: {self.phase.name}, wavelength: {self.wavelength})" + + @property + def theta(self): + return np.arctan2(np.array(self.reciprocal_spacing), 1 / self.wavelength) def plot(self, ax=None, annotate_peaks=False, fontsize=12, with_labels=True): """Plots the 1D diffraction pattern,""" diff --git a/diffsims/simulations/simulation2d.py b/diffsims/simulations/simulation2d.py index 3937eb4e..7a9d6494 100644 --- a/diffsims/simulations/simulation2d.py +++ b/diffsims/simulations/simulation2d.py @@ -194,7 +194,7 @@ def __next__(self): coords = self.coordinates[self.phase_index] else: coords = self.coordinates - if self.has_multiple_vectors: + if self.has_multiple_rotations: coords = coords[self.rotation_index] else: coords = coords @@ -374,9 +374,9 @@ def has_multiple_phases(self): return self.num_phases > 1 @property - def has_multiple_vectors(self): - """Returns True if the simulation has multiple vectors""" - return self.coordinates.size > 1 + def has_multiple_rotations(self): + """Returns True if the simulation has multiple rotations""" + return self.rotations.size > 1 def get_current_coordinates(self): """Returns the coordinates of the current phase and rotation""" @@ -384,7 +384,7 @@ def get_current_coordinates(self): return copy.deepcopy( self.coordinates[self.phase_index][self.rotation_index] ) - elif not self.has_multiple_phases and self.has_multiple_vectors: + elif not self.has_multiple_phases and self.has_multiple_rotations: return copy.deepcopy(self.coordinates[self.rotation_index]) else: return copy.deepcopy(self.coordinates) diff --git a/diffsims/tests/generators/test_simulation_generator.py b/diffsims/tests/generators/test_simulation_generator.py index 0356117a..f1116e6b 100644 --- a/diffsims/tests/generators/test_simulation_generator.py +++ b/diffsims/tests/generators/test_simulation_generator.py @@ -127,7 +127,7 @@ def test_matching_results( diffraction = diffraction_calculator.calculate_ed_data( local_structure, reciprocal_radius=5.0 ) - assert diffraction.coordinates.size == 72 + assert diffraction.coordinates.size == 69 def test_precession_simple( self, diffraction_calculator_precession_simple, local_structure @@ -136,7 +136,7 @@ def test_precession_simple( local_structure, reciprocal_radius=5.0, ) - assert diffraction.coordinates.size == 252 + assert diffraction.coordinates.size == 249 def test_precession_full( self, diffraction_calculator_precession_full, local_structure @@ -145,7 +145,7 @@ def test_precession_full( local_structure, reciprocal_radius=5.0, ) - assert diffraction.coordinates.size == 252 + assert diffraction.coordinates.size == 249 def test_custom_shape_func(self, diffraction_calculator_custom, local_structure): diffraction = diffraction_calculator_custom.calculate_ed_data( diff --git a/diffsims/tests/simulations/test_simulation1d.py b/diffsims/tests/simulations/test_simulation1d.py index 49b8c8d2..532c596e 100644 --- a/diffsims/tests/simulations/test_simulation1d.py +++ b/diffsims/tests/simulations/test_simulation1d.py @@ -40,6 +40,7 @@ def simulation1d(self): reciprocal_spacing=magnitudes, intensities=inten, reciprocal_radius=recip, + wavelength=0.025, ) def test_init(self, simulation1d): From d928173600405b6209f8217207578c83fc4569f4 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Sat, 13 Jan 2024 20:53:03 -0600 Subject: [PATCH 36/48] Testing: Increase coverage of simulation1d --- diffsims/tests/simulations/test_simulation1d.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/diffsims/tests/simulations/test_simulation1d.py b/diffsims/tests/simulations/test_simulation1d.py index 532c596e..b2121b20 100644 --- a/diffsims/tests/simulations/test_simulation1d.py +++ b/diffsims/tests/simulations/test_simulation1d.py @@ -29,6 +29,7 @@ class TestSingleSimulation: @pytest.fixture def simulation1d(self): al_phase = make_phase() + al_phase.name = "Al" hkls = np.array(["100", "110", "111"]) magnitudes = np.array([1, 2, 3]) inten = np.array([1, 2, 3]) @@ -58,3 +59,11 @@ def test_plot(self, simulation1d, annotate, ax, with_labels): if ax == "new": fig, ax = plt.subplots() fig = simulation1d.plot(annotate_peaks=annotate, ax=ax, with_labels=with_labels) + + def test_repr(self, simulation1d): + assert simulation1d.__repr__() == "Simulation1D(name: Al, wavelength: 0.025)" + + def test_theta(self, simulation1d): + np.testing.assert_almost_equal( + simulation1d.theta, np.array([0.02499479, 0.0499584, 0.07485985]) + ) From e4de1867424ed9184469fd11a32ccd0ac1fc3473 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Mon, 15 Jan 2024 16:28:30 -0600 Subject: [PATCH 37/48] Documentation: Add documentation to ``__init__`` function --- diffsims/generators/simulation_generator.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/diffsims/generators/simulation_generator.py b/diffsims/generators/simulation_generator.py index 51d8dcfa..7c04f46c 100644 --- a/diffsims/generators/simulation_generator.py +++ b/diffsims/generators/simulation_generator.py @@ -74,6 +74,25 @@ def __init__( minimum_intensity: float = 1e-20, **kwargs, ): + """ + Parameters + ---------- + accelerating_voltage + The accelerating voltage of the electrons in keV. + scattering_params + The scattering parameters to use. One of 'lobato', 'xtables' + precession_angle + The precession angle in degrees. If 0, no precession is applied. + shape_factor_model + The shape factor model to use. One of 'linear', 'atanc', 'sinc', 'sin2c', 'lorentzian' + approximate_precession + If True, the precession is approximated by a Lorentzian function. + minimum_intensity + The minimum intensity of a reflection to be included in the profile. + kwargs + Keyword arguments to pass to the shape factor model. + + """ self.accelerating_voltage = accelerating_voltage self.precession_angle = np.abs(precession_angle) self.approximate_precession = approximate_precession From 543d3f2283dba4ad293302d141015278648dda3b Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Thu, 1 Feb 2024 10:47:56 -0600 Subject: [PATCH 38/48] BugFix: Fix labels for miller indices. --- diffsims/simulations/simulation2d.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/diffsims/simulations/simulation2d.py b/diffsims/simulations/simulation2d.py index 7a9d6494..0bb23a39 100644 --- a/diffsims/simulations/simulation2d.py +++ b/diffsims/simulations/simulation2d.py @@ -389,6 +389,15 @@ def get_current_coordinates(self): else: return copy.deepcopy(self.coordinates) + def get_current_rotation(self): + """Returns the matrix for the current matrix""" + if self.has_multiple_phases: + return copy.deepcopy( + self.rotations[self.phase_index].to_matrix()[self.rotation_index] + ) + else: + return copy.deepcopy(self.rotations.to_matrix()[self.rotation_index]) + def plot_rotations(self, beam_direction: Vector3d = Vector3d.zvector()): """Plots the rotations of the current phase in stereographic projection""" if self.has_multiple_phases: @@ -497,7 +506,9 @@ def plot( ax.set_ylim(-self.reciporical_radius, self.reciporical_radius) if show_labels: - millers = coords.hkl.astype(np.int16) + millers = np.matmul(coords.hkl, self.get_current_rotation()).astype( + np.int16 + ) # only label the points inside the axes xlim = ax.get_xlim() ylim = ax.get_ylim() From f234a85874b02f7288200859bd5cd20a3e8c529d Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Thu, 1 Feb 2024 10:53:50 -0600 Subject: [PATCH 39/48] BugFix: Fix labels for miller indices, with rounding --- diffsims/simulations/simulation2d.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/diffsims/simulations/simulation2d.py b/diffsims/simulations/simulation2d.py index 0bb23a39..f730c691 100644 --- a/diffsims/simulations/simulation2d.py +++ b/diffsims/simulations/simulation2d.py @@ -506,9 +506,9 @@ def plot( ax.set_ylim(-self.reciporical_radius, self.reciporical_radius) if show_labels: - millers = np.matmul(coords.hkl, self.get_current_rotation()).astype( - np.int16 - ) + millers = np.round( + np.matmul(coords.hkl, self.get_current_rotation()) + ).astype(np.int16) # only label the points inside the axes xlim = ax.get_xlim() ylim = ax.get_ylim() From 32fdbe2259dd237fc5fd455a3738434dce3bf0fe Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Fri, 2 Feb 2024 17:23:04 -0600 Subject: [PATCH 40/48] BugFix: Fix euler rotation --- diffsims/crystallography/reciprocal_lattice_vector.py | 2 +- diffsims/simulations/simulation2d.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/diffsims/crystallography/reciprocal_lattice_vector.py b/diffsims/crystallography/reciprocal_lattice_vector.py index ef54c9f8..63c4b68d 100644 --- a/diffsims/crystallography/reciprocal_lattice_vector.py +++ b/diffsims/crystallography/reciprocal_lattice_vector.py @@ -533,7 +533,7 @@ def intensity(self, value): def rotate_from_matrix(self, rotation_matrix): return ReciprocalLatticeVector( - phase=self.phase, xyz=np.matmul(rotation_matrix, self.data.T).T + phase=self.phase, xyz=np.matmul(rotation_matrix.T, self.data.T).T ) @property diff --git a/diffsims/simulations/simulation2d.py b/diffsims/simulations/simulation2d.py index f730c691..726dc1b3 100644 --- a/diffsims/simulations/simulation2d.py +++ b/diffsims/simulations/simulation2d.py @@ -507,7 +507,7 @@ def plot( if show_labels: millers = np.round( - np.matmul(coords.hkl, self.get_current_rotation()) + np.matmul(coords.hkl, self.get_current_rotation().T) ).astype(np.int16) # only label the points inside the axes xlim = ax.get_xlim() From b7fa8f5d230f37886c9105a60a908ccf4265a86c Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Fri, 2 Feb 2024 17:49:55 -0600 Subject: [PATCH 41/48] Documentation: Add migration guide --- .../migration_guide.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 examples/creating_a_simulation_library/migration_guide.py diff --git a/examples/creating_a_simulation_library/migration_guide.py b/examples/creating_a_simulation_library/migration_guide.py new file mode 100644 index 00000000..41699418 --- /dev/null +++ b/examples/creating_a_simulation_library/migration_guide.py @@ -0,0 +1,84 @@ +""" +0.5.x --> 0.6.x Migration Guide +=============================== +This is a migration guide for version 0.5.x to 0.6.x. This guide helps to show the changes +that were made to the API and how to update your code to use the new API. + +Here you can see how to make an equivalent to a diffraction library +""" + + +# Old + +import numpy as np +import diffpy +from diffsims.libraries.structure_library import StructureLibrary +from diffsims.generators.diffraction_generator import DiffractionGenerator +from diffsims.generators.library_generator import DiffractionLibraryGenerator + + +latt = diffpy.structure.lattice.Lattice(4, 4, 4, 90, 90, 90) +atoms = [ + diffpy.structure.atom.Atom(atype="Al", xyz=[0.0, 0.0, 0.0], lattice=latt), + diffpy.structure.atom.Atom(atype="Al", xyz=[0.5, 0.5, 0.0], lattice=latt), + diffpy.structure.atom.Atom(atype="Al", xyz=[0.5, 0.0, 0.5], lattice=latt), + diffpy.structure.atom.Atom(atype="Al", xyz=[0.0, 0.5, 0.5], lattice=latt), +] +structure_matrix = diffpy.structure.Structure(atoms=atoms, lattice=latt) +euler_angles = np.array([[0, 0, 0], [10.0, 0.0, 0.0]]) +struct_library = StructureLibrary(["Al"], [structure_matrix], [euler_angles]) +diff_gen = DiffractionGenerator(accelerating_voltage=200) +lib_gen = DiffractionLibraryGenerator(diff_gen) +diff_lib = lib_gen.get_diffraction_library( + struct_library, + calibration=0.0262, + reciprocal_radius=1.6768, + half_shape=64, + with_direct_beam=True, + max_excitation_error=0.02, +) + + +# New + + +import diffpy +from orix.crystal_map import Phase +from orix.quaternion import Rotation +from diffsims.generators.simulation_generator import SimulationGenerator + +latt = diffpy.structure.lattice.Lattice(4, 4, 4, 90, 90, 90) +atoms = [ + diffpy.structure.atom.Atom(atype="Al", xyz=[0.0, 0.0, 0.0], lattice=latt), + diffpy.structure.atom.Atom(atype="Al", xyz=[0.5, 0.5, 0.0], lattice=latt), + diffpy.structure.atom.Atom(atype="Al", xyz=[0.5, 0.0, 0.5], lattice=latt), + diffpy.structure.atom.Atom(atype="Al", xyz=[0.0, 0.5, 0.5], lattice=latt), +] +structure_matrix = diffpy.structure.Structure(atoms=atoms, lattice=latt) +p = Phase("Al", point_group="m-3m", structure=structure_matrix) +gen = SimulationGenerator(accelerating_voltage=200) +rot = Rotation.from_euler([[0, 0, 0], [10.0, 0.0, 0.0]], degrees=True) +sim = gen.calculate_ed_data( + phase=p, + rotation=rot, + reciprocal_radius=1.6768, + max_excitation_error=0.02, + with_direct_beam=True, +) + + +import matplotlib.pyplot as plt + +fig, axs = plt.subplots(2, 2, figsize=(10, 10)) +for i in range(2): + diff_lib["Al"]["simulations"][i].plot( + size_factor=15, show_labels=True, ax=axs[i, 0] + ) + sim.irot[i].plot(ax=axs[i, 1], size_factor=15, show_labels=True) + axs[i, 0].set_xlim(-1.5, 1.5) + axs[i, 0].set_ylim(-1.5, 1.5) + axs[i, 1].set_xlim(-1.5, 1.5) + axs[i, 1].set_ylim(-1.5, 1.5) + +axs[0, 0].set_title("Old") +axs[0, 1].set_title("New") From 842734280de1a0adca173c4ee847c6f252abf3d0 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Fri, 2 Feb 2024 20:12:11 -0600 Subject: [PATCH 42/48] Coverage: Add tests for get_current_rotation --- diffsims/tests/simulations/test_simulation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/diffsims/tests/simulations/test_simulation.py b/diffsims/tests/simulations/test_simulation.py index 4ddcba31..d93881ab 100644 --- a/diffsims/tests/simulations/test_simulation.py +++ b/diffsims/tests/simulations/test_simulation.py @@ -184,6 +184,10 @@ def multi_simulation(self, al_phase): ) return sim + def test_get_current_rotation(self, multi_simulation): + rot = multi_simulation.get_current_rotation() + np.testing.assert_array_equal(rot, multi_simulation.rotations[0].to_matrix()[0]) + def test_init(self, multi_simulation): assert isinstance(multi_simulation, Simulation2D) assert isinstance(multi_simulation.phases, Phase) From d23285a63cc7c496146339c1d4aff3074c329d36 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Tue, 6 Feb 2024 21:09:23 -0600 Subject: [PATCH 43/48] BugFix: Fix hexagonal issues. --- diffsims/simulations/simulation2d.py | 5 ++++- diffsims/utils/sim_utils.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/diffsims/simulations/simulation2d.py b/diffsims/simulations/simulation2d.py index 726dc1b3..313b912d 100644 --- a/diffsims/simulations/simulation2d.py +++ b/diffsims/simulations/simulation2d.py @@ -507,7 +507,10 @@ def plot( if show_labels: millers = np.round( - np.matmul(coords.hkl, self.get_current_rotation().T) + np.matmul( + np.matmul(coords.data, self.get_current_rotation().T), + coords.phase.structure.lattice.base.T, + ) ).astype(np.int16) # only label the points inside the axes xlim = ax.get_xlim() diff --git a/diffsims/utils/sim_utils.py b/diffsims/utils/sim_utils.py index b6f5c19e..9eabc2a7 100644 --- a/diffsims/utils/sim_utils.py +++ b/diffsims/utils/sim_utils.py @@ -278,7 +278,7 @@ def _get_kinematical_structure_factor( # Express the atom positions in the same reference frame as the # Miller indices - mat = np.linalg.inv(np.dot(structure.lattice.stdbase, structure.lattice.recbase)) + mat = np.linalg.inv(np.dot(structure.lattice.stdbase, structure.lattice.recbase.T)) xyz = np.dot(xyz, mat) # Calculate the complex structure factor From 118ccf8fad8b9617088cef6000fc8b61a270ccde Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Wed, 14 Feb 2024 09:29:59 -0600 Subject: [PATCH 44/48] Refactor: Add methods for easier indexing related to Orientation Mapping --- diffsims/simulations/simulation2d.py | 54 ++++++++++++++++++- diffsims/tests/simulations/test_simulation.py | 16 ++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/diffsims/simulations/simulation2d.py b/diffsims/simulations/simulation2d.py index 313b912d..a081f6ee 100644 --- a/diffsims/simulations/simulation2d.py +++ b/diffsims/simulations/simulation2d.py @@ -21,6 +21,7 @@ import numpy as np import matplotlib.pyplot as plt +import math from orix.crystal_map import Phase from orix.quaternion import Rotation from orix.vector import Vector3d @@ -182,6 +183,31 @@ def __init__( self.iphase = PhaseGetter(self) self.irot = RotationGetter(self) + def get_simulation(self, item): + """Return the rotation and the phase index of the simulation""" + if self.has_multiple_phases and self.has_multiple_rotations: + cumsum = np.cumsum(self._num_rotations()) + ind = np.searchsorted(cumsum, item, side="right") + cumsum = np.insert(cumsum, 0, 0) + num_rot = cumsum[ind] + return ( + self.rotations[ind][item - num_rot], + ind, + self.coordinates[ind][item - num_rot], + ) + elif self.has_multiple_phases: + return self.rotations[item], item, self.coordinates[item] + elif self.has_multiple_rotations: + return self.rotations[item], 0, self.coordinates[item] + else: + return self.rotations[item], 0, self.coordinates + + def _num_rotations(self): + if self.has_multiple_phases: + return [r.size for r in self.rotations] + else: + return self.rotations.size + def __iter__(self): return self @@ -268,7 +294,7 @@ def rotate_shift_coordinates( ) return coords_new - def polar_flatten_simulations(self): + def polar_flatten_simulations(self, radial_axes=None, azimuthal_axes=None): """Flattens the simulations into polar coordinates for use in template matching. The resulting arrays are of shape (n_simulations, n_spots) where n_spots is the maximum number of spots in any simulation. @@ -285,12 +311,19 @@ def polar_flatten_simulations(self): r_templates = np.zeros((len(flattened_vectors), max_num_spots)) theta_templates = np.zeros((len(flattened_vectors), max_num_spots)) intensities_templates = np.zeros((len(flattened_vectors), max_num_spots)) - for i, v in enumerate(flattened_vectors): r, t, _ = v.to_polar() + if radial_axes is not None and azimuthal_axes is not None: + r = get_closest(radial_axes, r) + t = get_closest(azimuthal_axes, t) + r = r[r < len(radial_axes)] + t = t[t < len(azimuthal_axes)] r_templates[i, : len(r)] = r theta_templates[i, : len(t)] = t intensities_templates[i, : len(v.intensity)] = v.intensity + if radial_axes is not None and azimuthal_axes is not None: + r_templates = np.array(r_templates, dtype=int) + theta_templates = np.array(theta_templates, dtype=int) return r_templates, theta_templates, intensities_templates @@ -554,3 +587,20 @@ def plot( ax.set_xlabel("pixels") ax.set_ylabel("pixels") return ax, sp + + +def get_closest(array, values): + # make sure array is a numpy array + array = np.array(array) + + # get insert positions + idxs = np.searchsorted(array, values, side="left") + + # find indexes where previous index is closer + prev_idx_is_less = (idxs == len(array)) | ( + np.fabs(values - array[np.maximum(idxs - 1, 0)]) + < np.fabs(values - array[np.minimum(idxs, len(array) - 1)]) + ) + idxs[prev_idx_is_less] -= 1 + + return idxs diff --git a/diffsims/tests/simulations/test_simulation.py b/diffsims/tests/simulations/test_simulation.py index d93881ab..b8d75272 100644 --- a/diffsims/tests/simulations/test_simulation.py +++ b/diffsims/tests/simulations/test_simulation.py @@ -184,6 +184,12 @@ def multi_simulation(self, al_phase): ) return sim + def test_get_simulation(self, multi_simulation): + for i in range(4): + rotation, phase = multi_simulation.get_simulation(i) + assert isinstance(rotation, Rotation) + assert phase == 0 + def test_get_current_rotation(self, multi_simulation): rot = multi_simulation.get_current_rotation() np.testing.assert_array_equal(rot, multi_simulation.rotations[0].to_matrix()[0]) @@ -285,6 +291,16 @@ def test_init(self, multi_simulation): assert isinstance(multi_simulation.rotations, np.ndarray) assert isinstance(multi_simulation.coordinates, np.ndarray) + def test_get_simulation(self, multi_simulation): + for i in range(4): + rotation, phase = multi_simulation.get_simulation(i) + assert isinstance(rotation, Rotation) + assert phase == 0 + for i in range(4, 8): + rotation, phase = multi_simulation.get_simulation(i) + assert isinstance(rotation, Rotation) + assert phase == 1 + def test_iphase(self, multi_simulation): phase_slic = multi_simulation.iphase[0] assert isinstance(phase_slic, Simulation2D) From 28e34eb95b72b8cd8b2cfb0088f9670da649ee20 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Wed, 14 Feb 2024 10:26:07 -0600 Subject: [PATCH 45/48] Coverage: Increased coverage for simulation2d --- diffsims/simulations/simulation2d.py | 22 +++--- diffsims/tests/simulations/test_simulation.py | 74 ++++++++++++++++++- 2 files changed, 84 insertions(+), 12 deletions(-) diff --git a/diffsims/simulations/simulation2d.py b/diffsims/simulations/simulation2d.py index a081f6ee..682f424d 100644 --- a/diffsims/simulations/simulation2d.py +++ b/diffsims/simulations/simulation2d.py @@ -185,18 +185,19 @@ def __init__( def get_simulation(self, item): """Return the rotation and the phase index of the simulation""" - if self.has_multiple_phases and self.has_multiple_rotations: + if self.has_multiple_phases: cumsum = np.cumsum(self._num_rotations()) ind = np.searchsorted(cumsum, item, side="right") cumsum = np.insert(cumsum, 0, 0) num_rot = cumsum[ind] - return ( - self.rotations[ind][item - num_rot], - ind, - self.coordinates[ind][item - num_rot], - ) - elif self.has_multiple_phases: - return self.rotations[item], item, self.coordinates[item] + if self.has_multiple_rotations[ind]: + return ( + self.rotations[ind][item - num_rot], + ind, + self.coordinates[ind][item - num_rot], + ) + else: + return self.rotations[ind], ind, self.coordinates[ind] elif self.has_multiple_rotations: return self.rotations[item], 0, self.coordinates[item] else: @@ -409,7 +410,10 @@ def has_multiple_phases(self): @property def has_multiple_rotations(self): """Returns True if the simulation has multiple rotations""" - return self.rotations.size > 1 + if isinstance(self.rotations, Rotation): + return self.rotations.size > 1 + else: + return [r.size > 1 for r in self.rotations] def get_current_coordinates(self): """Returns the coordinates of the current phase and rotation""" diff --git a/diffsims/tests/simulations/test_simulation.py b/diffsims/tests/simulations/test_simulation.py index b8d75272..dae54b06 100644 --- a/diffsims/tests/simulations/test_simulation.py +++ b/diffsims/tests/simulations/test_simulation.py @@ -58,6 +58,11 @@ def test_init(self, single_simulation): assert isinstance(single_simulation.simulation_generator, SimulationGenerator) assert isinstance(single_simulation.rotations, Rotation) + def test_get_simulation(self, single_simulation): + rotation, phase, coords = single_simulation.get_simulation(0) + assert isinstance(rotation, Rotation) + assert phase == 0 + def test_iphase(self, single_simulation): with pytest.raises(ValueError): single_simulation.iphase[0] @@ -76,6 +81,9 @@ def test_iter(self, single_simulation): def test_plot(self, single_simulation): single_simulation.plot() + def test_num_rotations(self, single_simulation): + assert single_simulation._num_rotations() == 1 + def test_polar_flatten(self, single_simulation): ( r_templates, @@ -86,6 +94,20 @@ def test_polar_flatten(self, single_simulation): assert theta_templates.shape == (1, 1) assert intensities_templates.shape == (1, 1) + def test_polar_flatten_axes(self, single_simulation): + radial_axes = np.linspace(0, 1, 10) + theta_axes = np.linspace(0, 2 * np.pi, 10) + ( + r_templates, + theta_templates, + intensities_templates, + ) = single_simulation.polar_flatten_simulations( + radial_axes=radial_axes, azimuthal_axes=theta_axes + ) + assert r_templates.shape == (1, 1) + assert theta_templates.shape == (1, 1) + assert intensities_templates.shape == (1, 1) + def test_deepcopy(self, single_simulation): copied = single_simulation.deepcopy() assert copied is not single_simulation @@ -186,7 +208,7 @@ def multi_simulation(self, al_phase): def test_get_simulation(self, multi_simulation): for i in range(4): - rotation, phase = multi_simulation.get_simulation(i) + rotation, phase, coords = multi_simulation.get_simulation(i) assert isinstance(rotation, Rotation) assert phase == 0 @@ -293,11 +315,11 @@ def test_init(self, multi_simulation): def test_get_simulation(self, multi_simulation): for i in range(4): - rotation, phase = multi_simulation.get_simulation(i) + rotation, phase, coords = multi_simulation.get_simulation(i) assert isinstance(rotation, Rotation) assert phase == 0 for i in range(4, 8): - rotation, phase = multi_simulation.get_simulation(i) + rotation, phase, coords = multi_simulation.get_simulation(i) assert isinstance(rotation, Rotation) assert phase == 1 @@ -386,3 +408,49 @@ def test_polar_flatten(self, multi_simulation): def test_rotate_shift_coords(self, multi_simulation): rot = multi_simulation.rotate_shift_coordinates(angle=0.1) assert isinstance(rot, ReciprocalLatticeVector) + + +class TestMultiPhaseSingleSimulation: + @pytest.fixture + def al_phase(self): + p = Phase( + name="al", + space_group=225, + structure=Structure( + atoms=[Atom("al", [0, 0, 0])], + lattice=Lattice(0.405, 0.405, 0.405, 90, 90, 90), + ), + ) + return p + + @pytest.fixture + def multi_simulation(self, al_phase): + gen = SimulationGenerator(accelerating_voltage=200) + rot = Rotation.from_axes_angles([1, 0, 0], (0,), degrees=True) + rot2 = rot + coords = ReciprocalLatticeVector( + phase=al_phase, + xyz=[ + [1, 0, 0], + [0, -0.3, 0], + [1 / 0.405, 1 / -0.405, 0], + [0.1, -0.1, -0.3], + ], + ) + coords.intensity = 1 + vectors = coords + al_phase2 = al_phase.deepcopy() + al_phase2.name = "al2" + sim = Simulation2D( + phases=[al_phase, al_phase2], + simulation_generator=gen, + coordinates=[vectors, vectors], + rotations=[rot, rot2], + ) + return sim + + def test_get_simulation(self, multi_simulation): + for i in range(2): + rotation, phase, coords = multi_simulation.get_simulation(i) + assert isinstance(rotation, Rotation) + assert phase == i From 38ca9dd3b89c435505561450dfe22aba34822700 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Wed, 14 Feb 2024 15:11:33 -0600 Subject: [PATCH 46/48] Bugfix: Fix simulation for multiple phases with different lengths. --- diffsims/simulations/simulation2d.py | 10 +++++++--- diffsims/tests/generators/test_simulation_generator.py | 10 ++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/diffsims/simulations/simulation2d.py b/diffsims/simulations/simulation2d.py index 682f424d..115f9e71 100644 --- a/diffsims/simulations/simulation2d.py +++ b/diffsims/simulations/simulation2d.py @@ -152,8 +152,6 @@ def __init__( else: # iterable of Rotation rotations = np.array(rotations, dtype=object) coordinates = np.array(coordinates, dtype=object) - if len(coordinates.shape) != 2: - coordinates = coordinates[:, np.newaxis] phases = np.array(phases) if rotations.size != phases.size: raise ValueError( @@ -162,7 +160,13 @@ def __init__( ) for r, c in zip(rotations, coordinates): - if r.size != c.size: + if isinstance(c, ReciprocalLatticeVector): + c = np.array( + [ + c, + ] + ) + if r.size != len(c): raise ValueError( f"The number of rotations: {r.size} must match the number of " f"coordinates {c.shape[0]}" diff --git a/diffsims/tests/generators/test_simulation_generator.py b/diffsims/tests/generators/test_simulation_generator.py index f1116e6b..06c6ddea 100644 --- a/diffsims/tests/generators/test_simulation_generator.py +++ b/diffsims/tests/generators/test_simulation_generator.py @@ -21,6 +21,7 @@ import diffpy.structure from orix.crystal_map import Phase +from orix.quaternion import Rotation from diffsims.generators.simulation_generator import SimulationGenerator from diffsims.utils.shape_factor_models import ( @@ -226,6 +227,15 @@ def test_simulate_1d(self, is_hex): assert len(h) == 3 +def test_multiphase_multirotation_simulation(): + generator = SimulationGenerator(300) + silicon = make_phase(5) + big_silicon = make_phase(10) + rot = Rotation.from_euler([[0, 0, 0], [0.1, 0.1, 0.1]]) + rot2 = Rotation.from_euler([[0, 0, 0], [0.1, 0.1, 0.1], [0.2, 0.2, 0.2]]) + sim = generator.calculate_ed_data([silicon, big_silicon], rotation=[rot, rot2]) + + @pytest.mark.parametrize("scattering_param", ["lobato", "xtables"]) def test_param_check(scattering_param): generator = SimulationGenerator(300, scattering_params=scattering_param) From 64e49716ccd1b9c7fd0845078c59c50248956066 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Wed, 27 Mar 2024 09:30:04 -0500 Subject: [PATCH 47/48] Documentation: Clean up Deprecations and circular imports --- diffsims/libraries/diffraction_library.py | 7 ------- diffsims/sims/diffraction_simulation.py | 7 ------- diffsims/utils/_deprecated.py | 2 +- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/diffsims/libraries/diffraction_library.py b/diffsims/libraries/diffraction_library.py index 1929724b..728d51d5 100644 --- a/diffsims/libraries/diffraction_library.py +++ b/diffsims/libraries/diffraction_library.py @@ -20,7 +20,6 @@ import numpy as np -from diffsims.generators.diffraction_generator import DiffractionGenerator from diffsims.utils._deprecated import deprecated __all__ = [ @@ -116,12 +115,6 @@ class DiffractionLibrary(dict): """ - @deprecated( - since="0.6.0", - alternative="diffsims.generators.simulation_generator.SimulationGenerator", - alternative_is_function=False, - removal="0.8.0", - ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.identifiers = None diff --git a/diffsims/sims/diffraction_simulation.py b/diffsims/sims/diffraction_simulation.py index 04b13445..1df7745f 100644 --- a/diffsims/sims/diffraction_simulation.py +++ b/diffsims/sims/diffraction_simulation.py @@ -22,7 +22,6 @@ from diffsims.pattern.detector_functions import add_shot_and_point_spread from diffsims.utils import mask_utils -from diffsims.utils._deprecated import deprecated __all__ = [ @@ -51,12 +50,6 @@ class DiffractionSimulation: zero in each direction. """ - @deprecated( - since="0.6.0", - alternative="diffsims.simulation.Simulation2D", - alternative_is_function=False, - removal="0.8.0", - ) def __init__( self, coordinates, diff --git a/diffsims/utils/_deprecated.py b/diffsims/utils/_deprecated.py index a4f1bb18..d0fea82d 100644 --- a/diffsims/utils/_deprecated.py +++ b/diffsims/utils/_deprecated.py @@ -88,7 +88,7 @@ def __call__(self, func: Callable): @functools.wraps(func) def wrapped(*args, **kwargs): warnings.simplefilter( - action="always", category=np.VisibleDeprecationWarning, append=True + action="once", category=np.VisibleDeprecationWarning, append=True ) func_code = func.__code__ warnings.warn_explicit( From 6840707a1f852d73c29e9e96468d6058f5c0a03a Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Fri, 12 Apr 2024 16:49:02 -0500 Subject: [PATCH 48/48] BugFix: Fix simulation intensities. --- diffsims/utils/sim_utils.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/diffsims/utils/sim_utils.py b/diffsims/utils/sim_utils.py index 9eabc2a7..b6f5c19e 100644 --- a/diffsims/utils/sim_utils.py +++ b/diffsims/utils/sim_utils.py @@ -278,7 +278,7 @@ def _get_kinematical_structure_factor( # Express the atom positions in the same reference frame as the # Miller indices - mat = np.linalg.inv(np.dot(structure.lattice.stdbase, structure.lattice.recbase.T)) + mat = np.linalg.inv(np.dot(structure.lattice.stdbase, structure.lattice.recbase)) xyz = np.dot(xyz, mat) # Calculate the complex structure factor diff --git a/setup.py b/setup.py index eeacbcb6..f6042b73 100644 --- a/setup.py +++ b/setup.py @@ -79,7 +79,7 @@ "matplotlib >= 3.3", "numba", "numpy >= 1.17", - "orix >= 0.11", + "orix >= 0.12", "psutil", "scipy >= 1.2", "tqdm >= 4.9",