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

Improving the Differential Phase Contrast processing structure #1039

Open
magnunor opened this issue Mar 20, 2024 · 7 comments
Open

Improving the Differential Phase Contrast processing structure #1039

magnunor opened this issue Mar 20, 2024 · 7 comments

Comments

@magnunor
Copy link
Collaborator

As discussed in the pyxem 1.0.0 Issue (#624 (comment)), it would be nice to improve the DPC processing structure for that release.

Background

When I write "STEM-DPC", I refer to the technique which is used to measure the in-plane magnetic and electric field through the shift or deflection of the direct electron beam. This is caused by the Lorentz force F = -eE - (ev x B): https://doi.org/10.1016/j.ultramic.2016.03.006 . Some people have slightly different definitions of STEM-DPC, but I think the one where the main contrast contribution is from beam deflection/shift is a good one. Ergo: the main data processing routines involves finding how much the direct electron beam has been deflected for each probe position in a STEM scan.

Conceptual data processing steps

So, with regards to data processing "pipeline" it typically looks something like this:

  • Load 4-D dataset
  • Possibly crop the diffraction space to just get the direct beam
  • Use some algorithm to find the shift of the direct beam: this in itself is a huge topic, but the "standard" method is a simple center of mass. This gives the "shift vector": the x- and y-components of the deflection caused by the Lorentz force.
  • Correct for the "descan", which is due to the diffraction pattern moving on the detector as a function of probe position. This movement is monotonous, and often linear, and must be corrected somehow, as it will effectively conceal the "DPC-contrast".
  • Visualization is typically either the separate x- and y-vector components, or in the form a "colorwheel" plot

Simple example

Currently in pyXem, a simple processing pipeline for this: (see this link for a dataset, https://filesender.sikt.no/?s=download&token=3e32a0fe-7800-4ccd-b3ef-9d75c0b8805d . Note that the dataset link expires 19/04/2024.)

import hyperspy.api as hs
s = hs.load("stem_dpc_data.zspy", lazy=True) # Diffraction 2D class
s_nav = s.mean(axis=(-1, -2)).T
s_nav.compute()
s.plot(navigator=s_nav)
s_com = s.center_of_mass() # DPCSignal2D class
s_com_corr = s_com.correct_ramp()
s_com_corr.plot()
s_com_corr_color = s_com_corr.get_color_signal()
s_com_corr_color.plot()

More advanced example

A bit more advanced processing, where the descan is corrected on the 4-D dataset. This so the shifts of the direct beam can be manually inspected. As seen in the code, this should be improved.

import hyperspy.api as hs
s = hs.load("stem_dpc_data.zspy", lazy=True)
s_nav = s.mean(axis=(-1, -2)).T
s_nav.compute()
s.plot(navigator=s_nav)
s_com = s.center_of_mass()
s_com.data[0] = s.axes_manager.signal_shape[0] * 0.5 - s_com.data[0]
s_com.data[1] = s.axes_manager.signal_shape[1] * 0.5 - s_com.data[1]
s_bs = s_com.to_beamshift()
s_bs.make_linear_plane()
s1 = s.center_direct_beam(shifts=s_bs, inplace=False, lazy_output=False)
s_com_corr = s_com - s_bs.to_dpcsignal()
s_com_corr.plot()
s_com_corr_color = s_com_corr.get_color_signal()
s_com_corr_color.plot()

Future structure?

So the question is: is this an optimal class "structure"? (I discussed this a bit in this pull request comment: #1005 (review))

One idea could be to repurpose the DPCSignal class name, to be the "4-D container". Which inherits Diffraction2D. The current DPCSignal class could be renamed DPCVector? Which means DPCSignal.center_of_mass would return DPCVector classes.

There could also be further sub-class, like MagneticDPCSignal and ElectricDPCSignal, which has functionality specific for those. Like for example a 90 degrees vector "shift" in the magnetic signal, and automatic calculation to magnetic units if the data is calibrated.

@magnunor
Copy link
Collaborator Author

Actually, maybe HyperSpy's ComplexSignal could be used for the "vector" signal? Although amplitude and phase isn't the most optimal for some of the processing, however we should be able to handle that.

@CSSFrancis
Copy link
Member

@magnunor This sounds really good! I do think that we need to be better about documenting what "DPC" means to us. We could probably copy and paste the first part into the documentation.

Future structure?

So the question is: is this an optimal class "structure"? (I discussed this a bit in this pull request comment: #1005 (review))

One idea could be to repurpose the DPCSignal class name, to be the "4-D container". Which inherits Diffraction2D. The current DPCSignal class could be renamed DPCVector? Which means DPCSignal.center_of_mass would return DPCVector classes.

I don't __ love__ the idea of having more signals if there isn't anything particular to a DPC signal other than changing the output for certain methods. I feel like we already have a lot of signals and it's a bit hard to keep track/ manage workflows. We end up with lots of subclassing as a result it makes it difficult to follow where things come from in the documentation. That being said I could be convinced pretty easily.

I do like the idea of renaming DPCSignal to DPCVector as I think it better describes what we are measuring but I think that requires that we have the vector be the signal rather than the navigation. Already I think that BeamShift class could be combined with the DPCVector fairly easily. We could just call that something like ShiftVector? Or just use DPCVector for both.

@CSSFrancis
Copy link
Member

CSSFrancis commented Mar 22, 2024

@magnunor I was thinking something like:

s = hs.load("stem_dpc_data.zspy", lazy=True)
s_nav = s.mean(axis=(-1, -2)).T
s_nav.compute()
s.plot(navigator=s_nav)

shifts = s.get_direct_beam_position() # beam shift object
# Need to compute the shifts before making the linear plane... 
linear_plane = shifts.make_linear_plane() # Add extra options to this function for things like using the corners etc...
dpc_shifts = shifts-linear_plane

dpc_signal = dpc_shifts.T # Automatically converts a shifts (Vectors) to a signal by wrapping the .T hyperspy method.

@magnunor
Copy link
Collaborator Author

magnunor commented Apr 2, 2024

Actually, having thought about this a bit more: the reason I don't (currently) like to use BeamShift as the "DPC signal" is that the default BeamShift.plot() is pretty much useless:

beamshift_current.webm

However, if we simply changed the default plotting for BeamShift to be something like BeamShift.T.plot() it would solve that issue:

beamshift_transposed.webm

We could then move the functions from DPCSignal into the BeamShift class.


I think this would be a nicer implementation, as we keep the "navigation-dimension-is-probe-position". While still having the nice and easy visualization accessible via plot().


Things to change would then be:

  • Change the default plotting of BeamShift.plot()
  • Change BeamShift.make_linear_plane()` to return a new signal, instead of replacing the existing data
  • Move the functionality from DPCSignal to BeamShift. This would require some minor changes in each of those functions, since the data would be (probe x, probe y | shifts), vs (shifts | probe x, probe y).

@CSSFrancis
Copy link
Member

Actually, having thought about this a bit more: the reason I don't (currently) like to use BeamShift as the "DPC signal" is that the default BeamShift.plot() is pretty much useless:
However, if we simply changed the default plotting for BeamShift to be something like BeamShift.T.plot() it would solve that issue:

We could then move the functions from DPCSignal into the BeamShift class.

That sounds perfect.

I think this would be a nicer implementation, as we keep the "navigation-dimension-is-probe-position". While still having the nice and easy visualization accessible via plot().

Things to change would then be:

  • Change the default plotting of BeamShift.plot()
  • Change BeamShift.make_linear_plane()` to return a new signal, instead of replacing the existing data
  • Move the functionality from DPCSignal to BeamShift. This would require some minor changes in each of those functions, since the data would be (probe x, probe y | shifts), vs (shifts | probe x, probe y).

I think this sounds great. We would need to compute the signal before we plot but I think that is understandable. Otherwise the way to visualize the shifts would be to overlay the shifts over the signal and plot as a set of markers.

@magnunor
Copy link
Collaborator Author

magnunor commented Apr 7, 2024

I'll make a pull request with the changes outlined here.

@magnunor
Copy link
Collaborator Author

In the #1057 pull request, I did quite a few API-breaking changes. I'm guessing those changes should not be in the next release (0.18.0)?

Should there be a separate pull request with tagging the functions which will be removed or changed?

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

No branches or pull requests

2 participants