Skip to content

Commit

Permalink
Merge pull request #988 from k8macarthur/EDXCrossSections
Browse files Browse the repository at this point in the history
Conversation Between Z-Factors and Cross Section for EDX Quantification
  • Loading branch information
francisco-dlp committed May 23, 2016
2 parents ddfc216 + 81b40e4 commit 942b9e4
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 60 deletions.
3 changes: 2 additions & 1 deletion doc/user_guide/eds.rst
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,8 @@ is usually preferable. In the case of zeta-factors and cross sections, these mus
be determined experimentally using standards.

Zeta-factors should be provided in units of kg/m^2. The method is described further in [Watanabe1996]_ and [Watanabe2006]_ .
Cross sections should be provided in units of megabarns (Mb). Further details on the cross section method can be found in [MacArthur2016]_ .
Cross sections should be provided in units of barns (b). Further details on the cross section method can be found in [MacArthur2016]_ .
Conversion between zeta-factors and cross sections is possible using :py:meth:~._misc.eds.util.edx_cross_section_to_zeta or :py:meth:~._misc.eds.util.zeta_to_edx_cross_section .

Using the Cliff-Lorimer method as an example, quantification can be carried out as follows:

Expand Down
70 changes: 41 additions & 29 deletions hyperspy/_signals/eds_tem.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,6 @@ def get_calibration_from(self, ref, nb_pix=1):
mp.Acquisition_instrument.TEM.Detector.EDS.live_time = \
mp_ref.Detector.EDS.live_time / nb_pix


def quantification(self,
intensities,
method,
Expand All @@ -314,13 +313,13 @@ def quantification(self,
Set the quantification method: Cliff-Lorimer, zeta-factor, or
ionization cross sections.
factors: list of float
The list of kfactors, zeta-factors or cross sections in same order as
intensities. Note that intensities provided by Hyperspy are sorted
by the alphabetical order of the X-ray lines.
The list of kfactors, zeta-factors or cross sections in same order
as intensities. Note that intensities provided by Hyperspy are
sorted by the alphabetical order of the X-ray lines.
eg. factors =[0.982, 1.32, 1.60] for ['Al_Ka', 'Cr_Ka', 'Ni_Ka'].
composition_units: 'weight' or 'atomic'
The quantification returns the composition in atomic percent by default,
but can also return weight percent if specified.
The quantification returns the composition in atomic percent by
default, but can also return weight percent if specified.
navigation_mask : None or float or signal
The navigation locations marked as True are not used in the
quantification. If int is given the vacuum_mask method is used to
Expand Down Expand Up @@ -382,15 +381,17 @@ def quantification(self,
mass_thickness.data = results[1]
mass_thickness.metadata.General.title = 'Mass thickness'
elif method == 'cross_section':
results = utils_eds.quantification_cross_section(composition.data,
cross_sections=factors,
dose=self._get_dose(method))
results = utils_eds.quantification_cross_section(
composition.data,
cross_sections=factors,
dose=self._get_dose(method))
composition.data = results[0] * 100
number_of_atoms = utils.stack(intensities)
number_of_atoms.data = results[1]
number_of_atoms = number_of_atoms.split()
else:
raise ValueError ('Please specify method for quantification, as \'CL\', \'zeta\' or \'cross_section\'')
raise ValueError('Please specify method for quantification,'
'as \'CL\', \'zeta\' or \'cross_section\'')
composition = composition.split()
if composition_units == 'atomic':
if method != 'cross_section':
Expand All @@ -410,26 +411,27 @@ def quantification(self,
print("%s (%s): Composition = %.2f %s percent"
% (element, xray_line, composition[i].data,
composition_units))
if method=='cross_section':
if method == 'cross_section':
for i, xray_line in enumerate(xray_lines):
element, line = utils_eds._get_element_and_line(xray_line)
number_of_atoms[i].metadata.General.title = 'atom counts of ' +\
element
number_of_atoms[i].metadata.General.title = \
'atom counts of ' + element
number_of_atoms[i].metadata.set_item("Sample.elements",
([element]))
([element]))
number_of_atoms[i].metadata.set_item(
"Sample.xray_lines", ([xray_line]))
if plot_result and composition[i].axes_manager.signal_dimension != 0:
utils.plot.plot_signals(composition, **kwargs)
if method=='zeta':
if method == 'zeta':
self.metadata.set_item("Sample.mass_thickness", mass_thickness)
return composition, mass_thickness
elif method == 'cross_section':
return composition, number_of_atoms
elif method == 'CL':
return composition
else:
raise ValueError ('Please specify method for quantification, as \'CL\', \'zeta\' or \'cross_section\'')
raise ValueError('Please specify method for quantification, as \
''CL\', \'zeta\' or \'cross_section\'')

def vacuum_mask(self, threshold=1.0, closing=True, opening=False):
"""
Expand Down Expand Up @@ -569,7 +571,7 @@ def create_model(self, auto_background=True, auto_add_lines=True,
return model

def _get_dose(self, method, beam_current='auto', real_time='auto',
probe_area='auto'):
probe_area='auto'):
"""
Calculates the total electron dose for the zeta-factor or cross section
methods of quantification.
Expand All @@ -593,7 +595,8 @@ def _get_dose(self, method, beam_current='auto', real_time='auto',
The illumination area of the electron beam in nm^2.
If not set the value is extracted from the scale axes_manager.
Therefore we assume the probe is oversampling such that
the illumination area can be approximated to the pixel area of the spectrum image.
the illumination area can be approximated to the pixel area of the
spectrum image.
Returns
--------
Expand All @@ -608,20 +611,26 @@ def _get_dose(self, method, beam_current='auto', real_time='auto',

if beam_current is 'auto':
if 'beam_current' not in parameters:
raise Exception('Electron dose could not be calculated as beam_current is not set. '
'The beam current can be set by calling set_microscope_parameters()')
raise Exception('Electron dose could not be calculated as\
beam_current is not set.'
'The beam current can be set by calling \
set_microscope_parameters()')
else:
beam_current = parameters.beam_current

if real_time == 'auto':
real_time = parameters.Detector.EDS.real_time
if 'real_time' not in parameters.Detector.EDS:
raise Exception('Electron dose could not be calculated as real_time is not set. '
'The beam_current can be set by calling set_microscope_parameters()')
raise Exception('Electron dose could not be calculated as \
real_time is not set. '
'The beam_current can be set by calling \
set_microscope_parameters()')
elif real_time == 0.5:
warnings.warn('Please note that your real time is set to '
'the default value of 0.5 s. If this is not correct, you should change it using '
'set_microscope_parameters() and run quantification again.')
'the default value of 0.5 s. If this is not \
correct, you should change it using '
'set_microscope_parameters() and run \
quantification again.')

if method == 'cross_section':
if probe_area == 'auto':
Expand All @@ -630,14 +639,17 @@ def _get_dose(self, method, beam_current='auto', real_time='auto',
else:
pixel1 = self.axes_manager[0].scale
pixel2 = self.axes_manager[1].scale
if self.axes_manager[0].scale == 1 or self.axes_manager[1].scale == 1:
if pixel1 == 1 or pixel2 == 1:
warnings.warn('Please note your probe_area is set to'
'the default value of 1 nm^2. The function will still run. However if 1 nm^2 is not'
'correct, please read the user documentations for how to set this properly.')
'the default value of 1 nm^2. The \
function will still run. However if'
'1 nm^2 is not correct, please read the \
user documentations for how to set this \
properly.')
area = pixel1 * pixel2
return (real_time * beam_current * 1e-9) /(constants.e * area)
return (real_time * beam_current * 1e-9) / (constants.e * area)
# 1e-9 is included here because the beam_current is in nA.
elif method =='zeta':
elif method == 'zeta':
return real_time * beam_current * 1e-9 / constants.e
else:
raise Exception('Method need to be \'zeta\' or \'cross_section\'.')
94 changes: 79 additions & 15 deletions hyperspy/misc/eds/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

import numpy as np
import math
from scipy import constants

from hyperspy.misc.elements import elements as elements_db
from functools import reduce
Expand All @@ -11,8 +12,8 @@

def _get_element_and_line(xray_line):
"""
Returns the element name and line character for a particular X-ray line as a
tuple.
Returns the element name and line character for a particular X-ray line as
a tuple.
By example, if xray_line = 'Mn_Ka' this function returns ('Mn', 'Ka')
"""
Expand Down Expand Up @@ -103,7 +104,8 @@ def get_FWHM_at_Energy(energy_resolution_MnKa, E):
"""Calculates an approximate FWHM, accounting for peak broadening due to the
detector, for a peak at energy E given a known width at a reference energy.
The factor 2.5 is a constant derived by Fiori & Newbury as references below.
The factor 2.5 is a constant derived by Fiori & Newbury as references
below.
Parameters
----------
Expand Down Expand Up @@ -274,8 +276,8 @@ def take_off_angle(tilt_stage,
b = math.radians(azimuth_angle)
c = math.radians(elevation_angle)

return math.degrees(np.arcsin(-math.cos(a) * math.cos(b) * math.cos(c)
+ math.sin(a) * math.sin(c)))
return math.degrees(np.arcsin(-math.cos(a) * math.cos(b) * math.cos(c) +
math.sin(a) * math.sin(c)))


def xray_lines_model(elements,
Expand All @@ -284,7 +286,8 @@ def xray_lines_model(elements,
energy_resolution_MnKa=130,
energy_axis=None):
"""
Generate a model of X-ray lines using a Gaussian distribution for each peak.
Generate a model of X-ray lines using a Gaussian distribution for each
peak.
The area under a main peak (alpha) is equal to 1 and weighted by the
composition.
Expand Down Expand Up @@ -456,8 +459,8 @@ def _quantification_cliff_lorimer(intensities,


def quantification_zeta_factor(intensities,
zfactors,
dose):
zfactors,
dose):
"""
Quantification using the zeta-factor method
Expand All @@ -470,8 +473,9 @@ def quantification_zeta_factor(intensities,
The list of zeta-factors in the same order as intensities
e.g. zfactors = [628.10, 539.89] for ['As_Ka', 'Ga_Ka'].
dose: float
The total electron dose given by i*t*N, i the current, t the acquisition time
and N the number of electrons per unit electric charge (1/e).
The total electron dose given by i*t*N, i the current,
t the acquisition time and
N the number of electrons per unit electric charge (1/e).
Returns
------
Expand All @@ -488,9 +492,10 @@ def quantification_zeta_factor(intensities,
mass_thickness = sumzi / dose
return composition, mass_thickness


def quantification_cross_section(intensities,
cross_sections,
dose):
cross_sections,
dose):
"""
Quantification using EDX cross sections
Calculate the atomic compostion and the number of atoms per pixel
Expand All @@ -504,8 +509,9 @@ def quantification_cross_section(intensities,
List of X-ray scattering cross-sections in the same order as the
intensities.
dose: float
the dose per unit area given by i*t*N/A, i the current, t the acquisition time, and N
the number of electron by unit electric charge.
the dose per unit area given by i*t*N/A, i the current,
t the acquisition time, and
N the number of electron by unit electric charge.
Returns
-------
Expand All @@ -521,7 +527,65 @@ def quantification_cross_section(intensities,
for intensity, cross_section in zip(intensities, cross_sections):
total_atoms = total_atoms + (intensity/(dose * cross_section * 1e-10))
for i, (intensity, cross_section) in enumerate(zip(intensities,
cross_sections)):
cross_sections)):
number_of_atoms[i] = (intensity) / (dose * cross_section * 1e-10)
composition[i] = number_of_atoms[i] / total_atoms
return composition, number_of_atoms


def edx_cross_section_to_zeta(cross_sections, elements):
"""Convert a list of cross_sections in barns (b) to zeta-factors (kg/m^2).
Parameters
----------
cross_section: list of float
A list of cross sections in barns.
elements: list of str
A list of element chemical symbols in the same order as the
cross sections e.g. ['Al','Zn']
Returns
-------
zeta_factors : list of float
zeta_factors with units kg/m^2.
"""
if len(elements) != len(cross_sections):
raise ValueError(
'The number of elements must match the number of cross sections.')
zeta_factors = []
for i, element in enumerate(elements):
atomic_weight = elements_db[element]['General_properties'][
'atomic_weight']
zeta = atomic_weight / (cross_sections[i] * constants.Avogadro * 1E-25)
zeta_factors.append(zeta)
return zeta_factors


def zeta_to_edx_cross_section(zfactors, elements):
"""Convert a list of zeta-factors (kg/m^2) to cross_sections in barns (b).
Parameters
----------
zfactors: list of float
A list of zeta-factors.
elements: list of str
A list of element chemical symbols in the same order as the
cross sections e.g. ['Al','Zn']
Returns
-------
cross_sections : list of float
cross_sections with units in barns.
"""
if len(elements) != len(zfactors):
raise ValueError(
'The number of elements must match the number of cross sections.')
cross_sections = []
for i, element in enumerate(elements):
atomic_weight = elements_db[element]['General_properties'][
'atomic_weight']
xsec = atomic_weight / (zfactors[i] * constants.Avogadro * 1E-25)
cross_sections.append(xsec)
return cross_sections

0 comments on commit 942b9e4

Please sign in to comment.