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,655 @@
1
+ """ Part of eels_tools for pyTEMlib"""
2
+ from typing import Union
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ import scipy
6
+
7
+ import sidpy
8
+ from sidpy.proc.fitter import SidFitter
9
+
10
+ from ..utilities import get_wave_length, effective_collection_angle
11
+ from ..utilities import gauss, lorentz
12
+ from .zero_loss_tools import zl
13
+
14
+
15
+ def drude(energy_scale, peak_position, peak_width, gamma):
16
+ """dielectric function according to Drude theory"""
17
+
18
+ eps = (1 - (peak_position ** 2 - peak_width * energy_scale * 1j) /
19
+ (energy_scale ** 2 + 2 * energy_scale * gamma * 1j)) # Mod drude term
20
+ return eps
21
+
22
+
23
+ def drude_lorentz(eps_inf, leng, ep, eb, gamma, e, amplitude):
24
+ """dielectric function according to Drude-Lorentz theory"""
25
+ eps = eps_inf
26
+ for i in range(leng):
27
+ eps = eps + amplitude[i] * (1 / (e + ep[i] + gamma[i]*1j) -
28
+ 1 / (e - ep[i] + gamma[i]*1j))
29
+ return eps
30
+
31
+
32
+ def energy_loss_function(energy: np.ndarray, p: np.ndarray, anglog=1) -> np.ndarray:
33
+ """Energy loss function based on dielectric function."""
34
+ eps = 1 - p[0]**2/(energy**2+p[1]**2) + 1j * p[1] * p[0]**2/energy/(energy**2+p[1]**2)
35
+ elf = (-1/eps).imag
36
+ return elf*p[2]*anglog
37
+
38
+
39
+ def get_plasmon_losses(energy, params):
40
+ """ Volume plasmons for spectrum images"""
41
+ dset = np.zeros((params.shape[0], params.shape[1], energy.shape[0]))
42
+ for x in range(params.shape[0]):
43
+ for y in range(params.shape[1]):
44
+ dset[x, y] += energy_loss_function(energy, params[x, y])
45
+ return dset
46
+
47
+
48
+ def fit_plasmon(dataset: Union[sidpy.Dataset, np.ndarray],
49
+ start_fit_energy: float, end_fit_energy: float,
50
+ number_workers: int = 4, number_threads: int = 8
51
+ ) -> Union[sidpy.Dataset, np.ndarray]:
52
+ """
53
+ Fit plasmon peak positions and widths in a TEM dataset using a Drude model.
54
+
55
+ This function applies the Drude model to fit plasmon peaks in a dataset obtained
56
+ from transmission electron microscopy (TEM). It processes the dataset to determine
57
+ peak positions, widths, and amplitudes within a specified energy range. The function
58
+ can handle datasets with different dimensions and offers parallel processing capabilities.
59
+
60
+ Parameters:
61
+ dataset: sidpy.Dataset or numpy.ndarray
62
+ The dataset containing TEM spectral data.
63
+ start_fit_energy: float
64
+ The start energy of the fitting window.
65
+ end_fit_energy: float
66
+ The end energy of the fitting window.
67
+ plot_result: bool, optional
68
+ If True, plots the fitting results (default is False).
69
+ number_workers: int, optional
70
+ The number of workers for parallel processing (default is 4).
71
+ number_threads: int, optional
72
+ The number of threads for parallel processing (default is 8).
73
+
74
+ Returns:
75
+ fitted_dataset: sidpy.Dataset or numpy.ndarray
76
+ The dataset with fitted plasmon peak parameters. The dimensions and
77
+ format depend on the input dataset.
78
+
79
+ Raises:
80
+ ValueError: If the input dataset does not have the expected dimensions or format.
81
+
82
+ Notes:
83
+ - The function uses the Drude model to fit plasmon peaks.
84
+ - The fitting parameters are peak position (e_p), peak width (e_w), and amplitude (A).
85
+ - If `plot_result` is True, the function plots e_p, e_w, and A as separate subplots.
86
+ """
87
+ # define Drude function for plasmon fitting
88
+
89
+ anglog, _, _ = angle_correction(dataset)
90
+
91
+ def energy_loss_function2(e: np.ndarray, e_p: float,e_w: float,
92
+ amplitude: float) -> np.ndarray:
93
+ eps = 1 - e_p**2/(e**2+e_w**2) + 1j * e_w * e_p**2/e/(e**2+e_w**2)
94
+ elf = (-1/eps).imag
95
+ return amplitude*elf
96
+
97
+ # define window for fitting
98
+ energy = dataset.get_spectral_dims(return_axis=True)[0].values
99
+ start_fit_pixel = np.searchsorted(energy, start_fit_energy)
100
+ end_fit_pixel = np.searchsorted(energy, end_fit_energy)
101
+
102
+ # rechunk dataset
103
+ if dataset.ndim == 3:
104
+ dataset = dataset.rechunk(chunks=(1, 1, -1))
105
+ fit_dset = dataset[:, :, start_fit_pixel:end_fit_pixel]
106
+ elif dataset.ndim == 2:
107
+ dataset = dataset.rechunk(chunks=(1, -1))
108
+ fit_dset = dataset[:, start_fit_pixel:end_fit_pixel]
109
+ else:
110
+ fit_dset = np.array(dataset[start_fit_pixel:end_fit_pixel]/ anglog[start_fit_pixel:end_fit_pixel])
111
+ guess_pos = np.argmax(fit_dset)
112
+ guess_amplitude = fit_dset[guess_pos]
113
+ guess_width =(end_fit_energy-start_fit_energy)/4
114
+ guess_pos = energy[start_fit_pixel+guess_pos]
115
+
116
+ if guess_width >8:
117
+ guess_width=8
118
+ try:
119
+ popt, _ = scipy.optimize.curve_fit(energy_loss_function2, energy[start_fit_pixel:end_fit_pixel], fit_dset,
120
+ p0=[guess_pos, guess_width, guess_amplitude])
121
+ except:
122
+ end_fit_pixel = np.searchsorted(energy, 30)
123
+ fit_dset = np.array(dataset[start_fit_pixel:end_fit_pixel]/ anglog[start_fit_pixel:end_fit_pixel])
124
+ try:
125
+ popt, _ = scipy.optimize.curve_fit(energy_loss_function,
126
+ energy[start_fit_pixel:end_fit_pixel], fit_dset,
127
+ p0=[guess_pos, guess_width, guess_amplitude])
128
+ except:
129
+ popt=[0,0,0]
130
+
131
+ plasmon = dataset.like_data(energy_loss_function2(energy, popt[0], popt[1], popt[2]))
132
+ plasmon *= anglog
133
+ start_plasmon = np.searchsorted(energy, 0)+1
134
+ plasmon[:start_plasmon] = 0.
135
+
136
+ epsilon = drude(energy, popt[0], popt[1], 1) * popt[2]
137
+ epsilon[:start_plasmon] = 0.
138
+
139
+ plasmon.metadata['plasmon'] = {'parameter': popt, 'epsilon':epsilon}
140
+ return plasmon
141
+
142
+ # if it can be parallelized:
143
+ fitter = SidFitter(fit_dset, energy_loss_function, num_workers=number_workers,
144
+ threads=number_threads, return_cov=False, return_fit=False, return_std=False,
145
+ km_guess=False, num_fit_parms=3)
146
+ [fit_parameter] = fitter.do_fit()
147
+
148
+ plasmon_dset = dataset * 0.0
149
+ fit_parameter = np.array(fit_parameter)
150
+ plasmon_dset += get_plasmon_losses(np.array(energy), fit_parameter)
151
+ if 'plasmon' not in plasmon_dset.metadata:
152
+ plasmon_dset.metadata['plasmon'] = {}
153
+ plasmon_dset.metadata['plasmon'].update({'start_fit_energy': start_fit_energy,
154
+ 'end_fit_energy': end_fit_energy,
155
+ 'fit_parameter': fit_parameter,
156
+ 'original_low_loss': dataset.title})
157
+
158
+ return plasmon_dset
159
+
160
+
161
+ def angle_correction(spectrum):
162
+ """ angle correction per energy loss"""
163
+ acceleration_voltage = spectrum.metadata['experiment']['acceleration_voltage']
164
+ energy_scale = spectrum.get_spectral_dims(return_axis=True)[0]
165
+ # eff_beta = effective_collection_angle(energy_scale,
166
+ # spectrum.metadata['experiment']['convergence_angle'],
167
+ # spectrum.metadata['experiment']['collection_angle'],
168
+ # acceleration_voltage)
169
+
170
+ epc = energy_scale.slope # input('ev per channel : ')
171
+
172
+ alpha = spectrum.metadata['experiment']['convergence_angle'] # input('Alpha (mrad) : ')
173
+ beta = spectrum.metadata['experiment']['collection_angle'] # input('Beta (mrad) : ')
174
+ e = energy_scale.values
175
+ e0 = acceleration_voltage/1000 # input('E0 (keV) : ')
176
+
177
+ t = 1000.0*e0*(1.+e0/1022.12)/(1.0+e0/511.06)**2 # %eV # equ.5.2a or Appendix E p 427
178
+
179
+ tgt=e0*(1.+e0/1022.)/(1+e0/511.)
180
+ thetae=(e+1e-6)/tgt # % avoid NaN for e=0
181
+ # % A2,B2,T2 ARE SQUARES OF ANGLES IN RADIANS**2
182
+ a2=alpha*alpha*1e-6 + 1e-7 # % avoid inf for alpha=0
183
+ b2=beta*beta*1e-6
184
+ t2=thetae*thetae*1e-6
185
+ eta1=np.sqrt((a2+b2+t2)**2-4*a2*b2)-a2-b2-t2
186
+ eta2=2.*b2*np.log(0.5/t2*(np.sqrt((a2+t2-b2)**2+4.*b2*t2)+a2+t2-b2))
187
+ eta3=2.*a2*np.log(0.5/t2*(np.sqrt((b2+t2-a2)**2+4.*a2*t2)+b2+t2-a2))
188
+ # eta=(eta1+eta2+eta3)/a2/np.log(4./t2)
189
+ f1=(eta1+eta2+eta3)/2./a2/np.log(1.+b2/t2)
190
+ f2=f1
191
+ if alpha/beta > 1:
192
+ f2=f1*a2/b2
193
+
194
+ bstar=thetae*np.sqrt(np.exp(f2*np.log(1.+b2/t2))-1.)
195
+ anglog = f2
196
+ """
197
+ b = eff_beta/1000.0 # %rad
198
+ e0 = acceleration_voltage/1000.0 # %keV
199
+ t = 1000.0*e0*(1.+e0/1022.12)/(1.0+e0/511.06)**2 # %eV # equ.5.2a or Appendix E p 427
200
+ tgt = 1000*e0*(1022.12 + e0)/(511.06 + e0) # %eV Appendix E p 427
201
+
202
+ the = energy_scale/tgt # varies with energy loss! # Appendix E p 427
203
+ anglog = np.log(1.0+ b*b/the/the)
204
+ # 2 * t = m_0 v**2 !!! a_0 = 0.05292 nm epc is for sum over I0
205
+ """
206
+ return anglog, (np.pi*0.05292* t / 2.0)/epc, bstar[0]
207
+
208
+
209
+ def inelastic_mean_free_path(e_p, spectrum):
210
+ """ inelastc mean free path"""
211
+ acceleration_voltage = spectrum.metadata['experiment']['acceleration_voltage']
212
+ energy_scale = spectrum.get_spectral_dims(return_axis=True)[0].values
213
+
214
+ e0 = acceleration_voltage/1000.0 # %keV
215
+
216
+ eff_beta = effective_collection_angle(energy_scale,
217
+ spectrum.metadata['experiment']['convergence_angle'],
218
+ spectrum.metadata['experiment']['collection_angle'],
219
+ acceleration_voltage)
220
+ beta = eff_beta/1000.0 # %rad
221
+
222
+ # %eV # equ.5.2a or Appendix E p 427
223
+ t = 1000.0*e0*(1.+e0/1022.12)/(1.0+e0/511.06)**2
224
+ # Appendix E p 427
225
+ tgt = 1000*e0*(1022.12 + e0)/(511.06 + e0) # %eV
226
+ # Appendix E p 427
227
+ theta_e = e_p/tgt # varies with energy loss!
228
+
229
+ # 2 * T = m_0 v**2 !!!
230
+ a_0 = 0.05292 # nm
231
+ imfp = 4 * t* a_0 / e_p / np.log(1 + beta**2/ theta_e**2)
232
+ return imfp, theta_e
233
+
234
+
235
+ def multiple_scattering(energy_scale: np.ndarray, p: list, core_loss=False)-> np.ndarray:
236
+ """Multiple scattering calculation based on plasmon peak fitting parameters."""
237
+ p = np.abs(p)
238
+ tmfp = p[3]
239
+ if core_loss:
240
+ dif = 1
241
+ else:
242
+ dif = 16
243
+ ll_energie = np.linspace(1, 2048-1,2048)/dif
244
+
245
+ ssd = energy_loss_function(ll_energie, p)
246
+ ssd = np.fft.fft(ssd)
247
+ ssd2 = ssd.copy()
248
+
249
+ ### sum contribution from each order of scattering:
250
+ psd = np.zeros(len(ll_energie))
251
+ for order in range(15):
252
+ # This order convoluted spectrum
253
+ # convoluted ssd is SSD2
254
+ ssd2 = np.fft.ifft(ssd).real
255
+
256
+ # scale right (could be done better? GERD)
257
+ # And add this order to final spectrum
258
+ #using equation 4.1 of Egerton ed2
259
+ psd += ssd2*abs(sum(ssd)/sum(ssd2)) / scipy.special.factorial(order+1)*np.power(tmfp, (order+1))*np.exp(-tmfp)
260
+
261
+ # next order convolution
262
+ ssd = ssd * ssd2
263
+
264
+ psd /=tmfp*np.exp(-tmfp)
265
+ bgd_coef = scipy.interpolate.splrep(ll_energie, psd, s=0)
266
+ msd = scipy.interpolate.splev(energy_scale, bgd_coef)
267
+ start_plasmon = np.searchsorted(energy_scale, 0)+1
268
+ msd[:start_plasmon] = 0.0
269
+ return msd
270
+
271
+ def fit_multiple_scattering(dataset: Union[sidpy.Dataset, np.ndarray],
272
+ start_fit_energy: float, end_fit_energy: float, pin=None,
273
+ number_workers: int = 4, number_threads: int = 8
274
+ ) -> Union[sidpy.Dataset, np.ndarray]:
275
+ """
276
+ Fit multiple scattering of plasmon peak in a TEM dataset.
277
+
278
+ Parameters:
279
+ dataset: sidpy.Dataset or numpy.ndarray
280
+ The dataset containing TEM spectral data.
281
+ start_fit_energy: float
282
+ The start energy of the fitting window.
283
+ end_fit_energy: float
284
+ The end energy of the fitting window.
285
+ number_workers: int, optional
286
+ The number of workers for parallel processing (default is 4).
287
+ number_threads: int, optional
288
+ The number of threads for parallel processing (default is 8).
289
+
290
+ Returns:
291
+ fitted_dataset: sidpy.Dataset or numpy.ndarray
292
+ The dataset with fitted plasmon peak parameters. The dimensions and
293
+ format depend on the input dataset.
294
+
295
+ Raises:
296
+ ValueError: If the input dataset does not have the expected dimensions or format.
297
+
298
+ Notes:
299
+ - The function uses the Drude model to fit plasmon peaks.
300
+ - The fitting parameters are peak position (e_p), peak width (e_w), and amplitude (A).
301
+ - If `plot_result` is True, the function plots e_p, e_w, and A as separate subplots.
302
+ """
303
+ # define window for fitting
304
+ energy = dataset.get_spectral_dims(return_axis=True)[0].values
305
+ start_fit_pixel = np.searchsorted(energy, start_fit_energy)
306
+ end_fit_pixel = np.searchsorted(energy, end_fit_energy)
307
+
308
+ def errf_multi(p, y, x):
309
+ elf = multiple_scattering(x, p)
310
+ err = y - elf
311
+ return np.abs(err) # /np.sqrt(y)
312
+
313
+ if pin is None:
314
+ pin = np.array([9,1,.7, 0.3])
315
+ fit_dset = np.array(dataset[start_fit_pixel:end_fit_pixel])
316
+ popt, _ = scipy.optimize.leastsq(errf_multi, pin, args=(fit_dset,
317
+ energy[start_fit_pixel:end_fit_pixel]), maxfev=2000)
318
+ multi = dataset.like_data(multiple_scattering(energy, popt))
319
+ multi.metadata['multiple_scattering'] = {'parameter': popt}
320
+ return multi
321
+
322
+
323
+ def drude_simulation(dset, e, ep, ew, tnm, eb):
324
+ """probabilities of dielectric function eps relative to zero-loss integral (i0 = 1)
325
+
326
+ Gives probabilities of dielectric function eps relative to zero-loss integral (i0 = 1) per eV
327
+ Details in R.F.Egerton: EELS in the Electron Microscope, 3rd edition, Springer 2011
328
+
329
+ # Given the plasmon energy (ep), plasmon fwhm (ew) and binding energy(eb),
330
+ # this program generates:
331
+ # EPS1, EPS2 from modified Eq. (3.40), ELF=Im(-1/EPS) from Eq. (3.42),
332
+ # single scattering from Eq. (4.26) and SRFINT from Eq. (4.31)
333
+ # The output is e, ssd into the file drude.ssd (for use in Flog etc.)
334
+ # and e,eps1 ,eps2 into drude.eps (for use in Kroeger etc.)
335
+ # Gives probabilities relative to zero-loss integral (i0 = 1) per eV
336
+ # Details in R.F.Egerton: EELS in the Electron Microscope, 3rd edition, Springer 2011
337
+ # Version 10.11.26
338
+
339
+ """
340
+ energy_scale = dset.get_spectral_dims(return_axis=True)[0].values
341
+
342
+ epc = energy_scale[1] - energy_scale[0] # input('ev per channel : ')
343
+
344
+ b = dset.metadata['collection_angle'] / 1000. # rad
345
+ epc = dset.energy_scale[1] - dset.energy_scale[0] # input('ev per channel : ');
346
+ e0 = dset.metadata['acceleration_voltage'] / 1000. # input('incident energy e0(kev) : ');
347
+
348
+ # effective kinetic energy: t = m_o v^2/2,
349
+ # eV # equ.5.2a or Appendix E p 427
350
+ t = 1000.0 * e0 * (1. + e0 / 1022.12) / (1.0 + e0 / 511.06)**2
351
+
352
+ # 2 gamma t
353
+ tgt = 1000 * e0 * (1022.12 + e0) / (511.06 + e0) # eV Appendix E p 427
354
+
355
+ rk0 = 2590 * (1.0 + e0 / 511.06) * np.sqrt(2.0 * t / 511060)
356
+
357
+ # os = e[0]
358
+ ew_mod = eb
359
+ tags = dset.metadata
360
+
361
+ eps = 1 - (ep ** 2 - ew_mod * e * 1j) / (e ** 2 + 2 * e * ew * 1j) # Mod drude term
362
+
363
+ eps[np.nonzero(eps == 0.0)] = 1e-19
364
+ elf = np.imag(-1 / eps)
365
+
366
+ the = e / tgt # varies with energy loss! # Appendix E p 427
367
+ # srfelf = 4..*eps2./((1+eps1).^2+eps2.^2) - elf; %equivalent
368
+ srfelf = np.imag(-4. / (1.0 + eps)) - elf # for 2 surfaces
369
+ angdep = np.arctan(b / the) / the - b / (b * b + the * the)
370
+ srfint = angdep * srfelf / (3.1416 * 0.05292 * rk0 * t) # probability per eV
371
+ anglog = np.log(1.0 + b * b / the / the)
372
+ i0 = dset.sum() # *tags['counts2e']
373
+
374
+ # 2 * t = m_0 v**2 !!! a_0 = 0.05292 nm
375
+ volint = abs(tnm / (np.pi * 0.05292 * t * 2.0) * elf * anglog) # S equ 4.26% probability per eV
376
+ volint = volint * i0 / epc # S probability per channel
377
+ ssd = volint # + srfint
378
+
379
+ if e[0] < -1.0:
380
+ xs = int(abs(-e[0] / epc))
381
+
382
+ ssd[0:xs] = 0.0
383
+ volint[0:xs] = 0.0
384
+ srfint[0:xs] = 0.0
385
+
386
+ # if os <0:
387
+ # 2 surfaces but includes negative Begrenzung contribution
388
+ # p_s = np.trapezoid(e, srfint)
389
+
390
+ # integrated volume probability
391
+ p_v = abs(np.trapezoid(e, abs(volint / tags['spec'].sum())))
392
+ # our data have he same epc and the trapez formula does not include
393
+ p_v = (volint / i0).sum()
394
+ # does NOT depend on free-electron approximation (no damping).
395
+ lam = tnm / p_v
396
+ # Eq.(3.44) approximation
397
+ lamfe = 4.0 * 0.05292 * t / ep / np.log(1 + (b * tgt / ep) ** 2)
398
+
399
+ tags['eps'] = eps
400
+ tags['lam'] = lam
401
+ tags['lamfe'] = lamfe
402
+ tags['p_v'] = p_v
403
+
404
+ return ssd # /np.pi
405
+
406
+
407
+ def kroeger_core(e_data, a_data, eps_data, acceleration_voltage_kev, thickness, relativistic=True):
408
+ """This function calculates the differential scattering probability
409
+
410
+ .. math::
411
+ \\frac{d^2P}{d \\Omega d_e}
412
+ of the low-loss region for total loss and volume plasmon loss
413
+
414
+ Args:
415
+ e_data (array): energy scale [eV]
416
+ a_data (array): angle or momentum range [rad]
417
+ eps_data (array) dielectric function
418
+ acceleration_voltage_kev (float): acceleration voltage [keV]
419
+ thickness (float): thickness in nm
420
+ relativistic (boolean): relativistic correction
421
+
422
+ Returns:
423
+ P (numpy array 2d): total loss probability
424
+ p_vol (numpy array 2d): volume loss probability
425
+
426
+ return P, P*scale*1e2,p_vol*1e2, p_simple*1e2
427
+
428
+
429
+ $d^2P/(dEd\Omega) = \frac{1}{\pi^2 a_0 m_0 v^2} \Im \left[ \frac{t\mu^2}{\varepsilon \phi^2 } \right]
430
+
431
+ # Internally everything is calculated in SI units
432
+ # acceleration_voltage_kev = 200 #keV
433
+ # thick = 32.0*10-9 # m
434
+ """
435
+ a_data = np.array(a_data)
436
+ e_data = np.array(e_data)
437
+ # adjust input to si units
438
+ # wavelength = get_wave_length(acceleration_voltage_kev * 1e3) # in m
439
+ thickness = thickness * 1e-9 # input thickness now in m
440
+
441
+ # Define constants
442
+ m_0 = scipy.constants.electron_mass # REST electron mass in kg
443
+ hbar = scipy.constants.hbar
444
+
445
+ c = scipy.constants.speed_of_light # speed of light m/s
446
+ bohr = scipy.constants.physical_constants['Bohr radius'][0] # Bohr radius in meters
447
+ e = scipy.constants.e # electron charge in Coulomb
448
+
449
+ # Calculate fixed terms of equation
450
+ # acceleration_voltage_kev is incident energy in keV
451
+ va = 1 - (511. / (511. + acceleration_voltage_kev))**2
452
+ v = c * np.sqrt(va)
453
+
454
+ if relativistic:
455
+ beta = v / c # non-relativistic for =1
456
+ gamma = 1. / np.sqrt(1 - beta ** 2)
457
+ else:
458
+ beta = 1
459
+ gamma = 1 # set = 1 to correspond to E+B & Siegle
460
+
461
+ momentum = m_0 * v * gamma # used for xya, E&B have no gamma
462
+
463
+ # ##### Define mapped variables
464
+
465
+ # Define independent variables E, theta
466
+ [energy, theta] = np.meshgrid(e_data + 1e-12, a_data)
467
+ # Define CONJUGATE dielectric function variable eps
468
+ [eps, _] = np.meshgrid(np.conj(eps_data), a_data)
469
+
470
+ # ##### Calculate lambda in equation EB 2.3
471
+ theta2 = theta ** 2 + 1e-15
472
+
473
+ theta_e = energy * e / momentum / v # critical angle
474
+
475
+ lambda2 = theta2 - eps * theta_e ** 2 * beta ** 2 # Eq 2.3
476
+
477
+ lambd = np.sqrt(lambda2)
478
+ if (np.real(lambd) < 0).any():
479
+ print(' error negative lambda')
480
+
481
+ # ##### Calculate lambda0 in equation EB 2.4
482
+ # According to Kröger real(lambda0) is defined as positive!
483
+
484
+ phi2 = lambda2 + theta_e ** 2 # Eq. 2.2
485
+ lambda02 = theta2 - theta_e ** 2 * beta ** 2 # eta=1 Eq 2.4
486
+ lambda02[lambda02 < 0] = 0
487
+ lambda0 = np.sqrt(lambda02)
488
+ if not (np.real(lambda0) >= 0).any():
489
+ print(' error negative lambda0')
490
+
491
+ de = thickness * energy * e / (2.0 * hbar * v) # Eq 2.5
492
+ xya = lambd * de / theta_e # used in Eqs 2.6, 2.7, 4.4
493
+
494
+ lplus = lambda0 * eps + lambd * np.tanh(xya) # eta=1 %Eq 2.6
495
+ lminus = lambda0 * eps + lambd / np.tanh(xya) # eta=1 %Eq 2.7
496
+
497
+ mue2 = 1 - (eps * beta ** 2) # Eq. 4.5
498
+ phi20 = lambda02 + theta_e ** 2 # Eq 4.6
499
+ phi201 = theta2 + theta_e ** 2 * (1 - (eps + 1) * beta ** 2) # eta=1, eps-1 in E+b Eq.(4.7)
500
+
501
+ # Eq 4.2
502
+ a1 = phi201 ** 2 / eps
503
+ a2 = np.sin(de) ** 2 / lplus + np.cos(de) ** 2 / lminus
504
+ a = a1 * a2
505
+
506
+ # Eq 4.3
507
+ b1 = beta ** 2 * lambda0 * theta_e * phi201
508
+ b2 = (1. / lplus - 1. / lminus) * np.sin(2. * de)
509
+ b = b1 * b2
510
+
511
+ # Eq 4.4
512
+ c1 = -beta ** 4 * lambda0 * lambd * theta_e ** 2
513
+ c2 = np.cos(de) ** 2 * np.tanh(xya) / lplus
514
+ c3 = np.sin(de) ** 2 / np.tanh(xya) / lminus
515
+ c = c1 * (c2 + c3)
516
+
517
+ # Put all the pieces together...
518
+ p_coef = e / (bohr * np.pi ** 2 * m_0 * v ** 2)
519
+
520
+ p_v = thickness * mue2 / eps / phi2
521
+
522
+ p_s1 = 2. * theta2 * (eps - 1) ** 2 / phi20 ** 2 / phi2 ** 2 # ASSUMES eta=1
523
+ p_s2 = hbar / momentum
524
+ p_s3 = a + b + c
525
+
526
+ p_s = p_s1 * p_s2 * p_s3
527
+
528
+ # print(p_v.min(),p_v.max(),p_s.min(),p_s.max())
529
+ # Calculate P and p_vol (volume only)
530
+ dtheta = a_data[1] - a_data[0]
531
+ scale = np.sin(np.abs(theta)) * dtheta * 2 * np.pi
532
+
533
+ p = p_coef * np.imag(p_v - p_s) # Eq 4.1
534
+ p_vol = p_coef * np.imag(p_v) * scale
535
+
536
+ # lplus_min = e_data[np.argmin(np.real(lplus), axis=1)]
537
+ # lminus_min = e_data[np.argmin(np.imag(lminus), axis=1)]
538
+
539
+ p_simple = p_coef * np.imag(1 / eps) * thickness / (theta2 + theta_e ** 2) * scale
540
+ # Watch it: eps is conjugated dielectric function
541
+
542
+ return p, p * scale * 1e2, p_vol * 1e2, p_simple * 1e2 # ,lplus_min,lminus_min
543
+
544
+
545
+ def plot_dispersion(plotdata, units, a_data, e_data, max_p, title, ee):
546
+ """Plot loss function """
547
+
548
+ # [x, y] = np.meshgrid(e_data + 1e-12, a_data[1024:2048] * 1000)
549
+
550
+ z = plotdata
551
+ # lev = np.array([0.01, 0.05, 0.1, 0.25, 0.5, 1, 2, 3, 4, 4.9]) * max_p / 5
552
+
553
+ wavelength = get_wave_length(ee)
554
+ # q = a_data[1024:2048] / (wavelength * 1e9) # in [1/nm]
555
+ scale = np.array([0, a_data[-1], e_data[0], e_data[-1]])
556
+ ev2hertz = scipy.constants.value('electron volt-hertz relationship')
557
+
558
+ if units[0] == 'mrad':
559
+ units[0] = 'scattering angle [mrad]'
560
+ scale[1] = scale[1] * 1000.
561
+ light_line = scipy.constants.c * a_data # for mrad
562
+ elif units[0] == '1/nm':
563
+ units[0] = 'scattering vector [1/nm]'
564
+ scale[1] = scale[1] / (wavelength * 1e9)
565
+ light_line = 1 / (scipy.constants.c / ev2hertz) * 1e-9
566
+
567
+ if units[1] == 'eV':
568
+ units[1] = 'energy loss [eV]'
569
+
570
+ if units[2] == 'ppm':
571
+ units[2] = 'probability [ppm]'
572
+ if units[2] == '1/eV':
573
+ units[2] = 'probability [eV$^{-1}$ srad$^{-1}$]'
574
+
575
+ # alpha = 3. / 5. * ef / ep
576
+
577
+ ax2 = plt.gca()
578
+ fig2 = plt.gcf()
579
+ fig2.suptitle(title)
580
+ im = ax2.imshow(z.t, clim=(0, max_p), origin='lower', aspect='auto', extent=scale)
581
+ # co = ax2.contour(y, x, z, levels=lev, colors='k', origin='lower')
582
+ # ,extent=(-ang*1000.,ang*1000.,e_data[0],e_data[-1]))#, vmin = p_vol.min(), vmax = 1000)
583
+
584
+ fig2.colorbar(im, ax=ax2, label=units[2])
585
+
586
+ ax2.plot(a_data, light_line, c='r', label='light line')
587
+ # ax2.plot(e_data*light_line*np.sqrt(np.real(eps_data)),e_data, color='steelblue',
588
+ # label='$\omega = c q \sqrt{\epsilon_2}$')
589
+
590
+ # ax2.plot(q, Ep_disp, c='r')
591
+ ax2.plot([11.5 * light_line, 0.12], [11.5, 11.5], c='r')
592
+
593
+ ax2.text(.05, 11.7, 'surface plasmon', color='r')
594
+ ax2.plot([0.0, 0.12], [16.8, 16.8], c='r')
595
+ ax2.text(.05, 17, 'volume plasmon', color='r')
596
+ ax2.set_xlim(0, scale[1])
597
+ ax2.set_ylim(0, 20)
598
+ # Interband transitions
599
+ ax2.plot([0.0, 0.25], [4.2, 4.2], c='g', label='interband transitions')
600
+ ax2.plot([0.0, 0.25], [5.2, 5.2], c='g')
601
+ ax2.set_ylabel(units[1])
602
+ ax2.set_xlabel(units[0])
603
+ ax2.legend(loc='lower right')
604
+
605
+
606
+ def add_peaks(x, y, peaks, pin_in=None, peak_shape_in=None, shape='Gaussian'):
607
+ """ add peaks to fitting parameters"""
608
+ if pin_in is None:
609
+ return [], []
610
+ if peak_shape_in is None:
611
+ return [], []
612
+ pin = pin_in.copy()
613
+
614
+ peak_shape = peak_shape_in.copy()
615
+ if isinstance(shape, str): # if peak_shape is only a string make a list of it.
616
+ shape = [shape]
617
+
618
+ if len(shape) == 1:
619
+ shape = shape * len(peaks)
620
+ for i, peak in enumerate(peaks):
621
+ pin.append(x[peak])
622
+ pin.append(y[peak])
623
+ pin.append(.3)
624
+ peak_shape.append(shape[i])
625
+ return pin, peak_shape
626
+
627
+
628
+ def model3(x, p, number_of_peaks, peak_shape, p_zl, pin=None, restrict_pos=0, restrict_width=0):
629
+ """ model for fitting low-loss spectrum"""
630
+ if pin is None:
631
+ pin = p
632
+ y = np.zeros(len(x))
633
+
634
+ for i in range(number_of_peaks):
635
+ index = int(i * 3)
636
+ if restrict_pos > 0:
637
+ if p[index] > pin[index] * (1.0 + restrict_pos):
638
+ p[index] = pin[index] * (1.0 + restrict_pos)
639
+ if p[index] < pin[index] * (1.0 - restrict_pos):
640
+ p[index] = pin[index] * (1.0 - restrict_pos)
641
+
642
+ p[index + 1] = abs(p[index + 1])
643
+ # print(p[index + 1])
644
+ p[index + 2] = abs(p[index + 2])
645
+ if restrict_width > 0:
646
+ p[index + 2] = pin[index + 2]
647
+ if p[index + 2] > pin[index + 2] * (1.0 + restrict_width):
648
+ p[index + 2] = pin[index + 2] * (1.0 + restrict_width)
649
+ if peak_shape[i] == 'Lorentzian':
650
+ y = y + lorentz(x, p[index:])
651
+ elif peak_shape[i] == 'zl':
652
+ y = y + zl(x, p[index:], p_zl)
653
+ else:
654
+ y = y + gauss(x, p[index:])
655
+ return y