pyTEMlib 0.2025.4.2__py3-none-any.whl → 0.2025.9.1__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.

Potentially problematic release.


This version of pyTEMlib might be problematic. Click here for more details.

Files changed (94) hide show
  1. build/lib/pyTEMlib/__init__.py +33 -0
  2. build/lib/pyTEMlib/animation.py +640 -0
  3. build/lib/pyTEMlib/atom_tools.py +238 -0
  4. build/lib/pyTEMlib/config_dir.py +31 -0
  5. build/lib/pyTEMlib/crystal_tools.py +1219 -0
  6. build/lib/pyTEMlib/diffraction_plot.py +756 -0
  7. build/lib/pyTEMlib/dynamic_scattering.py +293 -0
  8. build/lib/pyTEMlib/eds_tools.py +826 -0
  9. build/lib/pyTEMlib/eds_xsections.py +432 -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 +274 -0
  17. build/lib/pyTEMlib/file_tools.py +811 -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 +197 -0
  24. build/lib/pyTEMlib/image/image_distortion.py +299 -0
  25. build/lib/pyTEMlib/image/image_fft.py +277 -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 +309 -0
  29. build/lib/pyTEMlib/image/image_window.py +421 -0
  30. build/lib/pyTEMlib/image_tools.py +699 -0
  31. build/lib/pyTEMlib/interactive_image.py +1 -0
  32. build/lib/pyTEMlib/kinematic_scattering.py +1196 -0
  33. build/lib/pyTEMlib/microscope.py +61 -0
  34. build/lib/pyTEMlib/probe_tools.py +906 -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 +314 -0
  39. build/lib/pyTEMlib/version.py +5 -0
  40. build/lib/pyTEMlib/xrpa_x_sections.py +20976 -0
  41. pyTEMlib/__init__.py +25 -3
  42. pyTEMlib/animation.py +31 -22
  43. pyTEMlib/atom_tools.py +29 -34
  44. pyTEMlib/config_dir.py +2 -28
  45. pyTEMlib/crystal_tools.py +129 -165
  46. pyTEMlib/eds_tools.py +559 -342
  47. pyTEMlib/eds_xsections.py +432 -0
  48. pyTEMlib/eels_tools/__init__.py +44 -0
  49. pyTEMlib/eels_tools/core_loss_tools.py +751 -0
  50. pyTEMlib/eels_tools/eels_database.py +134 -0
  51. pyTEMlib/eels_tools/low_loss_tools.py +655 -0
  52. pyTEMlib/eels_tools/peak_fit_tools.py +175 -0
  53. pyTEMlib/eels_tools/zero_loss_tools.py +264 -0
  54. pyTEMlib/file_reader.py +274 -0
  55. pyTEMlib/file_tools.py +260 -1130
  56. pyTEMlib/get_bote_salvat.py +69 -0
  57. pyTEMlib/graph_tools.py +101 -174
  58. pyTEMlib/graph_viz.py +150 -0
  59. pyTEMlib/image/__init__.py +37 -0
  60. pyTEMlib/image/image_atoms.py +270 -0
  61. pyTEMlib/image/image_clean.py +197 -0
  62. pyTEMlib/image/image_distortion.py +299 -0
  63. pyTEMlib/image/image_fft.py +277 -0
  64. pyTEMlib/image/image_graph.py +926 -0
  65. pyTEMlib/image/image_registration.py +316 -0
  66. pyTEMlib/image/image_utilities.py +309 -0
  67. pyTEMlib/image/image_window.py +421 -0
  68. pyTEMlib/image_tools.py +154 -928
  69. pyTEMlib/kinematic_scattering.py +1 -1
  70. pyTEMlib/probe_tools.py +1 -1
  71. pyTEMlib/test.py +437 -0
  72. pyTEMlib/utilities.py +314 -0
  73. pyTEMlib/version.py +2 -3
  74. pyTEMlib/xrpa_x_sections.py +14 -10
  75. {pytemlib-0.2025.4.2.dist-info → pytemlib-0.2025.9.1.dist-info}/METADATA +13 -16
  76. pytemlib-0.2025.9.1.dist-info/RECORD +86 -0
  77. {pytemlib-0.2025.4.2.dist-info → pytemlib-0.2025.9.1.dist-info}/WHEEL +1 -1
  78. pytemlib-0.2025.9.1.dist-info/top_level.txt +6 -0
  79. pyTEMlib/core_loss_widget.py +0 -721
  80. pyTEMlib/eels_dialog.py +0 -754
  81. pyTEMlib/eels_dialog_utilities.py +0 -1199
  82. pyTEMlib/eels_tools.py +0 -2359
  83. pyTEMlib/file_tools_qt.py +0 -193
  84. pyTEMlib/image_dialog.py +0 -158
  85. pyTEMlib/image_dlg.py +0 -146
  86. pyTEMlib/info_widget.py +0 -1086
  87. pyTEMlib/info_widget3.py +0 -1120
  88. pyTEMlib/low_loss_widget.py +0 -479
  89. pyTEMlib/peak_dialog.py +0 -1129
  90. pyTEMlib/peak_dlg.py +0 -286
  91. pytemlib-0.2025.4.2.dist-info/RECORD +0 -38
  92. pytemlib-0.2025.4.2.dist-info/top_level.txt +0 -1
  93. {pytemlib-0.2025.4.2.dist-info → pytemlib-0.2025.9.1.dist-info}/entry_points.txt +0 -0
  94. {pytemlib-0.2025.4.2.dist-info → pytemlib-0.2025.9.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,751 @@
1
+ """
2
+ #################################################################
3
+ # Core-Loss functions
4
+ #################################################################
5
+ of eels_tools
6
+ Model based quantification of electron energy-loss data
7
+ Copyright by Gerd Duscher
8
+
9
+ The University of Tennessee, Knoxville
10
+ Department of Materials Science & Engineering
11
+
12
+ Sources:
13
+ M. Tian et al.
14
+
15
+ Units:
16
+ everything is in SI units, except length is given in nm and angles in mrad.
17
+
18
+ Usage:
19
+ See the notebooks for examples of these routines
20
+
21
+ All the input and output is done through a dictionary which is to be found in the meta_data
22
+ attribute of the sidpy.Dataset
23
+
24
+ Update by Austin Houston, UTK 12-2023 : Parallization of spectrum images
25
+ """
26
+ from typing import Union
27
+ import numpy as np
28
+
29
+ import scipy
30
+ import sidpy
31
+
32
+ from ..utilities import major_edges, all_edges, elements
33
+ from ..utilities import effective_collection_angle
34
+ from ..utilities import get_z, get_x_sections, second_derivative
35
+
36
+
37
+
38
+ def list_all_edges(z: Union[str, int]=0, verbose=False)->list[str, dict]:
39
+ """List all ionization edges of an element with atomic number z
40
+
41
+ Parameters
42
+ ----------
43
+ z: int
44
+ atomic number
45
+ verbose: bool, optional
46
+ more info if set to True
47
+
48
+ Returns
49
+ -------
50
+ out_string: str
51
+ string with all major edges in energy range
52
+ """
53
+ x_section = get_x_sections(get_z(z))
54
+ out_string = ''
55
+ if verbose:
56
+ print('Major edges')
57
+ element = x_section.get('name', None)
58
+ edge_list = {element: {}}
59
+
60
+ for key in all_edges:
61
+ onset = x_section.get(key, {}).get('onset', None)
62
+ if onset is None:
63
+ continue
64
+ out = f" {element}-{key}: {onset:8.1f} eV "
65
+ if verbose:
66
+ print(out)
67
+ out_string = out_string + f"{out} /n"
68
+ edge_list[element][key] = onset
69
+ return out_string, edge_list
70
+
71
+
72
+ def find_all_edges(edge_onset: float,
73
+ maximal_chemical_shift: float=5.0,
74
+ major_edges_only: bool=False) -> str:
75
+ """Find all (major and minor) edges within an energy range
76
+
77
+ Parameters
78
+ ----------
79
+ edge_onset: float
80
+ approximate energy of ionization edge
81
+ maximal_chemical_shift: float, default = 5eV
82
+ range of energy window around edge_onset to look for major edges
83
+ major_edges_only: boolean, default = False
84
+ only major edges are considered if True
85
+ Returns
86
+ -------
87
+ text: str
88
+ string with all edges in energy range
89
+
90
+ """
91
+
92
+ out_text = ''
93
+ for z in np.arange(1, 93):
94
+ x_section = get_x_sections(z)
95
+ name = x_section.get('name', '')
96
+ for key in x_section:
97
+ if not isinstance(x_section[key], dict):
98
+ continue
99
+ onset = x_section[key].get('onset', 0)
100
+ if abs(onset - edge_onset) > maximal_chemical_shift:
101
+ continue
102
+ if major_edges_only:
103
+ if key in major_edges:
104
+ out_text += f"\n {name:2s}-{key}: {onset:8.1f} eV "
105
+ else:
106
+ out_text += f"\n {name:2s}-{key}: {onset:8.1f} eV "
107
+ return out_text
108
+
109
+
110
+ def find_associated_edges(dataset: sidpy.Dataset) -> None:
111
+ """Find edges associated with peaks in the dataset"""
112
+ onsets = []
113
+ core_loss = dataset.metadata.get('core_loss', {}).get('edges', {})
114
+ for key, edge in core_loss.items():
115
+ if key.isdigit():
116
+ onsets.append(edge['onset'])
117
+ core_loss[key]['associated_peaks'] = {}
118
+ peaks = dataset.metadata['peak_fit'].get('peaks', [])
119
+ for key, peak in enumerate(peaks):
120
+ distances = (onsets-peak[0]) * -1
121
+ distances[distances < -0.3] = 1e6
122
+ if np.min(distances) < 50:
123
+ index = np.argmin(distances)
124
+ core_loss[str(index)]['associated_peaks'][key] = peak
125
+
126
+
127
+ def find_white_lines(dataset: sidpy.Dataset) -> Union[None, dict]:
128
+ """Find white lines in the dataset"""
129
+ white_lines_out ={'sum': {}, 'ratio': {}}
130
+ white_lines = []
131
+ peaks = dataset.metadata.get('peak_fit', {}).get('peaks', [])
132
+ core_loss = dataset.metadata.get('core_loss', {})
133
+ for index, edge in core_loss.get('edges', {}).items():
134
+ if not index.isdigit():
135
+ continue
136
+ peaks = edge.get('associated_peaks', {})
137
+ if edge['symmetry'][-2:] == 'L3' and 'L3' in edge['all_edges']:
138
+ onset_l3 = edge['all_edges']['L3']['onset']
139
+ onset_l2 = edge['all_edges']['L2']['onset']
140
+ end_range1 = onset_l2 + edge['chemical_shift']
141
+ end_range2 = onset_l2*2 - onset_l3 + edge['chemical_shift']
142
+ white_lines = ['L3', 'L2']
143
+ elif edge['symmetry'][-2:] == 'M5' and 'M5' in edge['all_edges']:
144
+ onset_m5 = edge['all_edges']['M5']['onset']
145
+ onset_m4 = edge['all_edges']['M4']['onset']
146
+ end_range1 = onset_m4 + edge['chemical_shift']
147
+ end_range2 = onset_m4*2 - onset_m5 + edge['chemical_shift']
148
+ white_lines = ['M5', 'M4']
149
+ else:
150
+ continue
151
+ white_line_areas = [0., 0.]
152
+ for key, peak in peaks.items():
153
+ if not str(key).isdigit():
154
+ continue
155
+ area = np.sqrt(2 * np.pi) * peak[1] * np.abs(peak[2]/np.sqrt(2 * np.log(2)))
156
+ if peak[0] < end_range1:
157
+ white_line_areas[0] += area
158
+ elif peak[0] < end_range2:
159
+ white_line_areas[1] += area
160
+
161
+ edge['white_lines'] = {white_lines[0]: white_line_areas[0],
162
+ white_lines[1]: white_line_areas[1]}
163
+ reference_counts = edge['areal_density'] * core_loss['xsections'][int(index)].sum()
164
+ key = f"{edge['element']}-{white_lines[0]}+{white_lines[1]}"
165
+ white_lines_out['sum'][key] = (white_line_areas[0] + white_line_areas[1])/reference_counts
166
+ key = f"{edge['element']}-{white_lines[0]}/{white_lines[1]}"
167
+ white_lines_out['ratio'][key] = white_line_areas[0] / white_line_areas[1]
168
+ return white_lines_out
169
+
170
+
171
+ def find_edges(dataset: sidpy.Dataset) -> None:
172
+ """find edges within a sidpy.Dataset"""
173
+
174
+ energy_scale = dataset.get_spectral_dims(return_axis=True)[0].values
175
+
176
+ second_dif, noise_level = second_derivative(dataset)
177
+
178
+ [indices, peaks] = scipy.signal.find_peaks(second_dif, noise_level)
179
+
180
+ peaks['peak_positions'] = energy_scale[indices]
181
+ peaks['peak_indices'] = indices
182
+ edge_energies = [energy_scale[50]]
183
+ edge_indices = []
184
+
185
+ [indices, _] = scipy.signal.find_peaks(-second_dif, noise_level)
186
+ minima = energy_scale[indices]
187
+
188
+ for peak_number in range(len(peaks['peak_positions'])):
189
+ position = peaks['peak_positions'][peak_number]
190
+ if position - edge_energies[-1] > 20:
191
+ impossible = minima[minima < position]
192
+ impossible = impossible[impossible > position - 5]
193
+ if len(impossible) == 0:
194
+ possible = minima[minima > position]
195
+ possible = possible[possible < position + 5]
196
+ if len(possible) > 0:
197
+ edge_energies.append((position + possible[0])/2)
198
+ edge_indices.append(np.searchsorted(energy_scale, (position + possible[0])/2))
199
+
200
+ selected_edges = []
201
+ for peak in edge_indices:
202
+ if 525 < energy_scale[peak] < 533:
203
+ selected_edges.append('O-K1')
204
+ else:
205
+ selected_edge = ''
206
+ edges = find_all_edges(energy_scale[peak], 20, major_edges_only=True)
207
+ edges = edges.split('\n')
208
+ minimum_dist = 100.
209
+ for edge in edges[1:]:
210
+ edge = edge[:-3].split(':')
211
+ name = edge[0].strip()
212
+ energy = float(edge[1].strip())
213
+ if np.abs(energy - energy_scale[peak]) < minimum_dist:
214
+ minimum_dist = np.abs(energy - energy_scale[peak])
215
+ selected_edge = name
216
+ if selected_edge != '':
217
+ selected_edges.append(selected_edge)
218
+ return selected_edges
219
+
220
+
221
+ def assign_likely_edges(edge_channels: Union[list, np.ndarray], energy: np.ndarray):
222
+ """Assign likely edges to energy channels"""
223
+ edges_in_list = []
224
+ result = {}
225
+ for channel in edge_channels:
226
+ if channel not in edge_channels[edges_in_list]:
227
+ shift = 5
228
+ element_list = find_all_edges(energy[channel], maximal_chemical_shift=shift,
229
+ major_edges_only=True)
230
+ while len(element_list) < 1:
231
+ shift += 1
232
+ element_list = find_all_edges(energy[channel], maximal_chemical_shift=shift,
233
+ major_edges_only=True)
234
+ if len(element_list) > 1:
235
+ while len(element_list) > 0:
236
+ shift-=1
237
+ element_list = find_all_edges(energy[channel], maximal_chemical_shift=shift,
238
+ major_edges_only=True)
239
+ element_list = find_all_edges(energy[channel], maximal_chemical_shift=shift+1,
240
+ major_edges_only=True)
241
+ element = (element_list[:4]).strip()
242
+ z = get_z(element)
243
+ result[element] =[]
244
+ _, edge_list = list_all_edges(z)
245
+
246
+ for edge in edge_list.values():
247
+ possible_minor_edge = np.argmin(np.abs(energy[edge_channels]-edge))
248
+ if np.abs(energy[edge_channels[possible_minor_edge]]-edge) < 3:
249
+ edges_in_list.append(possible_minor_edge)
250
+ result[element].append(edge)
251
+ return result
252
+
253
+
254
+ def auto_id_edges(dataset):
255
+ """Automatically identifies edges in a dataset"""
256
+ edge_channels = identify_edges(dataset)
257
+ energy_scale = dataset.get_spectral_dims(return_axis=True)[0].values
258
+ found_edges = assign_likely_edges(edge_channels, energy_scale)
259
+ return found_edges
260
+
261
+
262
+ def identify_edges(dataset: sidpy.Dataset, noise_level: float=2.0):
263
+ """
264
+ Using first derivative to determine edge onsets
265
+ Any peak in first derivative higher than noise_level times standard deviation will be considered
266
+
267
+ Parameters
268
+ ----------
269
+ dataset: sidpy.Dataset
270
+ the spectrum
271
+ noise_level: float
272
+ ths number times standard deviation in first derivative decides
273
+ on whether an edge onset is significant
274
+
275
+ Return
276
+ ------
277
+ edge_channel: numpy.ndarray
278
+
279
+ """
280
+ energy_scale = dataset.get_spectral_dims(return_axis=True)[0]
281
+ dispersion = energy_scale.slope
282
+
283
+ spec = scipy.ndimage.gaussian_filter(dataset, 3/dispersion) # smooth with 3eV wideGaussian
284
+
285
+ first_derivative = spec - np.roll(spec, +2)
286
+ first_derivative[:3] = 0
287
+ first_derivative[-3:] = 0
288
+
289
+ # find if there is a strong edge at high energy_scale
290
+ noise_level = noise_level*np.std(first_derivative[3:50])
291
+ [edge_channels, _] = scipy.signal.find_peaks(first_derivative, noise_level)
292
+ return edge_channels
293
+
294
+
295
+ def add_element_to_dataset(dataset: sidpy.Dataset, z: Union[int, str]):
296
+ """Adds an element to the dataset"""
297
+ # We check whether this element is already in the
298
+ energy_scale = dataset.get_spectral_dims(return_axis=True)[0]
299
+
300
+ zz = get_z(z)
301
+ if 'edges' not in dataset.metadata:
302
+ dataset.metadata['edges'] = {'model': {}, 'use_low_loss': False}
303
+ index = 0
304
+ for key, edge in dataset.metadata['edges'].items():
305
+ if not key.isdigit():
306
+ continue
307
+ index += 1
308
+ if zz == edge.get('z', ''):
309
+ index = int(key)
310
+ break
311
+
312
+ major_edge = ''
313
+ minor_edge = ''
314
+ all_edges2 = {}
315
+ x_section = get_x_sections(zz)
316
+ edge_start = 10 # int(15./ft.get_slope(self.energy_scale)+0.5)
317
+ for key in x_section:
318
+ if len(key) == 2 and key[0] in ['K', 'L', 'M', 'N', 'O'] and key[1].isdigit():
319
+ if energy_scale[edge_start] < x_section[key]['onset'] < energy_scale[-edge_start]:
320
+ if key in ['K1', 'L3', 'M5', 'M3']:
321
+ major_edge = key
322
+ all_edges2[key] = {'onset': x_section[key]['onset']}
323
+
324
+ if major_edge != '':
325
+ key = major_edge
326
+ elif minor_edge != '':
327
+ key = minor_edge
328
+ else:
329
+ print(f'Could not find no edge of {zz} in spectrum')
330
+ return False
331
+
332
+ edge = dataset.metadata['edges'].setdefault(str(index), {})
333
+
334
+ start_exclude = x_section[key]['onset'] - x_section[key]['excl before']
335
+ end_exclude = x_section[key]['onset'] + x_section[key]['excl after']
336
+
337
+ edge.update({'z': zz, 'symmetry': key, 'element': elements[zz],
338
+ 'onset': x_section[key]['onset'], 'end_exclude': end_exclude,
339
+ 'start_exclude': start_exclude})
340
+ edge['all_edges'] = all_edges2
341
+ edge['chemical_shift'] = 0.0
342
+ edge['areal_density'] = 0.0
343
+ edge['original_onset'] = edge['onset']
344
+ return True
345
+
346
+
347
+ def make_edges(edges_present: dict, energy_scale: np.ndarray, e_0:float,
348
+ coll_angle:float, low_loss:np.ndarray=None)->dict:
349
+ """Makes the edges dictionary for quantification
350
+
351
+ Parameters
352
+ ----------
353
+ edges_present: list
354
+ list of edges
355
+ energy_scale: numpy array
356
+ energy scale on which to make cross-section
357
+ e_0: float
358
+ acceleration voltage (in V)
359
+ coll_angle: float
360
+ collection angle in mrad
361
+ low_loss: numpy array with same length as energy_scale
362
+ low_less spectrum with which to convolve the cross-section (default=None)
363
+
364
+ Returns
365
+ -------
366
+ edges: dict
367
+ dictionary with all information on cross-section
368
+ """
369
+ x_sections = get_x_sections()
370
+ edges = {}
371
+ for i, edge in enumerate(edges_present):
372
+ element, symmetry = edge.split('-')
373
+ z = 0
374
+ for key in x_sections:
375
+ if element == x_sections[key]['name']:
376
+ z = int(key)
377
+ edges[i] = {}
378
+ edges[i]['z'] = z
379
+ edges[i]['symmetry'] = symmetry
380
+ edges[i]['element'] = element
381
+
382
+ for key, edge in edges.items():
383
+ xsec = x_sections[str(edge['z'])]
384
+ if 'chemical_shift' not in edge:
385
+ edge['chemical_shift'] = 0
386
+ if 'symmetry' not in edge:
387
+ edge['symmetry'] = 'K1'
388
+ if 'K' in edge['symmetry']:
389
+ edge['symmetry'] = 'K1'
390
+ elif 'L' in edge['symmetry']:
391
+ edge['symmetry'] = 'L3'
392
+ elif 'M' in edge['symmetry']:
393
+ edge['symmetry'] = 'M5'
394
+ else:
395
+ edge['symmetry'] = edge['symmetry'][0:2]
396
+
397
+ edge['original_onset'] = xsec[edge['symmetry']]['onset']
398
+ edge['onset'] = edge['original_onset'] + edge['chemical_shift']
399
+ edge['start_exclude'] = edge['onset'] - xsec[edge['symmetry']]['excl before']
400
+ edge['end_exclude'] = edge['onset'] + xsec[edge['symmetry']]['excl after']
401
+
402
+ edges = make_cross_sections(edges, energy_scale, e_0, coll_angle, low_loss)
403
+ return edges
404
+
405
+
406
+ def auto_chemical_composition(dataset:sidpy.Dataset)->None:
407
+ """Automatically identifies edges in a dataset and adds them to the core_loss dictionary"""
408
+ found_edges = auto_id_edges(dataset)
409
+ for key in found_edges:
410
+ add_element_to_dataset(dataset, key)
411
+ fit_dataset(dataset)
412
+
413
+
414
+ def make_cross_sections(edges:dict, energy_scale:np.ndarray, e_0:float,
415
+ coll_angle:float, low_loss:np.ndarray=None)->dict:
416
+ """
417
+ Updates the edges dictionary with collection angle-integrated
418
+ X-ray photo-absorption cross-sections
419
+ """
420
+ for key in edges:
421
+ if str(key).isdigit():
422
+ if edges[key]['z'] <1:
423
+ break
424
+ # from barnes to 1/nm^2
425
+ edges[key]['data'] = xsec_xrpa(energy_scale, e_0 / 1000., edges[key]['z'], coll_angle,
426
+ edges[key]['chemical_shift']) / 1e10
427
+ if low_loss is not None:
428
+ low_loss = np.roll(np.array(low_loss), 1024 - np.argmax(np.array(low_loss)))
429
+ edges[key]['data'] = scipy.signal.convolve(edges[key]['data'],
430
+ low_loss/low_loss.sum(), mode='same')
431
+
432
+ edges[key]['onset'] = edges[key]['original_onset'] + edges[key]['chemical_shift']
433
+ edges[key]['X_section_type'] = 'XRPA'
434
+ edges[key]['X_section_source'] = 'pyTEMlib'
435
+
436
+ return edges
437
+
438
+
439
+ def power_law(energy: np.ndarray, a:float, r:float)->np.ndarray:
440
+ """power law for power_law_background"""
441
+ return a * np.power(energy, -r)
442
+
443
+
444
+ def power_law_background(spectrum:np.ndarray, energy_scale:np.ndarray,
445
+ fit_area:list, verbose:bool=False):
446
+ """fit of power law to spectrum """
447
+
448
+ # Determine energy window for background fit in pixels
449
+ startx = np.searchsorted(energy_scale, fit_area[0])
450
+ endx = np.searchsorted(energy_scale, fit_area[1])
451
+
452
+ x = np.array(energy_scale)[startx:endx]
453
+ y = np.array(spectrum)[startx:endx].flatten()
454
+
455
+ # Initial values of parameters
456
+ p0 = np.array([1.0E+20, 3])
457
+
458
+ # background fitting
459
+ def bgdfit(pp, yy, xx):
460
+ err = yy - power_law(xx, pp[0], pp[1])
461
+ return err
462
+
463
+ [p, _] = scipy.optimize.leastsq(bgdfit, p0, args=(y, x), maxfev=2000)
464
+
465
+ background_difference = y - power_law(x, p[0], p[1])
466
+ background_noise_level = std_dev = np.std(background_difference)
467
+ if verbose:
468
+ print(f'Power-law background with amplitude A: {p[0]:.1f} and exponent -r: {p[1]:.2f}')
469
+ print(background_difference.max() / background_noise_level)
470
+
471
+ print(f'Noise level in spectrum {std_dev:.3f} counts')
472
+
473
+ # Calculate background over the whole energy scale
474
+ background = power_law(energy_scale, p[0], p[1])
475
+ return background, p
476
+
477
+
478
+ def cl_model(xx, pp, number_of_edges, xsec):
479
+ """ core loss model for fitting"""
480
+ yy = pp[0] * xx**pp[1] + pp[2] + pp[3]* xx + pp[4] * xx * xx
481
+ for i in range(number_of_edges):
482
+ pp[i+5] = np.abs(pp[i+5])
483
+ yy = yy + pp[i+5] * xsec[i, :]
484
+ return yy
485
+
486
+
487
+ def get_mask(energy_scale, edges):
488
+ """ Create a mask for the fitting area"""
489
+ mask = np.ones(len(energy_scale))
490
+ edges.setdefault('fit_area', {})
491
+ background_fit_start = edges.get('fit_area', {}).get('fit_start', 0)
492
+ background_fit_end = edges.get('fit_area', {}).setdefault('fit_end', energy_scale[-1])
493
+ if background_fit_start == 0:
494
+ return mask
495
+ edges['fit_area']['fit_end'] = background_fit_end
496
+
497
+ start_bgd = np.searchsorted(energy_scale, background_fit_start)
498
+ end_bgd = np.searchsorted(energy_scale, background_fit_end)
499
+ # Determine fitting ranges and masks to exclude ranges
500
+
501
+ mask[0:start_bgd] = 0.0
502
+ mask[end_bgd:-1] = 0.0
503
+ for key in edges:
504
+ if not key.isdigit():
505
+ continue
506
+ start_exclude = np.searchsorted(energy_scale, edges[key]['start_exclude'])
507
+ end_exclude = np.searchsorted(energy_scale, edges[key]['end_exclude'])
508
+ if start_bgd+1 < start_exclude < end_bgd-2 and end_exclude < end_bgd:
509
+ start_exclude = max (start_exclude, 2)
510
+ mask[start_exclude:end_exclude] = 0.0
511
+ return mask
512
+
513
+ def fit_edges2(spectrum, energy_scale, edges):
514
+ """ Fit edges in a spectrum """
515
+ mask = get_mask(energy_scale, edges)
516
+
517
+ ########################
518
+ # Background Fit
519
+ ########################
520
+ bgd_fit_area = [edges['fit_area']['fit_start'], edges['fit_area']['fit_end']]
521
+ _, [amplitude, r] = power_law_background(spectrum, energy_scale, bgd_fit_area, verbose=False)
522
+
523
+ #######################
524
+ # Edge Fit
525
+ #######################
526
+
527
+ blurred = scipy.ndimage.gaussian_filter(spectrum, sigma=5)
528
+ blurred[np.where(blurred < 1e-8)] = 1e-8
529
+
530
+ xsec = []
531
+ number_of_edges = 0
532
+ for key in edges:
533
+ if key.isdigit():
534
+ xsec.append(edges[key]['data'])
535
+ number_of_edges += 1
536
+ xsec = np.array(xsec)
537
+
538
+ def model(xx, pp):
539
+ yy = pp[0] * xx**pp[1] + pp[2] + pp[3] * xx + pp[4] * xx**2
540
+ for i in range(number_of_edges):
541
+ pp[i+5] = np.abs(pp[i+5])
542
+ yy = yy + pp[i+5] * xsec[i, :]
543
+ return yy
544
+
545
+ def residuals(pp, xx, yy):
546
+ err = np.abs((yy - model(xx, pp)) * mask) / np.sqrt(np.abs(yy))
547
+ return err
548
+
549
+ scale = blurred[100]
550
+ pin = np.array([amplitude, -r, 10., 1., 0.00] + [scale/5] * number_of_edges)
551
+ [p, _] = scipy.optimize.leastsq(residuals, pin, args=(energy_scale, blurred))
552
+
553
+ for key in edges:
554
+ if key.isdigit():
555
+ edges[key]['areal_density'] = p[int(key)+5]
556
+ # print(p)
557
+ background = p[0] * np.power(energy_scale, -p[1])
558
+ background += p[2] + energy_scale**p[3] + p[4]*energy_scale**2
559
+ edges['model'] = {'background': background,
560
+ 'background-poly_0': p[2],
561
+ 'background-poly_1': p[3],
562
+ 'background-poly_2': p[4],
563
+ 'background-A': p[0],
564
+ 'background-r': p[1],
565
+ 'spectrum': model(energy_scale, p),
566
+ 'blurred': blurred,
567
+ 'mask': mask,
568
+ 'fit_parameter': p,
569
+ 'fit_area_start': edges['fit_area']['fit_start'],
570
+ 'fit_area_end': edges['fit_area']['fit_end'],
571
+ 'xsec': xsec}
572
+ return edges
573
+
574
+ def fit_dataset(dataset: sidpy.Dataset):
575
+ """Fit edges in a sidpy.Dataset"""
576
+ energy_scale = dataset.get_spectral_dims(return_axis=True)[0].values
577
+ dataset.metadata['edges'].setdefault('fit_area', {})
578
+ dataset.metadata['edges']['fit_area'].setdefault('fit_start', energy_scale[50])
579
+ dataset.metadata['edges']['fit_area'].setdefault('fit_end', energy_scale[-2])
580
+ dataset.metadata['edges'].setdefault('use_low_loss', False)
581
+
582
+ exp = dataset.metadata.get('experiment', {})
583
+ alpha = exp.get('convergence_angle', None)
584
+ if alpha is None:
585
+ raise ValueError('need a convergence_angle in experiment of metadata dictionary ')
586
+ beta = exp.get('collection_angle', 0)
587
+ beam_kv = exp.get('acceleration_voltage', 0)
588
+ eff_beta = effective_collection_angle(energy_scale, alpha, beta, beam_kv)
589
+ edges = make_cross_sections(dataset.metadata['edges'], energy_scale, beam_kv, eff_beta)
590
+ dataset.metadata['edges'] = fit_edges2(dataset, energy_scale, edges)
591
+ areal_density = []
592
+ element_list = []
593
+ for key in edges:
594
+ if key.isdigit(): # only edges have numbers in that dictionary
595
+ element_list.append(edges[key]['element'])
596
+ areal_density.append(edges[key]['areal_density'])
597
+ areal_density = np.array(areal_density)
598
+ out_string = '\nRelative composition: \n'
599
+ for i, element in enumerate(element_list):
600
+ out_string += f'{element}: {areal_density[i] / areal_density.sum() * 100:.1f}% '
601
+ print(out_string)
602
+
603
+
604
+ def core_loss_model(energy_scale, pp, number_of_edges, xsec):
605
+ """ core loss model from fitting parameters"""
606
+ xx = np.array(energy_scale)
607
+ yy = pp[0] * xx**pp[1] + pp[2] + pp[3]* xx + pp[4] * xx * xx
608
+ for i in range(number_of_edges):
609
+ pp[i+5] = np.abs(pp[i+5])
610
+ yy = yy + pp[i+5] * xsec[i, :]
611
+ return yy
612
+
613
+
614
+ def fit_edges(spectrum, energy_scale, region_tags, edges):
615
+ """fit edges for quantification"""
616
+
617
+ # Determine fitting ranges and masks to exclude ranges
618
+ mask = np.ones(len(spectrum))
619
+
620
+ background_fit_end = energy_scale[-1]
621
+ for key in region_tags:
622
+ end = region_tags[key]['start_x'] + region_tags[key]['width_x']
623
+
624
+ startx = np.searchsorted(energy_scale, region_tags[key]['start_x'])
625
+ endx = np.searchsorted(energy_scale, end)
626
+
627
+ if key == 'fit_area':
628
+ mask[0:startx] = 0.0
629
+ mask[endx:-1] = 0.0
630
+ else:
631
+ mask[startx:endx] = 0.0
632
+ background_fit_end = min(region_tags[key]['start_x'], background_fit_end)
633
+
634
+ fit_area = region_tags['fit_area']
635
+ ########################
636
+ # Background Fit
637
+ ########################
638
+ bgd_fit_area = [region_tags['fit_area']['start_x'], background_fit_end]
639
+ background, [amplitude, r] = power_law_background(spectrum, energy_scale,
640
+ bgd_fit_area, verbose=False)
641
+
642
+ #######################
643
+ # Edge Fit
644
+ #######################
645
+ x = energy_scale
646
+ blurred = scipy.ndimage.gaussian_filter(spectrum, sigma=5)
647
+
648
+ y = blurred # now in probability
649
+ y[np.where(y < 1e-8)] = 1e-8
650
+
651
+ xsec = []
652
+ number_of_edges = 0
653
+ for key in edges:
654
+ if key.isdigit():
655
+ xsec.append(edges[key]['data'])
656
+ number_of_edges += 1
657
+ xsec = np.array(xsec)
658
+
659
+ def model(xx, pp):
660
+ yy = background + pp[6] + pp[7] * xx + pp[8] * xx * xx
661
+ for i in range(number_of_edges):
662
+ pp[i] = np.abs(pp[i])
663
+ yy = yy + pp[i] * xsec[i, :]
664
+ return yy
665
+
666
+ def residuals(pp, xx, yy):
667
+ err = np.abs((yy - model(xx, pp)) * mask) # / np.sqrt(np.abs(y))
668
+ return err
669
+
670
+ scale = y[100]
671
+ pin = np.array([scale/5, scale/5, scale/5, scale/5, scale/5, scale/5, -scale/10, 1.0, 0.001])
672
+ [p, _] = scipy.optimize.leastsq(residuals, pin, args=(x, y))
673
+
674
+ for key in edges:
675
+ if key.isdigit():
676
+ edges[key]['areal_density'] = p[int(key) - 1]
677
+ edges['model'] = {}
678
+ edges['model']['background'] = background + p[6] + p[7] * x + p[8] * x**2
679
+ edges['model']['background-poly_0'] = p[6]
680
+ edges['model']['background-poly_1'] = p[7]
681
+ edges['model']['background-poly_2'] = p[8]
682
+ edges['model']['background-A'] = amplitude
683
+ edges['model']['background-r'] = r
684
+ edges['model']['spectrum'] = model(x, p)
685
+ edges['model']['blurred'] = blurred
686
+ edges['model']['mask'] = mask
687
+ edges['model']['fit_parameter'] = p
688
+ edges['model']['fit_area_sta' \
689
+ 'rt'] = fit_area['start_x']
690
+ edges['model']['fit_area_end'] = fit_area['start_x'] + fit_area['width_x']
691
+ return edges
692
+
693
+
694
+ def xsec_xrpa(energy_scale, e0, z, beta, shift=0):
695
+ """ Calculate momentum-integrated cross-section for EELS
696
+ from X-ray photo-absorption cross-sections.
697
+
698
+ X-ray photo-absorption cross-sections from NIST.
699
+ Momentum-integrated cross-section for EELS according to
700
+ Egerton Ultramicroscopy 50 (1993) 13-28 equation (4)
701
+
702
+ Parameters
703
+ ----------
704
+ energy_scale: numpy array
705
+ energy scale of spectrum to be analyzed
706
+ e0: float
707
+ acceleration voltage in keV
708
+ z: int
709
+ atomic number of element
710
+ beta: float
711
+ effective collection angle in mrad
712
+ shift: float
713
+ chemical shift of edge in eV
714
+ """
715
+ beta = beta * 0.001 # collection half angle theta [rad]
716
+ # theta_max = self.parent.spec[0].convAngle * 0.001 # collection half angle theta [rad]
717
+ dispersion = energy_scale[1] - energy_scale[0]
718
+
719
+ x_sections = get_x_sections(z)
720
+ enexs = x_sections['ene']
721
+ datxs = x_sections['dat']
722
+
723
+ #####
724
+ # Cross Section according to Egerton Ultramicroscopy 50 (1993) 13-28 equation (4)
725
+ #####
726
+
727
+ # Relativistic correction factors
728
+ t = 511060.0 * (1.0 - 1.0 / (1.0 + e0 / 511.06) ** 2) / 2.0
729
+ gamma = 1 + e0 / 511.06
730
+ a = 6.5 # e-14 *10**14
731
+ b = beta
732
+
733
+ theta_e = enexs / (2 * gamma * t)
734
+ # ToDo: is there and error in the (gamma-1) factor at the las should be (1-1/gamma**2)
735
+ g = 2 * np.log(gamma) - np.log((b**2 + theta_e**2) / (b**2 + theta_e**2 / gamma**2)) - (
736
+ (1-1/gamma**2)) * b**2 / (b**2 + theta_e**2 / gamma**2)
737
+ datxs = datxs * (a / enexs / t) * (np.log(1 + b**2 / theta_e**2) + g) / 1e8
738
+
739
+ datxs = datxs * dispersion # from per eV to per dispersion
740
+ # coeff = splrep(enexs, datxs, s=0) # now in areal density atoms / m^2
741
+ xsec = np.zeros(len(energy_scale))
742
+ # shift = 0# int(ek -onsetXRPS)#/dispersion
743
+ # Linear instead of spline interpolation to avoid oscillations.
744
+ lin = scipy.interpolate.interp1d(enexs, datxs, kind='linear')
745
+ if energy_scale[0] < enexs[0]:
746
+ start = np.searchsorted(energy_scale, enexs[0])+1
747
+ else:
748
+ start = 0
749
+ xsec[start:] = lin(energy_scale[start:] - shift)
750
+
751
+ return xsec