Hyperspy Tutorial

EELS analysis of perovskite oxides

This tutorial shows the various functionalities in HyperSpy 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 1.1 or later.

Author

7/6/2016 Magnus Nord - Developed for HyperSpy workshop at Scandem conference 2016

Changes

  • 3/8/2016 Updated for HyperSpy 1.1. Added note about Gatan Digital Micrograph GOS.

Table of contents

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,

In [ ]:
%matplotlib nbagg

Then import HyperSpy. This might give two warnings "WARNING:hyperspy_gui_traitsui", this can safely be ignored here.

In [ ]:
import hyperspy.api as hs

First we take a look at an EELS line scan across an La0.7Sr0.3MnO3/SrTiO3 thin film. The core loss data has several peaks: Ti-L23, O-K, Mn-L23 and La-M54. We can navigate the line scan using the navigation window (Figure 1), and by moving the red line.

In [ ]:
s = hs.load("datasets/LSMO_STO_linescan.hdf5")
In [ ]:
s.plot()

Now we can quantifiy the first edge (Ti-L23, 460 eV, 0-10 nm). Firstly by removing the background, then integrating the Ti-L23 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.hdf5"). Then rerun the s.remove_background() command.

In [ ]:
s.remove_background()

To integrate the Ti-L32 edge interactively we can use a region of interest:

In [ ]:
roi = hs.roi.SpanROI(left=450, right=600)
s.plot()
roi.add_widget(s, axes=["Energy loss"])

Finally, to integrate the signal in the selected ROI:

In [ ]:
s_ti = s.isig[roi].integrate1D(axis="Energy loss")
In [ ]:
s_ti.plot()

Notice that we can also perform the same operations in one single line if interactivity is not required:

In [ ]:
s = hs.load("datasets/LSMO_STO_linescan.hdf5")
In [ ]:
s_ti = s.remove_background(signal_range=(405.,448.)).isig[448.:480.].integrate1D(axis="Energy loss")
In [ ]:
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.

Firstly we'll have to tell HyperSpy where to find the Digital Micrograph Hartree-Slater cross section files, since they are not included in HyperSpy. Go to the "EELS" tab, then set "GOS directory" to the "H-S GOS Tables" folder. Note that unfortunately this requires a license of Gatan Digital Micrograph.

If you don't have Digital Micrograph you can still perform curve fitting of K and L edges using Ray Egerton's Hydrogenic cross-sections which are included in HyperSpy

In [ ]:
hs.preferences.gui()
In [ ]:
s_ll = hs.load("datasets/LSMO_STO_linescan_low_loss.hdf5")
In [ ]:
s = hs.load("datasets/LSMO_STO_linescan.hdf5")

Here, the metadata has been populated with some of the experimental parameters:

In [ ]:
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.

It is offset by approximetely 0.6 eV.

In [ ]:
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.

In [ ]:
s_ll.align_zero_loss_peak(subpixel=True, also_align=[s])

By doing this, we aligned both our low loss and core loss spectra.

In [ ]:
s_ll.plot()

We have to add the elements which is present in the sample to s

In [ ]:
s.add_elements(('Mn','O','Ti','La'))

Then we make a model out of the core loss spectrum. 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: creating this model requires using the GOS files from Gatan Digital Micrograph. If you don't have these files only K and L edges can be created using Hydrogenic cross sections. If you do have them but HyperSpy can't find them in the default location, you can specify the location using hs.preferences.gui().

In [ ]:
m = s.create_model(ll=s_ll)

The model new consist of many different EELSCLEdge components, including a component for the plasmon background

In [ ]:
m.components

We can fit the model to the experimental data by using the multifit function, with the smart fitting. Which is fits in a way optimized for EELS data, by fitting from the lowest to the highest energy losses.

In [ ]:
m.multifit(kind='smart')
In [ ]:
m.plot()

We can check the error of the fitting

In [ ]:
edges = ("Ti_L3", "La_M5", "Mn_L3","O_K")
In [ ]:
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 Hartree-Slater onsets interactively

In [ ]:
m.plot()
m.enable_adjust_position()

Or manually, by directly changing the parameters within the Hartree-Slater edges. The parameter is called onset_energy

In [ ]:
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()

In [ ]:
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-L3: the L2 and L1 is a set to an energy relative to the L3.

In [ ]:
m.components.Mn_L3.onset_energy.value
In [ ]:
m.components.Mn_L2.onset_energy.value
In [ ]:
m.components.Mn_L3.onset_energy.value = 638.5
In [ ]:
m.components.Mn_L2.onset_energy.value 
In [ ]:
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:

In [ ]:
m.enable_fine_structure()

This will produce a much better fit, but will be much slower (~2 minutes).

In [ ]:
m.multifit(kind='smart')

Now the fit is much better, due to the model taking into account the fine structure.

In [ ]:
m.plot()

Now we can can have a look at the relative intensity from the individual EELS-edges using plot_spectra

In [ ]:
edges = ("Ti_L3", "La_M5", "Mn_L3","O_K")
In [ ]:
hs.plot.plot_spectra([m[edge].intensity.as_signal() for edge in edges], legend=edges)

While the fitting looks nicer, we can clearly improve this. Firstly the intensities are negative where it should be zero. Secondly, the fine structure regions can be fine tuned. Especially the Mn-L1 fine structure window can be reduced

In [ ]:
m.components.Mn_L1.fine_structure_width = 15

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.

In [ ]:
m.components.Mn_L3.intensity.bmin = 0.0
In [ ]:
m.components.La_M5.intensity.bmin = 0.0
In [ ]:
m.components.Ti_L3.intensity.bmin = 0.0
In [ ]:
m.components.O_K.intensity.bmin = 0.0
In [ ]:
m.multifit(fitter="leastsq", kind='smart', bounded=True)
In [ ]:
m.plot()
In [ ]:
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 La0.7Sr0.3MnO3 thin film, where parts of the film has been bombarded with the electron beam for an extended time.

In [ ]:
s = hs.load("datasets/LSMO_linescan.hdf5")

Using the moving the red line in the 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.

In [ ]:
s.plot()

Using the low loss signal, we make sure the energy scale is properly calibrated

In [ ]:
s_ll = hs.load("datasets/LSMO_linescan_low_loss.hdf5")
In [ ]:
s_ll.plot()

The zero loss peak is not well aligned at 0 eV energy loss, so we should align it and the core loss

In [ ]:
s_ll.align_zero_loss_peak(also_align=[s])

Now the zero loss peak has been shifted to 0 energy loss, and likewise the core loss spectrum s has also been aligned

In [ ]:
s_ll.plot()

We can also calculate the relative thickness using the low loss. 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.

In [ ]:
s_thickness = s_ll.estimate_thickness(threshold=3.0)

This gives the relative thickness and, as expected, there is an increase towards the end of the line scan

In [ ]:
s_thickness.plot()

5. Fine structure: oxygen K-edge

Lets take a closer look at the oxygen-K edge, firstly by removing the plasmon background, then cropping the spectrum to only include the oxygen-K edge. Note: this will overwrite the s spectrum with the cropped one.

In [ ]:
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

In [ ]:
s_deconvolved = s.fourier_ratio_deconvolution(s_ll)
In [ ]:
s_deconvolved.plot()

Fine structure modelling

Having had a qualitatively 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).

In [ ]:
s.crop_signal1D()

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 got no ionization edges.

In [ ]:
m = s.create_model(ll=s_ll, auto_background=False)

So currently, the model does not contain any components

In [ ]:
m.components

We can try to model some of the fine structure with Gaussians

In [ ]:
g1 = hs.model.components1D.Gaussian()
In [ ]:
m.append(g1)

This added the gaussian component to the model

In [ ]:
m.components

Then we can fit this Gaussian to the largest of the O-K peaks 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

In [ ]:
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: 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

In [ ]:
g1.plot()

Using the same method we can also fit the second largest peak between 535 and 541 eV. Using the signal_range argument we don't have to select the region using the GUI.

In [ ]:
g2 = hs.model.components1D.Gaussian()
In [ ]:
m.append(g2)
In [ ]:
m.fit_component(g2, signal_range=(535.,541.), only_current=False)
In [ ]:
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)

In [ ]:
m.set_signal_range()
In [ ]:
m.multifit()

After fitting, we reset the signal range so we can see the full range of the signal

In [ ]:
m.reset_signal_range()
In [ ]:
m.plot()

Lastly, we can fit the small "pre-peak" as well. First we "lock" the two Gaussian we have already fitted.

In [ ]:
g1.set_parameters_not_free()
In [ ]:
g2.set_parameters_not_free()

Then we add another Gaussian, and fit it using fit_component between 522 and 527 eV

In [ ]:
g3 = hs.model.components1D.Gaussian()
In [ ]:
m.append(g3)
In [ ]:
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 eV to 541 eV

In [ ]:
m.set_signal_range(520.,541.)

And set the g1 and g2 components free

In [ ]:
g1.set_parameters_free()
In [ ]:
g2.set_parameters_free()
In [ ]:
m.multifit()

This fits all the three components to the experimental data, which hopefully gives a good fit

In [ ]:
m.reset_signal_range()
In [ ]:
m.plot()

We can then compare the different parameters in the components

In [ ]:
g1_g3_ratio = g1.A.as_signal()/g3.A.as_signal()
In [ ]:
g1_g3_ratio.plot()
In [ ]:
g1_g3_position = g1.centre.as_signal()-g3.centre.as_signal()
In [ ]:
g1_g3_position.plot()
In [ ]:
g1_g3_sigma = g1.sigma.as_signal()/g3.sigma.as_signal()
In [ ]:
g1_g3_sigma.plot()

In all of the comparisons there are some large changes in the regions with beam damage. However, the values can vary a great deal. This is most likely due to the g1 fitted to the pre-peak is not so clearly defined in these regions. Leading to potentially bad fitting.