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.
Files changed (92) hide show
  1. build/lib/pyTEMlib/__init__.py +36 -0
  2. build/lib/pyTEMlib/animation.py +667 -0
  3. build/lib/pyTEMlib/atom_tools.py +229 -0
  4. build/lib/pyTEMlib/config_dir.py +43 -0
  5. build/lib/pyTEMlib/crystal_tools.py +1219 -0
  6. build/lib/pyTEMlib/diffraction_plot.py +757 -0
  7. build/lib/pyTEMlib/dynamic_scattering.py +298 -0
  8. build/lib/pyTEMlib/eds_tools.py +901 -0
  9. build/lib/pyTEMlib/eds_xsections.py +268 -0
  10. build/lib/pyTEMlib/eels_tools/__init__.py +44 -0
  11. build/lib/pyTEMlib/eels_tools/core_loss_tools.py +751 -0
  12. build/lib/pyTEMlib/eels_tools/eels_database.py +134 -0
  13. build/lib/pyTEMlib/eels_tools/low_loss_tools.py +655 -0
  14. build/lib/pyTEMlib/eels_tools/peak_fit_tools.py +175 -0
  15. build/lib/pyTEMlib/eels_tools/zero_loss_tools.py +264 -0
  16. build/lib/pyTEMlib/file_reader.py +275 -0
  17. build/lib/pyTEMlib/file_tools.py +816 -0
  18. build/lib/pyTEMlib/get_bote_salvat.py +69 -0
  19. build/lib/pyTEMlib/graph_tools.py +1153 -0
  20. build/lib/pyTEMlib/graph_viz.py +599 -0
  21. build/lib/pyTEMlib/image/__init__.py +37 -0
  22. build/lib/pyTEMlib/image/image_atoms.py +270 -0
  23. build/lib/pyTEMlib/image/image_clean.py +198 -0
  24. build/lib/pyTEMlib/image/image_distortion.py +299 -0
  25. build/lib/pyTEMlib/image/image_fft.py +278 -0
  26. build/lib/pyTEMlib/image/image_graph.py +926 -0
  27. build/lib/pyTEMlib/image/image_registration.py +316 -0
  28. build/lib/pyTEMlib/image/image_utilities.py +308 -0
  29. build/lib/pyTEMlib/image/image_window.py +421 -0
  30. build/lib/pyTEMlib/image_tools.py +701 -0
  31. build/lib/pyTEMlib/interactive_image.py +1 -0
  32. build/lib/pyTEMlib/kinematic_scattering.py +1145 -0
  33. build/lib/pyTEMlib/microscope.py +62 -0
  34. build/lib/pyTEMlib/probe_tools.py +928 -0
  35. build/lib/pyTEMlib/sidpy_tools.py +153 -0
  36. build/lib/pyTEMlib/simulation_tools.py +104 -0
  37. build/lib/pyTEMlib/test.py +437 -0
  38. build/lib/pyTEMlib/utilities.py +431 -0
  39. build/lib/pyTEMlib/version.py +5 -0
  40. build/lib/pyTEMlib/xrpa_x_sections.py +20976 -0
  41. pyTEMlib/__init__.py +36 -0
  42. pyTEMlib/animation.py +667 -0
  43. pyTEMlib/atom_tools.py +229 -0
  44. pyTEMlib/config_dir.py +43 -0
  45. pyTEMlib/crystal_tools.py +1219 -0
  46. pyTEMlib/data/k_factors_Bruker_15keV.json +293 -0
  47. pyTEMlib/data/k_factors_Thermo_200keV.json +494 -0
  48. pyTEMlib/data/xrays_X_section_100kV.json +19334 -0
  49. pyTEMlib/data/xrays_X_section_200kV.json +18711 -0
  50. pyTEMlib/data/xrays_X_section_300kV.json +18347 -0
  51. pyTEMlib/data/xrays_X_section_60kV.json +20202 -0
  52. pyTEMlib/diffraction_plot.py +757 -0
  53. pyTEMlib/dynamic_scattering.py +298 -0
  54. pyTEMlib/eds_tools.py +901 -0
  55. pyTEMlib/eds_xsections.py +268 -0
  56. pyTEMlib/eels_tools/__init__.py +44 -0
  57. pyTEMlib/eels_tools/core_loss_tools.py +751 -0
  58. pyTEMlib/eels_tools/eels_database.py +134 -0
  59. pyTEMlib/eels_tools/low_loss_tools.py +655 -0
  60. pyTEMlib/eels_tools/peak_fit_tools.py +175 -0
  61. pyTEMlib/eels_tools/zero_loss_tools.py +264 -0
  62. pyTEMlib/file_reader.py +275 -0
  63. pyTEMlib/file_tools.py +816 -0
  64. pyTEMlib/get_bote_salvat.py +69 -0
  65. pyTEMlib/graph_tools.py +1153 -0
  66. pyTEMlib/graph_viz.py +599 -0
  67. pyTEMlib/image/__init__.py +37 -0
  68. pyTEMlib/image/image_atoms.py +270 -0
  69. pyTEMlib/image/image_clean.py +198 -0
  70. pyTEMlib/image/image_distortion.py +299 -0
  71. pyTEMlib/image/image_fft.py +278 -0
  72. pyTEMlib/image/image_graph.py +926 -0
  73. pyTEMlib/image/image_registration.py +316 -0
  74. pyTEMlib/image/image_utilities.py +308 -0
  75. pyTEMlib/image/image_window.py +421 -0
  76. pyTEMlib/image_tools.py +701 -0
  77. pyTEMlib/interactive_image.py +1 -0
  78. pyTEMlib/kinematic_scattering.py +1145 -0
  79. pyTEMlib/microscope.py +62 -0
  80. pyTEMlib/probe_tools.py +928 -0
  81. pyTEMlib/sidpy_tools.py +153 -0
  82. pyTEMlib/simulation_tools.py +104 -0
  83. pyTEMlib/test.py +437 -0
  84. pyTEMlib/utilities.py +431 -0
  85. pyTEMlib/version.py +5 -0
  86. pyTEMlib/xrpa_x_sections.py +20976 -0
  87. pytemlib-0.2025.12.0.dist-info/METADATA +100 -0
  88. pytemlib-0.2025.12.0.dist-info/RECORD +92 -0
  89. pytemlib-0.2025.12.0.dist-info/WHEEL +5 -0
  90. pytemlib-0.2025.12.0.dist-info/entry_points.txt +2 -0
  91. pytemlib-0.2025.12.0.dist-info/licenses/LICENSE +21 -0
  92. 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}