pyTEMlib 0.2025.12.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- build/lib/pyTEMlib/__init__.py +36 -0
- build/lib/pyTEMlib/animation.py +667 -0
- build/lib/pyTEMlib/atom_tools.py +229 -0
- build/lib/pyTEMlib/config_dir.py +43 -0
- build/lib/pyTEMlib/crystal_tools.py +1219 -0
- build/lib/pyTEMlib/diffraction_plot.py +757 -0
- build/lib/pyTEMlib/dynamic_scattering.py +298 -0
- build/lib/pyTEMlib/eds_tools.py +901 -0
- build/lib/pyTEMlib/eds_xsections.py +268 -0
- build/lib/pyTEMlib/eels_tools/__init__.py +44 -0
- build/lib/pyTEMlib/eels_tools/core_loss_tools.py +751 -0
- build/lib/pyTEMlib/eels_tools/eels_database.py +134 -0
- build/lib/pyTEMlib/eels_tools/low_loss_tools.py +655 -0
- build/lib/pyTEMlib/eels_tools/peak_fit_tools.py +175 -0
- build/lib/pyTEMlib/eels_tools/zero_loss_tools.py +264 -0
- build/lib/pyTEMlib/file_reader.py +275 -0
- build/lib/pyTEMlib/file_tools.py +816 -0
- build/lib/pyTEMlib/get_bote_salvat.py +69 -0
- build/lib/pyTEMlib/graph_tools.py +1153 -0
- build/lib/pyTEMlib/graph_viz.py +599 -0
- build/lib/pyTEMlib/image/__init__.py +37 -0
- build/lib/pyTEMlib/image/image_atoms.py +270 -0
- build/lib/pyTEMlib/image/image_clean.py +198 -0
- build/lib/pyTEMlib/image/image_distortion.py +299 -0
- build/lib/pyTEMlib/image/image_fft.py +278 -0
- build/lib/pyTEMlib/image/image_graph.py +926 -0
- build/lib/pyTEMlib/image/image_registration.py +316 -0
- build/lib/pyTEMlib/image/image_utilities.py +308 -0
- build/lib/pyTEMlib/image/image_window.py +421 -0
- build/lib/pyTEMlib/image_tools.py +701 -0
- build/lib/pyTEMlib/interactive_image.py +1 -0
- build/lib/pyTEMlib/kinematic_scattering.py +1145 -0
- build/lib/pyTEMlib/microscope.py +62 -0
- build/lib/pyTEMlib/probe_tools.py +928 -0
- build/lib/pyTEMlib/sidpy_tools.py +153 -0
- build/lib/pyTEMlib/simulation_tools.py +104 -0
- build/lib/pyTEMlib/test.py +437 -0
- build/lib/pyTEMlib/utilities.py +431 -0
- build/lib/pyTEMlib/version.py +5 -0
- build/lib/pyTEMlib/xrpa_x_sections.py +20976 -0
- pyTEMlib/__init__.py +36 -0
- pyTEMlib/animation.py +667 -0
- pyTEMlib/atom_tools.py +229 -0
- pyTEMlib/config_dir.py +43 -0
- pyTEMlib/crystal_tools.py +1219 -0
- pyTEMlib/data/k_factors_Bruker_15keV.json +293 -0
- pyTEMlib/data/k_factors_Thermo_200keV.json +494 -0
- pyTEMlib/data/xrays_X_section_100kV.json +19334 -0
- pyTEMlib/data/xrays_X_section_200kV.json +18711 -0
- pyTEMlib/data/xrays_X_section_300kV.json +18347 -0
- pyTEMlib/data/xrays_X_section_60kV.json +20202 -0
- pyTEMlib/diffraction_plot.py +757 -0
- pyTEMlib/dynamic_scattering.py +298 -0
- pyTEMlib/eds_tools.py +901 -0
- pyTEMlib/eds_xsections.py +268 -0
- pyTEMlib/eels_tools/__init__.py +44 -0
- pyTEMlib/eels_tools/core_loss_tools.py +751 -0
- pyTEMlib/eels_tools/eels_database.py +134 -0
- pyTEMlib/eels_tools/low_loss_tools.py +655 -0
- pyTEMlib/eels_tools/peak_fit_tools.py +175 -0
- pyTEMlib/eels_tools/zero_loss_tools.py +264 -0
- pyTEMlib/file_reader.py +275 -0
- pyTEMlib/file_tools.py +816 -0
- pyTEMlib/get_bote_salvat.py +69 -0
- pyTEMlib/graph_tools.py +1153 -0
- pyTEMlib/graph_viz.py +599 -0
- pyTEMlib/image/__init__.py +37 -0
- pyTEMlib/image/image_atoms.py +270 -0
- pyTEMlib/image/image_clean.py +198 -0
- pyTEMlib/image/image_distortion.py +299 -0
- pyTEMlib/image/image_fft.py +278 -0
- pyTEMlib/image/image_graph.py +926 -0
- pyTEMlib/image/image_registration.py +316 -0
- pyTEMlib/image/image_utilities.py +308 -0
- pyTEMlib/image/image_window.py +421 -0
- pyTEMlib/image_tools.py +701 -0
- pyTEMlib/interactive_image.py +1 -0
- pyTEMlib/kinematic_scattering.py +1145 -0
- pyTEMlib/microscope.py +62 -0
- pyTEMlib/probe_tools.py +928 -0
- pyTEMlib/sidpy_tools.py +153 -0
- pyTEMlib/simulation_tools.py +104 -0
- pyTEMlib/test.py +437 -0
- pyTEMlib/utilities.py +431 -0
- pyTEMlib/version.py +5 -0
- pyTEMlib/xrpa_x_sections.py +20976 -0
- pytemlib-0.2025.12.0.dist-info/METADATA +100 -0
- pytemlib-0.2025.12.0.dist-info/RECORD +92 -0
- pytemlib-0.2025.12.0.dist-info/WHEEL +5 -0
- pytemlib-0.2025.12.0.dist-info/entry_points.txt +2 -0
- pytemlib-0.2025.12.0.dist-info/licenses/LICENSE +21 -0
- pytemlib-0.2025.12.0.dist-info/top_level.txt +5 -0
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
"""
|
|
2
|
+
eds_tools
|
|
3
|
+
Model based quantification of energy-dispersive X-ray spectroscopy data
|
|
4
|
+
Copyright by Gerd Duscher
|
|
5
|
+
|
|
6
|
+
The University of Tennessee, Knoxville
|
|
7
|
+
Department of Materials Science & Engineering
|
|
8
|
+
|
|
9
|
+
Sources:
|
|
10
|
+
|
|
11
|
+
Units:
|
|
12
|
+
everything is in SI units, except length is given in nm and angles in mrad.
|
|
13
|
+
Usage:
|
|
14
|
+
See the notebooks for examples of these routines
|
|
15
|
+
|
|
16
|
+
All the input and output is done through a dictionary which is to be found in the meta_data
|
|
17
|
+
attribute of the sidpy.Dataset
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import csv
|
|
22
|
+
import json
|
|
23
|
+
import xml
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
import matplotlib.pyplot as plt
|
|
27
|
+
|
|
28
|
+
import scipy
|
|
29
|
+
import scipy.interpolate # use interp1d,
|
|
30
|
+
import scipy.optimize # leastsq # least square fitting routine fo scipy
|
|
31
|
+
import sklearn # .mixture import GaussianMixture
|
|
32
|
+
|
|
33
|
+
import sidpy
|
|
34
|
+
|
|
35
|
+
import pyTEMlib
|
|
36
|
+
import pyTEMlib.file_reader
|
|
37
|
+
from .utilities import elements as elements_list
|
|
38
|
+
from .eds_xsections import quantify_cross_section, quantification_k_factors
|
|
39
|
+
from .config_dir import config_path
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def detector_response(dataset):
|
|
44
|
+
"""
|
|
45
|
+
Calculate the detector response for the given dataset based on its metadata.
|
|
46
|
+
|
|
47
|
+
Parameters:
|
|
48
|
+
- dataset: A sidpy.Dataset object containing the spectral data and metadata.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
- A numpy array representing the detector efficiency across the energy scale.
|
|
52
|
+
"""
|
|
53
|
+
tags = dataset.metadata['EDS']
|
|
54
|
+
|
|
55
|
+
energy_scale = dataset.get_spectral_dims(return_axis=True)[0]
|
|
56
|
+
if 'start_channel' not in tags['detector']:
|
|
57
|
+
tags['detector']['start_channel'] = np.searchsorted(energy_scale, 100)
|
|
58
|
+
|
|
59
|
+
start = tags['detector']['start_channel']
|
|
60
|
+
detector_efficiency = np.zeros(len(dataset))
|
|
61
|
+
detector_efficiency[start:] += get_detector_response(tags, energy_scale[start:])
|
|
62
|
+
tags['detector']['detector_efficiency'] = detector_efficiency
|
|
63
|
+
return detector_efficiency
|
|
64
|
+
|
|
65
|
+
def get_absorption(z, thickness, energy_scale):
|
|
66
|
+
""" Calculate absorption for material with atomic number z and thickness t in m"""
|
|
67
|
+
x_sections = pyTEMlib.eels_tools.get_x_sections()
|
|
68
|
+
photoabsorption = x_sections[str(z)]['dat']/1e10/x_sections[str(z)]['photoabs_to_sigma']
|
|
69
|
+
lin = scipy.interpolate.interp1d(x_sections[str(z)]['ene'], photoabsorption, kind='linear')
|
|
70
|
+
mu = lin(energy_scale) * x_sections[str(z)]['nominal_density']*100 #1/cm -> 1/m
|
|
71
|
+
return np.exp(-mu * thickness)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_detector_response(detector_definition, energy_scale):
|
|
75
|
+
"""
|
|
76
|
+
Calculates response of Si drift detector for EDS spectrum background based
|
|
77
|
+
on detector parameters
|
|
78
|
+
|
|
79
|
+
Parameters:
|
|
80
|
+
----------
|
|
81
|
+
detector_definition: dictionary
|
|
82
|
+
definition of detector
|
|
83
|
+
energy_scale: numpy array (1 dim)
|
|
84
|
+
energy scale of spectrum should start at about 100eV!!
|
|
85
|
+
|
|
86
|
+
Return:
|
|
87
|
+
-------
|
|
88
|
+
response: numpy array with length(energy_scale)
|
|
89
|
+
detector response
|
|
90
|
+
|
|
91
|
+
Example
|
|
92
|
+
-------
|
|
93
|
+
|
|
94
|
+
tags ={}
|
|
95
|
+
tags['acceleration_voltage'] = 200000
|
|
96
|
+
|
|
97
|
+
tags['detector'] ={}
|
|
98
|
+
|
|
99
|
+
## layer thicknesses of common materials in EDS detectors in m
|
|
100
|
+
tags['detector']['layers'] = {13: {'thickness':= 0.05*1e-6, 'Z': 13, 'element': 'Al'},
|
|
101
|
+
6: {'thickness':= 0.15*1e-6, 'Z': 6, 'element': 'C'}
|
|
102
|
+
}
|
|
103
|
+
tags['detector']['SiDeadThickness'] = .13 *1e-6 # in m
|
|
104
|
+
tags['detector']['SiLiveThickness'] = 0.05 # in m
|
|
105
|
+
tags['detector']['detector_area'] = 30 * 1e-6 #in m2
|
|
106
|
+
tags['detector']['energy_resolution'] = 125 # in eV
|
|
107
|
+
tags['detector']['start_energy'] = 120 # in eV
|
|
108
|
+
tags['detector']['start_channel'] = np.searchsorted(spectrum.energy_scale.values,120)
|
|
109
|
+
|
|
110
|
+
energy_scale = np.linspace(.01, 20, 1199)*1000 # i eV
|
|
111
|
+
start = np.searchsorted(spectrum.energy, 100)
|
|
112
|
+
energy_scale = spectrum.energy[start:]
|
|
113
|
+
detector_Efficiency= pyTEMlib.eds_tools.detector_response(tags, spectrum.energy[start:])
|
|
114
|
+
|
|
115
|
+
p = np.array([1, 37, .3])/10000*3
|
|
116
|
+
E_0= 200000
|
|
117
|
+
background = np.zeros(len(spectrum))
|
|
118
|
+
bremsstrahlung = p[0] + p[1]*(E_0-energy_scale)/energy_scale
|
|
119
|
+
bremsstrahlung += p[2]*(E_0-energy_scale)**2/energy_scale
|
|
120
|
+
background[start:] = detector_Efficiency * bremsstrahlung
|
|
121
|
+
|
|
122
|
+
plt.figure()
|
|
123
|
+
plt.plot(spectrum.energy, spectrum, label = 'spec')
|
|
124
|
+
plt.plot(spectrum.energy, background, label = 'background')
|
|
125
|
+
plt.show()
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
response = np.ones(len(energy_scale))
|
|
129
|
+
|
|
130
|
+
for layer in detector_definition['detector']['layers'].values():
|
|
131
|
+
if layer['Z'] != 14:
|
|
132
|
+
response *= get_absorption(layer['Z'], layer['thickness'], energy_scale)
|
|
133
|
+
if 'SiDeadThickness' in detector_definition['detector']:
|
|
134
|
+
response *= get_absorption(14, detector_definition['detector']['SiDeadThickness'],
|
|
135
|
+
energy_scale)
|
|
136
|
+
if 'SiLiveThickness' in detector_definition['detector']:
|
|
137
|
+
response *= 1-get_absorption(14, detector_definition['detector']['SiLiveThickness'],
|
|
138
|
+
energy_scale)
|
|
139
|
+
return response
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def detect_peaks(dataset, minimum_number_of_peaks=30, prominence=10):
|
|
143
|
+
"""
|
|
144
|
+
Detect peaks in the given spectral dataset.
|
|
145
|
+
|
|
146
|
+
Parameters:
|
|
147
|
+
-----------
|
|
148
|
+
- dataset: A sidpy.Dataset object containing the spectral data.
|
|
149
|
+
- minimum_number_of_peaks: The minimum number of peaks to detect.
|
|
150
|
+
- prominence: The prominence threshold for peak detection.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
--------
|
|
154
|
+
- An array of indices representing the positions of detected peaks in the spectrum.
|
|
155
|
+
"""
|
|
156
|
+
if not isinstance(dataset, sidpy.Dataset):
|
|
157
|
+
raise TypeError('Needs an sidpy dataset')
|
|
158
|
+
if not dataset.data_type.name == 'SPECTRUM':
|
|
159
|
+
raise TypeError('Need a spectrum')
|
|
160
|
+
|
|
161
|
+
energy_scale = dataset.get_spectral_dims(return_axis=True)[0].values
|
|
162
|
+
if 'EDS' not in dataset.metadata:
|
|
163
|
+
dataset.metadata['EDS'] = {}
|
|
164
|
+
if 'detector' not in dataset.metadata['EDS']:
|
|
165
|
+
raise ValueError('No detector information found, add detector dictionary to metadata')
|
|
166
|
+
|
|
167
|
+
if 'energy_resolution' not in dataset.metadata['EDS']['detector']:
|
|
168
|
+
dataset.metadata['EDS']['detector']['energy_resolution'] = 138
|
|
169
|
+
print('Using energy resolution of 138 eV')
|
|
170
|
+
if 'start_channel' not in dataset.metadata['EDS']['detector']:
|
|
171
|
+
dataset.metadata['EDS']['detector']['start_channel'] = np.searchsorted(energy_scale, 100)
|
|
172
|
+
|
|
173
|
+
resolution = dataset.metadata['EDS']['detector']['energy_resolution']
|
|
174
|
+
start = dataset.metadata['EDS']['detector']['start_channel']
|
|
175
|
+
## we use half the width of the resolution for smearing
|
|
176
|
+
width = int(np.ceil(resolution/(energy_scale[1]-energy_scale[0])/2)+1)
|
|
177
|
+
new_spectrum = scipy.signal.savgol_filter(np.array(dataset)[start:], width, 2)
|
|
178
|
+
|
|
179
|
+
minor_peaks, _ = scipy.signal.find_peaks(new_spectrum, prominence=prominence)
|
|
180
|
+
|
|
181
|
+
while len(minor_peaks) > minimum_number_of_peaks:
|
|
182
|
+
prominence+=10
|
|
183
|
+
minor_peaks, _ = scipy.signal.find_peaks(new_spectrum, prominence=prominence)
|
|
184
|
+
return np.array(minor_peaks)+start
|
|
185
|
+
|
|
186
|
+
def peaks_element_correlation(spectrum, minor_peaks):
|
|
187
|
+
"""
|
|
188
|
+
Identify elements present in the spectrum based on detected minor peaks.
|
|
189
|
+
|
|
190
|
+
Parameters:
|
|
191
|
+
- spectrum: A sidpy.Dataset object containing the spectral data.
|
|
192
|
+
- minor_peaks: An array of indices representing the positions of
|
|
193
|
+
minor peaks in the spectrum.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
- A list of element symbols identified in the spectrum.
|
|
197
|
+
"""
|
|
198
|
+
if not isinstance(spectrum, sidpy.Dataset):
|
|
199
|
+
raise TypeError(' Need a sidpy dataset')
|
|
200
|
+
energy_scale = spectrum.get_spectral_dims(return_axis=True)[0].values
|
|
201
|
+
element_list = set()
|
|
202
|
+
peaks = minor_peaks[np.argsort(spectrum[minor_peaks])]
|
|
203
|
+
accounted_peaks = set()
|
|
204
|
+
for i, peak in reversed(list(enumerate(peaks))):
|
|
205
|
+
for z in range(5, 82):
|
|
206
|
+
if i in accounted_peaks:
|
|
207
|
+
continue
|
|
208
|
+
edge_info = pyTEMlib.eels_tools.get_x_sections(z)
|
|
209
|
+
# element = edge_info['name']
|
|
210
|
+
lines = edge_info.get('lines', {})
|
|
211
|
+
if abs(lines.get('K-L3', {}).get('position', 0) - energy_scale[peak]) <40:
|
|
212
|
+
element_list.add(edge_info['name'])
|
|
213
|
+
for key, line in lines.items():
|
|
214
|
+
dist = np.abs(energy_scale[peaks]-line.get('position', 0))
|
|
215
|
+
if key[0] == 'K' and np.min(dist)< 40:
|
|
216
|
+
ind = np.argmin(dist)
|
|
217
|
+
accounted_peaks.add(ind)
|
|
218
|
+
# This is a special case for boron and carbon
|
|
219
|
+
elif abs(lines.get('K-L2', {}).get('position', 0) - energy_scale[peak]) <30:
|
|
220
|
+
accounted_peaks.add(i)
|
|
221
|
+
element_list.add(edge_info['name'])
|
|
222
|
+
|
|
223
|
+
if abs(lines.get('L3-M5', {}).get('position', 0) - energy_scale[peak]) <50:
|
|
224
|
+
element_list.add(edge_info['name'])
|
|
225
|
+
for key, line in edge_info['lines'].items():
|
|
226
|
+
dist = np.abs(energy_scale[peaks]-line.get('position', 0))
|
|
227
|
+
if key[0] == 'L' and np.min(dist)< 40 and line['weight'] > 0.01:
|
|
228
|
+
ind = np.argmin(dist)
|
|
229
|
+
accounted_peaks.add(ind)
|
|
230
|
+
return list(element_list)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_elements(spectrum, minimum_number_of_peaks=10, verbose=False):
|
|
234
|
+
""" Get the elments in a EDS spectrum
|
|
235
|
+
Parameters:
|
|
236
|
+
-----------
|
|
237
|
+
minimum_number_of_peaks: int
|
|
238
|
+
approximate number of peaks in spectrum
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
-------
|
|
242
|
+
elements: list
|
|
243
|
+
list of all elements found
|
|
244
|
+
"""
|
|
245
|
+
if not isinstance(spectrum, sidpy.Dataset):
|
|
246
|
+
raise TypeError(' Need a sidpy dataset')
|
|
247
|
+
if not isinstance(minimum_number_of_peaks, int):
|
|
248
|
+
raise TypeError(' Need an integer for minimum_number_of_peaks')
|
|
249
|
+
|
|
250
|
+
minor_peaks = detect_peaks(spectrum, minimum_number_of_peaks=minimum_number_of_peaks)
|
|
251
|
+
|
|
252
|
+
keys = list(spectrum.metadata['EDS'].keys())
|
|
253
|
+
for key in keys:
|
|
254
|
+
if len(key) < 3:
|
|
255
|
+
del spectrum.metadata['EDS'][key]
|
|
256
|
+
|
|
257
|
+
elements = peaks_element_correlation(spectrum, minor_peaks)
|
|
258
|
+
if verbose:
|
|
259
|
+
print(elements)
|
|
260
|
+
spectrum.metadata['EDS'].update(get_x_ray_lines(spectrum, elements))
|
|
261
|
+
return elements
|
|
262
|
+
|
|
263
|
+
def get_x_ray_lines(spectrum, element_list):
|
|
264
|
+
"""
|
|
265
|
+
Analyze the given spectrum to identify and characterize the X-ray emission lines
|
|
266
|
+
associated with the specified elements.
|
|
267
|
+
|
|
268
|
+
Parameters:
|
|
269
|
+
- spectrum: A sidpy.Dataset object containing the spectral data.
|
|
270
|
+
- elements: A list of element symbols (e.g., ['Fe', 'Cu']) to look for in the spectrum.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
- A dictionary where each key is an element symbol and each value is another dictionary
|
|
274
|
+
containing information about the X-ray lines detected for that element.
|
|
275
|
+
|
|
276
|
+
alpha_k = 1e6
|
|
277
|
+
alpha_l = 6.5e7
|
|
278
|
+
alpha_m = 8*1e8 # 2.2e10
|
|
279
|
+
# My Fit
|
|
280
|
+
alpha_K = .9e6
|
|
281
|
+
alpha_l = 6.e7
|
|
282
|
+
alpha_m = 6*1e8 # 2.2e10
|
|
283
|
+
# omega_K = Z**4/(alpha_K+Z**4)
|
|
284
|
+
# omega_L = Z**4/(alpha_l+Z**4)
|
|
285
|
+
# omega_M = Z**4/(alpha_m+Z**4)
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
out_tags = {}
|
|
289
|
+
x_sections = pyTEMlib.xrpa_x_sections.x_sections
|
|
290
|
+
energy_scale = spectrum.get_spectral_dims(return_axis=True)[0].values
|
|
291
|
+
for element in element_list:
|
|
292
|
+
atomic_number = pyTEMlib.eds_tools.elements_list.index(element)
|
|
293
|
+
out_tags[element] ={'Z': atomic_number}
|
|
294
|
+
lines = pyTEMlib.xrpa_x_sections.x_sections.get(str(atomic_number), {}).get('lines', {})
|
|
295
|
+
if not lines:
|
|
296
|
+
break
|
|
297
|
+
line_dict = {'K': {'lines': [],
|
|
298
|
+
'main': None,
|
|
299
|
+
'weight': 0},
|
|
300
|
+
'L': {'lines': [],
|
|
301
|
+
'main': None,
|
|
302
|
+
'weight': 0},
|
|
303
|
+
'M': {'lines': [],
|
|
304
|
+
'main': None,
|
|
305
|
+
'weight': 0}}
|
|
306
|
+
|
|
307
|
+
for key, line in lines.items():
|
|
308
|
+
if key[0] in line_dict:
|
|
309
|
+
if line['position'] < energy_scale[-1]:
|
|
310
|
+
line_dict[key[0]]['lines'].append(key)
|
|
311
|
+
if line['weight'] > line_dict[key[0]]['weight']:
|
|
312
|
+
line_dict[key[0]]['weight'] = line['weight']
|
|
313
|
+
line_dict[key[0]]['main'] = key
|
|
314
|
+
|
|
315
|
+
for key, family in line_dict.items():
|
|
316
|
+
if family['weight'] > 0:
|
|
317
|
+
out_tags[element].setdefault(f'{key}-family', {}).update(family)
|
|
318
|
+
position = x_sections[str(atomic_number)]['lines'][family['main']]['position']
|
|
319
|
+
height = spectrum[np.searchsorted(energy_scale, position)].compute()
|
|
320
|
+
out_tags[element][f'{key}-family']['height'] = height/family['weight']
|
|
321
|
+
z = str(atomic_number)
|
|
322
|
+
for key in family['lines']:
|
|
323
|
+
out_tags[element][f'{key[0]}-family'][key] = x_sections[z]['lines'][key]
|
|
324
|
+
spectrum.metadata.setdefault('EDS', {}).update(out_tags)
|
|
325
|
+
return out_tags
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def get_fwhm(energy: float, energy_ref: float, fwhm_ref: float) -> float:
|
|
329
|
+
""" Calculate FWHM of Gaussians"""
|
|
330
|
+
return np.sqrt(2.5*(energy-energy_ref)+fwhm_ref**2)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def gaussian(energy_scale: np.ndarray, mu: float, fwhm: float) -> np.ndarray:
|
|
334
|
+
""" Gaussian function"""
|
|
335
|
+
sig = fwhm/2/np.sqrt(2*np.log(2))
|
|
336
|
+
return np.exp(-np.power(np.array(energy_scale) - mu, 2.) / (2 * np.power(sig, 2.)))
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def get_peak(energy: float, energy_scale: np.ndarray,
|
|
340
|
+
energy_ref: float = 5895.0, fwhm_ref: float = 136) -> np.ndarray:
|
|
341
|
+
""" Generate a normalized Gaussian peak for a given energy."""
|
|
342
|
+
# all energies in eV
|
|
343
|
+
fwhm = get_fwhm(energy, energy_ref, fwhm_ref)
|
|
344
|
+
gauss = gaussian(energy_scale, energy, fwhm)
|
|
345
|
+
|
|
346
|
+
return gauss /(gauss.sum()+1e-12)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def initial_model_parameter(spectrum):
|
|
350
|
+
""" Initialize model parameters based on the spectrum's metadata."""
|
|
351
|
+
tags = spectrum.metadata['EDS']
|
|
352
|
+
energy_scale = spectrum.get_spectral_dims(return_axis=True)[0]
|
|
353
|
+
p = []
|
|
354
|
+
peaks = []
|
|
355
|
+
keys = []
|
|
356
|
+
for element, lines in tags.items():
|
|
357
|
+
if 'K-family' in lines:
|
|
358
|
+
model = np.zeros(len(energy_scale))
|
|
359
|
+
for line, info in lines['K-family'].items():
|
|
360
|
+
if line[0] == 'K':
|
|
361
|
+
model += get_peak(info['position'], energy_scale)*info['weight']
|
|
362
|
+
lines['K-family']['peaks'] = model /model.sum() # *lines['K-family']['probability']
|
|
363
|
+
|
|
364
|
+
p.append(lines['K-family']['height'] / lines['K-family']['peaks'].max())
|
|
365
|
+
peaks.append(lines['K-family']['peaks'])
|
|
366
|
+
keys.append(element+':K-family')
|
|
367
|
+
if 'L-family' in lines:
|
|
368
|
+
model = np.zeros(len(energy_scale))
|
|
369
|
+
for line, info in lines['L-family'].items():
|
|
370
|
+
if line[0] == 'L':
|
|
371
|
+
model += get_peak(info['position'], energy_scale)*info['weight']
|
|
372
|
+
lines['L-family']['peaks'] = model /model.sum() # *lines['L-family']['probability']
|
|
373
|
+
p.append(lines['L-family']['height'] / lines['L-family']['peaks'].max())
|
|
374
|
+
peaks.append(lines['L-family']['peaks'])
|
|
375
|
+
keys.append(element+':L-family')
|
|
376
|
+
if 'M-family' in lines:
|
|
377
|
+
model = np.zeros(len(energy_scale))
|
|
378
|
+
for line, info in lines['M-family'].items():
|
|
379
|
+
if line[0] == 'M':
|
|
380
|
+
model += get_peak(info['position'], energy_scale)*info['weight']
|
|
381
|
+
peaks_max = lines['M-family'].get('peaks', np.zeros(3)).max()
|
|
382
|
+
model_normalized = model / model.sum()*lines['M-family'].get('probability', 0.0)
|
|
383
|
+
lines['M-family']['peaks'] = model_normalized
|
|
384
|
+
if peaks_max >0:
|
|
385
|
+
p.append(lines['M-family']['height'] / peaks_max)
|
|
386
|
+
else:
|
|
387
|
+
p.append(0)
|
|
388
|
+
peaks.append(lines['M-family']['peaks'])
|
|
389
|
+
keys.append(element+':M-family')
|
|
390
|
+
|
|
391
|
+
p.extend([1e7, 1e-3, 1500, 20])
|
|
392
|
+
return np.array(peaks), np.array(p), keys
|
|
393
|
+
|
|
394
|
+
def get_model(spectrum):
|
|
395
|
+
"""
|
|
396
|
+
Construct the model spectrum from the metadata in the given spectrum object.
|
|
397
|
+
|
|
398
|
+
Parameters:
|
|
399
|
+
- spectrum: The spectrum object containing metadata and spectral data.
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
- model: The constructed model spectrum as a numpy array.
|
|
403
|
+
"""
|
|
404
|
+
model = np.zeros(len(np.array(spectrum)))
|
|
405
|
+
for key in spectrum.metadata['EDS']:
|
|
406
|
+
if isinstance(spectrum.metadata['EDS'][key], dict) and key in elements_list:
|
|
407
|
+
for family in spectrum.metadata['EDS'][key]:
|
|
408
|
+
if '-family' in family:
|
|
409
|
+
intensity = spectrum.metadata['EDS'][key][family].get('areal_density', 0)
|
|
410
|
+
peaks = spectrum.metadata['EDS'][key][family].get('peaks', np.zeros(len(model)))
|
|
411
|
+
if peaks.sum() <0.1:
|
|
412
|
+
print('no intensity',key, family)
|
|
413
|
+
model += peaks * intensity
|
|
414
|
+
|
|
415
|
+
if 'detector_efficiency' in spectrum.metadata['EDS']['detector'].keys():
|
|
416
|
+
detector_efficiency = spectrum.metadata['EDS']['detector']['detector_efficiency']
|
|
417
|
+
else:
|
|
418
|
+
detector_efficiency = None
|
|
419
|
+
e_0 = spectrum.metadata['experiment']['acceleration_voltage']
|
|
420
|
+
pp = spectrum.metadata['EDS']['bremsstrahlung']
|
|
421
|
+
energy_scale = spectrum.get_spectral_dims(return_axis=True)[0].values
|
|
422
|
+
|
|
423
|
+
if detector_efficiency is not None:
|
|
424
|
+
bremsstrahlung = (pp[-3] + pp[-2] * (e_0 - energy_scale) / energy_scale +
|
|
425
|
+
pp[-1] * (e_0 - energy_scale) ** 2 / energy_scale)
|
|
426
|
+
model += bremsstrahlung
|
|
427
|
+
model *= detector_efficiency
|
|
428
|
+
return model
|
|
429
|
+
|
|
430
|
+
def fit_model(spectrum, use_detector_efficiency=False):
|
|
431
|
+
"""
|
|
432
|
+
Fit the EDS spectrum using a model composed of elemental peaks and bremsstrahlung background.
|
|
433
|
+
|
|
434
|
+
Parameters:
|
|
435
|
+
- spectrum: The EDS spectrum to fit.
|
|
436
|
+
- elements: List of elements to consider in the fit.
|
|
437
|
+
- use_detector_efficiency: Whether to include detector efficiency in the model.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
- peaks: The fitted peak shapes.
|
|
441
|
+
- p: The fitted parameters.
|
|
442
|
+
"""
|
|
443
|
+
peaks, pin, _ = initial_model_parameter(spectrum)
|
|
444
|
+
|
|
445
|
+
energy_scale = spectrum.get_spectral_dims(return_axis=True)[0].values
|
|
446
|
+
|
|
447
|
+
if 'detector' in spectrum.metadata['EDS'].keys():
|
|
448
|
+
start = spectrum.metadata['EDS'].get('detector', {}).get('start_channel', 120)
|
|
449
|
+
spectrum.metadata['EDS']['detector']['start_channel'] = np.searchsorted(energy_scale, start)
|
|
450
|
+
if use_detector_efficiency:
|
|
451
|
+
efficiency = spectrum.metadata['EDS']['detector'].get('detector_efficiency', [])
|
|
452
|
+
if not isinstance(efficiency, (list, np.ndarray)):
|
|
453
|
+
if len(efficiency) != len(spectrum):
|
|
454
|
+
efficiency = detector_response(spectrum)
|
|
455
|
+
else:
|
|
456
|
+
use_detector_efficiency = False
|
|
457
|
+
else:
|
|
458
|
+
print('need detector information to fit spectrum')
|
|
459
|
+
return None, None
|
|
460
|
+
|
|
461
|
+
e_0 = spectrum.metadata.get('experiment', {}).get('acceleration_voltage', 0.)
|
|
462
|
+
|
|
463
|
+
def residuals(pp, yy):
|
|
464
|
+
""" residuals for fit"""
|
|
465
|
+
model = np.zeros(len(yy))
|
|
466
|
+
for i in range(len(pp)-4):
|
|
467
|
+
model += peaks[i]*pp[i]
|
|
468
|
+
if use_detector_efficiency:
|
|
469
|
+
bremsstrahlung = (pp[-3] + pp[-2] * (e_0 - energy_scale) / energy_scale +
|
|
470
|
+
pp[-1] * (e_0 - energy_scale)**2 / energy_scale)
|
|
471
|
+
model += bremsstrahlung
|
|
472
|
+
model *= efficiency
|
|
473
|
+
err = np.abs(yy - model) # /np.sqrt(np.abs(yy[start:])+1e-12)
|
|
474
|
+
return err
|
|
475
|
+
|
|
476
|
+
y = np.array(spectrum) # .compute()
|
|
477
|
+
[p, _] = scipy.optimize.leastsq(residuals, pin, args=(y,), maxfev=10000)
|
|
478
|
+
|
|
479
|
+
update_fit_values(spectrum.metadata['EDS'], peaks, p)
|
|
480
|
+
return np.array(peaks), np.array(p)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def update_fit_values(out_tags, peaks, p):
|
|
484
|
+
"""
|
|
485
|
+
Update the out_tags dictionary with the fitted peak shapes and parameters.
|
|
486
|
+
|
|
487
|
+
Parameters:
|
|
488
|
+
- out_tags: Dictionary containing the initial tags for each element and line family.
|
|
489
|
+
- peaks: Array of fitted peak shapes.
|
|
490
|
+
- p: Array of fitted parameters.
|
|
491
|
+
"""
|
|
492
|
+
index = 0
|
|
493
|
+
for lines in out_tags.values():
|
|
494
|
+
if 'K-family' in lines:
|
|
495
|
+
lines['K-family']['areal_density'] = p[index]
|
|
496
|
+
lines['K-family']['peaks'] = peaks[index]
|
|
497
|
+
index += 1
|
|
498
|
+
if 'L-family' in lines:
|
|
499
|
+
lines['L-family']['areal_density'] = p[index]
|
|
500
|
+
lines['L-family']['peaks'] = peaks[index]
|
|
501
|
+
index += 1
|
|
502
|
+
if 'M-family' in lines:
|
|
503
|
+
lines['M-family']['areal_density'] =p[index]
|
|
504
|
+
lines['M-family']['peaks'] = peaks[index]
|
|
505
|
+
index += 1
|
|
506
|
+
out_tags['bremsstrahlung'] = p[-4:]
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def get_phases(dataset, mode='kmeans', number_of_phases=4):
|
|
510
|
+
"""
|
|
511
|
+
Perform phase segmentation on the dataset using the specified clustering mode.
|
|
512
|
+
|
|
513
|
+
Parameters:
|
|
514
|
+
- dataset: The dataset to be segmented.
|
|
515
|
+
- mode: The clustering mode to use ('kmeans' or other).
|
|
516
|
+
- number_of_phases: The number of phases (clusters) to identify.
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
None. The results are stored in the dataset's metadata.
|
|
520
|
+
"""
|
|
521
|
+
x_vec = np.array(dataset).reshape(dataset.shape[0]*dataset.shape[1], dataset.shape[2])
|
|
522
|
+
x_vec = np.divide(x_vec.T, x_vec.sum(axis=1)).T
|
|
523
|
+
if mode != 'kmeans':
|
|
524
|
+
#choose number of components
|
|
525
|
+
gmm = sklearn.mixture.GaussianMixture(n_components=number_of_phases, covariance_type="full")
|
|
526
|
+
|
|
527
|
+
gmm_results = gmm.fit(np.array(x_vec)) #we can intelligently fold the data and perform GM
|
|
528
|
+
gmm_labels = gmm_results.fit_predict(x_vec)
|
|
529
|
+
|
|
530
|
+
dataset.metadata['gaussian_mixing_model'] = {'map': gmm_labels.reshape(dataset.shape[0],
|
|
531
|
+
dataset.shape[1]),
|
|
532
|
+
'covariances': gmm.covariances_,
|
|
533
|
+
'weights': gmm.weights_,
|
|
534
|
+
'means': gmm_results.means_}
|
|
535
|
+
else:
|
|
536
|
+
km = sklearn.cluster.KMeans(number_of_phases, n_init =10) #choose number of clusters
|
|
537
|
+
km_results = km.fit(np.array(x_vec)) #we can intelligently fold the data and perform Kmeans
|
|
538
|
+
dataset.metadata['kmeans'] = {'map': km_results.labels_.reshape(dataset.shape[0],
|
|
539
|
+
dataset.shape[1]),
|
|
540
|
+
'means': km_results.cluster_centers_}
|
|
541
|
+
|
|
542
|
+
def plot_phases(dataset, image=None, survey_image=None):
|
|
543
|
+
"""
|
|
544
|
+
Plot the phase maps and corresponding spectra from the dataset.
|
|
545
|
+
|
|
546
|
+
Parameters:
|
|
547
|
+
- dataset: The dataset containing phase information.
|
|
548
|
+
- image: Optional. The image to overlay the phase map on.
|
|
549
|
+
- survey_image: Optional. A survey image to display alongside the phase maps.
|
|
550
|
+
"""
|
|
551
|
+
if survey_image is not None:
|
|
552
|
+
ncols = 3
|
|
553
|
+
else:
|
|
554
|
+
ncols = 2
|
|
555
|
+
axis_index = 0
|
|
556
|
+
fig, axes = plt.subplots(nrows=1, ncols=ncols, figsize = (10,3))
|
|
557
|
+
if survey_image is not None:
|
|
558
|
+
im = axes[0].imshow(survey_image.T)
|
|
559
|
+
axis_index += 1
|
|
560
|
+
|
|
561
|
+
if 'kmeans' not in dataset.metadata:
|
|
562
|
+
raise ValueError('No phase information found, run get_phases first')
|
|
563
|
+
phase_spectra = dataset.metadata['kmeans']['means']
|
|
564
|
+
map_data = dataset.metadata['kmeans']['map']
|
|
565
|
+
|
|
566
|
+
cmap = plt.get_cmap('jet', len(phase_spectra))
|
|
567
|
+
im = axes[axis_index].imshow(image.T,cmap='gray')
|
|
568
|
+
im = axes[axis_index].imshow(map_data.T, cmap=cmap,vmin=np.min(map_data) - 0.5,
|
|
569
|
+
vmax=np.max(map_data) + 0.5,alpha=0.2)
|
|
570
|
+
|
|
571
|
+
cbar = fig.colorbar(im, ax=axes[axis_index])
|
|
572
|
+
cbar.ax.set_yticks(np.arange(0, len(phase_spectra) ))
|
|
573
|
+
cbar.ax.set_ylabel("GMM Phase", fontsize = 14)
|
|
574
|
+
axis_index += 1
|
|
575
|
+
for index, spectrum in enumerate(phase_spectra):
|
|
576
|
+
axes[axis_index].plot(dataset.energy/1000, spectrum, color = cmap(index), label=str(index))
|
|
577
|
+
axes[axis_index].set_xlabel('energy (keV)')
|
|
578
|
+
plt.legend()
|
|
579
|
+
plt.tight_layout()
|
|
580
|
+
plt.show()
|
|
581
|
+
return fig
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def plot_lines(eds_quantification: dict, axis: plt.Axes):
|
|
585
|
+
"""
|
|
586
|
+
Plot EDS line strengths on the given matplotlib axis.
|
|
587
|
+
|
|
588
|
+
Parameters:
|
|
589
|
+
- eds_quantification: A dictionary containing EDS line data.
|
|
590
|
+
- axis: A matplotlib Axes object where the lines will be plotted.
|
|
591
|
+
"""
|
|
592
|
+
colors = plt.get_cmap('Dark2').colors # jet(np.linspace(0, 1, 10))
|
|
593
|
+
|
|
594
|
+
index = 0
|
|
595
|
+
for key, lines in eds_quantification.items():
|
|
596
|
+
color = colors[index % len(colors)]
|
|
597
|
+
if 'K-family' in lines:
|
|
598
|
+
intensity = lines['K-family']['height']
|
|
599
|
+
for line in lines['K-family']:
|
|
600
|
+
if line[0] == 'K':
|
|
601
|
+
pos = lines['K-family'][line]['position']
|
|
602
|
+
axis.plot([pos,pos], [0, intensity*lines['K-family'][line]['weight']],
|
|
603
|
+
color=color)
|
|
604
|
+
if line == lines['K-family']['main']:
|
|
605
|
+
axis.text(pos,0, key+'\n'+line, verticalalignment='top', color=color)
|
|
606
|
+
|
|
607
|
+
if 'L-family' in lines:
|
|
608
|
+
intensity = lines['L-family']['height']
|
|
609
|
+
for line in lines['L-family']:
|
|
610
|
+
if line[0] == 'L':
|
|
611
|
+
pos = lines['L-family'][line]['position']
|
|
612
|
+
axis.plot([pos,pos], [0, intensity*lines['L-family'][line]['weight']],
|
|
613
|
+
color=color)
|
|
614
|
+
if line in [lines['L-family']['main'], 'L3-M5', 'L3-N5', 'L1-M3']:
|
|
615
|
+
axis.text(pos,0, key+'\n'+line, verticalalignment='top', color=color)
|
|
616
|
+
|
|
617
|
+
if 'M-family' in lines:
|
|
618
|
+
intensity = lines['M-family']['height']
|
|
619
|
+
for line in lines['M-family']:
|
|
620
|
+
if line[0] == 'M':
|
|
621
|
+
pos = lines['M-family'][line]['position']
|
|
622
|
+
axis.plot([pos,pos],
|
|
623
|
+
[0, intensity*lines['M-family'][line]['weight']],
|
|
624
|
+
color=color)
|
|
625
|
+
if line in [lines['M-family']['main'], 'M5-N7', 'M4-N6']:
|
|
626
|
+
axis.text(pos,0, key+'\n'+line, verticalalignment='top', color=color)
|
|
627
|
+
|
|
628
|
+
index +=1
|
|
629
|
+
index = index % 10
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def get_eds_xsection(x_section, energy_scale, start_bgd, end_bgd):
|
|
633
|
+
"""
|
|
634
|
+
Calculate the EDS cross-section by subtracting the background and zeroing out
|
|
635
|
+
values outside the specified energy range.
|
|
636
|
+
The processed cross-section data with background removed
|
|
637
|
+
and values outside the energy range set to zero.
|
|
638
|
+
|
|
639
|
+
Parameters:
|
|
640
|
+
- x_section: The raw cross-section data.
|
|
641
|
+
- energy_scale: The energy scale corresponding to the cross-section data.
|
|
642
|
+
- start_bgd: The start energy for background calculation.
|
|
643
|
+
- end_bgd: The end energy for background calculation.
|
|
644
|
+
|
|
645
|
+
Returns:
|
|
646
|
+
- cross_section_core: np.array
|
|
647
|
+
"""
|
|
648
|
+
background = pyTEMlib.eels_tools.power_law_background(x_section, energy_scale,
|
|
649
|
+
[start_bgd, end_bgd], verbose=False)
|
|
650
|
+
cross_section_core = x_section- background[0]
|
|
651
|
+
cross_section_core[cross_section_core < 0] = 0.0
|
|
652
|
+
cross_section_core[energy_scale < end_bgd] = 0.0
|
|
653
|
+
return cross_section_core
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def add_k_factors(element_dict, element, k_factors):
|
|
657
|
+
"""Add k-factors to element dictionary."""
|
|
658
|
+
family = element_dict.get('K-family', {})
|
|
659
|
+
line = k_factors.get(element, {}).get('Ka1', False)
|
|
660
|
+
if not line:
|
|
661
|
+
line = k_factors.get(element, {}).get('Ka2', False)
|
|
662
|
+
if not line:
|
|
663
|
+
family = element_dict.get('L-family', {})
|
|
664
|
+
line = k_factors.get(element, {}).get('La1', False)
|
|
665
|
+
family['k_factor'] = float(line)
|
|
666
|
+
print('using L k-factor for', element)
|
|
667
|
+
if not line:
|
|
668
|
+
family = element_dict.get('M-family', False)
|
|
669
|
+
line = k_factors.get(element, {}).get('Ma1', False)
|
|
670
|
+
if line:
|
|
671
|
+
print('using M k-factor for', element)
|
|
672
|
+
family['k_factor'] = float(line)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def quantify_eds(spectrum, quantification_dict=None, mask=None ):
|
|
676
|
+
"""Calculate quantification for EDS spectrum with either k-factors or cross sections."""
|
|
677
|
+
|
|
678
|
+
for key in spectrum.metadata['EDS']:
|
|
679
|
+
element = 0
|
|
680
|
+
if isinstance(spectrum.metadata['EDS'][key], dict) and key in elements_list:
|
|
681
|
+
element = spectrum.metadata['EDS'][key].get('Z', 0)
|
|
682
|
+
if element < 1:
|
|
683
|
+
continue
|
|
684
|
+
if quantification_dict is None:
|
|
685
|
+
quantification_dict = {}
|
|
686
|
+
|
|
687
|
+
edge_info = pyTEMlib.eels_tools.get_x_sections(element)
|
|
688
|
+
spectrum.metadata['EDS'][key]['atomic_weight'] = edge_info['atomic_weight']
|
|
689
|
+
spectrum.metadata['EDS'][key]['nominal_density'] = edge_info['nominal_density']
|
|
690
|
+
|
|
691
|
+
for family, item in edge_info['fluorescent_yield'].items():
|
|
692
|
+
if spectrum.metadata['EDS'][key].get(f"{family}-family", {}):
|
|
693
|
+
spectrum.metadata['EDS'][key][f"{family}-family"]['fluorescent_yield'] = item
|
|
694
|
+
if quantification_dict.get('metadata', {}).get('type', '') == 'k_factor':
|
|
695
|
+
k_factors = quantification_dict.get('table', {})
|
|
696
|
+
add_k_factors(spectrum.metadata['EDS'][key], key, k_factors)
|
|
697
|
+
if quantification_dict is None:
|
|
698
|
+
print('using cross sections for quantification')
|
|
699
|
+
quantify_cross_section(spectrum, mask=mask)
|
|
700
|
+
elif not isinstance(quantification_dict, dict):
|
|
701
|
+
pass
|
|
702
|
+
elif quantification_dict.get('metadata', {}).get('type', '') == 'k_factor':
|
|
703
|
+
print('using k-factors for quantification')
|
|
704
|
+
quantification_k_factors(spectrum, mask=mask) # , quantification_dict['table'],
|
|
705
|
+
elif quantification_dict.get('metadata', {}).get('type', '') == 'cross_section':
|
|
706
|
+
print('using cross sections for quantification')
|
|
707
|
+
quantify_cross_section(spectrum, quantification_dict['table'], mask)
|
|
708
|
+
else:
|
|
709
|
+
print('using cross sections for quantification')
|
|
710
|
+
quantify_cross_section(spectrum, mask=mask)
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def read_esl_k_factors(filename, reduced=False):
|
|
714
|
+
""" Read k-factors from esl file."""
|
|
715
|
+
k_factors = {}
|
|
716
|
+
if not os.path.isfile(filename):
|
|
717
|
+
print('k-factor file not found', filename)
|
|
718
|
+
return None, 'k_factors_Bruker_15keV.json'
|
|
719
|
+
tree = xml.etree.ElementTree.parse(filename)
|
|
720
|
+
root = tree.getroot()
|
|
721
|
+
k_dict = pyTEMlib.file_reader.etree_to_dict(root)
|
|
722
|
+
k_dict = k_dict.get('TRTStandardLibrary', {})
|
|
723
|
+
k_factor_dict = (k_dict.get('ClassInstance', {}).get('CliffLorimerFactors', {}))
|
|
724
|
+
for index, item in enumerate(k_factor_dict.get('K_Factors', '').split(',')):
|
|
725
|
+
if index < 84:
|
|
726
|
+
if item.strip() != '0':
|
|
727
|
+
k_factors[elements_list[index]] = {'Ka1': float(item)}
|
|
728
|
+
else:
|
|
729
|
+
k_factors[elements_list[index]] = {}
|
|
730
|
+
for index, item in enumerate(k_factor_dict.get('L_Factors', '').split(',')):
|
|
731
|
+
if index < 84:
|
|
732
|
+
if item.strip() != '0':
|
|
733
|
+
k_factors[elements_list[index]]['La1'] = float(item)
|
|
734
|
+
for index, item in enumerate(k_factor_dict.get('M_Factors', '').split(',')):
|
|
735
|
+
if index < 84:
|
|
736
|
+
if item.strip() != '0':
|
|
737
|
+
k_factors[elements_list[index]]['Ma1'] = float(item)
|
|
738
|
+
primary = int(float(k_dict.get('ClassInstance', {}).get('Header', {}).get('PrimaryEnergy', 0)))
|
|
739
|
+
name = f'k_factors_Bruker_{primary}keV.json'
|
|
740
|
+
metadata = {'origin': 'pyTEMlib',
|
|
741
|
+
'source_file': filename,
|
|
742
|
+
'reduced': reduced,
|
|
743
|
+
'version': pyTEMlib.__version__,
|
|
744
|
+
'type': 'k-factors',
|
|
745
|
+
'spectroscopy': 'EDS',
|
|
746
|
+
'acceleration_voltage': primary,
|
|
747
|
+
'microscope': 'Bruker',
|
|
748
|
+
'name': name}
|
|
749
|
+
return k_factors, metadata
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def get_absorption_correction(spectrum, thickness=50):
|
|
753
|
+
"""
|
|
754
|
+
Calculate absorption correction for all elements in the spectrum based on thickness t in nm
|
|
755
|
+
Updates the element in spectrum.metadata['EDS']['GUI'] dictionary
|
|
756
|
+
Parameters:
|
|
757
|
+
- spectrum: A sidpy.Dataset object containing the spectral data and metadata.
|
|
758
|
+
- t: Thickness in nm
|
|
759
|
+
Returns:
|
|
760
|
+
None
|
|
761
|
+
"""
|
|
762
|
+
start_channel = np.searchsorted(spectrum.energy_scale.values, 120)
|
|
763
|
+
absorption = spectrum.energy_scale.values[start_channel:]*0.
|
|
764
|
+
take_off_angle = spectrum.metadata['EDS']['detector'].get('ElevationAngle', 0)
|
|
765
|
+
path_length = thickness *2 / np.cos(take_off_angle) * 1e-9 # /2? in m
|
|
766
|
+
count = 1
|
|
767
|
+
for element, lines in spectrum.metadata['EDS']['GUI'].items():
|
|
768
|
+
if element in elements_list:
|
|
769
|
+
part = lines['atom%']/100
|
|
770
|
+
if part > 0.01:
|
|
771
|
+
count += 1
|
|
772
|
+
absorption += get_absorption(pyTEMlib.utilities.get_atomic_number(element),
|
|
773
|
+
path_length*part,
|
|
774
|
+
spectrum.energy_scale[start_channel:])
|
|
775
|
+
|
|
776
|
+
for element, lines in spectrum.metadata['EDS']['GUI'].items():
|
|
777
|
+
symmetry = lines['symmetry']
|
|
778
|
+
peaks = []
|
|
779
|
+
if symmetry in spectrum.metadata['EDS'][element]:
|
|
780
|
+
peaks = spectrum.metadata['EDS'][element][symmetry].get('peaks', [])
|
|
781
|
+
if len(peaks) > 0:
|
|
782
|
+
peaks = peaks[start_channel:]
|
|
783
|
+
lines['absorption'] = (peaks * absorption / count).sum()
|
|
784
|
+
lines['thickness'] = thickness
|
|
785
|
+
else:
|
|
786
|
+
lines['absorption'] = 1.0
|
|
787
|
+
lines['thickness'] = 0.0
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def apply_absorption_correction(spectrum, thickness):
|
|
791
|
+
"""
|
|
792
|
+
Apply Absorption Correction to Quantification
|
|
793
|
+
Updates the element in spectrum.metadata['EDS']['GUI'] dictionary
|
|
794
|
+
Parameters:
|
|
795
|
+
- spectrum: A sidpy.Dataset object containing the spectral data and metadata.
|
|
796
|
+
- thickness: Thickness in nm
|
|
797
|
+
Returns:
|
|
798
|
+
None
|
|
799
|
+
"""
|
|
800
|
+
get_absorption_correction(spectrum, thickness)
|
|
801
|
+
|
|
802
|
+
atom_sum = 0.
|
|
803
|
+
weight_sum = 0.
|
|
804
|
+
for lines in spectrum.metadata['EDS']['GUI'].values():
|
|
805
|
+
atom_sum += lines.get('atom%', 0) / lines.get('absorption', 1)
|
|
806
|
+
weight_sum += lines.get('weight%', 0) / lines.get('absorption', 1)
|
|
807
|
+
for lines in spectrum.metadata['EDS']['GUI'].values():
|
|
808
|
+
absorb = lines.get('absorption', 1)
|
|
809
|
+
lines['corrected-atom%'] = lines.get('atom%', 0) / absorb / atom_sum * 100
|
|
810
|
+
lines['corrected-weight%'] = lines.get('weight%', 0) / absorb / weight_sum * 100
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def read_csv_k_factors(filename, reduced=True):
|
|
814
|
+
""" Read k-factors from csv file of ThermoFisher TEMs."""
|
|
815
|
+
k_factors = {}
|
|
816
|
+
with open(filename, newline='', encoding='utf-8') as csvfile:
|
|
817
|
+
reader = csv.reader(csvfile, delimiter=',', quotechar='|')
|
|
818
|
+
start = True
|
|
819
|
+
for row in reader:
|
|
820
|
+
if start:
|
|
821
|
+
k_column = row.index('K-factor')
|
|
822
|
+
start = False
|
|
823
|
+
else:
|
|
824
|
+
element, line = row[0].split('-')
|
|
825
|
+
if element not in k_factors:
|
|
826
|
+
k_factors[element] = {}
|
|
827
|
+
if reduced:
|
|
828
|
+
if line[-1:] == '1':
|
|
829
|
+
k_factors[element][line] = row[k_column]
|
|
830
|
+
else:
|
|
831
|
+
k_factors[element][line] = row[k_column]
|
|
832
|
+
metadata = {'origin': 'pyTEMlib',
|
|
833
|
+
'source_file': filename,
|
|
834
|
+
'reduced': reduced,
|
|
835
|
+
'microscope': 'ThermoFisher',
|
|
836
|
+
'acceleration_voltage': 200000,
|
|
837
|
+
'version': pyTEMlib.__version__,
|
|
838
|
+
'type': 'k-factors',
|
|
839
|
+
'spectroscopy': 'EDS',
|
|
840
|
+
'name': 'k_factors_Thermo_200keV.json'}
|
|
841
|
+
return k_factors, metadata
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def convert_k_factor_file(file_name, reduced=True, new_name=None):
|
|
845
|
+
""" Convert k-factor file to a dictionary."""
|
|
846
|
+
if not os.path.isfile(file_name):
|
|
847
|
+
print('k-factor file not found', file_name)
|
|
848
|
+
return None
|
|
849
|
+
_, filename = os.path.split(file_name)
|
|
850
|
+
_, extension = os.path.splitext(filename)
|
|
851
|
+
if extension == '.csv':
|
|
852
|
+
k_factors, metadata = read_csv_k_factors(file_name, reduced=reduced)
|
|
853
|
+
elif extension == '.esl':
|
|
854
|
+
k_factors, metadata = read_esl_k_factors(file_name)
|
|
855
|
+
else:
|
|
856
|
+
print('unknown k-factor file format', extension)
|
|
857
|
+
return None
|
|
858
|
+
if new_name is None:
|
|
859
|
+
new_name = metadata['name']
|
|
860
|
+
write_k_factors(k_factors, metadata, file_name=new_name)
|
|
861
|
+
return k_factors, metadata
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def get_k_factor_files():
|
|
865
|
+
""" Get list of k-factor files in the .pyTEMlib folder."""
|
|
866
|
+
k_factor_files = []
|
|
867
|
+
for file_name in os.listdir(config_path):
|
|
868
|
+
if 'k_factors' in file_name:
|
|
869
|
+
k_factor_files.append(file_name)
|
|
870
|
+
return k_factor_files
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def write_k_factors(k_factors, metadata, file_name='k_factors.json'):
|
|
874
|
+
""" Write k-factors to a json file."""
|
|
875
|
+
file_name = os.path.join(config_path, file_name)
|
|
876
|
+
save_dict = {"table" : k_factors, "metadata" : metadata}
|
|
877
|
+
with open(file_name, "w", encoding='utf-8') as json_file:
|
|
878
|
+
json.dump(save_dict, json_file, indent=4, encoding='utf-8')
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def read_k_factors(file_name='k_factors.json'):
|
|
882
|
+
""" Read k-factors from a json file."""
|
|
883
|
+
if not os.path.isfile(os.path.join(config_path, file_name)):
|
|
884
|
+
print('k-factor file not found', file_name)
|
|
885
|
+
return None
|
|
886
|
+
with open(os.path.join(config_path, file_name), 'r', encoding='utf-8') as json_file:
|
|
887
|
+
table, metadata = json.load(json_file)
|
|
888
|
+
return table, metadata
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def load_k_factors(reduced=True):
|
|
892
|
+
""" Load k-factors from csv files in the .pyTEMlib folder."""
|
|
893
|
+
k_factors = {}
|
|
894
|
+
metadata = {}
|
|
895
|
+
data_path = os.path.join(os.path.expanduser('~'), '.pyTEMlib')
|
|
896
|
+
for file_name in os.listdir(data_path):
|
|
897
|
+
if 'k-factors' in file_name:
|
|
898
|
+
path = os.path.join(data_path, file_name)
|
|
899
|
+
k_factors, metadata = read_csv_k_factors(path, reduced=reduced)
|
|
900
|
+
metadata['type'] = 'k_factor'
|
|
901
|
+
return {'table': k_factors, 'metadata': metadata}
|