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/selfenergies.py
CHANGED
|
@@ -13,15 +13,18 @@
|
|
|
13
13
|
|
|
14
14
|
import numpy as np
|
|
15
15
|
from .plotting import get_ax_fig_plt, add_fig_kwargs
|
|
16
|
-
from .constants import PREF
|
|
16
|
+
from .constants import PREF, KILO, K_B
|
|
17
17
|
|
|
18
18
|
class SelfEnergy:
|
|
19
|
-
r"""Self-energy
|
|
19
|
+
r"""Self-energy"""
|
|
20
20
|
|
|
21
|
-
def __init__(self, ekin_range, hnuminPhi,
|
|
21
|
+
def __init__(self, ekin_range, hnuminPhi, energy_resolution,
|
|
22
|
+
temperature, label, properties, parameters):
|
|
22
23
|
# core read-only state
|
|
23
24
|
self._ekin_range = ekin_range
|
|
24
25
|
self._hnuminPhi = hnuminPhi
|
|
26
|
+
self._energy_resolution = energy_resolution
|
|
27
|
+
self._temperature = temperature
|
|
25
28
|
self._label = label
|
|
26
29
|
|
|
27
30
|
# accept either a dict or a single-element list of dicts
|
|
@@ -29,8 +32,9 @@ class SelfEnergy:
|
|
|
29
32
|
if len(properties) == 1:
|
|
30
33
|
properties = properties[0]
|
|
31
34
|
else:
|
|
32
|
-
raise ValueError(
|
|
33
|
-
|
|
35
|
+
raise ValueError(
|
|
36
|
+
"`properties` must be a dict or a single dict in a list."
|
|
37
|
+
)
|
|
34
38
|
|
|
35
39
|
# single source of truth for all params (+ their *_sigma)
|
|
36
40
|
self._properties = dict(properties or {})
|
|
@@ -54,7 +58,9 @@ class SelfEnergy:
|
|
|
54
58
|
if self._class == "SpectralLinear" and (self._bare_mass is not None):
|
|
55
59
|
raise ValueError("`bare_mass` cannot be set for SpectralLinear.")
|
|
56
60
|
if self._class == "SpectralQuadratic" and (self._fermi_velocity is not None):
|
|
57
|
-
raise ValueError(
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"`fermi_velocity` cannot be set for SpectralQuadratic."
|
|
63
|
+
)
|
|
58
64
|
|
|
59
65
|
if self._side is not None and self._side not in ("left", "right"):
|
|
60
66
|
raise ValueError("`side` must be 'left' or 'right' if provided.")
|
|
@@ -78,12 +84,19 @@ class SelfEnergy:
|
|
|
78
84
|
self._imag = None
|
|
79
85
|
self._imag_sigma = None
|
|
80
86
|
|
|
87
|
+
# lazy caches for α²F(ω) extraction results
|
|
88
|
+
self._a2f_spectrum = None
|
|
89
|
+
self._a2f_model = None
|
|
90
|
+
self._a2f_omega_range = None
|
|
91
|
+
self._a2f_alpha_select = None
|
|
92
|
+
self._a2f_cost = None
|
|
93
|
+
|
|
81
94
|
def _check_mass_velocity_exclusivity(self):
|
|
82
95
|
"""Ensure that fermi_velocity and bare_mass are not both set."""
|
|
83
96
|
if (self._fermi_velocity is not None) and (self._bare_mass is not None):
|
|
84
97
|
raise ValueError(
|
|
85
|
-
"Cannot set both `fermi_velocity` and
|
|
86
|
-
"
|
|
98
|
+
"Cannot set both `fermi_velocity` and `bare_mass`: choose one "
|
|
99
|
+
"physical parametrization (SpectralLinear or SpectralQuadratic)."
|
|
87
100
|
)
|
|
88
101
|
|
|
89
102
|
# ---------------- core read-only axes ----------------
|
|
@@ -101,6 +114,16 @@ class SelfEnergy:
|
|
|
101
114
|
@property
|
|
102
115
|
def hnuminPhi(self):
|
|
103
116
|
return self._hnuminPhi
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def energy_resolution(self):
|
|
120
|
+
"""Energy resolution associated with the self-energy."""
|
|
121
|
+
return self._energy_resolution
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def temperature(self):
|
|
125
|
+
"""Temperature associated with the self-energy [K]."""
|
|
126
|
+
return self._temperature
|
|
104
127
|
|
|
105
128
|
# ---------------- identifiers ----------------
|
|
106
129
|
@property
|
|
@@ -158,8 +181,9 @@ class SelfEnergy:
|
|
|
158
181
|
@fermi_velocity.setter
|
|
159
182
|
def fermi_velocity(self, x):
|
|
160
183
|
if self._class == "SpectralQuadratic":
|
|
161
|
-
raise ValueError(
|
|
162
|
-
|
|
184
|
+
raise ValueError(
|
|
185
|
+
"`fermi_velocity` cannot be set for SpectralQuadratic."
|
|
186
|
+
)
|
|
163
187
|
self._fermi_velocity = x
|
|
164
188
|
self._parameters["fermi_velocity"] = x
|
|
165
189
|
# invalidate dependents
|
|
@@ -174,7 +198,9 @@ class SelfEnergy:
|
|
|
174
198
|
@bare_mass.setter
|
|
175
199
|
def bare_mass(self, x):
|
|
176
200
|
if self._class == "SpectralLinear":
|
|
177
|
-
raise ValueError(
|
|
201
|
+
raise ValueError(
|
|
202
|
+
"`bare_mass` cannot be set for SpectralLinear."
|
|
203
|
+
)
|
|
178
204
|
self._bare_mass = x
|
|
179
205
|
self._parameters["bare_mass"] = x
|
|
180
206
|
# invalidate dependents
|
|
@@ -252,7 +278,7 @@ class SelfEnergy:
|
|
|
252
278
|
# ---------------- derived outputs ----------------
|
|
253
279
|
@property
|
|
254
280
|
def peak_positions(self):
|
|
255
|
-
r"""k_parallel = peak * dtor * sqrt(ekin_range /
|
|
281
|
+
r"""k_parallel = peak * dtor * sqrt(ekin_range / PREF) (lazy)."""
|
|
256
282
|
if self._peak_positions is None:
|
|
257
283
|
if self._peak is None or self._ekin_range is None:
|
|
258
284
|
return None
|
|
@@ -263,13 +289,17 @@ class SelfEnergy:
|
|
|
263
289
|
"before accessing peak_positions and quantities that "
|
|
264
290
|
"depend on the latter."
|
|
265
291
|
)
|
|
266
|
-
kpar_mag =
|
|
267
|
-
np.
|
|
268
|
-
|
|
269
|
-
|
|
292
|
+
kpar_mag = (
|
|
293
|
+
np.sqrt(self._ekin_range / PREF)
|
|
294
|
+
* np.sin(np.deg2rad(np.abs(self._peak)))
|
|
295
|
+
)
|
|
296
|
+
self._peak_positions = ((-1.0 if self._side == "left"
|
|
297
|
+
else 1.0) * kpar_mag)
|
|
270
298
|
else:
|
|
271
|
-
self._peak_positions =
|
|
272
|
-
|
|
299
|
+
self._peak_positions = (
|
|
300
|
+
np.sqrt(self._ekin_range / PREF)
|
|
301
|
+
* np.sin(np.deg2rad(self._peak))
|
|
302
|
+
)
|
|
273
303
|
return self._peak_positions
|
|
274
304
|
|
|
275
305
|
|
|
@@ -279,10 +309,13 @@ class SelfEnergy:
|
|
|
279
309
|
if self._peak_positions_sigma is None:
|
|
280
310
|
if self._peak_sigma is None or self._ekin_range is None:
|
|
281
311
|
return None
|
|
282
|
-
self._peak_positions_sigma = (
|
|
283
|
-
|
|
284
|
-
* np.deg2rad(self.
|
|
312
|
+
self._peak_positions_sigma = (
|
|
313
|
+
np.sqrt(self._ekin_range / PREF)
|
|
314
|
+
* np.abs(np.cos(np.deg2rad(self._peak)))
|
|
315
|
+
* np.deg2rad(self._peak_sigma)
|
|
316
|
+
)
|
|
285
317
|
return self._peak_positions_sigma
|
|
318
|
+
|
|
286
319
|
|
|
287
320
|
@property
|
|
288
321
|
def imag(self):
|
|
@@ -290,82 +323,224 @@ class SelfEnergy:
|
|
|
290
323
|
if self._imag is None:
|
|
291
324
|
if self._broadening is None or self._ekin_range is None:
|
|
292
325
|
return None
|
|
293
|
-
|
|
294
|
-
if self._fermi_velocity is None:
|
|
295
|
-
raise AttributeError("Cannot compute `imag` "
|
|
296
|
-
"(SpectralLinear): set `fermi_velocity` first.")
|
|
297
|
-
self._imag = np.abs(self._fermi_velocity) * np.sqrt(self._ekin_range \
|
|
298
|
-
/ PREF) * self._broadening
|
|
299
|
-
else:
|
|
300
|
-
if self._bare_mass is None:
|
|
301
|
-
raise AttributeError("Cannot compute `imag` "
|
|
302
|
-
"(SpectralQuadratic): set `bare_mass` first.")
|
|
303
|
-
self._imag = (self._ekin_range * self._broadening) \
|
|
304
|
-
/ np.abs(self._bare_mass)
|
|
326
|
+
self._imag = self._compute_imag()
|
|
305
327
|
return self._imag
|
|
306
328
|
|
|
329
|
+
|
|
307
330
|
@property
|
|
308
331
|
def imag_sigma(self):
|
|
309
332
|
r"""Std. dev. of -Σ'' (lazy)."""
|
|
310
333
|
if self._imag_sigma is None:
|
|
311
334
|
if self._broadening_sigma is None or self._ekin_range is None:
|
|
312
335
|
return None
|
|
313
|
-
|
|
314
|
-
if self._fermi_velocity is None:
|
|
315
|
-
raise AttributeError("Cannot compute `imag_sigma` "
|
|
316
|
-
"(SpectralLinear): set `fermi_velocity` first.")
|
|
317
|
-
self._imag_sigma = np.abs(self._fermi_velocity) * \
|
|
318
|
-
np.sqrt(self._ekin_range / PREF) * self._broadening_sigma
|
|
319
|
-
else:
|
|
320
|
-
if self._bare_mass is None:
|
|
321
|
-
raise AttributeError("Cannot compute `imag_sigma` "
|
|
322
|
-
"(SpectralQuadratic): set `bare_mass` first.")
|
|
323
|
-
self._imag_sigma = (self._ekin_range * \
|
|
324
|
-
self._broadening_sigma) / np.abs(self._bare_mass)
|
|
336
|
+
self._imag_sigma = self._compute_imag_sigma()
|
|
325
337
|
return self._imag_sigma
|
|
326
338
|
|
|
339
|
+
|
|
327
340
|
@property
|
|
328
341
|
def real(self):
|
|
329
342
|
r"""Σ' (lazy)."""
|
|
330
343
|
if self._real is None:
|
|
331
344
|
if self._peak is None or self._ekin_range is None:
|
|
332
345
|
return None
|
|
333
|
-
|
|
334
|
-
if self._fermi_velocity is None or self._fermi_wavevector is None:
|
|
335
|
-
raise AttributeError("Cannot compute `real` "
|
|
336
|
-
"(SpectralLinear): set `fermi_velocity` and " \
|
|
337
|
-
"`fermi_wavevector` first.")
|
|
338
|
-
self._real = self.enel_range - self._fermi_velocity * \
|
|
339
|
-
(self.peak_positions - self._fermi_wavevector)
|
|
340
|
-
else:
|
|
341
|
-
if self._bare_mass is None or self._fermi_wavevector is None:
|
|
342
|
-
raise AttributeError("Cannot compute `real` "
|
|
343
|
-
"(SpectralQuadratic): set `bare_mass` and " \
|
|
344
|
-
"`fermi_wavevector` first.")
|
|
345
|
-
self._real = self.enel_range - (PREF / \
|
|
346
|
-
self._bare_mass) * (self.peak_positions**2 \
|
|
347
|
-
- self._fermi_wavevector**2)
|
|
346
|
+
self._real = self._compute_real()
|
|
348
347
|
return self._real
|
|
349
348
|
|
|
349
|
+
|
|
350
350
|
@property
|
|
351
351
|
def real_sigma(self):
|
|
352
352
|
r"""Std. dev. of Σ' (lazy)."""
|
|
353
353
|
if self._real_sigma is None:
|
|
354
354
|
if self._peak_sigma is None or self._ekin_range is None:
|
|
355
355
|
return None
|
|
356
|
-
|
|
357
|
-
if self._fermi_velocity is None:
|
|
358
|
-
raise AttributeError("Cannot compute `real_sigma` "
|
|
359
|
-
"(SpectralLinear): set `fermi_velocity` first.")
|
|
360
|
-
self._real_sigma = np.abs(self._fermi_velocity) * self.peak_positions_sigma
|
|
361
|
-
else:
|
|
362
|
-
if self._bare_mass is None or self._fermi_wavevector is None:
|
|
363
|
-
raise AttributeError("Cannot compute `real_sigma` "
|
|
364
|
-
"(SpectralQuadratic): set `bare_mass` and " \
|
|
365
|
-
"`fermi_wavevector` first.")
|
|
366
|
-
self._real_sigma = 2 * PREF * self.peak_positions_sigma \
|
|
367
|
-
* np.abs(self.peak_positions / self._bare_mass)
|
|
356
|
+
self._real_sigma = self._compute_real_sigma()
|
|
368
357
|
return self._real_sigma
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def a2f_spectrum(self):
|
|
361
|
+
"""Cached α²F(ω) spectrum from last extraction (or None)."""
|
|
362
|
+
return self._a2f_spectrum
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def a2f_model(self):
|
|
366
|
+
"""Cached MEM model spectrum from last extraction (or None)."""
|
|
367
|
+
return self._a2f_model
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def a2f_omega_range(self):
|
|
371
|
+
"""Cached ω grid for the last extraction (or None)."""
|
|
372
|
+
return self._a2f_omega_range
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def a2f_alpha_select(self):
|
|
376
|
+
"""Cached selected alpha from last extraction (or None)."""
|
|
377
|
+
return self._a2f_alpha_select
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def a2f_cost(self):
|
|
381
|
+
"""Cached cost from last bayesian_loop (or None)."""
|
|
382
|
+
return self._a2f_cost
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _compute_imag(self, fermi_velocity=None, bare_mass=None):
|
|
386
|
+
r"""Compute -Σ'' without touching caches."""
|
|
387
|
+
if self._broadening is None or self._ekin_range is None:
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
ekin = np.asarray(self._ekin_range)
|
|
391
|
+
broad = self._broadening
|
|
392
|
+
|
|
393
|
+
if self._class == "SpectralLinear":
|
|
394
|
+
vF = self._fermi_velocity if fermi_velocity is None else fermi_velocity
|
|
395
|
+
if vF is None:
|
|
396
|
+
raise AttributeError(
|
|
397
|
+
"Cannot compute `imag` (SpectralLinear): set `fermi_velocity` "
|
|
398
|
+
"first."
|
|
399
|
+
)
|
|
400
|
+
return np.abs(vF) * np.sqrt(ekin / PREF) * broad
|
|
401
|
+
|
|
402
|
+
if self._class == "SpectralQuadratic":
|
|
403
|
+
mb = self._bare_mass if bare_mass is None else bare_mass
|
|
404
|
+
if mb is None:
|
|
405
|
+
raise AttributeError(
|
|
406
|
+
"Cannot compute `imag` (SpectralQuadratic): set `bare_mass` "
|
|
407
|
+
"first."
|
|
408
|
+
)
|
|
409
|
+
return (ekin * broad) / np.abs(mb)
|
|
410
|
+
|
|
411
|
+
raise NotImplementedError(
|
|
412
|
+
f"_compute_imag is not implemented for spectral class '{self._class}'."
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _compute_imag_sigma(self, fermi_velocity=None, bare_mass=None):
|
|
417
|
+
r"""Compute std. dev. of -Σ'' without touching caches."""
|
|
418
|
+
if self._broadening_sigma is None or self._ekin_range is None:
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
ekin = np.asarray(self._ekin_range)
|
|
422
|
+
broad_sigma = self._broadening_sigma
|
|
423
|
+
|
|
424
|
+
if self._class == "SpectralLinear":
|
|
425
|
+
vF = self._fermi_velocity if fermi_velocity is None else fermi_velocity
|
|
426
|
+
if vF is None:
|
|
427
|
+
raise AttributeError(
|
|
428
|
+
"Cannot compute `imag_sigma` (SpectralLinear): set "
|
|
429
|
+
"`fermi_velocity` first."
|
|
430
|
+
)
|
|
431
|
+
return np.abs(vF) * np.sqrt(ekin / PREF) * broad_sigma
|
|
432
|
+
|
|
433
|
+
if self._class == "SpectralQuadratic":
|
|
434
|
+
mb = self._bare_mass if bare_mass is None else bare_mass
|
|
435
|
+
if mb is None:
|
|
436
|
+
raise AttributeError(
|
|
437
|
+
"Cannot compute `imag_sigma` (SpectralQuadratic): set "
|
|
438
|
+
"`bare_mass` first."
|
|
439
|
+
)
|
|
440
|
+
return (ekin * broad_sigma) / np.abs(mb)
|
|
441
|
+
|
|
442
|
+
raise NotImplementedError(
|
|
443
|
+
f"_compute_imag_sigma is not implemented for spectral class "
|
|
444
|
+
f"'{self._class}'."
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _compute_real(self, fermi_velocity=None, fermi_wavevector=None,
|
|
449
|
+
bare_mass=None):
|
|
450
|
+
r"""Compute Σ' without touching caches."""
|
|
451
|
+
if self._peak is None or self._ekin_range is None:
|
|
452
|
+
return None
|
|
453
|
+
|
|
454
|
+
enel = self.enel_range
|
|
455
|
+
kpar = self.peak_positions
|
|
456
|
+
if kpar is None:
|
|
457
|
+
return None
|
|
458
|
+
|
|
459
|
+
if self._class == "SpectralLinear":
|
|
460
|
+
vF = self._fermi_velocity if fermi_velocity is None else fermi_velocity
|
|
461
|
+
kF = (self._fermi_wavevector if fermi_wavevector is None
|
|
462
|
+
else fermi_wavevector)
|
|
463
|
+
if vF is None or kF is None:
|
|
464
|
+
raise AttributeError(
|
|
465
|
+
"Cannot compute `real` (SpectralLinear): set `fermi_velocity` "
|
|
466
|
+
"and `fermi_wavevector` first."
|
|
467
|
+
)
|
|
468
|
+
return enel - vF * (kpar - kF)
|
|
469
|
+
|
|
470
|
+
if self._class == "SpectralQuadratic":
|
|
471
|
+
mb = self._bare_mass if bare_mass is None else bare_mass
|
|
472
|
+
kF = (self._fermi_wavevector if fermi_wavevector is None
|
|
473
|
+
else fermi_wavevector)
|
|
474
|
+
if mb is None or kF is None:
|
|
475
|
+
raise AttributeError(
|
|
476
|
+
"Cannot compute `real` (SpectralQuadratic): set `bare_mass` "
|
|
477
|
+
"and `fermi_wavevector` first."
|
|
478
|
+
)
|
|
479
|
+
return enel - (PREF / mb) * (kpar**2 - kF**2)
|
|
480
|
+
|
|
481
|
+
raise NotImplementedError(
|
|
482
|
+
f"_compute_real is not implemented for spectral class '{self._class}'."
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
def _compute_real_sigma(self, fermi_velocity=None, fermi_wavevector=None,
|
|
486
|
+
bare_mass=None):
|
|
487
|
+
r"""Compute std. dev. of Σ' without touching caches."""
|
|
488
|
+
if self._peak_sigma is None or self._ekin_range is None:
|
|
489
|
+
return None
|
|
490
|
+
|
|
491
|
+
kpar_sigma = self.peak_positions_sigma
|
|
492
|
+
if kpar_sigma is None:
|
|
493
|
+
return None
|
|
494
|
+
|
|
495
|
+
if self._class == "SpectralLinear":
|
|
496
|
+
vF = self._fermi_velocity if fermi_velocity is None else fermi_velocity
|
|
497
|
+
if vF is None:
|
|
498
|
+
raise AttributeError(
|
|
499
|
+
"Cannot compute `real_sigma` (SpectralLinear): set "
|
|
500
|
+
"`fermi_velocity` first."
|
|
501
|
+
)
|
|
502
|
+
return np.abs(vF) * kpar_sigma
|
|
503
|
+
|
|
504
|
+
if self._class == "SpectralQuadratic":
|
|
505
|
+
mb = self._bare_mass if bare_mass is None else bare_mass
|
|
506
|
+
kF = (self._fermi_wavevector if fermi_wavevector is None
|
|
507
|
+
else fermi_wavevector)
|
|
508
|
+
if mb is None or kF is None:
|
|
509
|
+
raise AttributeError(
|
|
510
|
+
"Cannot compute `real_sigma` (SpectralQuadratic): set "
|
|
511
|
+
"`bare_mass` and `fermi_wavevector` first."
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
kpar = self.peak_positions
|
|
515
|
+
if kpar is None:
|
|
516
|
+
return None
|
|
517
|
+
return 2.0 * PREF * kpar_sigma * np.abs(kpar / mb)
|
|
518
|
+
|
|
519
|
+
raise ValueError(
|
|
520
|
+
f"Unsupported spectral class '{self._class}' in "
|
|
521
|
+
"_compute_real_sigma."
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _evaluate_self_energy_arrays(self, fermi_velocity=None, fermi_wavevector=None,
|
|
526
|
+
bare_mass=None):
|
|
527
|
+
r"""Evaluate Σ' / -Σ'' and 1σ uncertainties without mutating caches."""
|
|
528
|
+
real = self._compute_real(
|
|
529
|
+
fermi_velocity=fermi_velocity,
|
|
530
|
+
fermi_wavevector=fermi_wavevector,
|
|
531
|
+
bare_mass=bare_mass,
|
|
532
|
+
)
|
|
533
|
+
real_sigma = self._compute_real_sigma(
|
|
534
|
+
fermi_velocity=fermi_velocity,
|
|
535
|
+
fermi_wavevector=fermi_wavevector,
|
|
536
|
+
bare_mass=bare_mass,
|
|
537
|
+
)
|
|
538
|
+
imag = self._compute_imag(fermi_velocity=fermi_velocity, bare_mass=bare_mass)
|
|
539
|
+
imag_sigma = self._compute_imag_sigma(
|
|
540
|
+
fermi_velocity=fermi_velocity, bare_mass=bare_mass
|
|
541
|
+
)
|
|
542
|
+
return real, real_sigma, imag, imag_sigma
|
|
543
|
+
|
|
369
544
|
|
|
370
545
|
@property
|
|
371
546
|
def mdc_maxima(self):
|
|
@@ -389,7 +564,7 @@ class SelfEnergy:
|
|
|
389
564
|
self.peak_positions + self._center_wavevector
|
|
390
565
|
)
|
|
391
566
|
|
|
392
|
-
return self._mdc_maxima
|
|
567
|
+
return self._mdc_maxima
|
|
393
568
|
|
|
394
569
|
def _se_legend_labels(self):
|
|
395
570
|
"""Return (real_label, imag_label) for legend with safe subscripts."""
|
|
@@ -412,15 +587,37 @@ class SelfEnergy:
|
|
|
412
587
|
imag_label = rf"$-\Sigma_{{\mathrm{{{safe_label}}}}}''(E)$"
|
|
413
588
|
|
|
414
589
|
return real_label, imag_label
|
|
590
|
+
|
|
591
|
+
def _a2f_legend_labels(self):
|
|
592
|
+
"""Return (a2f_label, model_label) for legend with safe subscripts."""
|
|
593
|
+
se_label = getattr(self, "label", None)
|
|
594
|
+
|
|
595
|
+
if se_label is None:
|
|
596
|
+
return r"$\alpha^2F(\omega)$", r"$m(\omega)$"
|
|
597
|
+
|
|
598
|
+
safe_label = str(se_label).replace("_", r"\_")
|
|
599
|
+
if safe_label == "":
|
|
600
|
+
return r"$\alpha^2F(\omega)$", r"$m(\omega)$"
|
|
601
|
+
|
|
602
|
+
a2f_label = rf"$\alpha^2F_{{\mathrm{{{safe_label}}}}}(\omega)$"
|
|
603
|
+
model_label = rf"$m_{{\mathrm{{{safe_label}}}}}(\omega)$"
|
|
604
|
+
return a2f_label, model_label
|
|
415
605
|
|
|
416
606
|
@add_fig_kwargs
|
|
417
|
-
def plot_real(self, ax=None, **kwargs):
|
|
607
|
+
def plot_real(self, ax=None, scale="eV", resolution_range="absent", **kwargs):
|
|
418
608
|
r"""Plot the real part Σ' of the self-energy as a function of E-μ.
|
|
419
609
|
|
|
420
610
|
Parameters
|
|
421
611
|
----------
|
|
422
612
|
ax : Matplotlib-Axes or None
|
|
423
613
|
Axis to plot on. Created if not provided by the user.
|
|
614
|
+
scale : {"eV", "meV"}
|
|
615
|
+
Units for both axes. If "meV", x and y (and yerr) are multiplied by
|
|
616
|
+
`KILO`.
|
|
617
|
+
resolution_range : {"absent", "applied"}
|
|
618
|
+
If "applied", removes points with |E-μ| <= energy_resolution (around
|
|
619
|
+
the chemical potential). The energy resolution is taken from
|
|
620
|
+
``self.energy_resolution`` (in eV) and scaled consistently with `scale`.
|
|
424
621
|
**kwargs :
|
|
425
622
|
Additional keyword arguments passed to ``ax.errorbar``.
|
|
426
623
|
|
|
@@ -433,10 +630,31 @@ class SelfEnergy:
|
|
|
433
630
|
|
|
434
631
|
ax, fig, plt = get_ax_fig_plt(ax=ax)
|
|
435
632
|
|
|
633
|
+
if scale not in ("eV", "meV"):
|
|
634
|
+
raise ValueError("scale must be either 'eV' or 'meV'.")
|
|
635
|
+
if resolution_range not in ("absent", "applied"):
|
|
636
|
+
raise ValueError("resolution_range must be 'absent' or 'applied'.")
|
|
637
|
+
|
|
638
|
+
factor = KILO if scale == "meV" else 1.0
|
|
639
|
+
|
|
436
640
|
x = self.enel_range
|
|
437
641
|
y = self.real
|
|
438
642
|
y_sigma = self.real_sigma
|
|
439
643
|
|
|
644
|
+
if x is not None:
|
|
645
|
+
x = factor * np.asarray(x, dtype=float)
|
|
646
|
+
if y is not None:
|
|
647
|
+
y = factor * np.asarray(y, dtype=float)
|
|
648
|
+
|
|
649
|
+
if resolution_range == "applied" and x is not None and y is not None:
|
|
650
|
+
res = self.energy_resolution
|
|
651
|
+
if res is not None:
|
|
652
|
+
keep = np.abs(x) > (factor * float(res))
|
|
653
|
+
x = x[keep]
|
|
654
|
+
y = y[keep]
|
|
655
|
+
if y_sigma is not None:
|
|
656
|
+
y_sigma = np.asarray(y_sigma, dtype=float)[keep]
|
|
657
|
+
|
|
440
658
|
real_label, _ = self._se_legend_labels()
|
|
441
659
|
kwargs.setdefault("label", real_label)
|
|
442
660
|
|
|
@@ -446,23 +664,33 @@ class SelfEnergy:
|
|
|
446
664
|
"Warning: some Σ'(E) uncertainty values are missing. "
|
|
447
665
|
"Error bars omitted at those energies."
|
|
448
666
|
)
|
|
449
|
-
kwargs.setdefault("yerr", xprs.sigma_confidence * y_sigma)
|
|
667
|
+
kwargs.setdefault("yerr", xprs.sigma_confidence * factor * y_sigma)
|
|
450
668
|
|
|
451
669
|
ax.errorbar(x, y, **kwargs)
|
|
452
|
-
|
|
453
|
-
|
|
670
|
+
|
|
671
|
+
x_unit = "meV" if scale == "meV" else "eV"
|
|
672
|
+
ax.set_xlabel(rf"$E-\mu$ ({x_unit})")
|
|
673
|
+
ax.set_ylabel(rf"$\Sigma'(E)$ ({x_unit})")
|
|
454
674
|
ax.legend()
|
|
455
675
|
|
|
456
676
|
return fig
|
|
457
677
|
|
|
678
|
+
|
|
458
679
|
@add_fig_kwargs
|
|
459
|
-
def plot_imag(self, ax=None, **kwargs):
|
|
680
|
+
def plot_imag(self, ax=None, scale="eV", resolution_range="absent", **kwargs):
|
|
460
681
|
r"""Plot the imaginary part -Σ'' of the self-energy vs. E-μ.
|
|
461
682
|
|
|
462
683
|
Parameters
|
|
463
684
|
----------
|
|
464
685
|
ax : Matplotlib-Axes or None
|
|
465
686
|
Axis to plot on. Created if not provided by the user.
|
|
687
|
+
scale : {"eV", "meV"}
|
|
688
|
+
Units for both axes. If "meV", x and y (and yerr) are multiplied by
|
|
689
|
+
`KILO`.
|
|
690
|
+
resolution_range : {"absent", "applied"}
|
|
691
|
+
If "applied", removes points with |E-μ| <= energy_resolution (around
|
|
692
|
+
the chemical potential). The energy resolution is taken from
|
|
693
|
+
``self.energy_resolution`` (in eV) and scaled consistently with `scale`.
|
|
466
694
|
**kwargs :
|
|
467
695
|
Additional keyword arguments passed to ``ax.errorbar``.
|
|
468
696
|
|
|
@@ -475,10 +703,31 @@ class SelfEnergy:
|
|
|
475
703
|
|
|
476
704
|
ax, fig, plt = get_ax_fig_plt(ax=ax)
|
|
477
705
|
|
|
706
|
+
if scale not in ("eV", "meV"):
|
|
707
|
+
raise ValueError("scale must be either 'eV' or 'meV'.")
|
|
708
|
+
if resolution_range not in ("absent", "applied"):
|
|
709
|
+
raise ValueError("resolution_range must be 'absent' or 'applied'.")
|
|
710
|
+
|
|
711
|
+
factor = KILO if scale == "meV" else 1.0
|
|
712
|
+
|
|
478
713
|
x = self.enel_range
|
|
479
714
|
y = self.imag
|
|
480
715
|
y_sigma = self.imag_sigma
|
|
481
716
|
|
|
717
|
+
if x is not None:
|
|
718
|
+
x = factor * np.asarray(x, dtype=float)
|
|
719
|
+
if y is not None:
|
|
720
|
+
y = factor * np.asarray(y, dtype=float)
|
|
721
|
+
|
|
722
|
+
if resolution_range == "applied" and x is not None and y is not None:
|
|
723
|
+
res = self.energy_resolution
|
|
724
|
+
if res is not None:
|
|
725
|
+
keep = np.abs(x) > (factor * float(res))
|
|
726
|
+
x = x[keep]
|
|
727
|
+
y = y[keep]
|
|
728
|
+
if y_sigma is not None:
|
|
729
|
+
y_sigma = np.asarray(y_sigma, dtype=float)[keep]
|
|
730
|
+
|
|
482
731
|
_, imag_label = self._se_legend_labels()
|
|
483
732
|
kwargs.setdefault("label", imag_label)
|
|
484
733
|
|
|
@@ -488,31 +737,76 @@ class SelfEnergy:
|
|
|
488
737
|
"Warning: some -Σ''(E) uncertainty values are missing. "
|
|
489
738
|
"Error bars omitted at those energies."
|
|
490
739
|
)
|
|
491
|
-
kwargs.setdefault("yerr", xprs.sigma_confidence * y_sigma)
|
|
740
|
+
kwargs.setdefault("yerr", xprs.sigma_confidence * factor * y_sigma)
|
|
492
741
|
|
|
493
742
|
ax.errorbar(x, y, **kwargs)
|
|
494
|
-
|
|
495
|
-
|
|
743
|
+
|
|
744
|
+
x_unit = "meV" if scale == "meV" else "eV"
|
|
745
|
+
ax.set_xlabel(rf"$E-\mu$ ({x_unit})")
|
|
746
|
+
ax.set_ylabel(rf"$-\Sigma''(E)$ ({x_unit})")
|
|
496
747
|
ax.legend()
|
|
497
748
|
|
|
498
749
|
return fig
|
|
499
750
|
|
|
751
|
+
|
|
500
752
|
@add_fig_kwargs
|
|
501
|
-
def plot_both(self, ax=None, **kwargs):
|
|
502
|
-
r"""Plot Σ'(E) and -Σ''(E) vs. E-μ on the same axis.
|
|
753
|
+
def plot_both(self, ax=None, scale="eV", resolution_range="absent", **kwargs):
|
|
754
|
+
r"""Plot Σ'(E) and -Σ''(E) vs. E-μ on the same axis.
|
|
755
|
+
|
|
756
|
+
Parameters
|
|
757
|
+
----------
|
|
758
|
+
ax : Matplotlib-Axes or None
|
|
759
|
+
Axis to plot on. Created if not provided by the user.
|
|
760
|
+
scale : {"eV", "meV"}
|
|
761
|
+
Units for both axes. If "meV", x, y, and yerr are multiplied by
|
|
762
|
+
`KILO`.
|
|
763
|
+
resolution_range : {"absent", "applied"}
|
|
764
|
+
If "applied", removes points with |E-μ| <= energy_resolution (around
|
|
765
|
+
the chemical potential). The energy resolution is taken from
|
|
766
|
+
``self.energy_resolution`` (in eV) and scaled consistently with `scale`.
|
|
767
|
+
**kwargs :
|
|
768
|
+
Additional keyword arguments passed to ``ax.errorbar``.
|
|
769
|
+
"""
|
|
503
770
|
from . import settings_parameters as xprs
|
|
504
771
|
|
|
505
772
|
ax, fig, plt = get_ax_fig_plt(ax=ax)
|
|
506
773
|
|
|
774
|
+
if scale not in ("eV", "meV"):
|
|
775
|
+
raise ValueError("scale must be either 'eV' or 'meV'.")
|
|
776
|
+
if resolution_range not in ("absent", "applied"):
|
|
777
|
+
raise ValueError("resolution_range must be 'absent' or 'applied'.")
|
|
778
|
+
|
|
779
|
+
factor = KILO if scale == "meV" else 1.0
|
|
780
|
+
|
|
507
781
|
x = self.enel_range
|
|
508
782
|
real = self.real
|
|
509
783
|
imag = self.imag
|
|
510
784
|
real_sigma = self.real_sigma
|
|
511
785
|
imag_sigma = self.imag_sigma
|
|
512
786
|
|
|
787
|
+
if x is not None:
|
|
788
|
+
x = factor * np.asarray(x, dtype=float)
|
|
789
|
+
if real is not None:
|
|
790
|
+
real = factor * np.asarray(real, dtype=float)
|
|
791
|
+
if imag is not None:
|
|
792
|
+
imag = factor * np.asarray(imag, dtype=float)
|
|
793
|
+
|
|
794
|
+
if resolution_range == "applied" and x is not None:
|
|
795
|
+
res = self.energy_resolution
|
|
796
|
+
if res is not None:
|
|
797
|
+
keep = np.abs(x) > (factor * float(res))
|
|
798
|
+
x = x[keep]
|
|
799
|
+
if real is not None:
|
|
800
|
+
real = real[keep]
|
|
801
|
+
if imag is not None:
|
|
802
|
+
imag = imag[keep]
|
|
803
|
+
if real_sigma is not None:
|
|
804
|
+
real_sigma = np.asarray(real_sigma, dtype=float)[keep]
|
|
805
|
+
if imag_sigma is not None:
|
|
806
|
+
imag_sigma = np.asarray(imag_sigma, dtype=float)[keep]
|
|
807
|
+
|
|
513
808
|
real_label, imag_label = self._se_legend_labels()
|
|
514
809
|
|
|
515
|
-
# --- plot Σ'
|
|
516
810
|
kw_real = dict(kwargs)
|
|
517
811
|
if real_sigma is not None:
|
|
518
812
|
if np.isnan(real_sigma).any():
|
|
@@ -520,11 +814,10 @@ class SelfEnergy:
|
|
|
520
814
|
"Warning: some Σ'(E) uncertainty values are missing. "
|
|
521
815
|
"Error bars omitted at those energies."
|
|
522
816
|
)
|
|
523
|
-
kw_real.setdefault("yerr", xprs.sigma_confidence * real_sigma)
|
|
817
|
+
kw_real.setdefault("yerr", xprs.sigma_confidence * factor * real_sigma)
|
|
524
818
|
kw_real.setdefault("label", real_label)
|
|
525
819
|
ax.errorbar(x, real, **kw_real)
|
|
526
820
|
|
|
527
|
-
# --- plot -Σ''
|
|
528
821
|
kw_imag = dict(kwargs)
|
|
529
822
|
if imag_sigma is not None:
|
|
530
823
|
if np.isnan(imag_sigma).any():
|
|
@@ -532,16 +825,1362 @@ class SelfEnergy:
|
|
|
532
825
|
"Warning: some -Σ''(E) uncertainty values are missing. "
|
|
533
826
|
"Error bars omitted at those energies."
|
|
534
827
|
)
|
|
535
|
-
kw_imag.setdefault("yerr", xprs.sigma_confidence * imag_sigma)
|
|
828
|
+
kw_imag.setdefault("yerr", xprs.sigma_confidence * factor * imag_sigma)
|
|
536
829
|
kw_imag.setdefault("label", imag_label)
|
|
537
830
|
ax.errorbar(x, imag, **kw_imag)
|
|
538
831
|
|
|
539
|
-
|
|
540
|
-
ax.
|
|
832
|
+
x_unit = "meV" if scale == "meV" else "eV"
|
|
833
|
+
ax.set_xlabel(rf"$E-\mu$ ({x_unit})")
|
|
834
|
+
ax.set_ylabel(rf"$\Sigma'(E),\ -\Sigma''(E)$ ({x_unit})")
|
|
541
835
|
ax.legend()
|
|
542
836
|
|
|
543
837
|
return fig
|
|
544
838
|
|
|
839
|
+
@add_fig_kwargs
|
|
840
|
+
def plot_a2f(self, ax=None, abscissa="forward", **kwargs):
|
|
841
|
+
ax, fig, plt = get_ax_fig_plt(ax=ax)
|
|
842
|
+
|
|
843
|
+
xlim_in = ax.get_xlim()
|
|
844
|
+
ylim_in = ax.get_ylim()
|
|
845
|
+
|
|
846
|
+
if abscissa not in ("forward", "reversed"):
|
|
847
|
+
raise ValueError("abscissa must be either 'forward' or 'reversed'.")
|
|
848
|
+
|
|
849
|
+
omega = self.a2f_omega_range
|
|
850
|
+
spectrum = self.a2f_spectrum
|
|
851
|
+
|
|
852
|
+
if omega is None or spectrum is None:
|
|
853
|
+
raise AttributeError(
|
|
854
|
+
"No cached α²F(ω) spectrum found. Run `extract_a2f()` or "
|
|
855
|
+
"`bayesian_loop()` first."
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
if abscissa == "reversed":
|
|
859
|
+
omega = -omega[::-1]
|
|
860
|
+
spectrum = spectrum[::-1]
|
|
861
|
+
|
|
862
|
+
a2f_label, _ = self._a2f_legend_labels()
|
|
863
|
+
kwargs.setdefault("label", a2f_label)
|
|
864
|
+
ax.plot(omega, spectrum, **kwargs)
|
|
865
|
+
|
|
866
|
+
ax.set_xlabel(r"$\omega$ (meV)")
|
|
867
|
+
ax.set_ylabel(r"$\alpha^2F(\omega)$ (-)")
|
|
868
|
+
|
|
869
|
+
self._apply_spectra_axis_defaults(ax, omega, abscissa, xlim_in, ylim_in)
|
|
870
|
+
|
|
871
|
+
ax.legend()
|
|
872
|
+
return fig
|
|
873
|
+
|
|
874
|
+
@add_fig_kwargs
|
|
875
|
+
def plot_model(self, ax=None, abscissa="forward", **kwargs):
|
|
876
|
+
ax, fig, plt = get_ax_fig_plt(ax=ax)
|
|
877
|
+
|
|
878
|
+
xlim_in = ax.get_xlim()
|
|
879
|
+
ylim_in = ax.get_ylim()
|
|
880
|
+
|
|
881
|
+
if abscissa not in ("forward", "reversed"):
|
|
882
|
+
raise ValueError("abscissa must be either 'forward' or 'reversed'.")
|
|
883
|
+
|
|
884
|
+
omega = self.a2f_omega_range
|
|
885
|
+
model = self.a2f_model
|
|
886
|
+
|
|
887
|
+
if omega is None or model is None:
|
|
888
|
+
raise AttributeError(
|
|
889
|
+
"No cached model spectrum found. Run `extract_a2f()` or "
|
|
890
|
+
"`bayesian_loop()` first."
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
if abscissa == "reversed":
|
|
894
|
+
omega = -omega[::-1]
|
|
895
|
+
model = model[::-1]
|
|
896
|
+
|
|
897
|
+
_, model_label = self._a2f_legend_labels()
|
|
898
|
+
kwargs.setdefault("label", model_label)
|
|
899
|
+
ax.plot(omega, model, **kwargs)
|
|
900
|
+
|
|
901
|
+
ax.set_xlabel(r"$\omega$ (meV)")
|
|
902
|
+
ax.set_ylabel(r"$m(\omega)$ (-)")
|
|
903
|
+
|
|
904
|
+
self._apply_spectra_axis_defaults(ax, omega, abscissa, xlim_in, ylim_in)
|
|
905
|
+
|
|
906
|
+
ax.legend()
|
|
907
|
+
return fig
|
|
908
|
+
|
|
909
|
+
@add_fig_kwargs
|
|
910
|
+
def plot_spectra(self, ax=None, abscissa="forward", **kwargs):
|
|
911
|
+
ax, fig, plt = get_ax_fig_plt(ax=ax)
|
|
912
|
+
|
|
913
|
+
xlim_in = ax.get_xlim()
|
|
914
|
+
ylim_in = ax.get_ylim()
|
|
915
|
+
|
|
916
|
+
if abscissa not in ("forward", "reversed"):
|
|
917
|
+
raise ValueError("abscissa must be either 'forward' or 'reversed'.")
|
|
918
|
+
|
|
919
|
+
omega = self.a2f_omega_range
|
|
920
|
+
spectrum = self.a2f_spectrum
|
|
921
|
+
model = self.a2f_model
|
|
922
|
+
|
|
923
|
+
if omega is None or spectrum is None or model is None:
|
|
924
|
+
raise AttributeError(
|
|
925
|
+
"No cached spectra found. Run `extract_a2f()` or `bayesian_loop()` "
|
|
926
|
+
"first."
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
if abscissa == "reversed":
|
|
930
|
+
omega = -omega[::-1]
|
|
931
|
+
spectrum = spectrum[::-1]
|
|
932
|
+
model = model[::-1]
|
|
933
|
+
|
|
934
|
+
kw_a2f = dict(kwargs)
|
|
935
|
+
kw_model = dict(kwargs)
|
|
936
|
+
a2f_label, model_label = self._a2f_legend_labels()
|
|
937
|
+
kw_a2f.setdefault("label", a2f_label)
|
|
938
|
+
kw_model.setdefault("label", model_label)
|
|
939
|
+
|
|
940
|
+
ax.plot(omega, spectrum, **kw_a2f)
|
|
941
|
+
ax.plot(omega, model, **kw_model)
|
|
942
|
+
|
|
943
|
+
self._apply_spectra_axis_defaults(ax, omega, abscissa, xlim_in, ylim_in)
|
|
944
|
+
|
|
945
|
+
ax.set_xlabel(r"$\omega$ (meV)")
|
|
946
|
+
ax.set_ylabel(r"$\alpha^2F_n(\omega),~m_n(\omega)~(-)$")
|
|
947
|
+
ax.legend()
|
|
948
|
+
|
|
949
|
+
return fig
|
|
950
|
+
|
|
951
|
+
@staticmethod
|
|
952
|
+
def _apply_spectra_axis_defaults(ax, omega, abscissa, xlim_in, ylim_in):
|
|
953
|
+
"""Apply default spectra x-range and y-min, without stomping overrides.
|
|
954
|
+
|
|
955
|
+
Defaults are applied only if the incoming axis limits were Matplotlib's
|
|
956
|
+
defaults (0, 1), i.e. the caller did not pre-set them.
|
|
957
|
+
"""
|
|
958
|
+
if abscissa not in ("forward", "reversed"):
|
|
959
|
+
raise ValueError("abscissa must be either 'forward' or 'reversed'.")
|
|
960
|
+
|
|
961
|
+
omega_max = float(np.max(np.abs(omega)))
|
|
962
|
+
|
|
963
|
+
# --- X defaults (only if user did not pre-set xlim)
|
|
964
|
+
x0, x1 = xlim_in
|
|
965
|
+
x_is_default = np.isclose(x0, 0.0) and np.isclose(x1, 1.0)
|
|
966
|
+
if x_is_default:
|
|
967
|
+
if abscissa == "forward":
|
|
968
|
+
ax.set_xlim(0.0, omega_max)
|
|
969
|
+
else:
|
|
970
|
+
ax.set_xlim(-omega_max, 0.0)
|
|
971
|
+
|
|
972
|
+
# --- Y default: set only the bottom to 0 (only if user did not pre-set)
|
|
973
|
+
y0, y1 = ylim_in
|
|
974
|
+
y_is_default = np.isclose(y0, 0.0) and np.isclose(y1, 1.0)
|
|
975
|
+
if y_is_default:
|
|
976
|
+
ax.set_ylim(bottom=0.0)
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
@add_fig_kwargs
|
|
980
|
+
def extract_a2f(self, *, omega_min, omega_max, omega_num, omega_I, omega_M,
|
|
981
|
+
mem=None, ax=None, **mem_kwargs):
|
|
982
|
+
r"""
|
|
983
|
+
Extract Eliashberg function α²F(ω) from the self-energy. While working
|
|
984
|
+
with band maps and MDCs is more intuitive in eV, the self-energy
|
|
985
|
+
extraction is performed in eV.
|
|
986
|
+
|
|
987
|
+
Parameters
|
|
988
|
+
----------
|
|
989
|
+
ax : Matplotlib-Axes or None
|
|
990
|
+
Axis to plot on. Created if not provided by the user. (Not used yet;
|
|
991
|
+
reserved for future plotting.)
|
|
992
|
+
|
|
993
|
+
Returns
|
|
994
|
+
-------
|
|
995
|
+
spectrum : ndarray
|
|
996
|
+
Extracted α²F(ω).
|
|
997
|
+
model : ndarray
|
|
998
|
+
MEM model spectrum.
|
|
999
|
+
omega_range : ndarray
|
|
1000
|
+
ω grid used for the extraction.
|
|
1001
|
+
alpha_select : float
|
|
1002
|
+
Selected alpha returned by the chi2kink procedure.
|
|
1003
|
+
"""
|
|
1004
|
+
from . import settings_parameters as xprs
|
|
1005
|
+
|
|
1006
|
+
# Reserve the plot API now; not used yet, but this matches xARPES style.
|
|
1007
|
+
ax, fig, plt = get_ax_fig_plt(ax=ax)
|
|
1008
|
+
|
|
1009
|
+
mem_cfg = self._merge_defaults(xprs.mem_defaults, mem, mem_kwargs)
|
|
1010
|
+
|
|
1011
|
+
method = mem_cfg["method"]
|
|
1012
|
+
parts = mem_cfg["parts"]
|
|
1013
|
+
iter_max = int(mem_cfg["iter_max"])
|
|
1014
|
+
alpha_min = float(mem_cfg["alpha_min"])
|
|
1015
|
+
alpha_max = float(mem_cfg["alpha_max"])
|
|
1016
|
+
alpha_num = int(mem_cfg["alpha_num"])
|
|
1017
|
+
ecut_left = float(mem_cfg["ecut_left"])
|
|
1018
|
+
ecut_right = mem_cfg["ecut_right"]
|
|
1019
|
+
omega_S = float(mem_cfg["omega_S"])
|
|
1020
|
+
f_chi_squared = mem_cfg["f_chi_squared"]
|
|
1021
|
+
sigma_svd = float(mem_cfg["sigma_svd"])
|
|
1022
|
+
t_criterion = float(mem_cfg["t_criterion"])
|
|
1023
|
+
mu = float(mem_cfg["mu"])
|
|
1024
|
+
a_guess = float(mem_cfg["a_guess"])
|
|
1025
|
+
b_guess = float(mem_cfg["b_guess"])
|
|
1026
|
+
c_guess = float(mem_cfg["c_guess"])
|
|
1027
|
+
d_guess = float(mem_cfg["d_guess"])
|
|
1028
|
+
power = int(mem_cfg["power"])
|
|
1029
|
+
lambda_el = float(mem_cfg["lambda_el"])
|
|
1030
|
+
impurity_magnitude = float(mem_cfg["impurity_magnitude"])
|
|
1031
|
+
W = mem_cfg.get("W", None)
|
|
1032
|
+
|
|
1033
|
+
if omega_S < 0.0:
|
|
1034
|
+
raise ValueError("omega_S must be >= 0.")
|
|
1035
|
+
if f_chi_squared is None:
|
|
1036
|
+
f_chi_squared = 2.5 if parts == "both" else 2.0
|
|
1037
|
+
else:
|
|
1038
|
+
f_chi_squared = float(f_chi_squared)
|
|
1039
|
+
if d_guess <= 0.0:
|
|
1040
|
+
raise ValueError(
|
|
1041
|
+
"chi2kink requires d_guess > 0 to fix the logistic sign "
|
|
1042
|
+
"ambiguity."
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
h_n = mem_cfg.get("h_n", None)
|
|
1046
|
+
if h_n is None:
|
|
1047
|
+
raise ValueError(
|
|
1048
|
+
"`optimisation_parameters` must include 'h_n' for cost evaluation."
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
from . import (create_model_function, create_kernel_function,
|
|
1052
|
+
singular_value_decomposition, MEM_core)
|
|
1053
|
+
|
|
1054
|
+
omega_range = np.linspace(omega_min, omega_max, omega_num)
|
|
1055
|
+
model = create_model_function(omega_range, omega_I, omega_M, omega_S, h_n)
|
|
1056
|
+
|
|
1057
|
+
delta_omega = (omega_max - omega_min) / (omega_num - 1)
|
|
1058
|
+
model_in = model * delta_omega
|
|
1059
|
+
|
|
1060
|
+
energies_eV = self.enel_range
|
|
1061
|
+
|
|
1062
|
+
ecut_left_eV = ecut_left / KILO
|
|
1063
|
+
if ecut_right is None:
|
|
1064
|
+
ecut_right_eV = self.energy_resolution
|
|
1065
|
+
else:
|
|
1066
|
+
ecut_right_eV = float(ecut_right) / KILO
|
|
1067
|
+
|
|
1068
|
+
Emin = np.min(energies_eV)
|
|
1069
|
+
Elow = Emin + ecut_left_eV
|
|
1070
|
+
Ehigh = -ecut_right_eV
|
|
1071
|
+
mE = (energies_eV >= Elow) & (energies_eV <= Ehigh)
|
|
1072
|
+
|
|
1073
|
+
if not np.any(mE):
|
|
1074
|
+
raise ValueError(
|
|
1075
|
+
"Energy cutoffs removed all points; adjust ecut_left/right."
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
energies_eV_masked = energies_eV[mE]
|
|
1079
|
+
energies = energies_eV_masked * KILO
|
|
1080
|
+
k_BT = K_B * self.temperature * KILO
|
|
1081
|
+
|
|
1082
|
+
kernel = create_kernel_function(energies, omega_range, k_BT)
|
|
1083
|
+
|
|
1084
|
+
if lambda_el:
|
|
1085
|
+
if W is None:
|
|
1086
|
+
if self._class == "SpectralQuadratic":
|
|
1087
|
+
W = (PREF * self._fermi_wavevector**2 / self._bare_mass) * KILO
|
|
1088
|
+
else:
|
|
1089
|
+
raise ValueError(
|
|
1090
|
+
"lambda_el was provided, but W is None. For a linearised "
|
|
1091
|
+
"band (SpectralLinear), you must also provide W in meV: "
|
|
1092
|
+
"the electron–electron interaction scale."
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
energies_el = energies_eV_masked * KILO
|
|
1096
|
+
real_el, imag_el = self._el_el_self_energy(
|
|
1097
|
+
energies_el, k_BT, lambda_el, W, power
|
|
1098
|
+
)
|
|
1099
|
+
else:
|
|
1100
|
+
real_el = 0.0
|
|
1101
|
+
imag_el = 0.0
|
|
1102
|
+
|
|
1103
|
+
if parts == "both":
|
|
1104
|
+
real = self.real[mE] * KILO - real_el
|
|
1105
|
+
real_sigma = self.real_sigma[mE] * KILO
|
|
1106
|
+
imag = self.imag[mE] * KILO - impurity_magnitude - imag_el
|
|
1107
|
+
imag_sigma = self.imag_sigma[mE] * KILO
|
|
1108
|
+
dvec = np.concatenate((real, imag))
|
|
1109
|
+
wvec = np.concatenate((real_sigma**(-2), imag_sigma**(-2)))
|
|
1110
|
+
H = np.concatenate((np.real(kernel), -np.imag(kernel)))
|
|
1111
|
+
elif parts == "real":
|
|
1112
|
+
real = self.real[mE] * KILO - real_el
|
|
1113
|
+
real_sigma = self.real_sigma[mE] * KILO
|
|
1114
|
+
dvec = real
|
|
1115
|
+
wvec = real_sigma**(-2)
|
|
1116
|
+
H = np.real(kernel)
|
|
1117
|
+
else: # parts == "imag"
|
|
1118
|
+
imag = self.imag[mE] * KILO - impurity_magnitude - imag_el
|
|
1119
|
+
imag_sigma = self.imag_sigma[mE] * KILO
|
|
1120
|
+
dvec = imag
|
|
1121
|
+
wvec = imag_sigma**(-2)
|
|
1122
|
+
H = -np.imag(kernel)
|
|
1123
|
+
|
|
1124
|
+
V_Sigma, U, uvec = singular_value_decomposition(H, sigma_svd)
|
|
1125
|
+
|
|
1126
|
+
if method == "chi2kink":
|
|
1127
|
+
(spectrum_in, alpha_select, fit_curve, guess_curve,
|
|
1128
|
+
chi2kink_result) = self._chi2kink_a2f(
|
|
1129
|
+
dvec, model_in, uvec, mu, wvec, V_Sigma, U, alpha_min,
|
|
1130
|
+
alpha_max, alpha_num, a_guess, b_guess, c_guess, d_guess,
|
|
1131
|
+
f_chi_squared, t_criterion, iter_max, MEM_core
|
|
1132
|
+
)
|
|
1133
|
+
else:
|
|
1134
|
+
raise NotImplementedError(
|
|
1135
|
+
f"extract_a2f does not support method='{method}'."
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
# --- Plot on ax: always raw chi2 + guess; add fit only if success ---
|
|
1139
|
+
alpha_range = chi2kink_result["alpha_range"]
|
|
1140
|
+
alpha0 = float(alpha_range[0])
|
|
1141
|
+
x_plot = np.log10(alpha_range / alpha0)
|
|
1142
|
+
y_chi2 = chi2kink_result["log_chi_squared"]
|
|
1143
|
+
|
|
1144
|
+
ax.set_xlabel(r"log$_{10}(\alpha)$ (-)")
|
|
1145
|
+
ax.set_ylabel(r"log$_{10}(\chi^2)$ (-)")
|
|
1146
|
+
|
|
1147
|
+
ax.plot(x_plot, y_chi2, label="data")
|
|
1148
|
+
ax.plot(x_plot, guess_curve, label="guess")
|
|
1149
|
+
|
|
1150
|
+
if chi2kink_result["success"]:
|
|
1151
|
+
ax.plot(x_plot, fit_curve, label="fit")
|
|
1152
|
+
ax.axvline(
|
|
1153
|
+
np.log10(alpha_select / alpha0),
|
|
1154
|
+
linestyle="--",
|
|
1155
|
+
label=r"$\alpha_{\rm sel}$",
|
|
1156
|
+
)
|
|
1157
|
+
ax.legend()
|
|
1158
|
+
|
|
1159
|
+
# Abort extraction if fit failed (you asked to terminate in that case)
|
|
1160
|
+
if not chi2kink_result["success"]:
|
|
1161
|
+
raise RuntimeError(
|
|
1162
|
+
"chi2kink logistic fit failed; aborting extract_a2f after "
|
|
1163
|
+
"plotting chi2 and guess."
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
# From here on, we know spectrum_in and alpha_select exist
|
|
1167
|
+
spectrum = spectrum_in * omega_num / omega_max
|
|
1168
|
+
|
|
1169
|
+
self._a2f_spectrum = spectrum
|
|
1170
|
+
self._a2f_model = model
|
|
1171
|
+
self._a2f_omega_range = omega_range
|
|
1172
|
+
self._a2f_alpha_select = alpha_select
|
|
1173
|
+
self._a2f_cost = None
|
|
1174
|
+
|
|
1175
|
+
return fig, spectrum, model, omega_range, alpha_select
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
def bayesian_loop(self, *, omega_min, omega_max, omega_num, omega_I,
|
|
1179
|
+
omega_M, fermi_velocity=None,
|
|
1180
|
+
fermi_wavevector=None, bare_mass=None, vary=(),
|
|
1181
|
+
opt_method="Nelder-Mead", opt_options=None,
|
|
1182
|
+
mem=None, loop=None, **mem_kwargs):
|
|
1183
|
+
r"""
|
|
1184
|
+
Bayesian outer loop calling `_cost_function()`.
|
|
1185
|
+
|
|
1186
|
+
If `vary` is non-empty, runs a SciPy optimization over the selected
|
|
1187
|
+
parameters in `vary`.
|
|
1188
|
+
|
|
1189
|
+
Supported entries in `vary` depend on `self._class`:
|
|
1190
|
+
|
|
1191
|
+
- Common: "fermi_wavevector", "impurity_magnitude", "lambda_el", "h_n"
|
|
1192
|
+
- SpectralLinear: additionally "fermi_velocity"
|
|
1193
|
+
- SpectralQuadratic: additionally "bare_mass"
|
|
1194
|
+
|
|
1195
|
+
Notes
|
|
1196
|
+
-----
|
|
1197
|
+
**Convergence behaviour**
|
|
1198
|
+
|
|
1199
|
+
By default, convergence is controlled by a *custom patience criterion*:
|
|
1200
|
+
the optimization terminates when the absolute difference between the
|
|
1201
|
+
current cost and the best cost seen so far is smaller than `tole` for
|
|
1202
|
+
`converge_iters` consecutive iterations.
|
|
1203
|
+
|
|
1204
|
+
To instead rely on SciPy's native convergence criteria (e.g. Nelder–Mead
|
|
1205
|
+
`xatol` / `fatol`), disable the custom criterion by setting
|
|
1206
|
+
`converge_iters=0` or `tole=None`. In that case, SciPy termination options
|
|
1207
|
+
supplied via `opt_options` are used.
|
|
1208
|
+
|
|
1209
|
+
Parameters
|
|
1210
|
+
----------
|
|
1211
|
+
opt_options : dict, optional
|
|
1212
|
+
Options passed directly to `scipy.optimize.minimize`. These are only
|
|
1213
|
+
used for convergence if the custom criterion is disabled (see Notes).
|
|
1214
|
+
"""
|
|
1215
|
+
|
|
1216
|
+
fermi_velocity, fermi_wavevector, bare_mass = self._prepare_bare(
|
|
1217
|
+
fermi_velocity, fermi_wavevector, bare_mass)
|
|
1218
|
+
|
|
1219
|
+
vary = tuple(vary) if vary is not None else ()
|
|
1220
|
+
|
|
1221
|
+
allowed = {"fermi_wavevector", "impurity_magnitude", "lambda_el", "h_n"}
|
|
1222
|
+
|
|
1223
|
+
if self._class == "SpectralLinear":
|
|
1224
|
+
allowed.add("fermi_velocity")
|
|
1225
|
+
elif self._class == "SpectralQuadratic":
|
|
1226
|
+
allowed.add("bare_mass")
|
|
1227
|
+
else:
|
|
1228
|
+
raise NotImplementedError(
|
|
1229
|
+
f"bayesian_loop does not support spectral class '{self._class}'."
|
|
1230
|
+
)
|
|
1231
|
+
|
|
1232
|
+
unknown = set(vary).difference(allowed)
|
|
1233
|
+
if unknown:
|
|
1234
|
+
raise ValueError(
|
|
1235
|
+
f"Unsupported entries in vary: {sorted(unknown)}. "
|
|
1236
|
+
f"Allowed: {sorted(allowed)}."
|
|
1237
|
+
)
|
|
1238
|
+
|
|
1239
|
+
omega_num = int(omega_num)
|
|
1240
|
+
if omega_num < 2:
|
|
1241
|
+
raise ValueError("omega_num must be an integer >= 2.")
|
|
1242
|
+
|
|
1243
|
+
from . import settings_parameters as xprs
|
|
1244
|
+
|
|
1245
|
+
mem_cfg = self._merge_defaults(xprs.mem_defaults, mem, mem_kwargs)
|
|
1246
|
+
|
|
1247
|
+
parts = mem_cfg["parts"]
|
|
1248
|
+
sigma_svd = float(mem_cfg["sigma_svd"])
|
|
1249
|
+
ecut_left = float(mem_cfg["ecut_left"])
|
|
1250
|
+
ecut_right = mem_cfg["ecut_right"]
|
|
1251
|
+
omega_S = float(mem_cfg["omega_S"])
|
|
1252
|
+
imp0 = float(mem_cfg["impurity_magnitude"])
|
|
1253
|
+
lae0 = float(mem_cfg["lambda_el"])
|
|
1254
|
+
h_n0 = float(mem_cfg["h_n"])
|
|
1255
|
+
h_n_min = float(mem_cfg.get("h_n_min", 1e-8))
|
|
1256
|
+
|
|
1257
|
+
loop_overrides = {
|
|
1258
|
+
key: val for key, val in mem_kwargs.items()
|
|
1259
|
+
if (val is not None) and (key in xprs.loop_defaults)
|
|
1260
|
+
}
|
|
1261
|
+
loop_cfg = self._merge_defaults(xprs.loop_defaults, loop, loop_overrides)
|
|
1262
|
+
|
|
1263
|
+
tole = float(loop_cfg["tole"])
|
|
1264
|
+
converge_iters = int(loop_cfg["converge_iters"])
|
|
1265
|
+
opt_iter_max = int(loop_cfg["opt_iter_max"])
|
|
1266
|
+
scale_vF = float(loop_cfg["scale_vF"])
|
|
1267
|
+
scale_mb = float(loop_cfg["scale_mb"])
|
|
1268
|
+
scale_imp = float(loop_cfg["scale_imp"])
|
|
1269
|
+
scale_kF = float(loop_cfg["scale_kF"])
|
|
1270
|
+
scale_lambda_el = float(loop_cfg["scale_lambda_el"])
|
|
1271
|
+
scale_hn = float(loop_cfg["scale_hn"])
|
|
1272
|
+
|
|
1273
|
+
rollback_steps = int(loop_cfg.get("rollback_steps"))
|
|
1274
|
+
max_retries = int(loop_cfg.get("max_retries"))
|
|
1275
|
+
relative_best = float(loop_cfg.get("relative_best"))
|
|
1276
|
+
min_steps_for_regression = int(loop_cfg.get("min_steps_for_regression"))
|
|
1277
|
+
|
|
1278
|
+
if rollback_steps < 0:
|
|
1279
|
+
raise ValueError("rollback_steps must be >= 0.")
|
|
1280
|
+
if max_retries < 0:
|
|
1281
|
+
raise ValueError("max_retries must be >= 0.")
|
|
1282
|
+
if relative_best <= 0.0:
|
|
1283
|
+
raise ValueError("relative_best must be > 0.")
|
|
1284
|
+
if min_steps_for_regression < 0:
|
|
1285
|
+
raise ValueError("min_steps_for_regression must be >= 0.")
|
|
1286
|
+
|
|
1287
|
+
vF0 = float(fermi_velocity) if fermi_velocity is not None else None
|
|
1288
|
+
kF0 = float(fermi_wavevector) if fermi_wavevector is not None else None
|
|
1289
|
+
mb0 = float(bare_mass) if bare_mass is not None else None
|
|
1290
|
+
|
|
1291
|
+
if lae0 < 0.0:
|
|
1292
|
+
raise ValueError("Initial lambda_el must be >= 0.")
|
|
1293
|
+
if imp0 < 0.0:
|
|
1294
|
+
raise ValueError("Initial impurity_magnitude must be >= 0.")
|
|
1295
|
+
if omega_S < 0.0:
|
|
1296
|
+
raise ValueError("omega_S must be >= 0.")
|
|
1297
|
+
if h_n_min <= 0.0:
|
|
1298
|
+
raise ValueError("h_n_min must be > 0.")
|
|
1299
|
+
if h_n0 < h_n_min:
|
|
1300
|
+
raise ValueError(
|
|
1301
|
+
f"Initial h_n ({h_n0:g}) must be >= h_n_min ({h_n_min:g})."
|
|
1302
|
+
)
|
|
1303
|
+
if kF0 is None:
|
|
1304
|
+
raise ValueError(
|
|
1305
|
+
"bayesian_loop requires an initial fermi_wavevector."
|
|
1306
|
+
)
|
|
1307
|
+
if self._class == "SpectralLinear" and vF0 is None:
|
|
1308
|
+
raise ValueError(
|
|
1309
|
+
"bayesian_loop requires an initial fermi_velocity."
|
|
1310
|
+
)
|
|
1311
|
+
if self._class == "SpectralQuadratic" and mb0 is None:
|
|
1312
|
+
raise ValueError("bayesian_loop requires an initial bare_mass.")
|
|
1313
|
+
|
|
1314
|
+
from scipy.optimize import minimize
|
|
1315
|
+
from . import create_kernel_function, singular_value_decomposition
|
|
1316
|
+
|
|
1317
|
+
ecut_left = float(mem_cfg["ecut_left"])
|
|
1318
|
+
ecut_right = mem_cfg["ecut_right"]
|
|
1319
|
+
|
|
1320
|
+
ecut_left_eV = ecut_left / KILO
|
|
1321
|
+
if ecut_right is None:
|
|
1322
|
+
ecut_right_eV = self.energy_resolution
|
|
1323
|
+
else:
|
|
1324
|
+
ecut_right_eV = float(ecut_right) / KILO
|
|
1325
|
+
|
|
1326
|
+
energies_eV = self.enel_range
|
|
1327
|
+
Emin = np.min(energies_eV)
|
|
1328
|
+
Elow = Emin + ecut_left_eV
|
|
1329
|
+
Ehigh = -ecut_right_eV
|
|
1330
|
+
mE = (energies_eV >= Elow) & (energies_eV <= Ehigh)
|
|
1331
|
+
|
|
1332
|
+
if not np.any(mE):
|
|
1333
|
+
raise ValueError(
|
|
1334
|
+
"Energy cutoffs removed all points; adjust ecut_left/right."
|
|
1335
|
+
)
|
|
1336
|
+
|
|
1337
|
+
energies_eV_masked = energies_eV[mE]
|
|
1338
|
+
energies = energies_eV_masked * KILO
|
|
1339
|
+
|
|
1340
|
+
k_BT = K_B * self.temperature * KILO
|
|
1341
|
+
omega_range = np.linspace(omega_min, omega_max, omega_num)
|
|
1342
|
+
|
|
1343
|
+
kernel_raw = create_kernel_function(energies, omega_range, k_BT)
|
|
1344
|
+
|
|
1345
|
+
if parts == "both":
|
|
1346
|
+
kernel_used = np.concatenate((np.real(kernel_raw), -np.imag(kernel_raw)))
|
|
1347
|
+
elif parts == "real":
|
|
1348
|
+
kernel_used = np.real(kernel_raw)
|
|
1349
|
+
else: # parts == "imag"
|
|
1350
|
+
kernel_used = -np.imag(kernel_raw)
|
|
1351
|
+
|
|
1352
|
+
V_Sigma, U, uvec0 = singular_value_decomposition(kernel_used, sigma_svd)
|
|
1353
|
+
|
|
1354
|
+
_precomp = {
|
|
1355
|
+
"omega_range": omega_range,
|
|
1356
|
+
"mE": mE,
|
|
1357
|
+
"energies_eV_masked": energies_eV_masked,
|
|
1358
|
+
"V_Sigma": V_Sigma,
|
|
1359
|
+
"U": U,
|
|
1360
|
+
"uvec0": uvec0,
|
|
1361
|
+
"ecut_left": ecut_left,
|
|
1362
|
+
"ecut_right": ecut_right,
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
def _reflect_min(xi, p0, p_min, scale):
|
|
1366
|
+
"""Map R -> [p_min, +inf) using linear reflection around p_min."""
|
|
1367
|
+
return p_min + np.abs((float(p0) - p_min) + scale * float(xi))
|
|
1368
|
+
|
|
1369
|
+
def _unpack_params(x):
|
|
1370
|
+
params = {}
|
|
1371
|
+
|
|
1372
|
+
i = 0
|
|
1373
|
+
for name in vary:
|
|
1374
|
+
xi = float(x[i])
|
|
1375
|
+
|
|
1376
|
+
if name == "fermi_velocity":
|
|
1377
|
+
if vF0 is None:
|
|
1378
|
+
raise ValueError("Cannot vary fermi_velocity: no "
|
|
1379
|
+
"initial vF provided.")
|
|
1380
|
+
params["fermi_velocity"] = vF0 + scale_vF * xi
|
|
1381
|
+
|
|
1382
|
+
elif name == "bare_mass":
|
|
1383
|
+
if mb0 is None:
|
|
1384
|
+
raise ValueError("Cannot vary bare_mass: no initial "
|
|
1385
|
+
"bare_mass provided.")
|
|
1386
|
+
params["bare_mass"] = mb0 + scale_mb * xi
|
|
1387
|
+
|
|
1388
|
+
elif name == "fermi_wavevector":
|
|
1389
|
+
if kF0 is None:
|
|
1390
|
+
raise ValueError(
|
|
1391
|
+
"Cannot vary fermi_wavevector: no initial kF "
|
|
1392
|
+
"provided."
|
|
1393
|
+
)
|
|
1394
|
+
params["fermi_wavevector"] = kF0 + scale_kF * xi
|
|
1395
|
+
|
|
1396
|
+
elif name == "impurity_magnitude":
|
|
1397
|
+
params["impurity_magnitude"] = _reflect_min(xi, imp0, 0.0, scale_imp)
|
|
1398
|
+
|
|
1399
|
+
elif name == "lambda_el":
|
|
1400
|
+
params["lambda_el"] = _reflect_min(xi, lae0, 0.0, scale_lambda_el)
|
|
1401
|
+
|
|
1402
|
+
elif name == "h_n":
|
|
1403
|
+
params["h_n"] = _reflect_min(xi, h_n0, h_n_min, scale_hn)
|
|
1404
|
+
|
|
1405
|
+
i += 1
|
|
1406
|
+
|
|
1407
|
+
params.setdefault("fermi_wavevector", kF0)
|
|
1408
|
+
params.setdefault("impurity_magnitude", imp0)
|
|
1409
|
+
params.setdefault("lambda_el", lae0)
|
|
1410
|
+
params.setdefault("h_n", h_n0)
|
|
1411
|
+
|
|
1412
|
+
if self._class == "SpectralLinear":
|
|
1413
|
+
params.setdefault("fermi_velocity", vF0)
|
|
1414
|
+
elif self._class == "SpectralQuadratic":
|
|
1415
|
+
params.setdefault("bare_mass", mb0)
|
|
1416
|
+
|
|
1417
|
+
return params
|
|
1418
|
+
|
|
1419
|
+
def _evaluate_cost(params):
|
|
1420
|
+
optimisation_parameters = {
|
|
1421
|
+
"h_n": params["h_n"],
|
|
1422
|
+
"impurity_magnitude": params["impurity_magnitude"],
|
|
1423
|
+
"lambda_el": params["lambda_el"],
|
|
1424
|
+
"fermi_wavevector": params["fermi_wavevector"],
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
if self._class == "SpectralLinear":
|
|
1428
|
+
optimisation_parameters["fermi_velocity"] = params["fermi_velocity"]
|
|
1429
|
+
elif self._class == "SpectralQuadratic":
|
|
1430
|
+
optimisation_parameters["bare_mass"] = params["bare_mass"]
|
|
1431
|
+
else:
|
|
1432
|
+
raise NotImplementedError(
|
|
1433
|
+
f"_evaluate_cost does not support class '{self._class}'."
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
return self._cost_function(
|
|
1437
|
+
optimisation_parameters=optimisation_parameters,
|
|
1438
|
+
omega_min=omega_min, omega_max=omega_max, omega_num=omega_num,
|
|
1439
|
+
omega_I=omega_I, omega_M=omega_M, mem_cfg=mem_cfg,
|
|
1440
|
+
_precomp=_precomp
|
|
1441
|
+
)
|
|
1442
|
+
|
|
1443
|
+
last = {"cost": None, "spectrum": None, "model": None, "alpha": None}
|
|
1444
|
+
|
|
1445
|
+
iter_counter = {"n": 0}
|
|
1446
|
+
|
|
1447
|
+
class ConvergenceException(RuntimeError):
|
|
1448
|
+
"""Raised when optimisation has converged successfully."""
|
|
1449
|
+
|
|
1450
|
+
class RegressionException(RuntimeError):
|
|
1451
|
+
"""Raised when optimizer regresses toward the initial guess."""
|
|
1452
|
+
|
|
1453
|
+
if converge_iters is None:
|
|
1454
|
+
converge_iters = 0
|
|
1455
|
+
converge_iters = int(converge_iters)
|
|
1456
|
+
|
|
1457
|
+
if tole is not None:
|
|
1458
|
+
tole = float(tole)
|
|
1459
|
+
if tole < 0.0:
|
|
1460
|
+
raise ValueError("tole must be >= 0.")
|
|
1461
|
+
if converge_iters < 0:
|
|
1462
|
+
raise ValueError("converge_iters must be >= 0.")
|
|
1463
|
+
|
|
1464
|
+
# Track best solution seen across all obj calls (not just last).
|
|
1465
|
+
best_global = {
|
|
1466
|
+
"x": None,
|
|
1467
|
+
"params": None,
|
|
1468
|
+
"cost": np.inf,
|
|
1469
|
+
"spectrum": None,
|
|
1470
|
+
"model": None,
|
|
1471
|
+
"alpha": None,
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
history = []
|
|
1475
|
+
|
|
1476
|
+
# Cache most recent evaluation so the callback can read a cost without
|
|
1477
|
+
# forcing an extra objective evaluation.
|
|
1478
|
+
last_x = {"x": None}
|
|
1479
|
+
last_cost = {"cost": None}
|
|
1480
|
+
initial_cost = {"cost": None}
|
|
1481
|
+
|
|
1482
|
+
iter_counter = {"n": 0}
|
|
1483
|
+
|
|
1484
|
+
def _clean_params(params):
|
|
1485
|
+
"""Convert NumPy scalar values to plain Python scalars."""
|
|
1486
|
+
out = {}
|
|
1487
|
+
for key, val in params.items():
|
|
1488
|
+
if isinstance(val, np.generic):
|
|
1489
|
+
out[key] = float(val)
|
|
1490
|
+
else:
|
|
1491
|
+
out[key] = val
|
|
1492
|
+
return out
|
|
1493
|
+
|
|
1494
|
+
def obj(x):
|
|
1495
|
+
import warnings
|
|
1496
|
+
|
|
1497
|
+
iter_counter["n"] += 1
|
|
1498
|
+
|
|
1499
|
+
params = _unpack_params(x)
|
|
1500
|
+
|
|
1501
|
+
with warnings.catch_warnings():
|
|
1502
|
+
warnings.simplefilter("error", RuntimeWarning)
|
|
1503
|
+
try:
|
|
1504
|
+
cost, spectrum, model, alpha_select = _evaluate_cost(params)
|
|
1505
|
+
except RuntimeWarning as exc:
|
|
1506
|
+
raise ValueError(f"RuntimeWarning during cost eval: {exc}") from exc
|
|
1507
|
+
cost_f = float(cost)
|
|
1508
|
+
|
|
1509
|
+
history.append(
|
|
1510
|
+
{
|
|
1511
|
+
"x": np.array(x, dtype=float, copy=True),
|
|
1512
|
+
"params": _clean_params(params),
|
|
1513
|
+
"cost": cost_f,
|
|
1514
|
+
"spectrum": spectrum,
|
|
1515
|
+
"model": model,
|
|
1516
|
+
"alpha": float(alpha_select),
|
|
1517
|
+
}
|
|
1518
|
+
)
|
|
1519
|
+
|
|
1520
|
+
last["cost"] = cost_f
|
|
1521
|
+
last["spectrum"] = spectrum
|
|
1522
|
+
last["model"] = model
|
|
1523
|
+
last["alpha"] = float(alpha_select)
|
|
1524
|
+
|
|
1525
|
+
last_x["x"] = np.array(x, dtype=float, copy=True)
|
|
1526
|
+
last_cost["cost"] = cost_f
|
|
1527
|
+
|
|
1528
|
+
if initial_cost["cost"] is None:
|
|
1529
|
+
initial_cost["cost"] = cost_f
|
|
1530
|
+
|
|
1531
|
+
if cost_f < best_global["cost"]:
|
|
1532
|
+
best_global["x"] = np.array(x, dtype=float, copy=True)
|
|
1533
|
+
best_global["cost"] = cost_f
|
|
1534
|
+
best_global["params"] = _clean_params(params)
|
|
1535
|
+
best_global["spectrum"] = spectrum
|
|
1536
|
+
best_global["model"] = model
|
|
1537
|
+
best_global["alpha"] = float(alpha_select)
|
|
1538
|
+
|
|
1539
|
+
msg = [f"Iter {iter_counter['n']:4d} | cost = {cost: .4e}"]
|
|
1540
|
+
for key in sorted(params):
|
|
1541
|
+
msg.append(f"{key}={params[key]:.8g}")
|
|
1542
|
+
print(" | ".join(msg))
|
|
1543
|
+
|
|
1544
|
+
return cost_f
|
|
1545
|
+
|
|
1546
|
+
class TerminationCallback:
|
|
1547
|
+
def __init__(self, tole, converge_iters,
|
|
1548
|
+
min_steps_for_regression):
|
|
1549
|
+
self.tole = None if tole is None else float(tole)
|
|
1550
|
+
self.converge_iters = int(converge_iters)
|
|
1551
|
+
self.min_steps_for_regression = int(
|
|
1552
|
+
min_steps_for_regression
|
|
1553
|
+
)
|
|
1554
|
+
self.iter_count = 0
|
|
1555
|
+
self.call_count = 0
|
|
1556
|
+
|
|
1557
|
+
def __call__(self, xk):
|
|
1558
|
+
self.call_count += 1
|
|
1559
|
+
|
|
1560
|
+
if self.tole is None or self.converge_iters <= 0:
|
|
1561
|
+
return
|
|
1562
|
+
|
|
1563
|
+
current = last_cost["cost"]
|
|
1564
|
+
if current is None:
|
|
1565
|
+
return
|
|
1566
|
+
|
|
1567
|
+
best_cost = float(best_global["cost"])
|
|
1568
|
+
if np.isfinite(best_cost):
|
|
1569
|
+
if abs(current - best_cost) < self.tole:
|
|
1570
|
+
self.iter_count += 1
|
|
1571
|
+
else:
|
|
1572
|
+
self.iter_count = 0
|
|
1573
|
+
|
|
1574
|
+
if self.iter_count >= self.converge_iters:
|
|
1575
|
+
raise ConvergenceException(
|
|
1576
|
+
"Converged: |cost-best| < "
|
|
1577
|
+
f"{self.tole:g} for "
|
|
1578
|
+
f"{self.converge_iters} iterations."
|
|
1579
|
+
)
|
|
1580
|
+
|
|
1581
|
+
if self.call_count < self.min_steps_for_regression:
|
|
1582
|
+
return
|
|
1583
|
+
|
|
1584
|
+
init_cost = initial_cost["cost"]
|
|
1585
|
+
if init_cost is None:
|
|
1586
|
+
return
|
|
1587
|
+
|
|
1588
|
+
current = float(current)
|
|
1589
|
+
init_cost = float(init_cost)
|
|
1590
|
+
|
|
1591
|
+
if not np.isfinite(best_cost):
|
|
1592
|
+
return
|
|
1593
|
+
|
|
1594
|
+
if (
|
|
1595
|
+
abs(current - init_cost) * relative_best
|
|
1596
|
+
< abs(current - best_cost)
|
|
1597
|
+
):
|
|
1598
|
+
raise RegressionException(
|
|
1599
|
+
"Regression toward initial guess detected."
|
|
1600
|
+
)
|
|
1601
|
+
|
|
1602
|
+
callback = TerminationCallback(
|
|
1603
|
+
tole=tole,
|
|
1604
|
+
converge_iters=converge_iters,
|
|
1605
|
+
min_steps_for_regression=min_steps_for_regression,
|
|
1606
|
+
)
|
|
1607
|
+
|
|
1608
|
+
if not vary:
|
|
1609
|
+
params = _unpack_params(np.zeros(0, dtype=float))
|
|
1610
|
+
cost, spectrum, model, alpha_select = _evaluate_cost(params)
|
|
1611
|
+
return cost, spectrum, model, alpha_select
|
|
1612
|
+
|
|
1613
|
+
x0 = np.zeros(len(vary), dtype=float)
|
|
1614
|
+
|
|
1615
|
+
options = {} if opt_options is None else dict(opt_options)
|
|
1616
|
+
options.setdefault("maxiter", int(opt_iter_max))
|
|
1617
|
+
|
|
1618
|
+
use_patience = (tole is not None) and (int(converge_iters) > 0)
|
|
1619
|
+
if use_patience:
|
|
1620
|
+
options.pop("xatol", None)
|
|
1621
|
+
options.pop("fatol", None)
|
|
1622
|
+
|
|
1623
|
+
retry_count = 0
|
|
1624
|
+
res = None
|
|
1625
|
+
|
|
1626
|
+
while retry_count <= max_retries:
|
|
1627
|
+
best = {
|
|
1628
|
+
"x": None,
|
|
1629
|
+
"params": None,
|
|
1630
|
+
"cost": np.inf,
|
|
1631
|
+
"spectrum": None,
|
|
1632
|
+
"model": None,
|
|
1633
|
+
"alpha": None,
|
|
1634
|
+
}
|
|
1635
|
+
last_x["x"] = None
|
|
1636
|
+
last_cost["cost"] = None
|
|
1637
|
+
initial_cost["cost"] = None
|
|
1638
|
+
iter_counter["n"] = 0
|
|
1639
|
+
history.clear()
|
|
1640
|
+
|
|
1641
|
+
callback = TerminationCallback(
|
|
1642
|
+
tole=tole,
|
|
1643
|
+
converge_iters=converge_iters,
|
|
1644
|
+
min_steps_for_regression=min_steps_for_regression,
|
|
1645
|
+
)
|
|
1646
|
+
|
|
1647
|
+
try:
|
|
1648
|
+
res = minimize(
|
|
1649
|
+
obj,
|
|
1650
|
+
x0,
|
|
1651
|
+
method=opt_method,
|
|
1652
|
+
options=options,
|
|
1653
|
+
callback=callback,
|
|
1654
|
+
)
|
|
1655
|
+
break
|
|
1656
|
+
|
|
1657
|
+
except ConvergenceException as exc:
|
|
1658
|
+
print(str(exc))
|
|
1659
|
+
res = None
|
|
1660
|
+
break
|
|
1661
|
+
|
|
1662
|
+
except RegressionException as exc:
|
|
1663
|
+
print(f"{exc} Rolling back {rollback_steps} steps.")
|
|
1664
|
+
retry_count += 1
|
|
1665
|
+
|
|
1666
|
+
if rollback_steps <= 0 or not history:
|
|
1667
|
+
continue
|
|
1668
|
+
|
|
1669
|
+
back = min(int(rollback_steps), len(history))
|
|
1670
|
+
x0 = np.array(history[-back]["x"], dtype=float, copy=True)
|
|
1671
|
+
continue
|
|
1672
|
+
|
|
1673
|
+
except ValueError as exc:
|
|
1674
|
+
print(f"ValueError encountered: {exc}. Rolling back.")
|
|
1675
|
+
retry_count += 1
|
|
1676
|
+
|
|
1677
|
+
if rollback_steps <= 0 or not history:
|
|
1678
|
+
continue
|
|
1679
|
+
|
|
1680
|
+
back = min(int(rollback_steps), len(history))
|
|
1681
|
+
x0 = np.array(history[-back]["x"], dtype=float, copy=True)
|
|
1682
|
+
continue
|
|
1683
|
+
|
|
1684
|
+
if retry_count > max_retries:
|
|
1685
|
+
print("Max retries reached. Parameters may not be optimal.")
|
|
1686
|
+
|
|
1687
|
+
if best_global["params"] is None:
|
|
1688
|
+
params = _unpack_params(x0)
|
|
1689
|
+
cost, spectrum, model, alpha_select = _evaluate_cost(params)
|
|
1690
|
+
else:
|
|
1691
|
+
params = best_global["params"]
|
|
1692
|
+
cost = best_global["cost"]
|
|
1693
|
+
spectrum = best_global["spectrum"]
|
|
1694
|
+
model = best_global["model"]
|
|
1695
|
+
alpha_select = best_global["alpha"]
|
|
1696
|
+
|
|
1697
|
+
args = ", ".join(
|
|
1698
|
+
f"{key}={params[key]:.10g}" if isinstance(params[key], float)
|
|
1699
|
+
else f"{key}={params[key]}"
|
|
1700
|
+
for key in sorted(params)
|
|
1701
|
+
)
|
|
1702
|
+
print("Optimised parameters:")
|
|
1703
|
+
print(args)
|
|
1704
|
+
|
|
1705
|
+
# store inside class methods
|
|
1706
|
+
self._a2f_spectrum = spectrum
|
|
1707
|
+
self._a2f_model = model
|
|
1708
|
+
self._a2f_omega_range = omega_range
|
|
1709
|
+
self._a2f_alpha_select = alpha_select
|
|
1710
|
+
self._a2f_cost = cost
|
|
1711
|
+
|
|
1712
|
+
return spectrum, model, omega_range, alpha_select, cost, params
|
|
1713
|
+
|
|
1714
|
+
@staticmethod
|
|
1715
|
+
def _merge_defaults(defaults, override_dict=None, override_kwargs=None):
|
|
1716
|
+
"""Merge defaults with dict + kwargs overrides (kwargs win)."""
|
|
1717
|
+
cfg = dict(defaults)
|
|
1718
|
+
|
|
1719
|
+
if override_dict is not None:
|
|
1720
|
+
cfg.update(dict(override_dict))
|
|
1721
|
+
|
|
1722
|
+
if override_kwargs is not None:
|
|
1723
|
+
cfg.update({k: v for k, v in override_kwargs.items() if v is not None})
|
|
1724
|
+
|
|
1725
|
+
return cfg
|
|
1726
|
+
|
|
1727
|
+
def _prepare_bare(self, fermi_velocity, fermi_wavevector, bare_mass):
|
|
1728
|
+
"""Validate class-compatible band parameters and infer missing defaults.
|
|
1729
|
+
|
|
1730
|
+
Enforces:
|
|
1731
|
+
- SpectralLinear: bare_mass must be None; vF and kF must be available.
|
|
1732
|
+
- SpectralQuadratic: fermi_velocity must be None; bare_mass and kF must
|
|
1733
|
+
be available.
|
|
1734
|
+
|
|
1735
|
+
Returns
|
|
1736
|
+
-------
|
|
1737
|
+
fermi_velocity : float or None
|
|
1738
|
+
Initial vF (Linear) or None (Quadratic).
|
|
1739
|
+
fermi_wavevector : float
|
|
1740
|
+
Initial kF.
|
|
1741
|
+
bare_mass : float or None
|
|
1742
|
+
Initial bare mass (Quadratic) or None (Linear).
|
|
1743
|
+
"""
|
|
1744
|
+
if self._class == "SpectralLinear":
|
|
1745
|
+
if bare_mass is not None:
|
|
1746
|
+
raise ValueError(
|
|
1747
|
+
"SpectralLinear bayesian_loop does not accept "
|
|
1748
|
+
"`bare_mass`. Provide `fermi_velocity` instead."
|
|
1749
|
+
)
|
|
1750
|
+
|
|
1751
|
+
if fermi_velocity is None:
|
|
1752
|
+
fermi_velocity = getattr(self, "fermi_velocity", None)
|
|
1753
|
+
if fermi_velocity is None:
|
|
1754
|
+
raise ValueError(
|
|
1755
|
+
"SpectralLinear optimisation requires an initial "
|
|
1756
|
+
"fermi_velocity to be provided."
|
|
1757
|
+
)
|
|
1758
|
+
|
|
1759
|
+
if fermi_wavevector is None:
|
|
1760
|
+
fermi_wavevector = getattr(self, "fermi_wavevector", None)
|
|
1761
|
+
if fermi_wavevector is None:
|
|
1762
|
+
raise ValueError(
|
|
1763
|
+
"SpectralLinear optimisation requires an initial "
|
|
1764
|
+
"fermi_wavevector to be provided."
|
|
1765
|
+
)
|
|
1766
|
+
|
|
1767
|
+
return float(fermi_velocity), float(fermi_wavevector), None
|
|
1768
|
+
|
|
1769
|
+
elif self._class == "SpectralQuadratic":
|
|
1770
|
+
if fermi_velocity is not None:
|
|
1771
|
+
raise ValueError(
|
|
1772
|
+
"SpectralQuadratic bayesian_loop does not accept "
|
|
1773
|
+
"`fermi_velocity`. Provide `bare_mass` instead."
|
|
1774
|
+
)
|
|
1775
|
+
|
|
1776
|
+
if bare_mass is None:
|
|
1777
|
+
bare_mass = getattr(self, "_bare_mass", None)
|
|
1778
|
+
if bare_mass is None:
|
|
1779
|
+
raise ValueError(
|
|
1780
|
+
"SpectralQuadratic optimisation requires an initial "
|
|
1781
|
+
"bare_mass to be provided."
|
|
1782
|
+
)
|
|
1783
|
+
|
|
1784
|
+
if fermi_wavevector is None:
|
|
1785
|
+
fermi_wavevector = getattr(self, "fermi_wavevector", None)
|
|
1786
|
+
if fermi_wavevector is None:
|
|
1787
|
+
raise ValueError(
|
|
1788
|
+
"SpectralQuadratic optimisation requires an initial "
|
|
1789
|
+
"fermi_wavevector to be provided."
|
|
1790
|
+
)
|
|
1791
|
+
|
|
1792
|
+
return None, float(fermi_wavevector), float(bare_mass)
|
|
1793
|
+
|
|
1794
|
+
else:
|
|
1795
|
+
raise NotImplementedError(
|
|
1796
|
+
f"_prepare_bare is not implemented for spectral class "
|
|
1797
|
+
"'{self._class}'.")
|
|
1798
|
+
|
|
1799
|
+
def _cost_function(self, *, optimisation_parameters, omega_min, omega_max,
|
|
1800
|
+
omega_num, omega_I, omega_M, mem_cfg, _precomp):
|
|
1801
|
+
r"""TBD
|
|
1802
|
+
|
|
1803
|
+
Negative log-posterior cost function for Bayesian optimisation.
|
|
1804
|
+
|
|
1805
|
+
This mirrors `extract_a2f()` but recomputes the self-energy arrays for the
|
|
1806
|
+
candidate optimisation parameters instead of using cached `self.real/imag`.
|
|
1807
|
+
|
|
1808
|
+
Parameters
|
|
1809
|
+
----------
|
|
1810
|
+
optimisation_parameters : dict
|
|
1811
|
+
Must include at least keys: "h_n", "impurity_magnitude", "lambda_el".
|
|
1812
|
+
For SpectralLinear, must also include "fermi_velocity" and
|
|
1813
|
+
"fermi_wavevector". For SpectralQuadratic, "bare_mass" is optional
|
|
1814
|
+
(falls back to `self._bare_mass` if present).
|
|
1815
|
+
|
|
1816
|
+
Returns
|
|
1817
|
+
-------
|
|
1818
|
+
cost : float
|
|
1819
|
+
Negative log-posterior evaluated at the selected alpha.
|
|
1820
|
+
spectrum : ndarray
|
|
1821
|
+
Rescaled α²F(ω) spectrum (same scaling convention as `extract_a2f()`).
|
|
1822
|
+
model : ndarray
|
|
1823
|
+
The model spectrum used by MEM (same as `extract_a2f()`).
|
|
1824
|
+
alpha_select : float
|
|
1825
|
+
The selected alpha returned by `_chi2kink_a2f`.
|
|
1826
|
+
"""
|
|
1827
|
+
|
|
1828
|
+
required = {"h_n", "impurity_magnitude", "lambda_el"}
|
|
1829
|
+
missing = required.difference(optimisation_parameters)
|
|
1830
|
+
if missing:
|
|
1831
|
+
raise ValueError(
|
|
1832
|
+
f"Missing optimisation parameters: {sorted(missing)}"
|
|
1833
|
+
)
|
|
1834
|
+
|
|
1835
|
+
parts = mem_cfg["parts"]
|
|
1836
|
+
method = mem_cfg["method"]
|
|
1837
|
+
alpha_min = float(mem_cfg["alpha_min"])
|
|
1838
|
+
alpha_max = float(mem_cfg["alpha_max"])
|
|
1839
|
+
alpha_num = int(mem_cfg["alpha_num"])
|
|
1840
|
+
omega_S = float(mem_cfg["omega_S"])
|
|
1841
|
+
|
|
1842
|
+
mu = float(mem_cfg["mu"])
|
|
1843
|
+
a_guess = float(mem_cfg["a_guess"])
|
|
1844
|
+
b_guess = float(mem_cfg["b_guess"])
|
|
1845
|
+
c_guess = float(mem_cfg["c_guess"])
|
|
1846
|
+
d_guess = float(mem_cfg["d_guess"])
|
|
1847
|
+
|
|
1848
|
+
f_chi_squared = mem_cfg["f_chi_squared"]
|
|
1849
|
+
power = int(mem_cfg["power"])
|
|
1850
|
+
W = mem_cfg.get("W", None)
|
|
1851
|
+
t_criterion = float(mem_cfg["t_criterion"])
|
|
1852
|
+
iter_max = int(mem_cfg["iter_max"])
|
|
1853
|
+
|
|
1854
|
+
if f_chi_squared is None:
|
|
1855
|
+
f_chi_squared = 2.5 if parts == "both" else 2.0
|
|
1856
|
+
else:
|
|
1857
|
+
f_chi_squared = float(f_chi_squared)
|
|
1858
|
+
|
|
1859
|
+
if d_guess <= 0.0:
|
|
1860
|
+
raise ValueError(
|
|
1861
|
+
"chi2kink requires d_guess > 0 to fix the logistic sign ambiguity."
|
|
1862
|
+
)
|
|
1863
|
+
|
|
1864
|
+
if parts not in {"both", "real", "imag"}:
|
|
1865
|
+
raise ValueError("parts must be one of {'both', 'real', 'imag'}")
|
|
1866
|
+
|
|
1867
|
+
if method != "chi2kink":
|
|
1868
|
+
raise NotImplementedError(
|
|
1869
|
+
"Only method='chi2kink' is currently implemented."
|
|
1870
|
+
)
|
|
1871
|
+
|
|
1872
|
+
impurity_magnitude = float(optimisation_parameters["impurity_magnitude"])
|
|
1873
|
+
lambda_el = float(optimisation_parameters["lambda_el"])
|
|
1874
|
+
h_n = float(optimisation_parameters["h_n"])
|
|
1875
|
+
|
|
1876
|
+
fermi_velocity = None
|
|
1877
|
+
fermi_wavevector = None
|
|
1878
|
+
bare_mass = None
|
|
1879
|
+
|
|
1880
|
+
if self._class == "SpectralLinear":
|
|
1881
|
+
required_lin = {"fermi_velocity", "fermi_wavevector"}
|
|
1882
|
+
missing_lin = required_lin.difference(optimisation_parameters)
|
|
1883
|
+
if missing_lin:
|
|
1884
|
+
raise ValueError(
|
|
1885
|
+
"SpectralLinear requires optimisation_parameters to include "
|
|
1886
|
+
f"{sorted(missing_lin)}."
|
|
1887
|
+
)
|
|
1888
|
+
fermi_velocity = optimisation_parameters["fermi_velocity"]
|
|
1889
|
+
fermi_wavevector = optimisation_parameters["fermi_wavevector"]
|
|
1890
|
+
|
|
1891
|
+
elif self._class == "SpectralQuadratic":
|
|
1892
|
+
if "fermi_wavevector" not in optimisation_parameters:
|
|
1893
|
+
raise ValueError(
|
|
1894
|
+
"SpectralQuadratic requires optimisation_parameters to include "
|
|
1895
|
+
"'fermi_wavevector'."
|
|
1896
|
+
)
|
|
1897
|
+
fermi_wavevector = optimisation_parameters["fermi_wavevector"]
|
|
1898
|
+
|
|
1899
|
+
bare_mass = optimisation_parameters.get("bare_mass", None)
|
|
1900
|
+
if bare_mass is None:
|
|
1901
|
+
bare_mass = getattr(self, "_bare_mass", None)
|
|
1902
|
+
|
|
1903
|
+
else:
|
|
1904
|
+
raise NotImplementedError(
|
|
1905
|
+
f"_cost_function does not support class '{self._class}'."
|
|
1906
|
+
)
|
|
1907
|
+
|
|
1908
|
+
from . import create_model_function, MEM_core
|
|
1909
|
+
|
|
1910
|
+
if f_chi_squared is None:
|
|
1911
|
+
f_chi_squared = 2.5 if parts == "both" else 2.0
|
|
1912
|
+
|
|
1913
|
+
if _precomp is None:
|
|
1914
|
+
raise ValueError(
|
|
1915
|
+
"_precomp is None in _cost_function. Pass the precomputed"
|
|
1916
|
+
" kernel/SVD bundle from bayesian_loop."
|
|
1917
|
+
)
|
|
1918
|
+
|
|
1919
|
+
omega_range = _precomp["omega_range"]
|
|
1920
|
+
mE = _precomp["mE"]
|
|
1921
|
+
energies_eV_masked = _precomp["energies_eV_masked"]
|
|
1922
|
+
|
|
1923
|
+
V_Sigma = _precomp["V_Sigma"]
|
|
1924
|
+
U = _precomp["U"]
|
|
1925
|
+
uvec = np.array(_precomp["uvec0"], copy=True)
|
|
1926
|
+
|
|
1927
|
+
if f_chi_squared is None:
|
|
1928
|
+
f_chi_squared = 2.5 if parts == "both" else 2.0
|
|
1929
|
+
|
|
1930
|
+
model = create_model_function(omega_range, omega_I, omega_M, omega_S, h_n)
|
|
1931
|
+
|
|
1932
|
+
delta_omega = (omega_max - omega_min) / (omega_num - 1)
|
|
1933
|
+
model_in = model * delta_omega
|
|
1934
|
+
|
|
1935
|
+
k_BT = K_B * self.temperature * KILO
|
|
1936
|
+
|
|
1937
|
+
if lambda_el:
|
|
1938
|
+
if W is None:
|
|
1939
|
+
if self._class == "SpectralQuadratic":
|
|
1940
|
+
if fermi_wavevector is None or bare_mass is None:
|
|
1941
|
+
raise ValueError(
|
|
1942
|
+
"lambda_el is nonzero, but W is None and cannot be "
|
|
1943
|
+
"inferred. Provide W (meV), or pass both "
|
|
1944
|
+
"`fermi_wavevector` and `bare_mass`."
|
|
1945
|
+
)
|
|
1946
|
+
W = (PREF * fermi_wavevector**2 / bare_mass) * KILO
|
|
1947
|
+
else:
|
|
1948
|
+
raise ValueError(
|
|
1949
|
+
"lambda_el was provided, but W is None. For "
|
|
1950
|
+
"SpectralLinear you must provide W in meV."
|
|
1951
|
+
)
|
|
1952
|
+
|
|
1953
|
+
energies_el = energies_eV_masked * KILO
|
|
1954
|
+
real_el, imag_el = self._el_el_self_energy(
|
|
1955
|
+
energies_el, k_BT, lambda_el, W, power
|
|
1956
|
+
)
|
|
1957
|
+
else:
|
|
1958
|
+
real_el = 0.0
|
|
1959
|
+
imag_el = 0.0
|
|
1960
|
+
|
|
1961
|
+
real, real_sigma, imag, imag_sigma = self._evaluate_self_energy_arrays(
|
|
1962
|
+
fermi_velocity=fermi_velocity,
|
|
1963
|
+
fermi_wavevector=fermi_wavevector,
|
|
1964
|
+
bare_mass=bare_mass,
|
|
1965
|
+
)
|
|
1966
|
+
if real is None or imag is None:
|
|
1967
|
+
raise ValueError(
|
|
1968
|
+
"Cannot compute self-energy arrays for cost evaluation. "
|
|
1969
|
+
"Ensure the required band parameters and peak/broadening " \
|
|
1970
|
+
"inputs are set.")
|
|
1971
|
+
|
|
1972
|
+
real_m = real[mE] * KILO - real_el
|
|
1973
|
+
imag_m = imag[mE] * KILO - impurity_magnitude - imag_el
|
|
1974
|
+
|
|
1975
|
+
if parts == "both":
|
|
1976
|
+
real_sig_m = real_sigma[mE] * KILO
|
|
1977
|
+
imag_sig_m = imag_sigma[mE] * KILO
|
|
1978
|
+
dvec = np.concatenate((real_m, imag_m))
|
|
1979
|
+
wvec = np.concatenate((real_sig_m**(-2), imag_sig_m**(-2)))
|
|
1980
|
+
elif parts == "real":
|
|
1981
|
+
real_sig_m = real_sigma[mE] * KILO
|
|
1982
|
+
dvec = real_m
|
|
1983
|
+
wvec = real_sig_m**(-2)
|
|
1984
|
+
else:
|
|
1985
|
+
imag_sig_m = imag_sigma[mE] * KILO
|
|
1986
|
+
dvec = imag_m
|
|
1987
|
+
wvec = imag_sig_m**(-2)
|
|
1988
|
+
|
|
1989
|
+
(spectrum_in, alpha_select, fit_curve, guess_curve,
|
|
1990
|
+
chi2kink_result) = self._chi2kink_a2f(
|
|
1991
|
+
dvec, model_in, uvec, mu, wvec, V_Sigma, U, alpha_min, alpha_max,
|
|
1992
|
+
alpha_num, a_guess, b_guess, c_guess, d_guess, f_chi_squared,
|
|
1993
|
+
t_criterion, iter_max, MEM_core,
|
|
1994
|
+
)
|
|
1995
|
+
|
|
1996
|
+
T = V_Sigma @ (U.T @ spectrum_in)
|
|
1997
|
+
chi_squared = wvec @ ((T - dvec) ** 2)
|
|
1998
|
+
|
|
1999
|
+
mask = (spectrum_in > 0.0) & (model_in > 0.0)
|
|
2000
|
+
if not np.any(mask):
|
|
2001
|
+
raise ValueError(
|
|
2002
|
+
"Invalid spectrum/model for entropy: no positive entries "
|
|
2003
|
+
"after MEM."
|
|
2004
|
+
)
|
|
2005
|
+
|
|
2006
|
+
information_entropy = (
|
|
2007
|
+
np.sum(spectrum_in[mask] - model_in[mask])
|
|
2008
|
+
- np.sum(
|
|
2009
|
+
spectrum_in[mask]
|
|
2010
|
+
* np.log(spectrum_in[mask] / model_in[mask])
|
|
2011
|
+
)
|
|
2012
|
+
)
|
|
2013
|
+
|
|
2014
|
+
cost = (0.5 * chi_squared
|
|
2015
|
+
- alpha_select * information_entropy
|
|
2016
|
+
+ 0.5 * np.sum(np.log(2.0 * np.pi / wvec))
|
|
2017
|
+
- 0.5 * spectrum_in.size * np.log(alpha_select))
|
|
2018
|
+
|
|
2019
|
+
spectrum = spectrum_in * omega_num / omega_max
|
|
2020
|
+
|
|
2021
|
+
return (cost, spectrum, model, alpha_select)
|
|
2022
|
+
|
|
2023
|
+
|
|
2024
|
+
@staticmethod
|
|
2025
|
+
def _chi2kink_a2f(dvec, model_in, uvec, mu, wvec, V_Sigma, U, alpha_min,
|
|
2026
|
+
alpha_max, alpha_num, a_guess, b_guess, c_guess, d_guess,
|
|
2027
|
+
f_chi_squared, t_criterion, iter_max, MEM_core, *,
|
|
2028
|
+
plot=None):
|
|
2029
|
+
r"""
|
|
2030
|
+
Compute MEM spectrum using the chi2-kink alpha-selection procedure.
|
|
2031
|
+
|
|
2032
|
+
Notes
|
|
2033
|
+
-----
|
|
2034
|
+
This routine contains extensive logic to detect failure modes of the
|
|
2035
|
+
chi2-kink logistic fit, including (but not limited to):
|
|
2036
|
+
|
|
2037
|
+
- non-finite or non-positive chi² values,
|
|
2038
|
+
- lack of meaningful parameter updates relative to the initial guess,
|
|
2039
|
+
- absence of improvement in the residual sum of squares,
|
|
2040
|
+
- numerical instabilities or overflows in the logistic model,
|
|
2041
|
+
- invalid or non-finite alpha selection.
|
|
2042
|
+
|
|
2043
|
+
Despite these safeguards, it is **not possible to guarantee** that all
|
|
2044
|
+
failure modes are detected in a nonlinear least-squares problem.
|
|
2045
|
+
Consequently, a reported success should be interpreted as a *necessary*
|
|
2046
|
+
but *not sufficient* condition for physical or numerical reliability.
|
|
2047
|
+
|
|
2048
|
+
Callers **must** inspect the returned ``success`` flag (contained in
|
|
2049
|
+
``chi2kink_result``) before using the fitted curve, selected alpha, or
|
|
2050
|
+
MEM spectrum. When ``success`` is False, the returned quantities are
|
|
2051
|
+
limited to those required for diagnostic plotting only.
|
|
2052
|
+
"""
|
|
2053
|
+
from . import fit_least_squares, chi2kink_logistic
|
|
2054
|
+
|
|
2055
|
+
alpha_range = np.logspace(alpha_min, alpha_max, int(alpha_num))
|
|
2056
|
+
chi_squared = np.empty_like(alpha_range, dtype=float)
|
|
2057
|
+
|
|
2058
|
+
for i, alpha in enumerate(alpha_range):
|
|
2059
|
+
spectrum_in, uvec = MEM_core(
|
|
2060
|
+
dvec, model_in, uvec, mu, alpha, wvec, V_Sigma, U,
|
|
2061
|
+
t_criterion, iter_max
|
|
2062
|
+
)
|
|
2063
|
+
T = V_Sigma @ (U.T @ spectrum_in)
|
|
2064
|
+
chi_squared[i] = wvec @ ((T - dvec) ** 2)
|
|
2065
|
+
|
|
2066
|
+
if (not np.all(np.isfinite(chi_squared))) or np.any(chi_squared <= 0.0):
|
|
2067
|
+
raise ValueError(
|
|
2068
|
+
"chi_squared contains non-finite or non-positive values."
|
|
2069
|
+
)
|
|
2070
|
+
|
|
2071
|
+
log_alpha = np.log10(alpha_range)
|
|
2072
|
+
log_chi_squared = np.log10(chi_squared)
|
|
2073
|
+
|
|
2074
|
+
p0 = np.array([a_guess, b_guess, c_guess, d_guess], dtype=float)
|
|
2075
|
+
pfit, pcov, lsq_success = fit_least_squares(
|
|
2076
|
+
p0, log_alpha, log_chi_squared, chi2kink_logistic
|
|
2077
|
+
)
|
|
2078
|
+
|
|
2079
|
+
with np.errstate(over="ignore", invalid="ignore", divide="ignore"):
|
|
2080
|
+
guess_curve = chi2kink_logistic(log_alpha, *p0)
|
|
2081
|
+
|
|
2082
|
+
# Start from the necessary requirement: least_squares must say success
|
|
2083
|
+
success = bool(lsq_success)
|
|
2084
|
+
|
|
2085
|
+
# If the guess itself blows up, we can't trust anything
|
|
2086
|
+
if not np.all(np.isfinite(guess_curve)):
|
|
2087
|
+
success = False
|
|
2088
|
+
|
|
2089
|
+
fit_curve_tmp = None
|
|
2090
|
+
if success:
|
|
2091
|
+
pfit = np.asarray(pfit, dtype=float)
|
|
2092
|
+
|
|
2093
|
+
if np.allclose(pfit, p0, rtol=1e-12, atol=0.0):
|
|
2094
|
+
success = False
|
|
2095
|
+
else:
|
|
2096
|
+
with np.errstate(over="ignore", invalid="ignore",
|
|
2097
|
+
divide="ignore"):
|
|
2098
|
+
fit_curve_tmp = chi2kink_logistic(log_alpha, *pfit)
|
|
2099
|
+
|
|
2100
|
+
if not np.all(np.isfinite(fit_curve_tmp)):
|
|
2101
|
+
success = False
|
|
2102
|
+
else:
|
|
2103
|
+
r0 = guess_curve - log_chi_squared
|
|
2104
|
+
r1 = fit_curve_tmp - log_chi_squared
|
|
2105
|
+
sse0 = float(r0 @ r0)
|
|
2106
|
+
sse1 = float(r1 @ r1)
|
|
2107
|
+
|
|
2108
|
+
tol = 1e-12 * max(1.0, sse0)
|
|
2109
|
+
if (not np.isfinite(sse1)) or (sse1 >= sse0 - tol):
|
|
2110
|
+
success = False
|
|
2111
|
+
|
|
2112
|
+
alpha_select = None
|
|
2113
|
+
fit_curve = None
|
|
2114
|
+
spectrum_out = None
|
|
2115
|
+
|
|
2116
|
+
if success:
|
|
2117
|
+
fit_curve = fit_curve_tmp
|
|
2118
|
+
|
|
2119
|
+
cout = float(pfit[2])
|
|
2120
|
+
dout = float(pfit[3])
|
|
2121
|
+
exp10 = cout - float(f_chi_squared) / dout
|
|
2122
|
+
|
|
2123
|
+
if (not np.isfinite(exp10)) or (exp10 < -308.0) or (exp10 > 308.0):
|
|
2124
|
+
success = False
|
|
2125
|
+
fit_curve = None
|
|
2126
|
+
else:
|
|
2127
|
+
with np.errstate(over="raise", invalid="raise"):
|
|
2128
|
+
alpha_select = float(np.power(10.0, exp10))
|
|
2129
|
+
|
|
2130
|
+
spectrum_out, uvec = MEM_core(
|
|
2131
|
+
dvec, model_in, uvec, mu, alpha_select, wvec, V_Sigma, U,
|
|
2132
|
+
t_criterion, iter_max
|
|
2133
|
+
)
|
|
2134
|
+
|
|
2135
|
+
chi2kink_result = {
|
|
2136
|
+
"alpha_range": alpha_range,
|
|
2137
|
+
"chi_squared": chi_squared,
|
|
2138
|
+
"log_alpha": log_alpha,
|
|
2139
|
+
"log_chi_squared": log_chi_squared,
|
|
2140
|
+
"p0": p0,
|
|
2141
|
+
"pfit": pfit,
|
|
2142
|
+
"pcov": pcov,
|
|
2143
|
+
"success": bool(success),
|
|
2144
|
+
"alpha_select": alpha_select,
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
return spectrum_out, alpha_select, fit_curve, guess_curve, chi2kink_result
|
|
2148
|
+
|
|
2149
|
+
|
|
2150
|
+
@staticmethod
|
|
2151
|
+
def _el_el_self_energy(enel_range, k_BT, lambda_el, W, power):
|
|
2152
|
+
"""Electron–electron contribution to the self-energy."""
|
|
2153
|
+
x = enel_range / W
|
|
2154
|
+
denom = 1.0 - (np.pi * k_BT / W) ** 2
|
|
2155
|
+
|
|
2156
|
+
if denom == 0.0:
|
|
2157
|
+
raise ZeroDivisionError(
|
|
2158
|
+
"Invalid parameters: 1 - (π k_BT / W)^2 = 0."
|
|
2159
|
+
)
|
|
2160
|
+
|
|
2161
|
+
pref = lambda_el / (W * denom)
|
|
2162
|
+
|
|
2163
|
+
if power == 2:
|
|
2164
|
+
real_el = pref * x * ((np.pi * k_BT) ** 2 - W ** 2) / (1.0 + x ** 2)
|
|
2165
|
+
imag_el = (pref * (enel_range ** 2 + (np.pi * k_BT) ** 2)
|
|
2166
|
+
/ (1.0 + x ** 2))
|
|
2167
|
+
|
|
2168
|
+
elif power == 4:
|
|
2169
|
+
num = (
|
|
2170
|
+
(np.pi * k_BT) ** 2 * (1.0 + x ** 2)
|
|
2171
|
+
- W ** 2 * (1.0 - x ** 2)
|
|
2172
|
+
)
|
|
2173
|
+
real_el = pref * x * num / (1.0 + x ** 4)
|
|
2174
|
+
imag_el = (pref * np.sqrt(2.0) * (enel_range ** 2 + (np.pi * k_BT)
|
|
2175
|
+
** 2) / ( 1.0 + x ** 4))
|
|
2176
|
+
else:
|
|
2177
|
+
raise ValueError(
|
|
2178
|
+
"El-el coupling has not yet been implemented for the given " \
|
|
2179
|
+
"power."
|
|
2180
|
+
)
|
|
2181
|
+
|
|
2182
|
+
return real_el, imag_el
|
|
2183
|
+
|
|
545
2184
|
|
|
546
2185
|
class CreateSelfEnergies:
|
|
547
2186
|
r"""
|