This tutorial shows the various functionalities in eXSpy which is used to analyse Electron Energy Loss Spectroscopy data, using EELS datasets from a perovskite oxide heterostructure.
It assumes some knowledge on how to use HyperSpy, like loading datasets and how the basic signals work.
This notebook requires HyperSpy 2.0.0 or later. In addition eXSpy, which has the EELS and EDX specific parts of HyperSpy: https://hyperspy.org/exspy/
7/6/2016 Magnus Nord - Developed for HyperSpy workshop at Scandem conference 2016
This notebook was used for the HyperSpy workshop at the Norwegian University of Science and Technology for the Scandem 2016 conference, 7 June 2016.
The data was acquired on a Jeol ARM200cF using a Gatan Quantum ER with DualEELS capabilities.
The data itself is from La$_{0.7}$Sr$_{0.3}$MnO$_3$ thin films deposited on SrTiO$_3$. In the Fine Structure example parts of the film has been exposed to a very long electron beam exposure, inducing oxygen vacancies.
The datasets has been binned to reduce the file size and processing time.
Firstly we use some IPython magic to import the right plotting libraries. If you want the figures to be embedded within the Notebook itself, use %matplotlib widget
. If you want the figures to be in their separate windows, use %matplotlib qt5
%matplotlib widget
Then import HyperSpy.
import hyperspy.api as hs
First we take a look at an EELS line scan across an La$_{0.7}$Sr$_{0.3}$MnO$_3$/SrTiO$_3$ thin film, in the file datasets/LSMO_STO_linescan.hspy
.
s = hs.load("datasets/LSMO_STO_linescan.hspy")
The core loss data has several peaks: Ti-L$_{2,3}$, O-K, Mn-L$_{2,3}$ and La-M$_{4,5}$. Visualize this by using the plot()
method. We can navigate the line scan by using the navigation window, and moving the red line. Do this either by click-and-drag, or by using Ctrl + left/right arrow keys on your keyboard.
To compare spectra from different probe positions, press the E key to get a second navigator.
s.plot()
Now we can quantifiy the first edge (Ti-L$_{2,3}$, 460 eV, 0-10 nm). Firstly by removing the background with the remove_background
method, then integrating the Ti-L$_{2,3}$ edge. Move the red line in the navigation figure towards the top part (0-10 nm, x axis). Then drag a span from about 400 to 440 eV in Figure 2.
Next, untick the "Fast" button in the dialog box under figure 2, and press Apply.
Note: sometimes the background removal doesn't work properly. If this happens, reload the data using the command above (s = hs.load("datasets/LSMO_STO_linescan.hspy"
). Then rerun the s.remove_background()
command.
s.plot()
s.remove_background()
To integrate the Ti-L$_{2,3}$ edge interactively we can use a hs.roi.SpanROI
region of interest.
Drag the green span in the plot to just include the Ti-L$_{2,3}$ edge
roi = hs.roi.SpanROI(left=450, right=600)
s.plot()
roi.add_widget(s, axes=["Energy loss"])
Finally, we integrate the signal within the SpanROI
, then plot it
s_ti = s.isig[roi].integrate1D(axis="Energy loss").T
s_ti.plot()
We can also perform the same operations in one single line if interactivity is not required, by chaining the methods.
datasets/LSMO_STO_linescan.hspy
remove_background
), crop the signal to only include the Ti-L peak (isig
) and sum the energy loss axis (integrate1D
)s = hs.load("datasets/LSMO_STO_linescan.hspy")
s_ti = s.remove_background(signal_range=(405., 448.)).isig[448.:480.].integrate1D(axis="Energy loss").T
s_ti.plot()
Now, lets do some more advanced quantification using HyperSpy's extensive modelling framework. Firstly we load the low loss and core loss spectra: datasets/LSMO_STO_linescan_low_loss.hspy
and datasets/LSMO_STO_linescan.hspy
.
s_ll = hs.load("datasets/LSMO_STO_linescan_low_loss.hspy")
s = hs.load("datasets/LSMO_STO_linescan.hspy")
In these EELS signals, the metadata has been populated with some of the experimental parameters, which we can access in the metadata
attribute
s.metadata
Firstly we want to fix the zero point for the energy axis using the zero loss peak.
Plot it, and use the zoom functionality (the box button) in the Signal plot to zoom in on the zero loss peak.
s_ll.plot()
To fix this we use align_zero_loss_peak
. The subpixel argument interpolates the data, so we get sub-pixel alignment. Using the also_align
argument, we can also apply the alignment on a another signal. For example when using dualEELS, where both the low loss and core loss is acquired quasi-simultaneously. Note the other signals must have the same navigation shape as the low loss signals.
s_ll.align_zero_loss_peak(subpixel=True, also_align=[s])
By doing this, we aligned both our low loss and core loss spectra. Plot the low loss signal to see if the alignment worked, by zooming in on the zero loss peak.
s_ll.plot()
We have to add the elements which is present in the sample to s
using the add_elements
method: Mn
, O
and Ti
s.add_elements(('Mn','O','Ti', 'La'))
We can see which elements have been added to a signal with the metadata
attribute
s.metadata
Then we make a model out of the core loss spectrum using create_model
. The low loss spectrum is convolved with the model, which means plural scattering is automatically taken into account. In addition this leads to better fits.
Note: this will download a set of open source Generalised Oscillator Strengths (GOS) , which was computed and packaged by Leonhard Segger, Giulio Guzzinati and Helmut Kohl. For more information about the GOS files, see the Zenodo deposit. For more information about how these were computed, and the code used to compute them, see https://github.com/Br0Fi/goscalc.
m = s.create_model(low_loss=s_ll)
The model consist of many different EELSCLEdge components, including a component for the plasmon background. These are automatically created for the EELSSpectrum
signal class.
To see all the components in a model, use the components
attribute
m.components
We can fit the model to the experimental data by using the multifit
function, with the kind=smart
fitting. Which is fits in a way optimized for EELS data, by fitting from the lowest to the highest energy losses.
m.multifit(kind='smart')
Visualize the result of this fitting with the plot
method
m.plot()
We can check the error of the fitting
edges = ("Ti_L3", "Mn_L3", "O_K", 'La_M5')
hs.plot.plot_spectra([m[edge].intensity.as_signal("std") for edge in edges], legend=edges)
This fitted mostly ok, but it is still not very good. Firstly we can move the EELSCLedge component onsets interactively, by firstly plotting the model, and then running m.enable_adjust_position()
m.plot()
m.enable_adjust_position()
Or manually, by directly changing the parameters within the EELSCLedge component. The parameter is called onset_energy
, and the value itself is set with onset_energy.value
m.components.O_K.onset_energy.value = 528
However, to change it for all the probe positions we have to use assign_current_value_to_all()
m.components.O_K.onset_energy.assign_current_value_to_all()
We repeat this for the Manganese edges. Since this is an L-edge, there are 3 different ones. However, we only have to set the Mn-L$_3$: the L$_2$ and L$_1$ is a set to an energy relative to the L$_3$.
m.components.Mn_L3.onset_energy.value
m.components.Mn_L3.onset_energy.value = 638.5
m.components.Mn_L3.onset_energy.assign_current_value_to_all()
The bad fitting to the data is also due to the fine structure not currently taken into account by the model. To get a good fit, we can either not fit to the fine structure regions, or model them somehow.
The easiest way is defining certain regions as fine structure with the enable_fine_structure
method
m.enable_fine_structure()
This will produce a much better fit, but will be much slower (~2 minutes). Rerun the fitting using multifit
m.multifit(kind='smart')
Visualize the result using plot
.
Now the fit is much better, due to the model taking into account the fine structure.
m.plot()
Now we can can have a look at the relative intensity from the individual EELS-edges using plot_spectra
edges = ("Ti_L3", "Mn_L3", "O_K", "La_M5")
hs.plot.plot_spectra([m[edge].intensity.as_signal() for edge in edges], legend=edges)
To avoid the negative values we use bounded fitting, where we can constrain the parameter values between certain values. The bmin
and bmax
properties in the parameters are used for this.
Here, lets set bmin
to 0.0 for all the intensity parameters
m.components.Mn_L3.intensity.bmin = 0.0
m.components.Ti_L3.intensity.bmin = 0.0
m.components.O_K.intensity.bmin = 0.0
Use multifit
with bounded=True
and fitter="lm"
to do this model fitting
m.multifit(optimizer="lm", kind='smart', bounded=True)
Visualize the results
m.plot()
hs.plot.plot_spectra([m[edge].intensity.as_signal() for edge in edges], legend=edges)
Here we take a look at a linescan from a La$_{0.7}$Sr$_{0.3}$MnO$_3$ thin film, where parts of the film has been bombarded with the electron beam for an extended time.
Load the datasets/LSMO_linescan.hspy
file
s = hs.load("datasets/LSMO_linescan.hspy")
Plot the signal, and use the red line in the navigation plot to explore the data. There is clearly something going on in the middle on both the oxygen and the manganese edges. In addition, there are some thickness changes during the line scan.
s.plot()
Using the low loss signal, we make sure the energy scale is properly calibrated: datasets/LSMO_linescan_low_loss.hspy
s_ll = hs.load("datasets/LSMO_linescan_low_loss.hspy")
Plot the low loss signal
s_ll.plot()
The zero loss peak is not well aligned at 0 eV energy loss, so we should align both it and the core loss signal
s_ll.align_zero_loss_peak(also_align=[s])
Plot the low loss signal to check the alignment.
Now the zero loss peak has been shifted to 0 energy loss, and likewise the core loss spectrum s
has also been aligned
s_ll.plot()
We can also calculate the relative thickness using the low loss, with the estimate_thickness
method. We'll have to specify the end of the zero loss beam, which for cold field emissions guns 3.0 eV seems to work well.
s_thickness = s_ll.estimate_thickness(threshold=3.0)
It would also be possible to use hyperspy to determine the threshold itself using:
th = wedge.estimate_elastic_scattering_threshold()
s_ll.estimate_thickness(threshold=th)
Plotting this gives the relative thickness and, as expected, there is an increase towards the end of the line scan
s_thickness.T.plot()
Lets take a closer look at the oxygen-K edge, firstly by removing the plasmon background with remove_background
. Note: this will overwrite the s
spectrum with the cropped one.
s.plot()
s.remove_background()
This makes it much easier to compare the different positions. Pressing 'e' with the spectrum window highlighted gives a second spectrum picker, which can be moved independently of the first one
We can then do Fourier ratio deconvolution to remove the effects of plural scattering: fourier_ratio_deconvolution
s_deconvolved = s.fourier_ratio_deconvolution(s_ll)
Plotting this, we see that the thickness effects has been greatly reduced towards the end of the line scan
s_deconvolved.plot()
Having had a qualitative look at the data, we can try to quantify some of these changes. We do this by making making a model of the oxygen-K edge signal. Firstly we crop the signal, leaving only the Oxygen-K edge (490 to 590 eV).
Use the crop_signal
method in the core loss signal.
s.plot()
s.crop_signal()
Make a model from the cropped core loss signal.
As we've already removed the background, we set auto_background=False
. In addition, since we haven't added any elements to the signal, we get no ionization edges.
m = s.create_model(low_loss=s_ll, auto_background=False)
So currently, the model does not contain any components
m.components
We can try to model some of the fine structure with 1-D Gaussians components: hs.model.components1D.Gaussian
g1 = hs.model.components1D.Gaussian()
We add this component to the model using the append
method. (Similar to how you add an element to a Python list)
m.append(g1)
This added the gaussian component to the model
m.components
Then we can fit this Gaussian to the largest of the O-K peaks with the fit_component
method by dragging a span over the peak between 528 and 533 eV. Run it first with the "Only Current" option ticked, then run it without to fit the whole dataset
m.plot()
m.fit_component(g1)
Having fitted the Gaussian to the experimental data, we can plot how the Gaussian three parameters change over the line scan with the plot
method in the Gaussian component: A
, sigma
and centre
. The A
changes quite a bit, which is probably (among others) related to thickness changes. However, there are clear changes in the sigma
parameter in the region with the electron beam damage
g1.plot()
The next step is to fit the second largest peak (535 and 541 eV) with a Gaussian.
Make another Gaussian component, and add it to the model.
g2 = hs.model.components1D.Gaussian()
m.append(g2)
Use fit_component
with the signal_range=(535., 541.)
argument so we don't have to select the region using the GUI.
Also use only_current=False
to fit all the positions in the line scan.
m.fit_component(g2, signal_range=(535., 541.), only_current=False)
Plot the model fitting results with plot()
m.plot()
However, this time the final fit does not look very good. This is due to the two components being fitted independently of each other. We should fit both of them at the same time. Firstly, we have to set the signal_range
which is where the model will fit to the experimental data. Here we select the region spanning the two major peaks (528-541 eV)
m.plot()
m.set_signal_range(528., 541.)
Refit the model using multifit
m.multifit()
After fitting, we reset the signal range (reset_signal_range
) so we can see the full range of the signal
m.reset_signal_range()
Plot the model to check the fitting results
m.plot()
Lastly, we can fit the small "pre-peak" as well. First we "lock" the two Gaussian we have already fitted using the set_parameters_not_free
methond
g1.set_parameters_not_free()
g2.set_parameters_not_free()
Then we add another Gaussian, and fit it using fit_component
between 522 and 527 eV
g3 = hs.model.components1D.Gaussian()
m.append(g3)
m.fit_component(g3, signal_range=(522., 527.), only_current=False)
Then we set the signal range to cover all the three peaks, from 520 to 541 eV
m.set_signal_range(520., 541.)
And set the g1 and g2 components free using set_parameters_free
g1.set_parameters_free()
g2.set_parameters_free()
Then we refit the model using multifit
.
This fits all the three components to the experimental data, which hopefully gives a good fit.
m.multifit()
Reset the signal range, and plot the model to see the fitting results
m.reset_signal_range()
m.plot()
We can then compare the different parameters in the components, by getting the component values as signals using the as_signal
method in the components.
Firstly get the ratio of the A
parameters for the largest peak and the pre-peak, by using as_signal
and dividing the signals. Then plot the results.
g1_g3_ratio = g1.A.as_signal()/g3.A.as_signal()
g1_g3_ratio.plot()
Then get the difference in centre positions between the largest peak and the pre-peak, via the centre
parameter in the Gaussians.
g1_g3_position = g1.centre.as_signal()-g3.centre.as_signal()
g1_g3_position.plot()
Lastly, get the ratio of the sigma
parameters in the largest peak and the pre-peak.
g1_g3_sigma = g1.sigma.as_signal()/g3.sigma.as_signal()
g1_g3_sigma.plot()
In all of the comparisons there are some large changes in the region with beam damage. However, the values can vary a great deal. This is most likely due to the pre-peak almost disappearing at some points in the line scan, leading to bad fitting of g1 and g3.