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

Large restructure of differential phase contrast class structure #1057

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

magnunor
Copy link
Collaborator

@magnunor magnunor commented Apr 7, 2024

This pull request implements the changes discussed in #1039

This to make the pyXem class structure a bit clearer, by changing the default plotting behavior in the BeamShift class:

  • All the DPCSignal functionality has been moved to the BeamShift class
  • Some unit tests with random numbers which were prone to failing randomly has been removed
  • Some rarely used functions which are easy to implement via s.map in one or two lines of code has been removed

This pull request drastically break the API, so this should be implemented in an "API-breaking" release. Also, due to changes in other pull requests, there will probably be some merge conflicts.

Checklist

  • Move all the functions
  • Remove diffraction2d.center_of_mass
  • Update docstrings
  • Update dummy_data
  • Many style fixes
  • Clean up imports
  • Update documentation
  • Update the Changelog
  • Ready for review

Some points for discussion:

  • BeamShift.plot() : currently, the plot function simply does self.T.plot(). Is this a good way of doing this?
  • Many of the BeamShift functions will only work with signals with 2 navigation dimensions. How to best deal with this?
  • There is both a get_phase_signal and phase_retrivial, which returns very different phases: get_phase_signal is the "direction" of the beam shift "vector", while phase_retrival retrives the phase.
  • make_bivariate_histogram (which is not a class method) should be moved to some utils package?
  • pixelated_stem_tools should be cleaned up, with some of the DPC specific functionality being moved to a (new?) utils focusing on beam-shift and DPC.
  • I removed gaussian_blur, as (in my experience) it is not used very often, and can easily be replaced by two lines of code.

- All the DPCSignal functionality has been moved to the BeamShift class
- BeamShift.plot() default plotting has been changed
- Some unit tests with random numbers which were prone to failing
randomly has been removed
- Some rarely used functions has been removed
@coveralls
Copy link

coveralls commented Apr 7, 2024

Coverage Status

coverage: 92.648% (-0.4%) from 93.0%
when pulling b9f881f on magnunor:restructure_dpc_functionality
into 25b6f68 on pyxem:main.

@CSSFrancis
Copy link
Member

@magnunor I would rebase this based on PR that just got merged before you go too much further as that PR moves some things around.

Let me know if you need any help here. I would like to make this a subclass of Vector1D but I might have to think about how to make that class a little more robust.

@magnunor
Copy link
Collaborator Author

@magnunor I would rebase this based on PR that just got merged before you go too much further as that PR moves some things around.

Done! It was actually a lot less work than I feared :)

I would like to make this a subclass of Vector1D but I might have to think about how to make that class a little more robust.

I haven't followed the vector signals development too closely, but I think it makes a lot of sense to utilize a signal optimized for vectors for BeamShift and DPC data.

- Make new _beam_shift_tools module
- Remove Diffraction2D.center_of_mass:
-- Move to get_direct_beam_position(method="center_of_mass")
- Update many unit tests due to these changes
- Update many docstrings due to these changes
@magnunor
Copy link
Collaborator Author

magnunor commented Apr 14, 2024

Some more progress, major remaining thing is the documentation.

The number of added and deleted lines looks daunting, but most of those are due to files having been deleted with the code being moved to a different module. For example the DPCSignal functionality being moved to BeamShift.


  • I moved BeamShift.get_color_image_with_indicator to utils.plotting, as it returns a matplotlib object, and is mostly used for making image files rather than interactive visualization. Especially with the possibility to include the color wheel in get_color_signal via Better DPC Visualization #1031
  • I think Diffraction2D.center_direct_beam does pretty much the same thing as Diffraction2D.shift_diffraction, so Diffraction2D.shift_diffraction can probably be removed
  • I removed Diffraction2D.center_of_mass, as the same functionality is in get_direct_beam_position. However, there needs to be some additional work on the masking functionality.
  • In get_direct_beam_position(method="center_of_mass") there is both mask and signal_slice which serves a similar purpose. However, for the magnetic DPC processing, it is advantageous that the mask is circular. Not sure what the best solution for this is, but the current way is not optimal.
  • I made a new utils._beam_shift_utils module
  • Much more...

I'll make a proper list of the all the changes when they're all implemented.

@magnunor
Copy link
Collaborator Author

magnunor commented Apr 21, 2024

Two points which would be nice to discuss:


In this pull request, the BeamShift.plot function does this:

def plot(self):
        self.T.plot()

I'm not sure if this is the best ways of doing this? I'm thinking at least the args and kwargs should be passed. But I'm also wondering if this sub-optimal since the self.T creates a new signal which is discarded after the plotting is done.


In get_direct_beam_position(method="center_of_mass") there is both mask and signal_slice which serves a similar purpose. However, for the magnetic DPC processing, it is advantageous that the mask is circular, while signal_slice is a square. Not sure what the best solution for this is, but the current way is not optimal.

@CSSFrancis @pc494

@CSSFrancis
Copy link
Member

Two points which would be nice to discuss:

In this pull request, the BeamShift.plot function does this:

def plot(self):
        self.T.plot()

I'm not sure if this is the best ways of doing this? I'm thinking at least the args and kwargs should be passed. But I'm also wondering if this sub-optimal since the self.T creates a new signal which is discarded after the plotting is done.

I think we want to do a couple of things:

  1. If the signal is lazy then don't plot it. You pretty much have to compute the resulting shifts if you want to visualize them
  2. Make it easier to plot over a signal if you want to just quickly check to make sure it did decently well.
  3. Plot the shifts. I like to use the hs.plot.plot_images for this. Would a plot_shifts function be useful which just uses hs.plot.plot_images to plot the transpose.

In get_direct_beam_position(method="center_of_mass") there is both mask and signal_slice which serves a similar purpose. However, for the magnetic DPC processing, it is advantageous that the mask is circular, while signal_slice is a square. Not sure what the best solution for this is, but the current way is not optimal.

There is also the half_square_width to make things worse... I added the signal_slice because sometimes my beam center was way off and half_square_width didn't cut it.

The mask implementation should be better as well... But I'm not sure what the best way to handle that is....

@CSSFrancis @pc494

@magnunor
Copy link
Collaborator Author

magnunor commented May 3, 2024

I switched BeamShift.plot over to using hs.plot.plot_images, which was a really good idea @CSSFrancis :)

Copy link
Member

@CSSFrancis CSSFrancis left a comment

Choose a reason for hiding this comment

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

@magnunor This is starting to look quite good. Just some small changes that I might make if you are okay with that?

@pc494 What are your feelings on breaking the API here? There are quite a few changes to the API here. I know we have been trying to avoid that but we might be reaching the end game where we need to start :).

s_bs = self._deepcopy_with_new_data(plane_image)
return s_bs

def plot(self):
Copy link
Member

Choose a reason for hiding this comment

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

Add **kwargs here for passing to plot. Also add documentation saying this is equivalent to .T.plot



class BeamShift(Signal1D):
"""Signal class for working with shift of the direct beam."""

_signal_type = "beam_shift"

def make_linear_plane(self, mask=None):
def get_linear_plane(self, mask=None, fit_corners=None):
Copy link
Member

Choose a reason for hiding this comment

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

It might be worth testing this with a 5D dataset to make sure that it works with N-Dimensions.

Copy link
Member

Choose a reason for hiding this comment

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

Currently this doesn't work on 5 dimensions but we can wait on that for a later PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If we can assume that the probe positions are always the two first navigation dimensions, we could easily iterate over the remaining navigation dimensions.

We also need to handle BeamShift signals with 1 navigation dimension.

from hyperspy._signals.lazy import LazySignal
from hyperspy._signals.signal1d import Signal1D
from hyperspy.signals import Signal1D
from hyperspy.utils.plot import plot_images


class BeamShift(Signal1D):
Copy link
Member

Choose a reason for hiding this comment

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

@magnunor is it okay if I make a couple of adjustments to this? I think we should make BeamShift a subclass of Vector1D. I'm planning on moving some of the plotting capablilites to that class and then that can be a more general feature. I think the other place where that might be useful is with the StrainMap class which is essentially plotting 4 Vector quantities.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Go for it!

@CSSFrancis
Copy link
Member

@magnunor Do you know why those tests are failing? This seems like a regression but maybe it wasn't tested before.

@CSSFrancis
Copy link
Member

Once #1080 is merged do you want to rebase? If we figure out the testing I can take another pass through and then we can think about what it would take to merge?

@CSSFrancis CSSFrancis mentioned this pull request May 29, 2024
3 tasks
@magnunor
Copy link
Collaborator Author

Once #1080 is merged do you want to rebase? If we figure out the testing I can take another pass through and then we can think about what it would take to merge?

Sounds good! I'll do that either late tomorrow, or (most likely) Friday.

@CSSFrancis
Copy link
Member

Once #1080 is merged do you want to rebase? If we figure out the testing I can take another pass through and then we can think about what it would take to merge?

Sounds good! I'll do that either late tomorrow, or (most likely) Friday.

Perfect!

@magnunor
Copy link
Collaborator Author

I merged the current main branch into this branch.


Running BeamShift.plot() on a dataset with <undefined> units gives:

WARNING | Hyperspy | Axes labels were requested, but one or both of the axes units and/or name are undefined. Axes decorations have been set to 'ticks' instead. (hyperspy.drawing.utils:1167)

This will be a very annoying warning, so we should do something to avoid that.


Do you know why those tests are failing? This seems like a regression but maybe it wasn't tested before.

With regards to the failing tests: this is related to interaction between method="center_of_mass" and sig_slice in get_direct_beam_position. So I'm tempted to leave it for the rework of get_direct_beam_positions which I think we should do before the release: #1079 (comment)

@CSSFrancis
Copy link
Member

I merged the current main branch into this branch.

Yay

Running BeamShift.plot() on a dataset with <undefined> units gives:

WARNING | Hyperspy | Axes labels were requested, but one or both of the axes units and/or name are undefined. Axes decorations have been set to 'ticks' instead. (hyperspy.drawing.utils:1167)

This will be a very annoying warning, so we should do something to avoid that.

In this case we know the units are pixels, right? We can just set that? I think we should pass the axes_manager of the original signal as well to the beam shift and maybe allow pixel --> real units?

Do you know why those tests are failing? This seems like a regression but maybe it wasn't tested before.

With regards to the failing tests: this is related to interaction between method="center_of_mass" and sig_slice in get_direct_beam_position. So I'm tempted to leave it for the rework of get_direct_beam_positions which I think we should do before the release: #1079 (comment)

Hmmm I don't love merging non-passing code into main, but it might be nice to seperate that into a smaller PR.

@CSSFrancis
Copy link
Member

@magnunor do you have time to work on this or do you want me to give it a shot. On second thought I think the warnining is totally reasonable... People should have axes labels on their data including units. The more we can pester people into doing that (I think) the better, if it's really that annoying it's only a warning, you can silence it.

I can look into fixing that failing test/adjusting it and then we can think about what merging might look like. It might also be nice to make a small Example using a dummy test dataset to show how to do some of the more simple operations.

@magnunor
Copy link
Collaborator Author

magnunor commented Jun 3, 2024

@magnunor do you have time to work on this or do you want me to give it a shot.

Go for it! My schedule got a bit too full the next days.

On second thought I think the warnining is totally reasonable... People should have axes labels on their data including units. The more we can pester people into doing that (I think) the better, if it's really that annoying it's only a warning, you can silence it.

I think these warnings just get very annoying. I think it by default should set it to pixels or arbitrary units. To avoid the warning.

It might also be nice to make a small Example using a dummy test dataset to show how to do some of the more simple operations.

Agreed! We should be able to make nice dataset to show the DPC features.

@CSSFrancis
Copy link
Member

