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

Adding support for beam precession #137

Merged
merged 10 commits into from
Nov 27, 2020
Merged

Conversation

din14970
Copy link
Contributor

@din14970 din14970 commented Nov 15, 2020

What I'm working on here is very premature, opening this PR to get some opinions/feedback and to give you a preview of the code. Apologies for wall of text.

What does this PR do? Please describe and/or link to an open issue.

  • I fixed a small bug in the beam_directions_grid_to_euler function.
  • I added the possibility to include the effect of beam precession in the diffraction patterns
  • I added/modified some shape factor models
  • I modified the way diffraction libraries are generated

More details
In the context of my issue #124 and the other discussions we've had elsewhere, I've started looking into the entire chain of operations I would need for template matching with Pyxem. It seems @pc494 you are also kind of working on something in this direction judging by pyxem/pyxem#656?

The things I need for this are:

  • a good orientation grid generator (solved in Improving and enhancing the creation of beam grid #130)
  • simulation of those orientations (currently looking at this point)
  • a modified indexer that calculates in-plane rotation, likely via polar transform
  • a way to visualize and plot the results; especially quality maps are of interest. I've thrown together a quick stereographic plot functionality for orix but it's too crappy for a PR

The simulated diffraction patterns generated with diffsims looked very odd for my material, in some cases only one diffraction spot was in the pattern. So I looked into:

  • the relrod functions in shape_factor_models
  • the way spot patterns are generated and the available kwargs in DiffractionGenerator.calculate_ed_data

With regards to the shape factor function, I saw the default shape is linear. I looked into W&C and at least for a 2-beam case, Howie-Whelan predicts a (sin(x)/x)^2 - like function, but the exact shape depends also on the extinction distance and thickness. I messed around a bit with the shape factor functions, which you can see here:

afbeelding

sinc was a function that was already there, and seems to follow Howie-Whelan quite well. I also tried sinc^2 but this tapers off the tails too strongly. To get a smooth function that approximately follows the shape of the Howie-Whelan rel-rod I also implemented an arctan(x)/x function. I changed all the functions so that the maximum at s=0 hits 1. I figured it doesn't hurt to have a couple more shape factor functions.

To the second point, I felt like the options available for generating a diffraction library "the official way" are a bit lacking:

diff_gen = DiffractionGenerator(accelerating_voltage=200, debye_waller_factors)
lib_gen = DiffractionLibraryGenerator(diff_gen)
diff_lib = lib_gen.get_diffraction_library(library_austenite,
                                           calibration=diffraction_calibration,
                                           reciprocal_radius=reciprocal_radius,
                                           half_shape=(half_size, half_size),
                                           with_direct_beam=False)

Many of the options in calculate_ed_data don't seem to be available this way. I saw that one used to be able to set max_excitation_error in DiffractionGenerator.__init__ but no longer, what is the reason for this?

Personally, I would like to have full control over the way the patterns are simulated. So I'm inclined to change everything either in the init of DiffractionGenerator or in the get_diffraction_library function.

In the process of looking into these diffraction patterns, I added the option to include the effect of beam precession. To calculate this I only use geometry; I don't make any additional assumptions. The Ewald sphere is precessed through the rel rods and the shape factor of each spot is calculated by averaging the shape factor function via integration. The difference is minor but there is some:
afbeelding
Especially further away reflections become a bit brighter with precession. The influence depends on the chosen shape factor model - above I use the atanc function. Below is with linear
afbeelding
I think the math on the precession is sound but a second pair of eyes might verify this. Of course with this implementation, precession is much more expensive, since I do an integration for each relevant diffraction spot.

Are there any known issues? Do you need help?
The main thing I'm unsure about is which arguments should go where, and why certain arguments were removed.

Work in progress?

  • I haven't included any testing yet
  • My alteration of the existing functions could break existing tests

For reviewers

  • Check that the PR title is short, concise, and will make sense 1 year
    later.
  • Check that new functions are imported in corresponding __init__.py.
  • Check that new features, API changes, and deprecations are mentioned in
    the unreleased section in CHANGELOG.md.

@pc494
Copy link
Member

pc494 commented Nov 15, 2020

A little difficult to structure a response, as there is a lot of very good stuff here. So I've just worked through it point by point, I think that will get us going in the right direction.

What does this PR do? Please describe and/or link to an open issue.

* I fixed a small bug in the `beam_directions_grid_to_euler` function.

* I added the possibility to include the effect of beam precession in the diffraction patterns

* I added/modified some shape factor models

* I modified the way diffraction libraries are generated

The first three points look good, I'll have a look at the changes for diffraction library generation once I've finished this comment

More details
In the context of my issue #124 and the other discussions we've had elsewhere, I've started looking into the entire chain of operations I would need template matching with Pyxem. It seems @pc494 you are also kind of working on something in this direction judging by pyxem/pyxem#656?

I'm not really working on this at the moment, but once we had your gridding in diffsims, it seemed worth making it clear in pyxem that the workflow you describe is an option (and probably a target)

simulation of those orientations (currently looking at this point)

So I think you can create those simulations with the code as it currently is, but it's clear that we want to improve the pipeline in this regard

a modified indexer that calculates in-plane rotation, likely via polar transform

I have thoughts about this, but I think I'll leave them to somewhere else

a way to visualize and plot the results; especially quality maps are of interest. I've thrown together a quick stereographic plot functionality for orix but it's too crappy for a PR

This one is a mixture of pyxem and orix (CrystalMap) and should be fairly simple to deal with. pyxem/pyxem#654 will be of interest.

The simulated diffraction patterns generated with diffsims looked very odd for my material, in some cases only one diffraction spot was in the pattern.

In my experience this tends to happens when your relrods are too short.

So I looked into:

the relrod functions in shape_factor_models

Think this is probably a case of the more the merrier

the way spot patterns are generated and the available kwargs in DiffractionGenerator.calculate_ed_data

To the second point, I felt like the options available for generating a diffraction library "the official way" are a bit lacking:

diff_gen = DiffractionGenerator(accelerating_voltage=200, debye_waller_factors)
lib_gen = DiffractionLibraryGenerator(diff_gen)
diff_lib = lib_gen.get_diffraction_library(library_austenite,
                                           calibration=diffraction_calibration,
                                           reciprocal_radius=reciprocal_radius,
                                           half_shape=(half_size, half_size),
                                           with_direct_beam=False)

This is a bug, get_diffraction_library() should have kwargs that get passed to calculate_ed_data. Good find.

Many of the options in calculate_ed_data don't seem to be available this way. I saw that one used to be able to set max_excitation_error in DiffractionGenerator.__init__ but no longer, what is the reason for this?

Philosophical. The DiffractionGenerator is meant to be the Microscope, with sample properties specified as arguments to calculate_ed_data, so we shuffled max_excitation_error around during a recent refactor.

Personally, I would like to have full control over the way the patterns are simulated. So I'm inclined to change everything either in the init of DiffractionGenerator or in the get_diffraction_library function.

Does the solution proposed above cover these needs?

In the process of looking into these diffraction patterns, I added the option to include the effect of beam precession. To calculate this I only use geometry; I don't make any additional assumptions. The Ewald sphere is precessed through the rel rods and the shape factor of each spot is calculated by averaging the shape factor function via integration. The difference is minor but there is some: I think the math on the precession is sound but a second pair of eyes might verify this. Of course with this implementation, precession is much more expensive, since I do an integration for each relevant diffraction spot.

Have you seen https://journals-iucr-org.ezp.lib.cam.ac.uk/b/issues/2019/04/00/je5015/ ? I think that would save you doing the integrations (and could be written in as a "shape factor model").

Copy link
Member

@pc494 pc494 left a comment

Choose a reason for hiding this comment

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

Had a read of the code, but I think sorting out the "descriptive" stuff before attempting a code review is wise.

@din14970
Copy link
Contributor Author

Thanks for the quick thorough response, sorry for always hitting you with these long posts. They help me keep the overview myself.

In my experience this tends to happens when your relrods are too short.

Yes, exactly, which got me looking into how it was working in the background. It then seems that the default max_excitation_error=1e-2 is quite small, 1e-1 gave me better results. I won't mess with defaults though, I will leave that up to the maintainers.

This is a bug, get_diffraction_library() should have kwargs that get passed to calculate_ed_data. Good find.
Philosophical. The DiffractionGenerator is meant to be the Microscope, with sample properties specified as arguments to calculate_ed_data, so we shuffled max_excitation_error around during a recent refactor.

Ok, that clarifies a lot for me, thanks. But if the sample dependent things are to be split of, would the following also not fit best in get_diffraction_library?:

  • debye_waller_factors
  • scattering_params
  • shape_factor_model
  • kwargs for shape_factor_model

Have you seen https://journals-iucr-org.ezp.lib.cam.ac.uk/b/issues/2019/04/00/je5015/ ?

Do you have a DOI link or so to this? I don't seem to have access. Indeed having an approximation for the precession wouldn't be a bad idea, especially for the library generation. But you might as well have the option for a full calculation if you only need a few DP's for some reason.

Had a read of the code, but I think sorting out the "descriptive" stuff before attempting a code review is wise.

No worries, as I said it's still a preliminary mess, but in this way you can check the progress if you want

@pc494
Copy link
Member

pc494 commented Nov 15, 2020

Thanks for the quick thorough response, sorry for always hitting you with these long posts. They help me keep the overview myself.

In my experience this tends to happens when your relrods are too short.

Yes, exactly, which got me looking into how it was working in the background. It then seems that the default max_excitation_error=1e-2 is quite small, 1e-1 gave me better results. I won't mess with defaults though, I will leave that up to the maintainers.

I think for a while we had written in the documentation described as often 1/sample_thickness and 1e-1 corresponded to a very thin sample (10nm maybe, I would have to check)

This is a bug, get_diffraction_library() should have kwargs that get passed to calculate_ed_data. Good find.
Philosophical. The DiffractionGenerator is meant to be the Microscope, with sample properties specified as arguments to calculate_ed_data, so we shuffled max_excitation_error around during a recent refactor.

Ok, that clarifies a lot for me, thanks. But if the sample dependent things are to be split of, would the following also not fit best in get_diffraction_library?:

* `debye_waller_factors`
* `scattering_params`

I'm not sure. They are currently quiet underdeveloped parts of the code and I would have to investigate before coming to a conclusion.

* `shape_factor_model`
* `kwargs` for `shape_factor_model`

I think adding a **kwargs options to get_diffraction_library would mean that you could do those at that level? I would expect:

diff_lib = lib_gen.get_diffraction_library(library_austenite,
                                           calibration=diffraction_calibration,
                                           reciprocal_radius=reciprocal_radius,
                                           half_shape=(half_size, half_size),
                                           with_direct_beam=False,
                                           shape_factor_model="linear")

to work.

Have you seen https://journals-iucr-org.ezp.lib.cam.ac.uk/b/issues/2019/04/00/je5015/ ? Do you have a DOI link or so to this? I don't seem to have access.

https://doi.org/10.1107/S2052520619007534

Sorry, forgot that my browser extension makes a mess of copy+pasting links.

Indeed having an approximation for the precession wouldn't be a bad idea, especially for the library generation. But you might as well have the option for a full calculation if you only need a few DP's for some reason.

Yes, I agree.

@din14970
Copy link
Contributor Author

@pc494 Thanks for the link to the paper, I've implemented both the lorentzian and the closed form approximation for precession using the lorentzian. I modified the function slightly from the paper so that it also takes the parameter max_excitation_error, defined as 1/thickness. I've also shuffled around the parameters to align it with the idea DiffractionGenerator = instrument state/physics model and get_diffraction_library= sample/material parameters. The split I've made (new parameters I've introduced are in bold):

DiffractionGenerator stores values:

  • accelerating_voltage
  • scattering_params
  • minimum_intensity
  • precession_angle
  • approximate_precession
  • shape_factor_model
  • kwargs for shape factor model

get_diffraction_library accepts arguments:

  • structure_library
  • calibration
  • reciprocal_radius
  • half_shape
  • with_direct_beam
  • max_excitation_error
  • debye_waller_factors

I've decided that shape_factor_model is rather something related to the model we choose, rather than something dependent on the material. max_excitation_error and debye_waller_factors do depend on the sample.

To me this is a logical split, and I think it is now more or less ready for my purposes. All the docstring documentation should be ok. Before I start messing around in the tests, I wanted to hear your thoughts on this way of organizing the parameters. If ok, I will update the broken tests and write new tests for the stuff I added.

@pc494
Copy link
Member

pc494 commented Nov 17, 2020

@din14970, at a first glance this look like a good set of decisions to me. I'll try to get a review of the code back to you later today or tomorrow if that's okay with you.

Copy link
Member

@pc494 pc494 left a comment

Choose a reason for hiding this comment

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

This looks like a very substantial improvement. Once it's got some tests I'll have another pass and try my best to double check all the physics.

diffsims/generators/diffraction_generator.py Outdated Show resolved Hide resolved
diffsims/generators/diffraction_generator.py Outdated Show resolved Hide resolved
diffsims/generators/diffraction_generator.py Outdated Show resolved Hide resolved
diffsims/generators/diffraction_generator.py Outdated Show resolved Hide resolved
diffsims/generators/diffraction_generator.py Show resolved Hide resolved
diffsims/utils/shape_factor_models.py Outdated Show resolved Hide resolved
return sinc(excitation_error, max_excitation_error, minima_number)**2


def atanc(excitation_error, max_excitation_error):
Copy link
Member

Choose a reason for hiding this comment

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

this should have a minima_number I think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

to be consistent with the sinc functions yes, but its a bit awkward since it's a smooth function. And just like max_excitation error, the parameter just rescales in the x-direction. I added it, it can be removed later if necessary. In any case, I would expect the Lorentzian to become the default, since there is a reference for it.

diffsims/utils/shape_factor_models.py Outdated Show resolved Hide resolved
diffsims/utils/shape_factor_models.py Show resolved Hide resolved
diffsims/generators/diffraction_generator.py Show resolved Hide resolved
@dnjohnstone
Copy link
Member

Just to say - this looks great, thanks! I'd quite like to review it though at some point, can someone ping me when appropriate.

@din14970
Copy link
Contributor Author

@dnjohnstone @pc494 I would tentatively say it's ready for a more thorough review. The functionality that I wanted is there. The test coverage is again 100%, but correctness of the physics is of course best verified with additional pairs of eyes. @pc494 I didn't implement 1 of your comments above, hope you understand my reasoning.

@pc494
Copy link
Member

pc494 commented Nov 20, 2020

I will aim to review this tomorrow, and then ping dnjohnstone, it might take a week or so to get this all done, I hope that's okay with you @din14970.

@pc494 pc494 self-requested a review November 20, 2020 19:44
@din14970
Copy link
Contributor Author

@pc494 I'm in no rush, it's your package take your time. I'm working on the next step for the indexation.

@dnjohnstone
Copy link
Member

dnjohnstone commented Nov 21, 2020

@pc494 I'm in no rush, it's your package take your time. I'm working on the next step for the indexation.

Just to say @din14970 - I think we're getting to the point where if you're planning to continue contributing around the level you have been recently, then we can have a conversation about making "your package" more like "our package" - ping us an e-mail at pyxem.team@gmail.com and we can set that chat up... It would certainly be good for us to add another person to the core developer team for diffsims before too long if something like that might fit with your personal roadmap.

@dnjohnstone dnjohnstone added the enhancement New feature, request, or improvement label Nov 21, 2020
@dnjohnstone dnjohnstone added this to the v0.4.0 milestone Nov 21, 2020
@pc494
Copy link
Member

pc494 commented Nov 21, 2020

I made an algebraic error, which means I thought the function below was incorrect, it's not, but for clarity I reproduce my working and thinking, the conclusion of which is that while this could be better documented, it is essentially correct.

def _z_sphere_precession(phi, r_spot, wavelength, theta):
    """
    Returns the z-coordinate in reciprocal space of the Ewald sphere at
    distance r from the origin along the x-axis
    Parameters
    ----------
    phi : float
        The angle the beam is currently precessed to in degrees.
        If the beam were projected on the x-y plane, the angle
        is the angle between this projection with the x-axis.
    r_spot : float
        The distance to the point on the x-axis where we calculate z in A^-1
    wavelength : float
        The electron wavelength in A^-1
    theta : float
        The precession angle in degrees (angle between beam and optical axis)
    Returns
    -------
    z : float
        The height of the ewald sphere at the point r in A^-1
    """
    phi = np.deg2rad(phi)
    r = 1 / wavelength
    theta = np.deg2rad(theta)
    return -np.sqrt(
        r ** 2 * (1 - np.sin(theta) ** 2 * np.sin(phi) ** 2)
        - (r_spot - r * np.sin(theta) * np.cos(phi)) ** 2
    ) + r * np.cos(theta)

So that we're clear on our terms.
theta is the precession angle, simple enough, in my sketch (of the x-z plane) it's inplane
phi is the azimuthal angle, which we are integrating over, it forms a circle about the z axis which runs through (0,0)
We're using the wavelength to calculate an Ewald Sphere radius, in what follows I will denote this radius R

So we arrive at perhaps my first misunderstanding, r_spot. I think it's the two dimensional distance from our spot to the origin, excluding height. So for a spot (x,y,z) it is equal in value to sqrt(x ** 2 + y ** 2). Although this is confusing with the current documentation this is an easy fix, it does actually simplify the algebra a bit, so fair enough.

So, I'm now going to try and reproduce the final formula.

Firstly, our origin is the origin of the reciprocal lattice (0,0,0). For our given geometry the center of the Ewald sphere is a function of phi (and theta and R)

(R sin theta cos phi , R sin theta sin phi R cos theta) = {for readability later} (A,B,C)

Meanwhile our spot lies at

( X , Y, Z )

where Z will be our output. Assuming what I wrote about r_spot was correct, and because we will always integrate over everything (I don't think this would work if we weren't integrating a full cycle) we can replace our spot with

(r_spot, 0 , Z)

We want to solve for Z, which we do by stating that the distance between these two things will be R

So we get to

R**2 = (r_spot - A) ** 2 + (Z-C) ** 2 + B ** 2
(Z - C)**2 = (- (r_spot - A) ** 2 - B ** 2 + R ** 2)
Z = C - sqrt((r_spot - A) ** 2 + B ** 2 - R^2)

Compared to:

-np.sqrt(
        r ** 2 * (1 - np.sin(theta) ** 2 * np.sin(phi) ** 2)
        - (r_spot - r * np.sin(theta) * np.cos(phi)) ** 2
    ) + r * np.cos(theta)

So the last term in the python code is C (great), and the 1 in the first set of braces is R**2, the term beyond is B and the r_spot - A term is also good.

Excellent, and expensive set of typing for an algebra mistake on my part, but we can now be fairly sure that this function is correct. Going to go do something else for a bit and then will see what's next for review.

Copy link
Member

@dnjohnstone dnjohnstone left a comment

Choose a reason for hiding this comment

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

looking pretty good here - some relatively superficial points.

diffsims/generators/diffraction_generator.py Outdated Show resolved Hide resolved
diffsims/generators/diffraction_generator.py Outdated Show resolved Hide resolved
diffsims/generators/diffraction_generator.py Show resolved Hide resolved
diffsims/generators/diffraction_generator.py Outdated Show resolved Hide resolved
diffsims/generators/diffraction_generator.py Outdated Show resolved Hide resolved
diffsims/generators/diffraction_generator.py Outdated Show resolved Hide resolved
scattering_params : str
"lobato" or "xtables"
minimum_intensity : float
Copy link
Member

Choose a reason for hiding this comment

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

Would it be better if this were relative to the most intense spot? I'm not actually sure personally, but it's tricky to know how to set this without units... Probably it's fine as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

before it was just hard-coded as 1e-20 inside the function, I figured it might as well be a parameter. I guess some absolute value cut-off is most fair across different diffraction patterns in the library, resembling a kind of detection limit of a detector. On the other hand I guess different phases would require different cut-offs. Is this logic correct?

diffsims/generators/diffraction_generator.py Outdated Show resolved Hide resolved
scattering_params="lobato",
precession_angle=0,
shape_factor_model="linear",
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to change the default here now that we have better options?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My vote is on Lorenzian since we can point to a reference and should approximate a 2-beam rel-rod. Linear seems a bit arbitrary.

Copy link
Member

Choose a reason for hiding this comment

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

Yes. Let's change this to Lorenzian.

diffsims/generators/sphere_mesh_generators.py Show resolved Hide resolved
@dnjohnstone
Copy link
Member

Also, don't forget a changelog bump

@dnjohnstone
Copy link
Member

@din14970 @pc494 - this isn't really the right place, but thought you might both like a look at this paper if you haven't seen it yet: https://scripts.iucr.org/cgi-bin/paper?iv5009

Copy link
Member

@pc494 pc494 left a comment

Choose a reason for hiding this comment

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

The Lorenzian functions look good to me.

diffsims/utils/shape_factor_models.py Outdated Show resolved Hide resolved
diffsims/utils/shape_factor_models.py Outdated Show resolved Hide resolved
@pc494
Copy link
Member

pc494 commented Nov 23, 2020

@pc494 I'm in no rush, it's your package take your time. I'm working on the next step for the indexation.

Which step have you moved on to?

@din14970
Copy link
Contributor Author

din14970 commented Nov 23, 2020

contributing around the level you have been recently

While I don't mind helping out, I don't know whether I can commit to this for certain at this point. I'm now dealing with a specific problem which I would like solved within reasonable time, and pyxem/diffsims provides the perfect scaffold for this. It is mostly my frustrations with the rigidity of ASTAR that is driving me at this point. Still I'd like to hear from you where you think the package is headed. I'll send you an e-mail.

Which step have you moved on to?

The indexation which simultaneously solves for in plane angle. I'm close to a kind of prototype which I'll share maybe in a Pyxem issue. Then we can discuss how to best integrate it there.

@pc494
Copy link
Member

pc494 commented Nov 23, 2020

email has arrived in the correct box and we can continue that conversation there.

Which step have you moved on to?

The indexation which simultaneously solves for in plane angle. I'm close to a kind of prototype which I'll share maybe in a Pyxem issue. Then we can discuss how to best integrate it there.

Yep sounds good to me. Fingers crossed, it should be fairly easy to slot into the framework we currently have.

Copy link
Member

@pc494 pc494 left a comment

Choose a reason for hiding this comment

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

There is one default that we should change, but other than that this is good to go as far as I am concerned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature, request, or improvement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants