1212from .zero_loss_tools import zl
1313
1414
15- def drude (energy_scale , peak_position , peak_width , gamma ) :
15+ def drude (energy_scale : np . ndarray , parameters : list ) -> np . ndarray :
1616 """dielectric function according to Drude theory"""
17+ peak_position , peak_width , gamma = parameters [:3 ]
18+ if energy_scale [0 ] < 0 :
19+ zero_pixel = np .searchsorted (energy_scale , 0 )+ 1
20+ else :
21+ zero_pixel = 0
22+ energy_eps = energy_scale [zero_pixel :]
23+ eps = (1 - (peak_position ** 2 - peak_width * energy_eps * 1j ) /
24+ (energy_eps ** 2 + 2 * energy_eps * gamma * 1j )) # Mod drude term
25+ out_array = np .zeros (len (energy_scale ), dtype = complex )
26+ out_array [zero_pixel :] = eps
27+ return out_array
28+
1729
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
2130
2231
2332def drude_lorentz (eps_inf , leng , ep , eb , gamma , e , amplitude ):
@@ -31,9 +40,10 @@ def drude_lorentz(eps_inf, leng, ep, eb, gamma, e, amplitude):
3140
3241def energy_loss_function (energy : np .ndarray , p : np .ndarray , anglog = 1 ) -> np .ndarray :
3342 """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 )
43+ eps = drude (energy , p )
44+ eps [eps == 0.0 ]= 1e-19
3545 elf = (- 1 / eps ).imag
36- return elf * p [2 ] * anglog
46+ return elf * p [3 ] * anglog
3747
3848
3949def get_plasmon_losses (energy , params ):
@@ -44,8 +54,68 @@ def get_plasmon_losses(energy, params):
4454 dset [x , y ] += energy_loss_function (energy , params [x , y ])
4555 return dset
4656
57+ def get_anglog (energy_scale , acceleration_voltage , beta ):
58+ e0 = acceleration_voltage / 1000
59+ gamma = 1 + e0 / 511.06
60+ T = e0 * (e0 + 1022.12 )/ (e0 + 511.06 ) # Appendix E p 427 in keV
61+ theta_E = energy_scale / (2 * gamma * T * 1000 ) # Appendix E p 427 now in eV
62+
63+ theta_E [np .where (theta_E <= 0 )] = 1e-9
64+ return np .log (1.0 + (beta / theta_E )** 2 )
65+
66+
67+ def fit_plasmon (spectrum , start_fit_energy , end_fit_energy ):
68+ """
69+ Fit plasmon peak positions and widths in a TEM dataset using a Drude model.
70+
71+ This function applies the Drude model to fit plasmon peaks in a dataset obtained
72+ from transmission electron microscopy (TEM). It processes the dataset to determine
73+ peak positions, widths, and amplitudes within a specified energy range. The function
74+ can handle datasets with different dimensions and offers parallel processing capabilities.
75+
76+ Parameters:
77+ dataset: sidpy.Dataset or numpy.ndarray
78+ The dataset containing TEM spectral data.
79+ start_fit_energy: float
80+ The start energy of the fitting window.
81+ end_fit_energy: float
82+ The end energy of the fitting window.
83+
84+ Returns:
85+ fitted_dataset: numpy.ndarray
86+ The dataset with fitted plasmon peak parameters. The dimensions and
87+ format depend on the input dataset.
88+
89+ """
90+ energy = spectrum .get_spectral_dims (return_axis = True )[0 ].values
91+
92+ start_fit_pixel = np .searchsorted (energy , start_fit_energy )
93+ end_fit_pixel = np .searchsorted (energy , end_fit_energy )
94+ zero_pixel = np .searchsorted (energy , 0 )
95+ print (zero_pixel , start_fit_pixel , end_fit_pixel )
96+ acceleration_eV = spectrum .metadata ['experiment' ]['acceleration_voltage' ]
97+ convergence_angle = spectrum .metadata ['experiment' ]['convergence_angle' ]
98+
99+ anglog = get_anglog (energy [start_fit_pixel :end_fit_pixel ], acceleration_eV , convergence_angle )
100+
101+ def residuals (parameters , energy , data ):
102+ return data - energy_loss_function (energy , parameters , anglog )
103+
104+ guess = np .array ([start_fit_energy + (end_fit_energy - start_fit_energy )/ 2 , 4 , 1.1 , 1000 ])
105+ fit_p = scipy .optimize .least_squares (residuals , guess , args = (energy [start_fit_pixel :end_fit_pixel ],
106+ np .array (spectrum )[start_fit_pixel :end_fit_pixel ]),
107+ method = 'lm' )
108+ anglog = get_anglog (energy , acceleration_eV , convergence_angle )
109+
110+ low_loss = energy_loss_function (energy , fit_p ['x' ], anglog )
111+ low_loss [: zero_pixel ] = 0.0
112+ spectrum .metadata .setdefault ('plasmon' , {})['single_scattering_fit' ]= {'parameters' : fit_p ['x' ],
113+ 'fit_range' : (start_fit_energy , end_fit_energy ),
114+ 'function' : 'Drude' }
115+ return low_loss , fit_p ['x' ]
47116
48- def fit_plasmon (dataset : Union [sidpy .Dataset , np .ndarray ],
117+
118+ def fit_plasmons (dataset : Union [sidpy .Dataset , np .ndarray ],
49119 start_fit_energy : float , end_fit_energy : float ,
50120 number_workers : int = 4 , number_threads : int = 8
51121 ) -> Union [sidpy .Dataset , np .ndarray ]:
@@ -158,6 +228,17 @@ def energy_loss_function2(e: np.ndarray, e_p: float,e_w: float,
158228 return plasmon_dset
159229
160230
231+ def angle_correction (spectrum ):
232+ acceleration_voltage = spectrum .metadata ['experiment' ]['acceleration_voltage' ]
233+ energy_scale = spectrum .get_spectral_dims (return_axis = True )[0 ]
234+ eff_beta = effective_collection_angle (energy_scale ,
235+ spectrum .metadata ['experiment' ]['convergence_angle' ],
236+ spectrum .metadata ['experiment' ]['collection_angle' ],
237+ acceleration_voltage )
238+
239+
240+
241+
161242def angle_correction (spectrum ):
162243 """ angle correction per energy loss"""
163244 acceleration_voltage = spectrum .metadata ['experiment' ]['acceleration_voltage' ]
@@ -232,43 +313,72 @@ def inelastic_mean_free_path(e_p, spectrum):
232313 return imfp , theta_e
233314
234315
235- def multiple_scattering (energy_scale : np .ndarray , p : list , core_loss = False )-> np .ndarray :
316+ def multiple_scattering (energy_scale : np .ndarray , p : list , anglog : np . ndarray )-> np .ndarray :
236317 """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
318+ tmfp = p [4 ]
319+ if energy_scale [0 ] < 0 :
320+ zero_pixel = np .searchsorted (energy_scale , 0 )+ 1
241321 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 )
322+ zero_pixel = 0
323+
324+ SSD = energy_loss_function (energy_scale , p , anglog )[zero_pixel :]
325+ ssd = np .fft .fft (SSD )
247326 ssd2 = ssd .copy ()
248-
327+
249328 ### sum contribution from each order of scattering:
250- psd = np .zeros (len (ll_energie ))
329+ PSD = np .zeros (len (energy_scale [ zero_pixel :] ))
251330 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-
331+ # This order convoluted spectum
332+ # convoluted SSD is SSD2
333+ SSD2 = np .fft .ifft (ssd ).real
334+
335+ # And add this order to final spectrum #using equation 4.1 of egerton 2nd edition
336+ PSD += ( SSD2 * abs ( sum ( SSD ) / sum ( SSD2 ))
337+ / scipy . special . factorial ( order + 1 )
338+ * np .power (tmfp , (order + 1 )) * np .exp (- tmfp ) )
339+
261340 # next order convolution
262341 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
342+
343+ PSD /= tmfp * np .exp (- tmfp )
344+ msd = np .zeros (len (energy_scale ))
345+ msd [zero_pixel :] = PSD
269346 return msd
270347
271- def fit_multiple_scattering (dataset : Union [sidpy .Dataset , np .ndarray ],
348+ def fit_multiple_scattering (spectrum , anglog = 1 , end_fit_energy = 55 ):
349+ """
350+ Fit multiple scattering of plasmon peak in a TEM dataset.
351+
352+ Parameters:
353+ dataset: sidpy.Dataset or numpy.ndarray
354+ The dataset containing TEM spectral data.
355+ end_fit_energy: float
356+ The end energy of the fitting window.
357+ Returns:
358+ fitted_dataset: numpy.ndarray
359+ """
360+ energy_scale = spectrum .energy_loss .values
361+ p0 = list (spectrum .metadata ['plasmon' ]['single_scattering_fit' ]['parameters' ])+ [.37 ]
362+
363+ def errf_multi (p , y , x ):
364+ elf = multiple_scattering (x , p , anglog [:endFit ])
365+ return np .abs (y - elf ) # /np.sqrt(y)
366+
367+ endFit = np .searchsorted (energy_scale , end_fit_energy )
368+
369+ p2 = scipy .optimize .least_squares (errf_multi , p0 ,
370+ args = (np .array (spectrum )[:endFit ],
371+ energy_scale [:endFit ]),
372+ method = 'lm' )
373+ p2 = p2 ['x' ]
374+ cts = multiple_scattering (energy_scale , p2 , anglog )
375+ # print(f"relative thickness t/lambda: {p2[4]:.3f}")
376+ spectrum .metadata ['plasmon' ]['multiple_scattering_fit' ] = {'parameters' : p2 ,
377+ 'tmfp' : p2 [4 ]}
378+ return cts
379+
380+
381+ def fit_multiple_scattering2 (dataset : Union [sidpy .Dataset , np .ndarray ],
272382 start_fit_energy : float , end_fit_energy : float , pin = None ,
273383 number_workers : int = 4 , number_threads : int = 8
274384 ) -> Union [sidpy .Dataset , np .ndarray ]:
@@ -320,6 +430,35 @@ def errf_multi(p, y, x):
320430 return multi
321431
322432
433+ def estimate_thickness (spectrum , anglog ):
434+ "estimate thickness from plasmon fit"
435+ energy_scale = spectrum .get_spectral_dims (return_axis = True )[0 ].values
436+ p2 = spectrum .metadata ['plasmon' ]['multiple_scattering_fit' ]['parameters' ]
437+
438+ eps = drude (energy_scale , p2 )
439+ eps [eps == 0.0 ]= 1e-19
440+ elf = (- 1 / eps ).imag * anglog * p2 [3 ]
441+ e0 = spectrum .metadata ['experiment' ]['acceleration_voltage' ]/ 1000
442+ beta = spectrum .metadata ['experiment' ]['collection_angle' ]/ 1000
443+ T = 1000.0 * e0 * (1. + e0 / 1022.12 )/ (1.0 + e0 / 511.06 )** 2 ;# %eV # equ.5.2a or Appendix E p 427
444+
445+ tnm = spectrum .metadata ['plasmon' ]['multiple_scattering_fit' ]['parameters' ][4 ]
446+ volint = abs (tnm / (np .pi * 0.05292 * T * 2.0 )* elf * anglog )
447+ Pv = (volint / spectrum ).sum () ## our data have he same epc and the trapz formula does not include
448+ ep = p2 [0 ]
449+ tgt = 1000 * e0 * (1022.12 + e0 )/ (511.06 + e0 );# %eV Appendix E p 427
450+ lambda_pv = tnm / Pv ; #% does NOT depend on free-electron approximation (no damping).
451+ lambda_fe = 4.0 * 0.05292 * T / ep / np .log (1 + (beta * tgt / ep ) ** 2 ); #% Eq.(3.44) approximation
452+
453+ print (f'Volume-plasmon MFP = { lambda_pv :.2f} nm' )
454+ print (f'Free-electron MFP = { lambda_fe :.2f} nm' )
455+ print ('--------------------------------' )
456+ print (f"relative thickness t/lambda: { tnm :.3f} " )
457+ print (f'estimated thickness = { lambda_fe * tnm :.2f} nm\n ' )
458+ return lambda_pv , lambda_fe ,
459+
460+
461+
323462def drude_simulation (dset , e , ep , ew , tnm , eb ):
324463 """probabilities of dielectric function eps relative to zero-loss integral (i0 = 1)
325464
0 commit comments