@magnunor I fixed the bug causing the tests to fail and adjusted some of the code. I think this is starting to look pretty good, the only thing I would like to look into is what other visualizations might be of interest.

We could think about things like:

  1. Vector map overlays
  2. Colormap overlays with a color key (for the navigator)
  3. Stream plots (This one is a little more iffy)
  4. Showing the direct beam shift as a circle for both shifted and the unshifted vector.

@magnunor
Copy link
Collaborator Author

magnunor commented Jun 4, 2024

@magnunor I fixed the bug causing the tests to fail and adjusted some of the code.

Very nice!

We could think about things like:

1. Vector map overlays

2. Colormap overlays with a color key (for the navigator)

3. Stream plots (This one is a little more iffy)

4. Showing the direct beam shift as a circle for both shifted and the  unshifted vector.

I agree that these would be very good. However, since these are additions, rather than API-breaking, we should wait with these for the next release.

@CSSFrancis
Copy link
Member

@magnunor sounds good. I'm going to review the changes again. If you have time can you go over my additions?

Then we can merge.

@magnunor
Copy link
Collaborator Author

magnunor commented Jun 4, 2024

I'll try to get this done today, or most likely tomorrow.

Copy link
Member

@CSSFrancis CSSFrancis left a comment

Choose a reason for hiding this comment

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

Just some small comments of things we could do eventually.



class BeamShift(Signal1D):
"""Signal class for working with shift of the direct beam."""

_signal_type = "beam_shift"

def make_linear_plane(self, mask=None):
def get_linear_plane(self, mask=None, fit_corners=None):
Copy link
Member

Choose a reason for hiding this comment

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

Currently this doesn't work on 5 dimensions but we can wait on that for a later PR.

Comment on lines +97 to +99
if fit_corners is not None:
mask = bst._get_corner_mask(self.isig[0], corner_size=fit_corners)

Copy link
Member

Choose a reason for hiding this comment

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

This could be more efficient. Right now this requires that you calculate the entire shifts when really we only need to calculate the corners.

"""
# Rotate the phase by -30 degrees in the color "wheel", to get better
# visualization in the vertical and horizontal direction.
Copy link
Member

Choose a reason for hiding this comment

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

I adjusted this but before the rotation was always rotation=30

@sivborg
Copy link
Contributor

sivborg commented Jun 4, 2024

Hello, I work quite a bit with this functionality, so I can take a gander at some parts tomorrow and the day after, at least!

@@ -23,7 +23,7 @@
# In many instances the zero beam position will vary systematically with the scan position.
# This can be corrected by fitting a linear plane to the zero beam position using the
# :meth:`make_linear_plane` method.
shifts.make_linear_plane() # Making a linear plane to remove the systematic shift
shifts.get_linear_plane() # Making a linear plane to remove the systematic shift
Copy link
Contributor

Choose a reason for hiding this comment

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

The functionality is changed here so that the plane is returned. So should AFAIK be shifts = shifts.get_linear_plane()

@sivborg
Copy link
Contributor

sivborg commented Jun 5, 2024

I looked into using old DPC signals with the new BeamShift. However, the conversion between the old DPC signals and BeamShift requires a transpose, which I am wondering if it's necessary? This is the code I'm running:

import pyxem as pxm
import hyperspy.api as hs
s = hs.load("old_dpc.hspy")
print(s)
print(pxm.signals.BeamShift(s))
print(pxm.signals.BeamShift(s.T))

which has the output:

WARNING | Hyperspy | `signal_type='dpc'` not understood. See `hs.print_known_signal_types()` for a list of installed signal types or https://github.com/hyperspy/hyperspy-extensions-list for the list of all hyperspy extensions providing signals. (hyperspy.io:743)
WARNING:hyperspy.io:`signal_type='dpc'` not understood. See `hs.print_known_signal_types()` for a list of installed signal types or https://github.com/hyperspy/hyperspy-extensions-list for the list of all hyperspy extensions providing signals.
<Signal2D, title: , dimensions: (2|256, 256)>
<BeamShift, title: , dimensions: (256, 2|256)>
WARNING | Hyperspy | `signal_type='dpc'` not understood. See `hs.print_known_signal_types()` for a list of installed signal types or https://github.com/hyperspy/hyperspy-extensions-list for the list of all hyperspy extensions providing signals. (hyperspy.io:743)
WARNING:hyperspy.io:`signal_type='dpc'` not understood. See `hs.print_known_signal_types()` for a list of installed signal types or https://github.com/hyperspy/hyperspy-extensions-list for the list of all hyperspy extensions providing signals.
<BeamShift, title: , dimensions: (256, 256|2)>

In the past you would use the to_beamshift method which would do this for you. But is it good to maybe consider doing it automatically when sending in a (2|nx, ny) signal instead of giving a (256, 2| 256) signal? That way old data is more compatible.

s_rgb.change_dtype("rgb16")
return s_rgb

def get_color_signal(
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be a good idea to rename this function? As it can easily be confused with get_phase_signal since it's also very colourful. Maybe something like get_magnitude_phase_signal. Easier to search for too.

@magnunor
Copy link
Collaborator Author

magnunor commented Jun 5, 2024

In the past you would use the to_beamshift method which would do this for you. But is it good to maybe consider doing it automatically when sending in a (2|nx, ny) signal instead of giving a (256, 2| 256) signal? That way old data is more compatible.

This is a good point: how to best handle the old files. I'm not sure how to best do this, as we would like to drop the DPCSignal in HyperSpy extensions? Alternatively, we could keep a small placeholder class which has one function to convert the data into a BeamShift signal?

This could be more efficient. Right now this requires that you calculate the entire shifts when really we only need to calculate the corners.

This is a very good point! This should also be useful if different masks are used, essentially that we only calculate this for the chunks which we really need.

Currently this doesn't work on 5 dimensions but we can wait on that for a later PR.

Agreed, we should add a NotImplementedError for signals with the unsupported dimensions.

@CSSFrancis
Copy link
Member

In the past you would use the to_beamshift method which would do this for you. But is it good to maybe consider doing it automatically when sending in a (2|nx, ny) signal instead of giving a (256, 2| 256) signal? That way old data is more compatible.

I think this is totally reasonable. We should keep the DPC class and signal type but just transpose the data and set the signal type to beam_shift for consistency. For things like loading data I think I'm okay for keeping deprecation warnings along for longer.

@CSSFrancis
Copy link
Member

@magnunor If you don't have time to get to this today I might try to make things a bit more backwards compatible and then merge.

@magnunor
Copy link
Collaborator Author

magnunor commented Jun 5, 2024

Go for it! I can review it.

@ericpre
Copy link
Contributor

ericpre commented Jun 5, 2024

To read files containing "dpc" signal type, the conversion can also be done in rosettasciio - in a similar way as markers are converted from hyperspy 1.x to 2.x for example. This has the advantage of not having to keep dpc signal for ever in pyxem.

@magnunor
Copy link
Collaborator Author

magnunor commented Jun 5, 2024

To read files containing "dpc" signal type, the conversion can also be done in rosettasciio - in a similar way as markers are converted from hyperspy 1.x to 2.x for example. This has the advantage of not having to keep dpc signal for ever in pyxem.

That sounds tempting. How would this work? Looking for the dpc signal type? What if someone else wants to use the dpc namespace in the future?

@ericpre
Copy link
Contributor

ericpre commented Jun 5, 2024

There should be enough metadata (see for example https://github.com/hyperspy/rosettasciio/blob/31bd677cc4c02c5787ae9b61250bc1431f352cba/rsciio/_hierarchical.py#L309 and https://github.com/hyperspy/rosettasciio/blob/31bd677cc4c02c5787ae9b61250bc1431f352cba/rsciio/utils/tools.py#L466) that is saved in the file to figure out when it needs to be converted and not interfere with other "dpc" signal type.

Note that keeping an empty class in pyxem will prevent other use of this signal type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants