Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detector orientation with euler angles: simulations from master patterns and GeometricalKikuchiPatternSimulation #691

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from

Conversation

tgwoodcock
Copy link

Description of the change

Hi @hakonanes and the kikuchipy team,
firstly, many thanks for kikuchipy - it's a great library which we use a lot in our work!

The focus of this PR is to include the detector Euler angles as a means of describing the full orientation of the detector and to use these in simulating EBSD patterns from master patterns and in producing lines to overlay on those patterns using KikuchiPatternSimulator.on_detector().

To achieve this, the following main changes have been made:

  1. Added a property "euler" to EBSDDetector, which is used to describe the full orientation of the detector via the Euler angles (in degrees).
    • As EBSDDetector.euler[0] is the same as EBSDDetector.azimuthal and EBSDDetector.euler[1] is the same as EBSDDetector.tilt + 90, getters and setters for EBSDDetector.tilt, EBSDDetector.azimuthal and EBSDDetector.euler have been added, which ensure consistency with the other properties when setting one of them.
    • Tests have been added to check the consistency when setting these properties.
    • Added the euler to EBSDDetector.__repr__().
    • Added properties "u_s" and "u_s_inv" to EBSDDetector, which are orientation matrices used to transform from CSs to CSd and the reverse. The matrix "u_s_inv" is used in the new functions for getting the direction cosines of the detector (see 3.).
  2. Modified KikuchiPatternSimulator.on_detector() so that the euler angles of the detector are now used in calculating the orientation matrix "u_s_bruker". Previously, only the EBSDDetector.tilt seemed to be considered.
  3. Modified functions for getting direction cosines of the detector pixels (these are in kikuchipy/signals/util/_master_pattern.py). The new approach uses the gnomonic coordinates of the EBSDDetector and orientation matrices, which consider all three detector euler angles (previously only tilt and azimuthal were considered).
    • Removed the now unneeded function, _get_cosine_sine_of_alpha_and_azimuthal(),
    • updated all tests calling the functions for direction cosines and
    • updated the signatures of the direction cosine functions in the objective functions and solvers for indexing.
  4. Added a new test class TestFitPatternDetectorOrientation to tests/test_signals/test_ebsd_master_pattern.py. This checks the fit between an EBSD pattern simulated from a master pattern and the associated GeometricalKikuchiPatternSimulation. This seemed to be worth testing because with the current version of the functions to get the direction cosines from the detector, there was a rotation of the EBSD simulation from the master pattern compared to the GeometricalKikuchiPatternSimulation if both tilt and azimuthal were non-zero:
    current_method_tilt_40_azimuthal_40
    Whereas with the new method for getting the direction cosines in this PR, the EBSD pattern and GeometricalKikuchiPatternSimulation fit perfectly for any combination of detector euler angles:
    new_method_tilt_40_azimuthal_40

The entire kikuchipy test suite passes. I was not able to build the documentation on Windows but I think everything should be fine with the tutorial notebooks.

Progress of the #PR

Minimal example of the bug fix or new feature

>>> import kikuchipy as kp
>>> det = kp.detectors.EBSDDetector()
>>> det
<EBSDDetector(shape=(1, 1), pc=(0.5, 0.5, 0.5), sample_tilt=70.0, tilt=0.0, azimuthal=0.0, euler=(0.0, 90.0, 0.0), binning=1.0, px_size=1.0 um)>
>>> det.euler = [3.4, 96.6, 0.7]
>>> det
<EBSDDetector(shape=(1, 1), pc=(0.5, 0.5, 0.5), sample_tilt=70.0, tilt=6.6, azimuthal=3.4, euler=(3.4, 96.6, 0.7), binning=1.0, px_size=1.0 um)>

For reviewers

  • The PR title is short, concise, and will make sense 1 year later.
  • New functions are imported in corresponding __init__.py.
  • New features, API changes, and deprecations are mentioned in the unreleased
    section in CHANGELOG.rst.
  • New contributors are added to kikuchipy/__init__.py and .zenodo.json.

@hakonanes
Copy link
Member

Hi @tgwoodcock,

Thank you for this PR and for the kind words! Glad you use kikuchipy and find it useful :)

I don't have time to look at your changes in detail now. From the top comment it seems like they fix a bug and add some nice new features. Although I believe we have to iterate on the additions to the public API (naming of the Euler angles and matrices). I hope to get back to you within a week at the latest so we can get this in.

@hakonanes hakonanes added bug Something isn't working enhancement New feature or request labels Oct 13, 2024
@hakonanes hakonanes added this to the v0.11.0 milestone Oct 13, 2024
@hakonanes hakonanes removed this from the v0.11.0 milestone Nov 9, 2024
@hakonanes hakonanes added this to the v0.12.0 milestone Nov 12, 2024
@hakonanes
Copy link
Member

Hi @tgwoodcock, is it OK if I push changes that makes the test suite pass?

And have you done any benchmarking of refinement and projection from master pattern to detector after making your changes?

@@ -239,10 +240,96 @@ def __repr__(self) -> str:
f"sample_tilt={sample_tilt}, "
f"tilt={tilt}, "
f"azimuthal={azimuthal}, "
f"euler={tuple(self.euler)}, "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to drop this, as the string representation is already too long. The same information is anyways contained in the tilt and azimuthal angles.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we define a third angle (see other comments), then I guess we could drop euler and just have the other 3 angles instead.

self.sample_tilt = float(sample_tilt)
self._euler = np.array([self.azimuthal, 90.0 + self.tilt, 0.0]).astype(float)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to me like we get the same functionality if we just make EBSDDetector.euler a dependent property

@property
def euler(self) -> np.ndarray:
    return np.array([self.azimuthal, 90. + self.tilt, 0.], dtype=float)

This way we don't have to handle the tilts separately. We should have only one way of updating the angles, and I believe this should be via EBSDDetector.tilt and EBSDDetector.azimuthal.

What do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are plenty of examples where the third detector Euler angle is non-zero. At least for the Oxford Instruments software, the detector orientation is stored in the h5oina as Euler angles. I think there is a case for being able to set the detector Euler angles directly e.g. when using the kp.load() function to read a dataset. On the other hand, if you prefer to define the angles separately, it would be possible to define a third angle, something like EBSDDetector.twist, which would be equivalent to EBSDDectector.euler[2].

@@ -495,6 +582,30 @@ def r_max(self) -> np.ndarray:
corners[..., 3] = self.x_min**2 + self.y_min**2 # Lo. left
return np.atleast_2d(np.sqrt(np.max(corners, axis=-1)))

@property
def u_s(self) -> np.ndarray:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a great idea to have this transformation as a dependent property.

We aim for as tight an integration as possible with orix, so this should return a orix.quaternion.Rotation. People can then get the matrix themselves by calling Rotation.to_matrix().

We must call the property something self-explanatory. I suggest EBSDDetector.sample_to_detector. Do you have another suggestion? We can give the reference to the relevant equation in the Britton et al. (2016) paper, and even give the U_s symbol in the docstring.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, it would be easier to have a more self-explanatory name.

return u_s

@property
def u_s_inv(self) -> np.ndarray:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to drop this and instead explain that the rotation can be obtained from ~EBSDDetector.sample_to_detector.

@@ -518,6 +629,7 @@ def load(cls, fname: Path | str) -> EBSDDetector:
"binning",
"tilt",
"azimuthal",
"euler",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a dependent property, we don't have to include it in IO.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we go down the route of defining a third angle (e.g. twist), then that could be added to IO and we can safely drop euler.

tilt: float,
azimuthal: float,
sample_tilt: float,
u_s_inv: np.ndarray,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, we should rename to something self-explanatory, such as om_detector_to_sample (om is used for orientation matrix in orix and elsewhere).

r_g_array[i, 1] = (det_gn_y[rows[i]] - y_half_step) * pcz
r_g_array[i, 2] = pcz

# rotate vectors from detector reference frame CSd to
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd remove the comment and rather rename the orientation matrix variable to be self-explanatory.


phase = kp.data.nickel_ebsd_master_pattern_small().phase

pqr_p = [(1, 1, 1), (2, 0, 0), (2, 2, 0), (3, 1, 1)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rename to hkl which is self-explanatory.

u_a = phase.structure.lattice.base

def setup_detector_sim_and_u_os(self, tilt, azimuthal, euler_2):
"""Setup the detector with the correct tilt, azimuthal and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd consider this docstring to be superfluous as the code is easy to read and self-documenting. I'd remove it.

return None


def get_coordinate_conversions(detector):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the next two functions may be useful to have as functionality accessible via the EBSD detector?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they could easily be moved into the EBSDDetector class.

Added an attribute self._euler, which holds the three
detector Euler angles in degrees. These describe the
orientation of the detector.

The attributes self.tilt and self.azimuthal were made
into "non-public" attributes i.e. self._tilt and
self._azimuthal.

self._tilt, self._azimuthal and self._euler
are set directly in self.__init__() i.e. NOT by calling the setters.
These values will therefore always be self consistent:
self._euler is not an argument for self.__init_().

Getters and setters were added for tilt, azimuthal and
euler. These are designed to retain self.consistency -
e.g. if you set self.azimuthal or self.tilt, self.euler
will be updated accordingly and vice versa.

self.euler[0] == self.azimuthal
self.euler[1] == 90 + self.tilt

The methods EBSDDetector.save() and EBSDDetector.load() were
updated so that the EBSDDetector.euler property is additionally
saved. This is needed because EBSDDetector.euler[2] may not be
zero and is currently not represented by any other EBSDDetector
property or attribute.

The test method TestSaveLoadDetector.test_save_load_detector()
in tests/test_detectors/test_ebsd_detector.py has been updated
to include an extra assert statement to check that the saved and
read euler angles are the same. All tests in TestSaveLoadDetector
pass with the current code.

The tests test_set_tilt(), test_set_azimuthal() and
test_set_euler() were added to the class TestEBSDDetector
in tests/test_detectors/test_ebsd_detector.py. These help
to ensure that when setting one of the properties, the others
are updated for consistency.

All tests in tests/test_detectors/test_ebsd_detector.py
pass with the current code.

Signed-off-by: tgwoodcock <[email protected]>
These properties return the orientation matrix u_s,
which transforms vectors in the sample coordinate
system CSs, to the detector coordinate system, CSd,
and the inverse matrix, u_s_inv,for the opposite
transformation.

Signed-off-by: tgwoodcock <[email protected]>
…ed test

The __repr__() method in the EBSDDetector class in
src/kikuchipy/detectors/ebsd_detector.py was modified so
that the detector euler angles are also printed.

The associated test, TestEBSDDetector.test_repr() in
tests/test_detectors/test_ebsd_detector.py was modified
accordingly so that it still passes.

Signed-off-by: tgwoodcock <[email protected]>
Modified the calculation of the orientation matrix
u_s_bruker in KikuchiPatternSimulator.on_detector().
Previously u_s_bruker was determined from the 'total_tilt',
which only considered EBSDDetector.sample_tilt and
EBSDDetector.tilt but NOT EBSDDetector.azimuthal.
Now EBSDDetector has the detector Euler angles, which
describe the full orientation of the detector and these can
be used to calculate u_s_bruker. In this way, the full
orientation of the detector can be considered when making
simulations to overlay on patterns. This will give a much
better fit for cases where the detector azimuthal angle
was non-zero (i.e. EBSDDetector.euler[0] != 0) or where the
third detector Euler angle was non-zero.

Signed-off-by: tgwoodcock <[email protected]>
….util._master_pattern.py

The functions _get_direction_cosines_for_fixed_pc() and
_get_direction_cosines_for_varying_pc() have been modified. The
previous functions used an algorithm adapted from EMSoft to
calculate the direction cosines of the detector pixels. This
has now been changed and the functions use an orientation matrix
approach, adapted from that in the tutorial paper by Britton et al.
This enables the detector tilt, azimuthal and euler[2],
i.e. all 3 detector euler angles, to be correctly
incorporated in the pattern simulation.

The function _get_direction_cosines_from_detector() has been
updated to reflect the new signatures of the other two functions.

Signed-off-by: tgwoodcock <[email protected]>
…sociated test

The funtion _get_cosine_sine_of_alpha_and_azimuthal() has been
removed from src/kikuchipy/signals/util/_master_pattern.py as it
is no longer needed.

The function import and associated test in
tests/test_signals/test_ebsd_master_pattern.py has also been
removed.

Signed-off-by: tgwoodcock <[email protected]>
…direction_cosines_for_varying_pc()

In tests/test_rotation/test_rotation.py:
In TestRotationVectorTools.test_rotate_vector(), added the instantiation
of an EBSDDetector so that the gnomonic_bounds can be passed into the
new signature of _get_direction_cosines_for_fixed_pc(). Modified the
arguments in the function call accordingly.

In tests/test_signals/test_ebsd_master_pattern.py:
In TestProjectFromLambert.test_get_direction_cosines(), updated the
arguments in the function call to _get_direction_cosines_for_fixed_pc().
Added a further call to that function but WITHOUT .py_func and added a
further assert statement to check that the results with and without
.py_func are the same (as recommended in the contributing guidelines).

In TestProjectFromLambert.test_get_direction_cosines_for_multiple_pcs(),
updated the arguments in the function call to
_get_direction_cosines_for_varying_pc().
Added a further call to that function but WITHOUT .py_func and added a
further assert statement to check that the results with and without
.py_func are the same (as recommended in the contributing guidelines).

In TestProjectFromLambert.test_detector_azimuthal()
As the function_get_direction_cosines_for_fixed_pc() has been
modified, the small rotation in the EBSD pattern simulated from
the master pattern for tilt!=0 and azimuthal!=0 is no longer
present. The mean values of the data in the simulated patterns
sim2 and sim3 in the above test are therefore very slightly
different to the old values. The mean values have been updated.

The above tests are now all passing.

Signed-off-by: tgwoodcock <[email protected]>
…for indexing

In src/kikuchipy/indexing/_refinement/_objective_functions.py,
_refine_pc_objective_function() and _refine_orientation_pc_objective_function()
were modified. Instead of calling _get_direction_cosines_for_fixed_pc()
directly, an EBSDDetector is instantiated and _get_direction_cosines_from_detector()
is called instead. This handles the changed function signature of
_get_direction_cosines_for_fixed_pc().

In src/kikuchipy/indexing/_refinement/_solvers.py,
_refine_orientation_solver_scipy() and _refine_orientation_solver_nlopt()
were modified. The block under "if direction_cosines is None:" has been
changed so that an EBSDDetector is instantiated and then
_get_direction_cosines_from_detector() is called.

Imports to these two modules have been added/modified accordingly.

Signed-off-by: tgwoodcock <[email protected]>
The above function in the suite TestEBSDRefineOrientationPC
has been modified by adding a trust region as an argument
in the call to s.refine_orientation_projection_centre() with
the method "differential_evolution". This was necessary because
the new version of _refine_orientation_pc_objective_function()
in indexing\_refinement\_objective_functions.py
requires the instatiation of an EBSDDetector and thus the
subsequent calculation of its gnomonic_bounds. If pc is
(0, 0, 0), this leads to strange results which cause errors
in the function _get_direction_cosines_for_fixed_pc() in
signals\util\_master_pattern.py. Adding the trust
region (same parameters as in the earlier call to
s.refine_orientation_projection_centre() within the same
test function) solves this problem.

The test test_refine_orientation_pc_pseudo_symmetry_scipy() now
passes, as do ALL the tests in tests/test_indexing/test_ebsd_refinement.py.

Signed-off-by: tgwoodcock <[email protected]>
…GeometricalKikuchiPatternSimulation

The class TestFitPatternDetectorOrientation has been added to
tests/test_signals/test_ebsd_master_pattern.py. The class contains
the test test_fit_detector_orientation(), which is parametrised with
8 sets of detector tilt, azimuthal and euler_2 angles, and lists
of associated zone axes, which are on the pattern simulated under
those conditions. For each set of parameters, we expect that the
fit between the simulated pattern and the GeometricalKikuchiPatternSimulation
is perfect.

These tests are currently all passing.

Signed-off-by: tgwoodcock <[email protected]>
@tgwoodcock
Copy link
Author

Hi @tgwoodcock, is it OK if I push changes that makes the test suite pass?

I hope I have fixed the problems now - the test suite passes on my Windows machine with Python3.12. Have updated my remote branch.

And have you done any benchmarking of refinement and projection from master pattern to detector after making your changes?

Not with refinement but I did benchmark getting the direction cosines. The performance was slower with the new version. I'll look into whether this can be improved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants