xarpes 0.5.0__py3-none-any.whl → 0.6.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.
- xarpes/__init__.py +1 -1
- xarpes/bandmap.py +105 -34
- xarpes/functions.py +276 -51
- xarpes/mdcs.py +49 -6
- xarpes/selfenergies.py +1728 -89
- xarpes/settings_parameters.py +45 -0
- {xarpes-0.5.0.dist-info → xarpes-0.6.1.dist-info}/METADATA +10 -5
- xarpes-0.6.1.dist-info/RECORD +15 -0
- xarpes-0.5.0.dist-info/RECORD +0 -15
- {xarpes-0.5.0.dist-info → xarpes-0.6.1.dist-info}/LICENSE +0 -0
- {xarpes-0.5.0.dist-info → xarpes-0.6.1.dist-info}/WHEEL +0 -0
- {xarpes-0.5.0.dist-info → xarpes-0.6.1.dist-info}/entry_points.txt +0 -0
xarpes/__init__.py
CHANGED
xarpes/bandmap.py
CHANGED
|
@@ -14,28 +14,49 @@
|
|
|
14
14
|
import numpy as np
|
|
15
15
|
from igor2 import binarywave
|
|
16
16
|
from .plotting import get_ax_fig_plt, add_fig_kwargs
|
|
17
|
-
from .functions import
|
|
17
|
+
from .functions import fit_least_squares, extend_function
|
|
18
18
|
from .distributions import FermiDirac, Linear
|
|
19
19
|
from .constants import PREF
|
|
20
20
|
|
|
21
21
|
class BandMap:
|
|
22
22
|
r"""
|
|
23
|
-
|
|
23
|
+
Band map container for ARPES intensity data.
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
Prefer
|
|
25
|
+
Notes
|
|
26
|
+
-----
|
|
27
|
+
Prefer :meth:`from_ibw_file` or :meth:`from_np_arrays`. The ``__init__``
|
|
28
|
+
expects canonical arrays (no file I/O).
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
See Also
|
|
31
|
+
--------
|
|
32
|
+
BandMap.from_ibw_file
|
|
33
|
+
BandMap.from_np_arrays
|
|
33
34
|
"""
|
|
34
35
|
|
|
35
36
|
@classmethod
|
|
36
37
|
def from_ibw_file(cls, datafile, transpose=False, flip_ekin=False,
|
|
37
38
|
flip_angles=False, **kwargs):
|
|
38
|
-
r"""
|
|
39
|
+
r"""
|
|
40
|
+
Construct a `BandMap` from an IGOR binary wave (.ibw).
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
datafile : path-like
|
|
45
|
+
Path to the .ibw file.
|
|
46
|
+
transpose : bool, optional
|
|
47
|
+
If True, transpose the loaded intensity array and swap axes metadata.
|
|
48
|
+
flip_ekin : bool, optional
|
|
49
|
+
If True, reverse the kinetic-energy axis.
|
|
50
|
+
flip_angles : bool, optional
|
|
51
|
+
If True, reverse the angle axis.
|
|
52
|
+
**kwargs
|
|
53
|
+
Passed to `BandMap.__init__`.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
BandMap
|
|
58
|
+
New instance constructed from the file contents.
|
|
59
|
+
"""
|
|
39
60
|
data = binarywave.load(datafile)
|
|
40
61
|
intensities = data['wave']['wData']
|
|
41
62
|
|
|
@@ -66,7 +87,27 @@ class BandMap:
|
|
|
66
87
|
@classmethod
|
|
67
88
|
def from_np_arrays(cls, intensities=None, angles=None, ekin=None, enel=None,
|
|
68
89
|
**kwargs):
|
|
69
|
-
r"""
|
|
90
|
+
r"""
|
|
91
|
+
Construct a `BandMap` from NumPy arrays.
|
|
92
|
+
|
|
93
|
+
Exactly one of `ekin` or `enel` must be provided.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
intensities : array-like
|
|
98
|
+
Intensity map with shape (n_energy, n_angle).
|
|
99
|
+
angles : array-like
|
|
100
|
+
Angle axis values.
|
|
101
|
+
ekin, enel : array-like
|
|
102
|
+
Provide exactly one: kinetic energy (`ekin`) or binding energy (`enel`).
|
|
103
|
+
**kwargs
|
|
104
|
+
Passed to `BandMap.__init__`.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
BandMap
|
|
109
|
+
New instance constructed from the provided arrays.
|
|
110
|
+
"""
|
|
70
111
|
if intensities is None or angles is None:
|
|
71
112
|
raise ValueError('Please provide intensities and angles.')
|
|
72
113
|
if (ekin is None) == (enel is None):
|
|
@@ -273,23 +314,51 @@ class BandMap:
|
|
|
273
314
|
|
|
274
315
|
def mdc_set(self, angle_min, angle_max, energy_value=None,
|
|
275
316
|
energy_range=None):
|
|
276
|
-
r"""
|
|
277
|
-
|
|
317
|
+
r"""Return a set of momentum distribution curves (MDCs).
|
|
318
|
+
|
|
319
|
+
This method extracts MDCs from the stored ARPES intensity map from a
|
|
320
|
+
specified angular interval and either selecting a single energy slice
|
|
321
|
+
or an energy window.
|
|
322
|
+
|
|
278
323
|
Parameters
|
|
279
324
|
----------
|
|
280
325
|
angle_min : float
|
|
281
|
-
Minimum angle of integration interval [degrees]
|
|
326
|
+
Minimum angle of the integration interval [degrees].
|
|
282
327
|
angle_max : float
|
|
283
|
-
Maximum angle of integration interval [degrees]
|
|
328
|
+
Maximum angle of the integration interval [degrees].
|
|
329
|
+
energy_value : float, optional
|
|
330
|
+
Energy value [same units as ``self.enel``] at which a single MDC
|
|
331
|
+
is extracted. Exactly one of ``energy_value`` or ``energy_range``
|
|
332
|
+
must be provided.
|
|
333
|
+
energy_range : array-like, optional
|
|
334
|
+
Energy interval [same units as ``self.enel``] over which MDCs are
|
|
335
|
+
extracted. Exactly one of ``energy_value`` or ``energy_range``
|
|
336
|
+
must be provided.
|
|
284
337
|
|
|
285
338
|
Returns
|
|
286
339
|
-------
|
|
287
|
-
angle_range : ndarray
|
|
288
|
-
Array of size n containing the angular values
|
|
289
|
-
energy_range : ndarray
|
|
290
|
-
Array of size m containing the energy values
|
|
291
340
|
mdcs : ndarray
|
|
292
|
-
|
|
341
|
+
Extracted MDC intensities. Shape is ``(n_angles,)`` when a single
|
|
342
|
+
``energy_value`` is provided, or ``(n_energies, n_angles)`` when
|
|
343
|
+
an ``energy_range`` is provided.
|
|
344
|
+
angle_range : ndarray
|
|
345
|
+
Angular values corresponding to the MDCs [degrees].
|
|
346
|
+
angle_resolution : float
|
|
347
|
+
Angular resolution associated with the MDCs.
|
|
348
|
+
energy_resolution : float
|
|
349
|
+
Energy resolution associated with the MDCs.
|
|
350
|
+
temperature: float
|
|
351
|
+
Temperature associated with the band map [K].
|
|
352
|
+
energy_range : ndarray or float
|
|
353
|
+
Energy value (scalar) or energy array corresponding to the MDCs.
|
|
354
|
+
hnuminPhi : float
|
|
355
|
+
Photon-energy-related offset propagated from the BandMap.
|
|
356
|
+
|
|
357
|
+
Raises
|
|
358
|
+
------
|
|
359
|
+
ValueError
|
|
360
|
+
If neither or both of ``energy_value`` and ``energy_range`` are
|
|
361
|
+
provided.
|
|
293
362
|
|
|
294
363
|
"""
|
|
295
364
|
|
|
@@ -316,8 +385,8 @@ class BandMap:
|
|
|
316
385
|
mdcs = self.intensities[energy_indices,
|
|
317
386
|
angle_min_index:angle_max_index + 1]
|
|
318
387
|
|
|
319
|
-
return mdcs, angle_range_out, self.angle_resolution,
|
|
320
|
-
|
|
388
|
+
return (mdcs, angle_range_out, self.angle_resolution,
|
|
389
|
+
self.energy_resolution, self.temperature, enel_range_out, self.hnuminPhi)
|
|
321
390
|
|
|
322
391
|
@add_fig_kwargs
|
|
323
392
|
def plot(self, abscissa='momentum', ordinate='electron_energy',
|
|
@@ -677,9 +746,10 @@ class BandMap:
|
|
|
677
746
|
|
|
678
747
|
extra_args = (self.temperature,)
|
|
679
748
|
|
|
680
|
-
popt, pcov =
|
|
681
|
-
|
|
682
|
-
|
|
749
|
+
popt, pcov, success = fit_least_squares(
|
|
750
|
+
p0=parameters, xdata=energy_range, ydata=integrated_intensity,
|
|
751
|
+
function=fdir_initial, resolution=self.energy_resolution,
|
|
752
|
+
yerr=None, bounds=None, extra_args=extra_args)
|
|
683
753
|
|
|
684
754
|
# Update hnuminPhi; automatically sets self.enel
|
|
685
755
|
self.hnuminPhi = popt[0]
|
|
@@ -757,9 +827,8 @@ class BandMap:
|
|
|
757
827
|
angle_range = self.angles[angle_min_index:angle_max_index + 1]
|
|
758
828
|
energy_range = self.ekin[ekin_min_index:ekin_max_index + 1]
|
|
759
829
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
stds = np.zeros(angle_shape)
|
|
830
|
+
nmps = np.zeros_like(angle_range, dtype=float)
|
|
831
|
+
stds = np.zeros_like(angle_range, dtype=float)
|
|
763
832
|
|
|
764
833
|
hnuminPhi_left = hnuminPhi_guess - (true_angle - angle_min) \
|
|
765
834
|
* slope_guess
|
|
@@ -778,9 +847,10 @@ class BandMap:
|
|
|
778
847
|
for indx in range(angle_max_index - angle_min_index + 1):
|
|
779
848
|
edge = Intensities[:, indx]
|
|
780
849
|
|
|
781
|
-
parameters, pcov =
|
|
782
|
-
|
|
783
|
-
|
|
850
|
+
parameters, pcov, success = fit_least_squares(
|
|
851
|
+
p0=parameters, xdata=energy_range, ydata=edge,
|
|
852
|
+
function=fdir_initial, resolution=self.energy_resolution,
|
|
853
|
+
yerr=None, bounds=None, extra_args=extra_args)
|
|
784
854
|
|
|
785
855
|
nmps[indx] = parameters[0]
|
|
786
856
|
stds[indx] = np.sqrt(np.diag(pcov)[0])
|
|
@@ -793,11 +863,12 @@ class BandMap:
|
|
|
793
863
|
|
|
794
864
|
lin_fun = Linear(offset_guess, slope_guess, 'Linear')
|
|
795
865
|
|
|
796
|
-
popt, pcov =
|
|
797
|
-
|
|
866
|
+
popt, pcov, success = fit_least_squares(p0=parameters, xdata=angle_range,
|
|
867
|
+
ydata=nmps, function=lin_fun, resolution=None,
|
|
868
|
+
yerr=stds, bounds=None)
|
|
798
869
|
|
|
799
870
|
linsp = lin_fun(angle_range, popt[0], popt[1])
|
|
800
|
-
|
|
871
|
+
|
|
801
872
|
# Update hnuminPhi; automatically sets self.enel
|
|
802
873
|
self.hnuminPhi = lin_fun(true_angle, popt[0], popt[1])
|
|
803
874
|
self.hnuminPhi_std = np.sqrt(true_angle**2 * pcov[1, 1] + pcov[0, 0]
|
xarpes/functions.py
CHANGED
|
@@ -148,8 +148,8 @@ def extend_function(abscissa_range, abscissa_resolution):
|
|
|
148
148
|
return extend, step, numb
|
|
149
149
|
|
|
150
150
|
|
|
151
|
-
def error_function(p, xdata, ydata, function, resolution, yerr, extra_args):
|
|
152
|
-
r"""The error function used inside the
|
|
151
|
+
def error_function(p, xdata, ydata, function, resolution, yerr, *extra_args):
|
|
152
|
+
r"""The error function used inside the fit_least_squares function.
|
|
153
153
|
|
|
154
154
|
Parameters
|
|
155
155
|
----------
|
|
@@ -187,66 +187,53 @@ def error_function(p, xdata, ydata, function, resolution, yerr, extra_args):
|
|
|
187
187
|
return residual
|
|
188
188
|
|
|
189
189
|
|
|
190
|
-
def
|
|
191
|
-
|
|
192
|
-
r"""
|
|
190
|
+
def fit_least_squares(p0, xdata, ydata, function, resolution=None, yerr=None,
|
|
191
|
+
bounds=None, extra_args=None):
|
|
192
|
+
r"""Least-squares fit using `scipy.optimize.least_squares`.
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
p0 : ndarray
|
|
197
|
-
Initial guess for parameters to be optimized.
|
|
198
|
-
xdata : ndarray
|
|
199
|
-
Abscissa values the function is evaluated on.
|
|
200
|
-
ydata : ndarray
|
|
201
|
-
Measured values to compare to.
|
|
202
|
-
function : callable
|
|
203
|
-
Function or class with __call__ method to evaluate.
|
|
204
|
-
resolution : float or None, optional
|
|
205
|
-
Convolution resolution (sigma), if applicable.
|
|
206
|
-
yerr : ndarray or None, optional
|
|
207
|
-
Standard deviations of ydata. Defaults to ones if None.
|
|
208
|
-
extra_args : tuple
|
|
209
|
-
Additional arguments passed to the function.
|
|
194
|
+
Default behavior is Levenberg–Marquardt (`method="lm"`) when unbounded.
|
|
195
|
+
If `bounds` is provided, switches to trust-region reflective (`"trf"`).
|
|
210
196
|
|
|
211
|
-
Returns
|
|
212
|
-
|
|
213
|
-
pfit_leastsq : ndarray
|
|
214
|
-
Optimized parameters.
|
|
215
|
-
pcov : ndarray or float
|
|
216
|
-
Scaled covariance matrix of the optimized parameters.
|
|
217
|
-
If the covariance could not be estimated, returns np.inf.
|
|
197
|
+
Returns (pfit, pcov, success) in the same style as the old `leastsq`
|
|
198
|
+
wrapper, with an additional boolean `success` from SciPy.
|
|
218
199
|
"""
|
|
219
|
-
from scipy.optimize import
|
|
200
|
+
from scipy.optimize import least_squares
|
|
220
201
|
|
|
221
202
|
if yerr is None:
|
|
222
203
|
yerr = np.ones_like(ydata)
|
|
223
204
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
205
|
+
if extra_args is None:
|
|
206
|
+
extra_args = ()
|
|
207
|
+
|
|
208
|
+
def _residuals(p):
|
|
209
|
+
return error_function(
|
|
210
|
+
p, xdata, ydata, function, resolution, yerr, *extra_args
|
|
211
|
+
)
|
|
230
212
|
|
|
231
|
-
if
|
|
232
|
-
|
|
233
|
-
error_function(pfit, xdata, ydata, function, resolution,
|
|
234
|
-
yerr, extra_args) ** 2
|
|
235
|
-
).sum() / (len(ydata) - len(p0))
|
|
236
|
-
pcov *= s_sq
|
|
213
|
+
if bounds is None:
|
|
214
|
+
res = least_squares(_residuals, p0, method="lm")
|
|
237
215
|
else:
|
|
238
|
-
|
|
216
|
+
res = least_squares(_residuals, p0, method="trf", bounds=bounds)
|
|
239
217
|
|
|
240
|
-
|
|
218
|
+
pfit = res.x
|
|
219
|
+
success = bool(getattr(res, "success", False))
|
|
241
220
|
|
|
221
|
+
m = len(ydata)
|
|
222
|
+
n = pfit.size
|
|
242
223
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
224
|
+
if (m > n) and (res.jac is not None) and res.jac.size:
|
|
225
|
+
resid = res.fun
|
|
226
|
+
s_sq = (resid ** 2).sum() / (m - n)
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
jtj = res.jac.T @ res.jac
|
|
230
|
+
pcov = np.linalg.inv(jtj) * s_sq
|
|
231
|
+
except np.linalg.LinAlgError:
|
|
232
|
+
pcov = np.inf
|
|
233
|
+
else:
|
|
234
|
+
pcov = np.inf
|
|
235
|
+
|
|
236
|
+
return pfit, pcov, success
|
|
250
237
|
|
|
251
238
|
|
|
252
239
|
|
|
@@ -400,4 +387,242 @@ def set_script_dir():
|
|
|
400
387
|
# If __file__ isn't defined, fall back to current working directory
|
|
401
388
|
script_dir = os.getcwd()
|
|
402
389
|
|
|
403
|
-
return script_dir
|
|
390
|
+
return script_dir
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def MEM_core(dvec, model_in, uvec, mu, alpha, wvec, V_Sigma, U,
|
|
394
|
+
t_criterion, iter_max):
|
|
395
|
+
r"""
|
|
396
|
+
Implementation of Bryan's algorithm (not to be confused with Bryan's
|
|
397
|
+
'method' for determining the Lagrange multiplier alpha. For details, see
|
|
398
|
+
Eur. Biophys. J. 18, 165 (1990).
|
|
399
|
+
"""
|
|
400
|
+
import numpy as np
|
|
401
|
+
import warnings
|
|
402
|
+
|
|
403
|
+
spectrum_in = model_in * np.exp(U @ uvec) # Eq. 9
|
|
404
|
+
alphamu = alpha + mu
|
|
405
|
+
|
|
406
|
+
converged = False
|
|
407
|
+
iter_count = 0
|
|
408
|
+
while not converged and iter_count < iter_max:
|
|
409
|
+
|
|
410
|
+
T = V_Sigma @ (U.T @ spectrum_in) # Below Eq. 7
|
|
411
|
+
gvec = V_Sigma.T @ (wvec * (T - dvec)) # Eq. 10
|
|
412
|
+
M = V_Sigma.T @ (wvec[:, None] * V_Sigma) # Above Eq. 11
|
|
413
|
+
K = U.T @ (spectrum_in[:, None] * U) # Above Eq. 11
|
|
414
|
+
|
|
415
|
+
xi, P = np.linalg.eigh(K) # Eq. 13
|
|
416
|
+
sqrt_xi = np.sqrt(xi)
|
|
417
|
+
P_sqrt_xi = P * sqrt_xi[None, :]
|
|
418
|
+
A = P_sqrt_xi.T @ (M @ P_sqrt_xi) # Between Eqs. 13 and 14
|
|
419
|
+
Lambda, R = np.linalg.eigh(A) # Eq. 14
|
|
420
|
+
Y_inv = R.T @ (sqrt_xi[:, None] * P.T) # Below Eq. 15
|
|
421
|
+
|
|
422
|
+
# From Eq. 16:
|
|
423
|
+
Y_inv_du = -(Y_inv @ (alpha * uvec + gvec)) / (alphamu + Lambda)
|
|
424
|
+
d_uvec = (
|
|
425
|
+
-alpha * uvec - gvec - M @ (Y_inv.T @ Y_inv_du)
|
|
426
|
+
) / alphamu # Eq. 20
|
|
427
|
+
|
|
428
|
+
uvec += d_uvec
|
|
429
|
+
spectrum_in = model_in * np.exp(U @ uvec) # Eq. 9
|
|
430
|
+
|
|
431
|
+
# Convergence block: Section 2.3
|
|
432
|
+
alpha_K_u = alpha * (K @ uvec) # Skipping the minus sign twice
|
|
433
|
+
K_g = K @ gvec
|
|
434
|
+
tcon = (
|
|
435
|
+
2 * np.linalg.norm(alpha_K_u + K_g)**2
|
|
436
|
+
/ (np.linalg.norm(alpha_K_u) + np.linalg.norm(K_g))**2
|
|
437
|
+
)
|
|
438
|
+
converged = (tcon < t_criterion)
|
|
439
|
+
|
|
440
|
+
iter_count += 1
|
|
441
|
+
|
|
442
|
+
if not converged:
|
|
443
|
+
with warnings.catch_warnings():
|
|
444
|
+
warnings.simplefilter("always", RuntimeWarning)
|
|
445
|
+
warnings.warn(
|
|
446
|
+
f"MEM_core did not converge within iter_max={iter_max} "
|
|
447
|
+
f"(performed {iter_count} iterations).",
|
|
448
|
+
category=RuntimeWarning,
|
|
449
|
+
stacklevel=2,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
return spectrum_in, uvec
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def bose_einstein(omega, k_BT):
|
|
456
|
+
"""Bose-Einstein distribution n_B(omega) for k_BT > 0 and omega >= 0."""
|
|
457
|
+
x_over = np.log(np.finfo(float).max) # ~709.78 for float64
|
|
458
|
+
|
|
459
|
+
x = omega / k_BT
|
|
460
|
+
|
|
461
|
+
out = np.empty_like(omega, dtype=float)
|
|
462
|
+
|
|
463
|
+
momega0 = (omega == 0)
|
|
464
|
+
if np.any(momega0):
|
|
465
|
+
out[momega0] = np.inf
|
|
466
|
+
|
|
467
|
+
mpos_big = (x > x_over) & (omega != 0)
|
|
468
|
+
if np.any(mpos_big):
|
|
469
|
+
out[mpos_big] = 0.0
|
|
470
|
+
|
|
471
|
+
mnorm = (omega != 0) & ~mpos_big
|
|
472
|
+
if np.any(mnorm):
|
|
473
|
+
out[mnorm] = 1.0 / np.expm1(x[mnorm])
|
|
474
|
+
|
|
475
|
+
return out
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def fermi(omega, k_BT):
|
|
479
|
+
"""Fermi-Dirac distribution f(omega) for k_BT > 0 and omega >= 0.
|
|
480
|
+
Could potentially be made a core block of the FermiDirac distribution."""
|
|
481
|
+
x_over = np.log(np.finfo(float).max) # ~709.78 for float64
|
|
482
|
+
|
|
483
|
+
x = omega / k_BT
|
|
484
|
+
out = np.empty_like(omega, dtype=float)
|
|
485
|
+
|
|
486
|
+
mover = x > x_over
|
|
487
|
+
out[mover] = 0.0
|
|
488
|
+
|
|
489
|
+
mnorm = ~mover
|
|
490
|
+
y = np.exp(-x[mnorm])
|
|
491
|
+
out[mnorm] = y / (1.0 + y)
|
|
492
|
+
|
|
493
|
+
return out
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def create_kernel_function(enel, omega, k_BT):
|
|
497
|
+
r"""Kernel function. Eq. 17 from https://arxiv.org/abs/2508.13845.
|
|
498
|
+
|
|
499
|
+
Returns
|
|
500
|
+
-------
|
|
501
|
+
K : ndarray, complex
|
|
502
|
+
Shape (enel.size, omega.size) if enel and omega are 1D.
|
|
503
|
+
"""
|
|
504
|
+
from scipy.special import digamma
|
|
505
|
+
|
|
506
|
+
enel = enel[:, None] # (Ne, 1)
|
|
507
|
+
omega = omega[None, :] # (1, Nw)
|
|
508
|
+
|
|
509
|
+
denom = 2.0 * np.pi * k_BT
|
|
510
|
+
|
|
511
|
+
K = (digamma(0.5 - 1j * (enel - omega) / denom)
|
|
512
|
+
- digamma(0.5 - 1j * (enel + omega) / denom)
|
|
513
|
+
- 2j * np.pi * (bose_einstein(omega, k_BT) + 0.5))
|
|
514
|
+
|
|
515
|
+
return K
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def singular_value_decomposition(kernel, sigma_svd):
|
|
519
|
+
r"""
|
|
520
|
+
Some papers use kernel = U Sigma V^T; we follow Bryan's algorithm.
|
|
521
|
+
"""
|
|
522
|
+
V, Sigma, U_transpose = np.linalg.svd(kernel)
|
|
523
|
+
U = U_transpose.T
|
|
524
|
+
Sigma = Sigma[Sigma > sigma_svd]
|
|
525
|
+
s_reduced = Sigma.size
|
|
526
|
+
V = V[:, :s_reduced]
|
|
527
|
+
U = U[:, :s_reduced]
|
|
528
|
+
V_Sigma = V * Sigma[None, :]
|
|
529
|
+
|
|
530
|
+
uvec = np.zeros(s_reduced)
|
|
531
|
+
|
|
532
|
+
print('Dimensionality has been reduced from a matrix of rank ' + str(min(kernel.shape)) +
|
|
533
|
+
' to ' + str(int(s_reduced)) + ' in the singular space.')
|
|
534
|
+
|
|
535
|
+
return V_Sigma, U, uvec
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def create_model_function(omega, omega_I, omega_M, omega_S, h_n):
|
|
539
|
+
r"""Piecewise model m_n(omega) defined on the omega grid.
|
|
540
|
+
|
|
541
|
+
Implements the piecewise definition in the figure, interpreting
|
|
542
|
+
omega_min/max as omega.min()/omega.max().
|
|
543
|
+
|
|
544
|
+
Parameters
|
|
545
|
+
----------
|
|
546
|
+
omega : ndarray
|
|
547
|
+
Frequency grid (assumed sorted, but only min/max are used).
|
|
548
|
+
omega_I : float
|
|
549
|
+
ω_n^I
|
|
550
|
+
omega_M : float
|
|
551
|
+
ω_n^M
|
|
552
|
+
omega_S : float
|
|
553
|
+
ω_n^S
|
|
554
|
+
h_n : float
|
|
555
|
+
h_n in the prefactor m_n(omega) = 2 h_n * ( ... ).
|
|
556
|
+
|
|
557
|
+
Returns
|
|
558
|
+
-------
|
|
559
|
+
model : ndarray
|
|
560
|
+
m_n(omega) evaluated on the omega grid.
|
|
561
|
+
"""
|
|
562
|
+
w_min = omega.min()
|
|
563
|
+
w_max = omega.max()
|
|
564
|
+
|
|
565
|
+
if omega_I <= 0:
|
|
566
|
+
raise ValueError("omega_I must be > 0.")
|
|
567
|
+
denom = w_max + omega_S - omega_M
|
|
568
|
+
if denom == 0:
|
|
569
|
+
raise ValueError("omega_max + omega_S - omega_M must be nonzero.")
|
|
570
|
+
|
|
571
|
+
w_I_half = 0.5 * omega_I
|
|
572
|
+
w_mid = 0.5 * (w_max + omega_S + omega_M)
|
|
573
|
+
|
|
574
|
+
domains = np.empty_like(omega)
|
|
575
|
+
|
|
576
|
+
m1 = (omega >= w_min) & (omega < w_I_half)
|
|
577
|
+
domains[m1] = (omega[m1] / omega_I) ** 2
|
|
578
|
+
|
|
579
|
+
m2 = (omega >= w_I_half) & (omega < omega_I)
|
|
580
|
+
domains[m2] = 0.5 - (omega[m2] / omega_I - 1.0) ** 2
|
|
581
|
+
|
|
582
|
+
m3 = (omega >= omega_I) & (omega < omega_M)
|
|
583
|
+
domains[m3] = 0.5
|
|
584
|
+
|
|
585
|
+
m4 = (omega >= omega_M) & (omega < w_mid)
|
|
586
|
+
domains[m4] = 0.5 - ((omega[m4] - omega_M) / denom) ** 2
|
|
587
|
+
|
|
588
|
+
m5 = (omega >= w_mid) & (omega <= w_max)
|
|
589
|
+
domains[m5] = ((omega[m5] - omega_M) / denom - 1.0) ** 2
|
|
590
|
+
|
|
591
|
+
return 2.0 * h_n * domains
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def chi2kink_logistic(x, a, b, c, d):
|
|
595
|
+
"""Four-parameter logistic (scaled sigmoid), evaluated stably.
|
|
596
|
+
|
|
597
|
+
Parameters
|
|
598
|
+
----------
|
|
599
|
+
x : array_like
|
|
600
|
+
Input values.
|
|
601
|
+
a : float
|
|
602
|
+
Lower asymptote.
|
|
603
|
+
b : float
|
|
604
|
+
Amplitude (upper - lower).
|
|
605
|
+
c : float
|
|
606
|
+
Midpoint (inflection point).
|
|
607
|
+
d : float
|
|
608
|
+
Slope parameter (steepness).
|
|
609
|
+
|
|
610
|
+
Returns
|
|
611
|
+
-------
|
|
612
|
+
phi : ndarray
|
|
613
|
+
Logistic curve evaluated at x.
|
|
614
|
+
"""
|
|
615
|
+
z = d * (x - c)
|
|
616
|
+
|
|
617
|
+
phi = np.empty_like(z, dtype=float)
|
|
618
|
+
|
|
619
|
+
mpos = z >= 0
|
|
620
|
+
if np.any(mpos):
|
|
621
|
+
phi[mpos] = a + b / (1.0 + np.exp(-z[mpos]))
|
|
622
|
+
|
|
623
|
+
mneg = ~mpos
|
|
624
|
+
if np.any(mneg):
|
|
625
|
+
expz = np.exp(z[mneg])
|
|
626
|
+
phi[mneg] = a + b * expz / (1.0 + expz)
|
|
627
|
+
|
|
628
|
+
return phi
|
xarpes/mdcs.py
CHANGED
|
@@ -32,6 +32,10 @@ class MDCs:
|
|
|
32
32
|
Angular grid corresponding to the MDCs [degrees].
|
|
33
33
|
angle_resolution : float
|
|
34
34
|
Angular step size or effective angular resolution [degrees].
|
|
35
|
+
energy_resolution : float
|
|
36
|
+
Energy resolution associated with the MDCs [eV].
|
|
37
|
+
temperature: float
|
|
38
|
+
Temperature associated with the band map [K].
|
|
35
39
|
enel : ndarray or float
|
|
36
40
|
Electron binding energies of the MDC slices [eV].
|
|
37
41
|
Can be a scalar for a single MDC.
|
|
@@ -83,11 +87,13 @@ class MDCs:
|
|
|
83
87
|
|
|
84
88
|
"""
|
|
85
89
|
|
|
86
|
-
def __init__(self, intensities, angles, angle_resolution,
|
|
87
|
-
|
|
90
|
+
def __init__(self, intensities, angles, angle_resolution,
|
|
91
|
+
energy_resolution, temperature, enel, hnuminPhi):
|
|
88
92
|
self._intensities = intensities
|
|
89
93
|
self._angles = angles
|
|
90
94
|
self._angle_resolution = angle_resolution
|
|
95
|
+
self._energy_resolution = energy_resolution
|
|
96
|
+
self._temperature = temperature
|
|
91
97
|
self._enel = enel
|
|
92
98
|
self._hnuminPhi = hnuminPhi
|
|
93
99
|
|
|
@@ -104,8 +110,39 @@ class MDCs:
|
|
|
104
110
|
|
|
105
111
|
@property
|
|
106
112
|
def angle_resolution(self):
|
|
107
|
-
"""Angular
|
|
113
|
+
"""Angular resolution (float)."""
|
|
108
114
|
return self._angle_resolution
|
|
115
|
+
|
|
116
|
+
@angle_resolution.setter
|
|
117
|
+
def angle_resolution(self, _):
|
|
118
|
+
"""Setter for the angle resolution. This raises an attribute error
|
|
119
|
+
as the angle resolution needs to be derived from the band map."""
|
|
120
|
+
raise AttributeError("`angle_resolution` is read-only; set it via the "
|
|
121
|
+
"constructor.")
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def energy_resolution(self):
|
|
125
|
+
"""Energy resolution (float)."""
|
|
126
|
+
return self._energy_resolution
|
|
127
|
+
|
|
128
|
+
@energy_resolution.setter
|
|
129
|
+
def energy_resolution(self, _):
|
|
130
|
+
"""Setter for the energy resolution. This raises an attribute error
|
|
131
|
+
as the energy resolution needs to be derived from the band map."""
|
|
132
|
+
raise AttributeError("`energy_resolution` is read-only; set it via the "
|
|
133
|
+
"constructor.")
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def temperature(self):
|
|
137
|
+
"""Temperature (float)."""
|
|
138
|
+
return self._temperature
|
|
139
|
+
|
|
140
|
+
@temperature.setter
|
|
141
|
+
def temperature(self, _):
|
|
142
|
+
"""Setter for the temperature. This raises an attribute error as the
|
|
143
|
+
temperature needs to be derived from the band map."""
|
|
144
|
+
raise AttributeError("`temperature` is read-only; set it via the "
|
|
145
|
+
"constructor.")
|
|
109
146
|
|
|
110
147
|
@property
|
|
111
148
|
def enel(self):
|
|
@@ -114,7 +151,8 @@ class MDCs:
|
|
|
114
151
|
|
|
115
152
|
@enel.setter
|
|
116
153
|
def enel(self, _):
|
|
117
|
-
raise AttributeError("`enel` is read-only; set it via the
|
|
154
|
+
raise AttributeError("`enel` is read-only; set it via the " \
|
|
155
|
+
"constructor.")
|
|
118
156
|
|
|
119
157
|
@property
|
|
120
158
|
def hnuminPhi(self):
|
|
@@ -962,6 +1000,10 @@ class MDCs:
|
|
|
962
1000
|
Kinetic-energy grid corresponding to the selected label.
|
|
963
1001
|
hnuminPhi : float
|
|
964
1002
|
Photoelectron work-function offset.
|
|
1003
|
+
energy_resolution : float
|
|
1004
|
+
Energy resolution associated with the extracted self-energy data.
|
|
1005
|
+
temperature : float
|
|
1006
|
+
Temperature [K] associated with the extracted self-energy data.
|
|
965
1007
|
label : str
|
|
966
1008
|
Label of the selected distribution.
|
|
967
1009
|
selected_properties : dict or list of dict
|
|
@@ -1031,5 +1073,6 @@ class MDCs:
|
|
|
1031
1073
|
if key not in ("label", "_class"):
|
|
1032
1074
|
exported_parameters[key] = val
|
|
1033
1075
|
|
|
1034
|
-
return (self._ekin_range, self.hnuminPhi,
|
|
1035
|
-
selected_properties,
|
|
1076
|
+
return (self._ekin_range, self.hnuminPhi, self.energy_resolution,
|
|
1077
|
+
self.temperature, select_label, selected_properties,
|
|
1078
|
+
exported_parameters)
|