Skip to content

Commit a3d90c7

Browse files
committed
update eels
1 parent a606198 commit a3d90c7

5 files changed

Lines changed: 193 additions & 620 deletions

File tree

notebooks/Spectroscopy/EDS.ipynb

Lines changed: 0 additions & 578 deletions
This file was deleted.

pyTEMlib/eels_tools/__init__.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99
from ..utilities import get_wavelength, effective_collection_angle, set_default_metadata
1010
from ..utilities import lorentz, gauss, get_x_sections, get_z, get_spectrum
1111

12-
from .zero_loss_tools import zero_loss_function, get_resolution_functions
12+
from .zero_loss_tools import zero_loss_function, get_resolution_functions, get_resolution_function
1313
from .zero_loss_tools import get_zero_loss_energy, shift_energy, align_zero_loss
1414

1515
from .low_loss_tools import drude_simulation, kroeger_core
1616
from .low_loss_tools import get_plasmon_losses, drude, drude_lorentz
1717
from .low_loss_tools import energy_loss_function, angle_correction, fit_plasmon
18-
from .low_loss_tools import fit_multiple_scattering, multiple_scattering
19-
from .low_loss_tools import inelastic_mean_free_path, model3, add_peaks
18+
from .low_loss_tools import fit_multiple_scattering, multiple_scattering, get_anglog
19+
from .low_loss_tools import inelastic_mean_free_path, model3, add_peaks, estimate_thickness
2020

2121
from .peak_fit_tools import model_smooth, gaussian_mixture_model, find_peaks, find_maxima
2222
from .peak_fit_tools import sort_peaks
@@ -33,10 +33,10 @@
3333
__all__ = ['major_edges', 'all_edges', 'first_close_edges', 'elements', 'get_wavelength',
3434
'effective_collection_angle', 'set_default_metadata', 'lorentz', 'gauss',
3535
'get_x_sections', 'get_z', 'get_spectrum', 'zero_loss_function',
36-
'get_resolution_functions', 'get_zero_loss_energy', 'shift_energy', 'align_zero_loss',
37-
'drude_simulation', 'kroeger_core','get_plasmon_losses', 'drude', 'drude_lorentz',
36+
'get_resolution_functions', 'get_resolution_function', 'get_zero_loss_energy', 'shift_energy', 'align_zero_loss',
37+
'drude_simulation', 'kroeger_core','get_plasmon_losses', 'drude', 'drude_lorentz', 'get_anglog',
3838
'energy_loss_function', 'angle_correction', 'fit_plasmon', 'fit_multiple_scattering',
39-
'multiple_scattering', 'inelastic_mean_free_path', 'model3', 'add_peaks',
39+
'multiple_scattering', 'inelastic_mean_free_path', 'model3', 'add_peaks', 'estimate_thickness',
4040
'model_smooth', 'gaussian_mixture_model', 'find_peaks', 'find_maxima', 'sort_peaks',
4141
'make_cross_sections', 'fit_edges2', 'power_law_background', 'list_all_edges',
4242
'find_all_edges', 'find_associated_edges', 'find_white_lines', 'find_edges',

pyTEMlib/eels_tools/low_loss_tools.py

Lines changed: 174 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,21 @@
1212
from .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

2332
def 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

3241
def 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

3949
def 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+
161242
def 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+
323462
def drude_simulation(dset, e, ep, ew, tnm, eb):
324463
"""probabilities of dielectric function eps relative to zero-loss integral (i0 = 1)
325464

pyTEMlib/eels_tools/zero_loss_tools.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,18 @@ def get_zero_losses(energy, z_loss_params):
152152
return z_loss_dset
153153

154154

155+
def get_resolution_function(dataset: sidpy.Dataset) -> np.ndarray:
156+
"""
157+
Analyze and fit low-loss EELS data within a specified energy range to determine zero-loss peaks.
158+
"""
159+
def residuals(parameters, energy, data):
160+
return data - zero_loss_function(energy, parameters)
161+
162+
guess = np.array([.2, 10000, .1, -0.2, 10000, .1])
163+
fit_p = scipy.optimize.least_squares(residuals, guess, args=(dataset.energy_loss.values, np.array(dataset)),method='lm')
164+
dataset.metadata.setdefault('zero_loss', {})['fit'] ={'parameters': fit_p['x'], 'function': 'product of lorentzians'}
165+
166+
return zero_loss_function(dataset.energy_loss.values, fit_p['x'])
155167

156168

157169
def get_resolution_functions(dataset: sidpy.Dataset, start_fit_energy: float=-1,

pyTEMlib/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
22
version
33
"""
4-
__version__ = '0.2026.5.0'
4+
__version__ = '0.2026.6.0'
55
__time__ = '2026-04-28 20:58:26'

0 commit comments

Comments
 (0)