xarpes 0.4.0__py3-none-any.whl → 0.5.0__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 +35 -5
- xarpes/bandmap.py +829 -0
- xarpes/constants.py +9 -8
- xarpes/distributions.py +45 -43
- xarpes/functions.py +18 -6
- xarpes/mdcs.py +1035 -0
- xarpes/plotting.py +1 -47
- xarpes/selfenergies.py +621 -0
- xarpes/settings_parameters.py +30 -0
- xarpes/settings_plots.py +54 -0
- {xarpes-0.4.0.dist-info → xarpes-0.5.0.dist-info}/METADATA +5 -4
- xarpes-0.5.0.dist-info/RECORD +15 -0
- {xarpes-0.4.0.dist-info → xarpes-0.5.0.dist-info}/WHEEL +1 -1
- xarpes/spectral.py +0 -2476
- xarpes-0.4.0.dist-info/RECORD +0 -11
- {xarpes-0.4.0.dist-info/licenses → xarpes-0.5.0.dist-info}/LICENSE +0 -0
- {xarpes-0.4.0.dist-info → xarpes-0.5.0.dist-info}/entry_points.txt +0 -0
xarpes/bandmap.py
ADDED
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
# Copyright (C) 2025 xARPES Developers
|
|
2
|
+
# This program is free software under the terms of the GNU GPLv3 license.
|
|
3
|
+
|
|
4
|
+
# get_ax_fig_plt and add_fig_kwargs originate from pymatgen/util/plotting.py.
|
|
5
|
+
# Copyright (C) 2011-2024 Shyue Ping Ong and the pymatgen Development Team
|
|
6
|
+
# Pymatgen is released under the MIT License.
|
|
7
|
+
|
|
8
|
+
# See also abipy/tools/plotting.py.
|
|
9
|
+
# Copyright (C) 2021 Matteo Giantomassi and the AbiPy Group
|
|
10
|
+
# AbiPy is free software under the terms of the GNU GPLv2 license.
|
|
11
|
+
|
|
12
|
+
"""File containing the band map class."""
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from igor2 import binarywave
|
|
16
|
+
from .plotting import get_ax_fig_plt, add_fig_kwargs
|
|
17
|
+
from .functions import fit_leastsq, extend_function
|
|
18
|
+
from .distributions import FermiDirac, Linear
|
|
19
|
+
from .constants import PREF
|
|
20
|
+
|
|
21
|
+
class BandMap:
|
|
22
|
+
r"""
|
|
23
|
+
Class for the band map from the ARPES experiment.
|
|
24
|
+
|
|
25
|
+
Construction
|
|
26
|
+
------------
|
|
27
|
+
Prefer the alternate constructors:
|
|
28
|
+
|
|
29
|
+
- BandMap._from_ibw_file(path, ...)
|
|
30
|
+
- BandMap._from_np_arrays(intensities=..., angles=..., ekin=... or enel=...)
|
|
31
|
+
|
|
32
|
+
The __init__ takes *canonical* arrays (no file I/O).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_ibw_file(cls, datafile, transpose=False, flip_ekin=False,
|
|
37
|
+
flip_angles=False, **kwargs):
|
|
38
|
+
r"""Construct BandMap from an IGOR binary wave (.ibw)."""
|
|
39
|
+
data = binarywave.load(datafile)
|
|
40
|
+
intensities = data['wave']['wData']
|
|
41
|
+
|
|
42
|
+
fnum, anum = data['wave']['wave_header']['nDim'][0:2]
|
|
43
|
+
fstp, astp = data['wave']['wave_header']['sfA'][0:2]
|
|
44
|
+
fmin, amin = data['wave']['wave_header']['sfB'][0:2]
|
|
45
|
+
|
|
46
|
+
if intensities.shape != (fnum, anum):
|
|
47
|
+
raise ValueError('nDim and shape of wData do not match.')
|
|
48
|
+
|
|
49
|
+
if transpose:
|
|
50
|
+
intensities = intensities.T
|
|
51
|
+
fnum, anum = anum, fnum
|
|
52
|
+
fstp, astp = astp, fstp
|
|
53
|
+
fmin, amin = amin, fmin
|
|
54
|
+
|
|
55
|
+
if flip_ekin:
|
|
56
|
+
intensities = intensities[::-1, :]
|
|
57
|
+
|
|
58
|
+
if flip_angles:
|
|
59
|
+
intensities = intensities[:, ::-1]
|
|
60
|
+
|
|
61
|
+
angles = np.linspace(amin, amin + (anum - 1) * astp, anum)
|
|
62
|
+
ekin = np.linspace(fmin, fmin + (fnum - 1) * fstp, fnum)
|
|
63
|
+
|
|
64
|
+
return cls(intensities=intensities, angles=angles, ekin=ekin, **kwargs)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_np_arrays(cls, intensities=None, angles=None, ekin=None, enel=None,
|
|
68
|
+
**kwargs):
|
|
69
|
+
r"""Construct BandMap from NumPy arrays."""
|
|
70
|
+
if intensities is None or angles is None:
|
|
71
|
+
raise ValueError('Please provide intensities and angles.')
|
|
72
|
+
if (ekin is None) == (enel is None):
|
|
73
|
+
raise ValueError('Provide exactly one of ekin or enel.')
|
|
74
|
+
return cls(intensities=intensities, angles=angles, ekin=ekin, enel=enel,
|
|
75
|
+
**kwargs)
|
|
76
|
+
|
|
77
|
+
def __init__(self, intensities=None, angles=None, ekin=None, enel=None,
|
|
78
|
+
energy_resolution=None, angle_resolution=None, temperature=None,
|
|
79
|
+
hnuminPhi=None, hnuminPhi_std=None):
|
|
80
|
+
|
|
81
|
+
# --- Required arrays ------------------------------------------------
|
|
82
|
+
if intensities is None:
|
|
83
|
+
raise ValueError('Please provide intensities.')
|
|
84
|
+
if angles is None:
|
|
85
|
+
raise ValueError('Please provide angles.')
|
|
86
|
+
|
|
87
|
+
self.intensities = intensities
|
|
88
|
+
self.angles = angles
|
|
89
|
+
|
|
90
|
+
# --- Initialize energy axes (raw slots) ----------------------------
|
|
91
|
+
self._ekin = None
|
|
92
|
+
self._enel = None
|
|
93
|
+
|
|
94
|
+
# Apply user overrides or file ekin
|
|
95
|
+
if ekin is not None and enel is not None:
|
|
96
|
+
raise ValueError('Provide only one of ekin or enel, not both.')
|
|
97
|
+
|
|
98
|
+
if ekin is not None:
|
|
99
|
+
self._ekin = ekin
|
|
100
|
+
elif enel is not None:
|
|
101
|
+
self._enel = enel
|
|
102
|
+
elif file_ekin is not None:
|
|
103
|
+
self._ekin = file_ekin
|
|
104
|
+
else:
|
|
105
|
+
raise ValueError('Please provide datafile, ekin, or enel.')
|
|
106
|
+
|
|
107
|
+
# Scalars / metadata
|
|
108
|
+
self.energy_resolution = energy_resolution
|
|
109
|
+
self.angle_resolution = angle_resolution
|
|
110
|
+
self.temperature = temperature
|
|
111
|
+
|
|
112
|
+
# Work-function combo and its std
|
|
113
|
+
self._hnuminPhi = None
|
|
114
|
+
self._hnuminPhi_std = None
|
|
115
|
+
self.hnuminPhi = hnuminPhi
|
|
116
|
+
self.hnuminPhi_std = hnuminPhi_std
|
|
117
|
+
|
|
118
|
+
# --- 1) Track which axis is authoritative --------------------------
|
|
119
|
+
self._ekin_explicit = ekin is not None or (file_ekin is not None
|
|
120
|
+
and enel is None)
|
|
121
|
+
self._enel_explicit = enel is not None
|
|
122
|
+
|
|
123
|
+
# --- 2) Derive missing axis if possible ----------------------------
|
|
124
|
+
if self._ekin is None and self._enel is not None \
|
|
125
|
+
and self._hnuminPhi is not None:
|
|
126
|
+
self._ekin = self._enel + self._hnuminPhi
|
|
127
|
+
if self._enel is None and self._ekin is not None \
|
|
128
|
+
and self._hnuminPhi is not None:
|
|
129
|
+
self._enel = self._ekin - self._hnuminPhi
|
|
130
|
+
|
|
131
|
+
# -------------------- Properties: data arrays ---------------------------
|
|
132
|
+
@property
|
|
133
|
+
def intensities(self):
|
|
134
|
+
return self._intensities
|
|
135
|
+
|
|
136
|
+
@intensities.setter
|
|
137
|
+
def intensities(self, x):
|
|
138
|
+
self._intensities = x
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def angles(self):
|
|
142
|
+
return self._angles
|
|
143
|
+
|
|
144
|
+
@angles.setter
|
|
145
|
+
def angles(self, x):
|
|
146
|
+
self._angles = x
|
|
147
|
+
|
|
148
|
+
# -------------------- 3) Resolution / temperature ----------------------
|
|
149
|
+
@property
|
|
150
|
+
def angle_resolution(self):
|
|
151
|
+
return self._angle_resolution
|
|
152
|
+
|
|
153
|
+
@angle_resolution.setter
|
|
154
|
+
def angle_resolution(self, x):
|
|
155
|
+
self._angle_resolution = x
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def energy_resolution(self):
|
|
159
|
+
return self._energy_resolution
|
|
160
|
+
|
|
161
|
+
@energy_resolution.setter
|
|
162
|
+
def energy_resolution(self, x):
|
|
163
|
+
self._energy_resolution = x
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def temperature(self):
|
|
167
|
+
return self._temperature
|
|
168
|
+
|
|
169
|
+
@temperature.setter
|
|
170
|
+
def temperature(self, x):
|
|
171
|
+
self._temperature = x
|
|
172
|
+
|
|
173
|
+
# -------------------- 4) Sync ekin / enel / hnuminPhi ------------------
|
|
174
|
+
@property
|
|
175
|
+
def ekin(self):
|
|
176
|
+
if self._ekin is None and self._enel is not None \
|
|
177
|
+
and self._hnuminPhi is not None:
|
|
178
|
+
return self._enel + self._hnuminPhi
|
|
179
|
+
return self._ekin
|
|
180
|
+
|
|
181
|
+
@ekin.setter
|
|
182
|
+
def ekin(self, x):
|
|
183
|
+
if getattr(self, "_enel_explicit", False):
|
|
184
|
+
raise AttributeError('enel is explicit; set hnuminPhi instead.')
|
|
185
|
+
self._ekin = x
|
|
186
|
+
self._ekin_explicit = True
|
|
187
|
+
if not getattr(self, "_enel_explicit", False) \
|
|
188
|
+
and self._hnuminPhi is not None and x is not None:
|
|
189
|
+
self._enel = x - self._hnuminPhi
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def enel(self):
|
|
193
|
+
if self._enel is None and self._ekin is not None \
|
|
194
|
+
and self._hnuminPhi is not None:
|
|
195
|
+
return self._ekin - self._hnuminPhi
|
|
196
|
+
return self._enel
|
|
197
|
+
|
|
198
|
+
@enel.setter
|
|
199
|
+
def enel(self, x):
|
|
200
|
+
if getattr(self, "_ekin_explicit", False):
|
|
201
|
+
raise AttributeError('ekin is explicit; set hnuminPhi instead.')
|
|
202
|
+
self._enel = x
|
|
203
|
+
self._enel_explicit = True
|
|
204
|
+
if not getattr(self, "_ekin_explicit", False) \
|
|
205
|
+
and self._hnuminPhi is not None and x is not None:
|
|
206
|
+
self._ekin = x + self._hnuminPhi
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def hnuminPhi(self):
|
|
210
|
+
r"""Returns the photon energy minus the work function in eV if it has
|
|
211
|
+
been set, either during instantiation, with the setter, or by fitting
|
|
212
|
+
the Fermi-Dirac distribution to the integrated weight.
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
hnuminPhi : float, None
|
|
217
|
+
Kinetic energy minus the work function [eV]
|
|
218
|
+
|
|
219
|
+
"""
|
|
220
|
+
return self._hnuminPhi
|
|
221
|
+
|
|
222
|
+
@hnuminPhi.setter
|
|
223
|
+
def hnuminPhi(self, x):
|
|
224
|
+
r"""TBD
|
|
225
|
+
"""
|
|
226
|
+
self._hnuminPhi = x
|
|
227
|
+
# Re-derive the non-explicit axis if possible
|
|
228
|
+
if not getattr(self, "_ekin_explicit", False) \
|
|
229
|
+
and self._enel is not None and x is not None:
|
|
230
|
+
self._ekin = self._enel + x
|
|
231
|
+
if not getattr(self, "_enel_explicit", False) \
|
|
232
|
+
and self._ekin is not None and x is not None:
|
|
233
|
+
self._enel = self._ekin - x
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def hnuminPhi_std(self):
|
|
237
|
+
r"""Returns standard deviation of the photon energy minus the work
|
|
238
|
+
function in eV.
|
|
239
|
+
|
|
240
|
+
Returns
|
|
241
|
+
-------
|
|
242
|
+
hnuminPhi_std : float
|
|
243
|
+
Standard deviation of energy minus the work function [eV]
|
|
244
|
+
|
|
245
|
+
"""
|
|
246
|
+
return self._hnuminPhi_std
|
|
247
|
+
|
|
248
|
+
@hnuminPhi_std.setter
|
|
249
|
+
def hnuminPhi_std(self, x):
|
|
250
|
+
r"""Manually sets the standard deviation of photon energy minus the
|
|
251
|
+
work function in eV.
|
|
252
|
+
|
|
253
|
+
Parameters
|
|
254
|
+
----------
|
|
255
|
+
hnuminPhi_std : float
|
|
256
|
+
Standard deviation of energy minus the work function [eV]
|
|
257
|
+
|
|
258
|
+
"""
|
|
259
|
+
self._hnuminPhi_std = x
|
|
260
|
+
|
|
261
|
+
def shift_angles(self, shift):
|
|
262
|
+
r"""
|
|
263
|
+
Shifts the angles by the specified amount in degrees. Used to shift
|
|
264
|
+
from the detector angle to the material angle.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
shift : float
|
|
269
|
+
Angular shift [degrees]
|
|
270
|
+
|
|
271
|
+
"""
|
|
272
|
+
self.angles = self.angles + shift
|
|
273
|
+
|
|
274
|
+
def mdc_set(self, angle_min, angle_max, energy_value=None,
|
|
275
|
+
energy_range=None):
|
|
276
|
+
r"""Returns a set of MDCs. Documentation is to be further completed.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
angle_min : float
|
|
281
|
+
Minimum angle of integration interval [degrees]
|
|
282
|
+
angle_max : float
|
|
283
|
+
Maximum angle of integration interval [degrees]
|
|
284
|
+
|
|
285
|
+
Returns
|
|
286
|
+
-------
|
|
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
|
+
mdcs : ndarray
|
|
292
|
+
Array of size n x m containing the MDC intensities
|
|
293
|
+
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
if (energy_value is None and energy_range is None) or \
|
|
297
|
+
(energy_value is not None and energy_range is not None):
|
|
298
|
+
raise ValueError('Please provide either energy_value or ' +
|
|
299
|
+
'energy_range.')
|
|
300
|
+
|
|
301
|
+
angle_min_index = np.abs(self.angles - angle_min).argmin()
|
|
302
|
+
angle_max_index = np.abs(self.angles - angle_max).argmin()
|
|
303
|
+
angle_range_out = self.angles[angle_min_index:angle_max_index + 1]
|
|
304
|
+
|
|
305
|
+
if energy_value is not None:
|
|
306
|
+
energy_index = np.abs(self.enel - energy_value).argmin()
|
|
307
|
+
enel_range_out = self.enel[energy_index]
|
|
308
|
+
mdcs = self.intensities[energy_index,
|
|
309
|
+
angle_min_index:angle_max_index + 1]
|
|
310
|
+
|
|
311
|
+
if energy_range:
|
|
312
|
+
energy_indices = np.where((self.enel >= np.min(energy_range))
|
|
313
|
+
& (self.enel <= np.max(energy_range))) \
|
|
314
|
+
[0]
|
|
315
|
+
enel_range_out = self.enel[energy_indices]
|
|
316
|
+
mdcs = self.intensities[energy_indices,
|
|
317
|
+
angle_min_index:angle_max_index + 1]
|
|
318
|
+
|
|
319
|
+
return mdcs, angle_range_out, self.angle_resolution, \
|
|
320
|
+
enel_range_out, self.hnuminPhi
|
|
321
|
+
|
|
322
|
+
@add_fig_kwargs
|
|
323
|
+
def plot(self, abscissa='momentum', ordinate='electron_energy',
|
|
324
|
+
self_energies=None, ax=None, markersize=None,
|
|
325
|
+
plot_dispersions='none', **kwargs):
|
|
326
|
+
r"""
|
|
327
|
+
Plot the band map. Optionally overlay a collection of self-energies,
|
|
328
|
+
e.g. a CreateSelfEnergies instance or any iterable of self-energy
|
|
329
|
+
objects. Self-energies are *not* stored internally; they are used
|
|
330
|
+
only for this plotting call.
|
|
331
|
+
|
|
332
|
+
When self-energies are present and ``abscissa='momentum'``, their
|
|
333
|
+
MDC maxima are overlaid with 95 % confidence intervals.
|
|
334
|
+
|
|
335
|
+
The `plot_dispersions` argument controls bare-band plotting:
|
|
336
|
+
|
|
337
|
+
- "full" : use the full momentum range of the map (default)
|
|
338
|
+
- "none" : do not plot bare dispersions
|
|
339
|
+
- "kink" : for each self-energy, use the min/max of its own
|
|
340
|
+
momentum range (typically its MDC maxima), with
|
|
341
|
+
`len(self.angles)` points.
|
|
342
|
+
- "domain" : for SpectralQuadratic, use only the left or right
|
|
343
|
+
domain relative to `center_wavevector`, based on the self-energy
|
|
344
|
+
attribute `side` ("left" / "right"); for other cases this behaves
|
|
345
|
+
as "full".
|
|
346
|
+
"""
|
|
347
|
+
import warnings
|
|
348
|
+
from . import settings_parameters as xprs
|
|
349
|
+
|
|
350
|
+
plot_disp_mode = plot_dispersions
|
|
351
|
+
valid_disp_modes = ('full', 'none', 'kink', 'domain')
|
|
352
|
+
if plot_disp_mode not in valid_disp_modes:
|
|
353
|
+
raise ValueError(
|
|
354
|
+
f"Invalid plot_dispersions '{plot_disp_mode}'. "
|
|
355
|
+
f"Valid options: {valid_disp_modes}."
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
valid_abscissa = ('angle', 'momentum')
|
|
359
|
+
valid_ordinate = ('kinetic_energy', 'electron_energy')
|
|
360
|
+
|
|
361
|
+
if abscissa not in valid_abscissa:
|
|
362
|
+
raise ValueError(
|
|
363
|
+
f"Invalid abscissa '{abscissa}'. "
|
|
364
|
+
f"Valid options: {valid_abscissa}"
|
|
365
|
+
)
|
|
366
|
+
if ordinate not in valid_ordinate:
|
|
367
|
+
raise ValueError(
|
|
368
|
+
f"Invalid ordinate '{ordinate}'. "
|
|
369
|
+
f"Valid options: {valid_ordinate}"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
if self_energies is not None:
|
|
373
|
+
|
|
374
|
+
# MDC maxima are defined in momentum space, not angle space
|
|
375
|
+
if abscissa == 'angle':
|
|
376
|
+
raise ValueError(
|
|
377
|
+
"MDC maxima cannot be plotted against angles; they are "
|
|
378
|
+
"defined in momentum space. Use abscissa='momentum' "
|
|
379
|
+
"when passing self-energies."
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
ax, fig, plt = get_ax_fig_plt(ax=ax)
|
|
383
|
+
|
|
384
|
+
Angl, Ekin = np.meshgrid(self.angles, self.ekin)
|
|
385
|
+
|
|
386
|
+
if abscissa == 'angle':
|
|
387
|
+
ax.set_xlabel('Angle ($\\degree$)')
|
|
388
|
+
if ordinate == 'kinetic_energy':
|
|
389
|
+
mesh = ax.pcolormesh(
|
|
390
|
+
Angl, Ekin, self.intensities,
|
|
391
|
+
shading='auto',
|
|
392
|
+
cmap=plt.get_cmap('bone').reversed())
|
|
393
|
+
ax.set_ylabel('$E_{\\mathrm{kin}}$ (eV)')
|
|
394
|
+
elif ordinate == 'electron_energy':
|
|
395
|
+
Enel = Ekin - self.hnuminPhi
|
|
396
|
+
mesh = ax.pcolormesh(
|
|
397
|
+
Angl, Enel, self.intensities,
|
|
398
|
+
shading='auto',
|
|
399
|
+
cmap=plt.get_cmap('bone').reversed())
|
|
400
|
+
ax.set_ylabel('$E-\\mu$ (eV)')
|
|
401
|
+
|
|
402
|
+
elif abscissa == 'momentum':
|
|
403
|
+
ax.set_xlabel(r'$k_{//}$ ($\mathrm{\AA}^{-1}$)')
|
|
404
|
+
|
|
405
|
+
with warnings.catch_warnings(record=True) as wlist:
|
|
406
|
+
warnings.filterwarnings(
|
|
407
|
+
"always",
|
|
408
|
+
message=(
|
|
409
|
+
"The input coordinates to pcolormesh are "
|
|
410
|
+
"interpreted as cell centers, but are not "
|
|
411
|
+
"monotonically increasing or decreasing."
|
|
412
|
+
),
|
|
413
|
+
category=UserWarning,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
Mome = np.sqrt(Ekin / PREF) * np.sin(np.deg2rad(Angl))
|
|
417
|
+
mome_min = np.min(Mome)
|
|
418
|
+
mome_max = np.max(Mome)
|
|
419
|
+
full_disp_momenta = np.linspace(
|
|
420
|
+
mome_min, mome_max, len(self.angles)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
if ordinate == 'kinetic_energy':
|
|
424
|
+
mesh = ax.pcolormesh(
|
|
425
|
+
Mome, Ekin, self.intensities,
|
|
426
|
+
shading='auto',
|
|
427
|
+
cmap=plt.get_cmap('bone').reversed())
|
|
428
|
+
ax.set_ylabel('$E_{\\mathrm{kin}}$ (eV)')
|
|
429
|
+
elif ordinate == 'electron_energy':
|
|
430
|
+
Enel = Ekin - self.hnuminPhi
|
|
431
|
+
mesh = ax.pcolormesh(
|
|
432
|
+
Mome, Enel, self.intensities,
|
|
433
|
+
shading='auto',
|
|
434
|
+
cmap=plt.get_cmap('bone').reversed())
|
|
435
|
+
ax.set_ylabel('$E-\\mu$ (eV)')
|
|
436
|
+
|
|
437
|
+
y_lims = ax.get_ylim()
|
|
438
|
+
|
|
439
|
+
if any("cell centers" in str(w.message) for w in wlist):
|
|
440
|
+
warnings.warn(
|
|
441
|
+
"Conversion from angle to momenta causes warping of the "
|
|
442
|
+
"cell centers. \n Cell edges of the mesh plot may look "
|
|
443
|
+
"irregular.",
|
|
444
|
+
UserWarning,
|
|
445
|
+
stacklevel=2,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
if abscissa == 'momentum' and self_energies is not None:
|
|
449
|
+
for self_energy in self_energies:
|
|
450
|
+
|
|
451
|
+
mdc_maxima = getattr(self_energy, "mdc_maxima", None)
|
|
452
|
+
|
|
453
|
+
# If this self-energy doesn't contain maxima, don't plot
|
|
454
|
+
if mdc_maxima is None:
|
|
455
|
+
continue
|
|
456
|
+
|
|
457
|
+
# Reserve a colour from the axes cycle for this self-energy,
|
|
458
|
+
# and use it consistently for MDC maxima and dispersion.
|
|
459
|
+
line_color = ax._get_lines.get_next_color()
|
|
460
|
+
|
|
461
|
+
peak_sigma = getattr(
|
|
462
|
+
self_energy, "peak_positions_sigma", None
|
|
463
|
+
)
|
|
464
|
+
xerr = xprs.sigma_confidence * peak_sigma if peak_sigma is \
|
|
465
|
+
not None else None
|
|
466
|
+
|
|
467
|
+
if ordinate == 'kinetic_energy':
|
|
468
|
+
y_vals = self_energy.ekin_range
|
|
469
|
+
else:
|
|
470
|
+
y_vals = self_energy.enel_range
|
|
471
|
+
|
|
472
|
+
x_vals = mdc_maxima
|
|
473
|
+
label = getattr(self_energy, "label", None)
|
|
474
|
+
|
|
475
|
+
# First plot the MDC maxima, using the reserved colour
|
|
476
|
+
if xerr is not None:
|
|
477
|
+
ax.errorbar(
|
|
478
|
+
x_vals, y_vals, xerr=xerr, fmt='o',
|
|
479
|
+
linestyle='', label=label,
|
|
480
|
+
markersize=markersize,
|
|
481
|
+
color=line_color, ecolor=line_color,
|
|
482
|
+
)
|
|
483
|
+
else:
|
|
484
|
+
ax.plot(
|
|
485
|
+
x_vals, y_vals, linestyle='',
|
|
486
|
+
marker='o', label=label,
|
|
487
|
+
markersize=markersize,
|
|
488
|
+
color=line_color,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# Bare-band dispersion for SpectralLinear / SpectralQuadratic
|
|
492
|
+
spec_class = getattr(
|
|
493
|
+
self_energy, "_class",
|
|
494
|
+
self_energy.__class__.__name__,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
if (plot_disp_mode != 'none'
|
|
498
|
+
and spec_class in ("SpectralLinear",
|
|
499
|
+
"SpectralQuadratic")):
|
|
500
|
+
|
|
501
|
+
# Determine momentum grid for the dispersion
|
|
502
|
+
if plot_disp_mode == 'kink':
|
|
503
|
+
x_arr = np.asarray(x_vals)
|
|
504
|
+
mask = np.isfinite(x_arr)
|
|
505
|
+
if not np.any(mask):
|
|
506
|
+
# No valid k-points to define a range
|
|
507
|
+
continue
|
|
508
|
+
k_min = np.min(x_arr[mask])
|
|
509
|
+
k_max = np.max(x_arr[mask])
|
|
510
|
+
disp_momenta = np.linspace(
|
|
511
|
+
k_min, k_max, len(self.angles)
|
|
512
|
+
)
|
|
513
|
+
elif (plot_disp_mode == 'domain'
|
|
514
|
+
and spec_class == "SpectralQuadratic"):
|
|
515
|
+
side = getattr(self_energy, "side", None)
|
|
516
|
+
if side == 'left':
|
|
517
|
+
disp_momenta = np.linspace(
|
|
518
|
+
mome_min, self_energy.center_wavevector,
|
|
519
|
+
len(self.angles)
|
|
520
|
+
)
|
|
521
|
+
elif side == 'right':
|
|
522
|
+
disp_momenta = np.linspace(
|
|
523
|
+
self_energy.center_wavevector, mome_max,
|
|
524
|
+
len(self.angles)
|
|
525
|
+
)
|
|
526
|
+
else:
|
|
527
|
+
# Fallback: no valid side, use full range
|
|
528
|
+
disp_momenta = full_disp_momenta
|
|
529
|
+
else:
|
|
530
|
+
# 'full' or 'domain' for SpectralLinear
|
|
531
|
+
disp_momenta = full_disp_momenta
|
|
532
|
+
|
|
533
|
+
# --- Robust parameter checks before computing base_disp ---
|
|
534
|
+
if spec_class == "SpectralLinear":
|
|
535
|
+
fermi_vel = getattr(
|
|
536
|
+
self_energy, "fermi_velocity", None
|
|
537
|
+
)
|
|
538
|
+
fermi_k = getattr(
|
|
539
|
+
self_energy, "fermi_wavevector", None
|
|
540
|
+
)
|
|
541
|
+
if fermi_vel is None or fermi_k is None:
|
|
542
|
+
missing = []
|
|
543
|
+
if fermi_vel is None:
|
|
544
|
+
missing.append("fermi_velocity")
|
|
545
|
+
if fermi_k is None:
|
|
546
|
+
missing.append("fermi_wavevector")
|
|
547
|
+
raise TypeError(
|
|
548
|
+
"Cannot plot bare dispersion for "
|
|
549
|
+
"SpectralLinear: "
|
|
550
|
+
f"{', '.join(missing)} is None."
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
base_disp = (
|
|
554
|
+
fermi_vel * (disp_momenta - fermi_k)
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
else: # SpectralQuadratic
|
|
558
|
+
bare_mass = getattr(
|
|
559
|
+
self_energy, "bare_mass", None
|
|
560
|
+
)
|
|
561
|
+
center_k = getattr(
|
|
562
|
+
self_energy, "center_wavevector", None
|
|
563
|
+
)
|
|
564
|
+
fermi_k = getattr(
|
|
565
|
+
self_energy, "fermi_wavevector", None
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
missing = []
|
|
569
|
+
if bare_mass is None:
|
|
570
|
+
missing.append("bare_mass")
|
|
571
|
+
if center_k is None:
|
|
572
|
+
missing.append("center_wavevector")
|
|
573
|
+
if fermi_k is None:
|
|
574
|
+
missing.append("fermi_wavevector")
|
|
575
|
+
|
|
576
|
+
if missing:
|
|
577
|
+
raise TypeError(
|
|
578
|
+
"Cannot plot bare dispersion for "
|
|
579
|
+
"SpectralQuadratic: "
|
|
580
|
+
f"{', '.join(missing)} is None."
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
dk = disp_momenta - center_k
|
|
584
|
+
base_disp = PREF * (dk ** 2 - fermi_k ** 2) / bare_mass
|
|
585
|
+
# --- end parameter checks and base_disp construction ---
|
|
586
|
+
|
|
587
|
+
if ordinate == 'electron_energy':
|
|
588
|
+
disp_vals = base_disp
|
|
589
|
+
else: # kinetic energy
|
|
590
|
+
disp_vals = base_disp + self.hnuminPhi
|
|
591
|
+
|
|
592
|
+
band_label = getattr(self_energy, "label", None)
|
|
593
|
+
if band_label is not None:
|
|
594
|
+
band_label = f"{band_label} (bare)"
|
|
595
|
+
|
|
596
|
+
ax.plot(
|
|
597
|
+
disp_momenta, disp_vals,
|
|
598
|
+
label=band_label,
|
|
599
|
+
linestyle='--',
|
|
600
|
+
color=line_color,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
handles, labels = ax.get_legend_handles_labels()
|
|
604
|
+
if any(labels):
|
|
605
|
+
ax.legend()
|
|
606
|
+
|
|
607
|
+
ax.set_ylim(y_lims)
|
|
608
|
+
|
|
609
|
+
plt.colorbar(mesh, ax=ax, label='counts (-)')
|
|
610
|
+
return fig
|
|
611
|
+
|
|
612
|
+
@add_fig_kwargs
|
|
613
|
+
def fit_fermi_edge(self, hnuminPhi_guess, background_guess=0.0,
|
|
614
|
+
integrated_weight_guess=1.0, angle_min=-np.inf,
|
|
615
|
+
angle_max=np.inf, ekin_min=-np.inf,
|
|
616
|
+
ekin_max=np.inf, ax=None, **kwargs):
|
|
617
|
+
r"""Fits the Fermi edge of the band map and plots the result.
|
|
618
|
+
Also sets hnuminPhi, the kinetic energy minus the work function in eV.
|
|
619
|
+
The fitting includes an energy convolution with an abscissa range
|
|
620
|
+
expanded by 5 times the energy resolution standard deviation.
|
|
621
|
+
|
|
622
|
+
Parameters
|
|
623
|
+
----------
|
|
624
|
+
hnuminPhi_guess : float
|
|
625
|
+
Initial guess for kinetic energy minus the work function [eV]
|
|
626
|
+
background_guess : float
|
|
627
|
+
Initial guess for background intensity [counts]
|
|
628
|
+
integrated_weight_guess : float
|
|
629
|
+
Initial guess for integrated spectral intensity [counts]
|
|
630
|
+
angle_min : float
|
|
631
|
+
Minimum angle of integration interval [degrees]
|
|
632
|
+
angle_max : float
|
|
633
|
+
Maximum angle of integration interval [degrees]
|
|
634
|
+
ekin_min : float
|
|
635
|
+
Minimum kinetic energy of integration interval [eV]
|
|
636
|
+
ekin_max : float
|
|
637
|
+
Maximum kinetic energy of integration interval [eV]
|
|
638
|
+
ax : Matplotlib-Axes / NoneType
|
|
639
|
+
Axis for plotting the Fermi edge on. Created if not provided by
|
|
640
|
+
the user.
|
|
641
|
+
|
|
642
|
+
Other parameters
|
|
643
|
+
----------------
|
|
644
|
+
**kwargs : dict, optional
|
|
645
|
+
Additional arguments passed on to add_fig_kwargs.
|
|
646
|
+
|
|
647
|
+
Returns
|
|
648
|
+
-------
|
|
649
|
+
fig : Matplotlib-Figure
|
|
650
|
+
Figure containing the Fermi edge fit
|
|
651
|
+
|
|
652
|
+
"""
|
|
653
|
+
from scipy.ndimage import gaussian_filter
|
|
654
|
+
|
|
655
|
+
ax, fig, plt = get_ax_fig_plt(ax=ax)
|
|
656
|
+
|
|
657
|
+
min_angle_index = np.argmin(np.abs(self.angles - angle_min))
|
|
658
|
+
max_angle_index = np.argmin(np.abs(self.angles - angle_max))
|
|
659
|
+
|
|
660
|
+
min_ekin_index = np.argmin(np.abs(self.ekin - ekin_min))
|
|
661
|
+
max_ekin_index = np.argmin(np.abs(self.ekin - ekin_max))
|
|
662
|
+
|
|
663
|
+
energy_range = self.ekin[min_ekin_index:max_ekin_index]
|
|
664
|
+
|
|
665
|
+
integrated_intensity = np.trapz(
|
|
666
|
+
self.intensities[min_ekin_index:max_ekin_index,
|
|
667
|
+
min_angle_index:max_angle_index], axis=1)
|
|
668
|
+
|
|
669
|
+
fdir_initial = FermiDirac(temperature=self.temperature,
|
|
670
|
+
hnuminPhi=hnuminPhi_guess,
|
|
671
|
+
background=background_guess,
|
|
672
|
+
integrated_weight=integrated_weight_guess,
|
|
673
|
+
name='Initial guess')
|
|
674
|
+
|
|
675
|
+
parameters = np.array(
|
|
676
|
+
[hnuminPhi_guess, background_guess, integrated_weight_guess])
|
|
677
|
+
|
|
678
|
+
extra_args = (self.temperature,)
|
|
679
|
+
|
|
680
|
+
popt, pcov = fit_leastsq(
|
|
681
|
+
parameters, energy_range, integrated_intensity, fdir_initial,
|
|
682
|
+
self.energy_resolution, None, *extra_args)
|
|
683
|
+
|
|
684
|
+
# Update hnuminPhi; automatically sets self.enel
|
|
685
|
+
self.hnuminPhi = popt[0]
|
|
686
|
+
self.hnuminPhi_std = np.sqrt(np.diag(pcov)[0])
|
|
687
|
+
|
|
688
|
+
fdir_final = FermiDirac(temperature=self.temperature,
|
|
689
|
+
hnuminPhi=self.hnuminPhi, background=popt[1],
|
|
690
|
+
integrated_weight=popt[2],
|
|
691
|
+
name='Fitted result')
|
|
692
|
+
|
|
693
|
+
ax.set_xlabel(r'$E_{\mathrm{kin}}$ (-)')
|
|
694
|
+
ax.set_ylabel('Counts (-)')
|
|
695
|
+
ax.set_xlim([ekin_min, ekin_max])
|
|
696
|
+
|
|
697
|
+
ax.plot(energy_range, integrated_intensity, label='Data')
|
|
698
|
+
|
|
699
|
+
extend, step, numb = extend_function(energy_range,
|
|
700
|
+
self.energy_resolution)
|
|
701
|
+
|
|
702
|
+
initial_result = gaussian_filter(fdir_initial.evaluate(extend),
|
|
703
|
+
sigma=step)[numb:-numb if numb else None]
|
|
704
|
+
|
|
705
|
+
final_result = gaussian_filter(fdir_final.evaluate(extend),
|
|
706
|
+
sigma=step)[numb:-numb if numb else None]
|
|
707
|
+
|
|
708
|
+
ax.plot(energy_range, initial_result, label=fdir_initial.name)
|
|
709
|
+
ax.plot(energy_range, final_result, label=fdir_final.name)
|
|
710
|
+
|
|
711
|
+
ax.legend()
|
|
712
|
+
|
|
713
|
+
return fig
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
@add_fig_kwargs
|
|
717
|
+
def correct_fermi_edge(self, hnuminPhi_guess=None, background_guess=0.0,
|
|
718
|
+
integrated_weight_guess=1.0, angle_min=-np.inf,
|
|
719
|
+
angle_max=np.inf, ekin_min=-np.inf, ekin_max=np.inf,
|
|
720
|
+
slope_guess=0, offset_guess=None,
|
|
721
|
+
true_angle=0, ax=None, **kwargs):
|
|
722
|
+
r"""TBD
|
|
723
|
+
hnuminPhi_guess should be estimate at true angle
|
|
724
|
+
|
|
725
|
+
Parameters
|
|
726
|
+
----------
|
|
727
|
+
hnuminPhi_guess : float, optional
|
|
728
|
+
Initial guess for kinetic energy minus the work function [eV].
|
|
729
|
+
|
|
730
|
+
Other parameters
|
|
731
|
+
----------------
|
|
732
|
+
**kwargs : dict, optional
|
|
733
|
+
Additional arguments passed on to add_fig_kwargs.
|
|
734
|
+
|
|
735
|
+
Returns
|
|
736
|
+
-------
|
|
737
|
+
fig : Matplotlib-Figure
|
|
738
|
+
Figure containing the Fermi edge fit
|
|
739
|
+
|
|
740
|
+
"""
|
|
741
|
+
from scipy.ndimage import map_coordinates
|
|
742
|
+
from . import settings_parameters as xprs
|
|
743
|
+
|
|
744
|
+
if hnuminPhi_guess is None:
|
|
745
|
+
raise ValueError('Please provide an initial guess for ' +
|
|
746
|
+
'hnuminPhi.')
|
|
747
|
+
|
|
748
|
+
# Here some loop where it fits all the Fermi edges
|
|
749
|
+
angle_min_index = np.abs(self.angles - angle_min).argmin()
|
|
750
|
+
angle_max_index = np.abs(self.angles - angle_max).argmin()
|
|
751
|
+
|
|
752
|
+
ekin_min_index = np.abs(self.ekin - ekin_min).argmin()
|
|
753
|
+
ekin_max_index = np.abs(self.ekin - ekin_max).argmin()
|
|
754
|
+
|
|
755
|
+
Intensities = self.intensities[ekin_min_index:ekin_max_index + 1,
|
|
756
|
+
angle_min_index:angle_max_index + 1]
|
|
757
|
+
angle_range = self.angles[angle_min_index:angle_max_index + 1]
|
|
758
|
+
energy_range = self.ekin[ekin_min_index:ekin_max_index + 1]
|
|
759
|
+
|
|
760
|
+
angle_shape = angle_range.shape
|
|
761
|
+
nmps = np.zeros(angle_shape)
|
|
762
|
+
stds = np.zeros(angle_shape)
|
|
763
|
+
|
|
764
|
+
hnuminPhi_left = hnuminPhi_guess - (true_angle - angle_min) \
|
|
765
|
+
* slope_guess
|
|
766
|
+
|
|
767
|
+
fdir_initial = FermiDirac(temperature=self.temperature,
|
|
768
|
+
hnuminPhi=hnuminPhi_left,
|
|
769
|
+
background=background_guess,
|
|
770
|
+
integrated_weight=integrated_weight_guess,
|
|
771
|
+
name='Initial guess')
|
|
772
|
+
|
|
773
|
+
parameters = np.array(
|
|
774
|
+
[hnuminPhi_left, background_guess, integrated_weight_guess])
|
|
775
|
+
|
|
776
|
+
extra_args = (self.temperature,)
|
|
777
|
+
|
|
778
|
+
for indx in range(angle_max_index - angle_min_index + 1):
|
|
779
|
+
edge = Intensities[:, indx]
|
|
780
|
+
|
|
781
|
+
parameters, pcov = fit_leastsq(
|
|
782
|
+
parameters, energy_range, edge, fdir_initial,
|
|
783
|
+
self.energy_resolution, None, *extra_args)
|
|
784
|
+
|
|
785
|
+
nmps[indx] = parameters[0]
|
|
786
|
+
stds[indx] = np.sqrt(np.diag(pcov)[0])
|
|
787
|
+
|
|
788
|
+
# Offset at true angle if not set before
|
|
789
|
+
if offset_guess is None:
|
|
790
|
+
offset_guess = hnuminPhi_guess - slope_guess * true_angle
|
|
791
|
+
|
|
792
|
+
parameters = np.array([offset_guess, slope_guess])
|
|
793
|
+
|
|
794
|
+
lin_fun = Linear(offset_guess, slope_guess, 'Linear')
|
|
795
|
+
|
|
796
|
+
popt, pcov = fit_leastsq(parameters, angle_range, nmps, lin_fun, None,
|
|
797
|
+
stds)
|
|
798
|
+
|
|
799
|
+
linsp = lin_fun(angle_range, popt[0], popt[1])
|
|
800
|
+
|
|
801
|
+
# Update hnuminPhi; automatically sets self.enel
|
|
802
|
+
self.hnuminPhi = lin_fun(true_angle, popt[0], popt[1])
|
|
803
|
+
self.hnuminPhi_std = np.sqrt(true_angle**2 * pcov[1, 1] + pcov[0, 0]
|
|
804
|
+
+ 2 * true_angle * pcov[0, 1])
|
|
805
|
+
|
|
806
|
+
Angl, Ekin = np.meshgrid(self.angles, self.ekin)
|
|
807
|
+
|
|
808
|
+
ax, fig, plt = get_ax_fig_plt(ax=ax)
|
|
809
|
+
|
|
810
|
+
ax.set_xlabel('Angle ($\degree$)')
|
|
811
|
+
ax.set_ylabel('$E_{\mathrm{kin}}$ (eV)')
|
|
812
|
+
mesh = ax.pcolormesh(Angl, Ekin, self.intensities,
|
|
813
|
+
shading='auto', cmap=plt.get_cmap('bone').reversed(),
|
|
814
|
+
zorder=1)
|
|
815
|
+
|
|
816
|
+
ax.errorbar(angle_range, nmps, yerr=xprs.sigma_confidence * stds, zorder=1)
|
|
817
|
+
ax.plot(angle_range, linsp, zorder=2)
|
|
818
|
+
|
|
819
|
+
cbar = plt.colorbar(mesh, ax=ax, label='counts (-)')
|
|
820
|
+
|
|
821
|
+
# Fermi-edge correction
|
|
822
|
+
rows, cols = self.intensities.shape
|
|
823
|
+
shift_values = popt[1] * self.angles / (self.ekin[0] - self.ekin[1])
|
|
824
|
+
row_coords = np.arange(rows).reshape(-1, 1) - shift_values
|
|
825
|
+
col_coords = np.arange(cols).reshape(1, -1).repeat(rows, axis=0)
|
|
826
|
+
self.intensities = map_coordinates(self.intensities,
|
|
827
|
+
[row_coords, col_coords], order=1)
|
|
828
|
+
|
|
829
|
+
return fig
|