xarpes 0.2.4__py3-none-any.whl → 0.6.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 +34 -6
- xarpes/bandmap.py +897 -0
- xarpes/constants.py +13 -0
- xarpes/distributions.py +516 -245
- xarpes/functions.py +573 -79
- xarpes/mdcs.py +1078 -0
- xarpes/plotting.py +37 -35
- xarpes/selfenergies.py +1816 -0
- xarpes/settings_parameters.py +75 -0
- xarpes/settings_plots.py +54 -0
- {xarpes-0.2.4.dist-info → xarpes-0.6.0.dist-info}/LICENSE +0 -0
- xarpes-0.6.0.dist-info/METADATA +181 -0
- xarpes-0.6.0.dist-info/RECORD +15 -0
- {xarpes-0.2.4.dist-info → xarpes-0.6.0.dist-info}/WHEEL +1 -1
- xarpes-0.6.0.dist-info/entry_points.txt +3 -0
- xarpes/.ipynb_checkpoints/__init__-checkpoint.py +0 -8
- xarpes/band_map.py +0 -302
- xarpes-0.2.4.dist-info/METADATA +0 -122
- xarpes-0.2.4.dist-info/RECORD +0 -10
xarpes/selfenergies.py
ADDED
|
@@ -0,0 +1,1816 @@
|
|
|
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 .plotting import get_ax_fig_plt, add_fig_kwargs
|
|
16
|
+
from .constants import PREF, KILO, K_B
|
|
17
|
+
|
|
18
|
+
class SelfEnergy:
|
|
19
|
+
r"""Self-energy"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, ekin_range, hnuminPhi, energy_resolution,
|
|
22
|
+
temperature, label, properties, parameters):
|
|
23
|
+
# core read-only state
|
|
24
|
+
self._ekin_range = ekin_range
|
|
25
|
+
self._hnuminPhi = hnuminPhi
|
|
26
|
+
self._energy_resolution = energy_resolution
|
|
27
|
+
self._temperature = temperature
|
|
28
|
+
self._label = label
|
|
29
|
+
|
|
30
|
+
# accept either a dict or a single-element list of dicts
|
|
31
|
+
if isinstance(properties, list):
|
|
32
|
+
if len(properties) == 1:
|
|
33
|
+
properties = properties[0]
|
|
34
|
+
else:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
"`properties` must be a dict or a single dict in a list."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# single source of truth for all params (+ their *_sigma)
|
|
40
|
+
self._properties = dict(properties or {})
|
|
41
|
+
self._class = self._properties.get("_class", None)
|
|
42
|
+
|
|
43
|
+
# ---- enforce supported classes at construction
|
|
44
|
+
if self._class not in ("SpectralLinear", "SpectralQuadratic"):
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"Unsupported spectral class '{self._class}'. "
|
|
47
|
+
"Only 'SpectralLinear' or 'SpectralQuadratic' are allowed."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# grab user parameters
|
|
51
|
+
self._parameters = dict(parameters or {})
|
|
52
|
+
self._fermi_wavevector = self._parameters.get("fermi_wavevector")
|
|
53
|
+
self._fermi_velocity = self._parameters.get("fermi_velocity")
|
|
54
|
+
self._bare_mass = self._parameters.get("bare_mass")
|
|
55
|
+
self._side = self._parameters.get("side", None)
|
|
56
|
+
|
|
57
|
+
# ---- class-specific parameter constraints
|
|
58
|
+
if self._class == "SpectralLinear" and (self._bare_mass is not None):
|
|
59
|
+
raise ValueError("`bare_mass` cannot be set for SpectralLinear.")
|
|
60
|
+
if self._class == "SpectralQuadratic" and (self._fermi_velocity is not None):
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"`fermi_velocity` cannot be set for SpectralQuadratic."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if self._side is not None and self._side not in ("left", "right"):
|
|
66
|
+
raise ValueError("`side` must be 'left' or 'right' if provided.")
|
|
67
|
+
if self._side is not None:
|
|
68
|
+
self._parameters["side"] = self._side
|
|
69
|
+
|
|
70
|
+
# convenience attributes (read from properties)
|
|
71
|
+
self._amplitude = self._properties.get("amplitude")
|
|
72
|
+
self._amplitude_sigma = self._properties.get("amplitude_sigma")
|
|
73
|
+
self._peak = self._properties.get("peak")
|
|
74
|
+
self._peak_sigma = self._properties.get("peak_sigma")
|
|
75
|
+
self._broadening = self._properties.get("broadening")
|
|
76
|
+
self._broadening_sigma = self._properties.get("broadening_sigma")
|
|
77
|
+
self._center_wavevector = self._properties.get("center_wavevector")
|
|
78
|
+
|
|
79
|
+
# lazy caches
|
|
80
|
+
self._peak_positions = None
|
|
81
|
+
self._peak_positions_sigma = None
|
|
82
|
+
self._real = None
|
|
83
|
+
self._real_sigma = None
|
|
84
|
+
self._imag = None
|
|
85
|
+
self._imag_sigma = None
|
|
86
|
+
|
|
87
|
+
def _check_mass_velocity_exclusivity(self):
|
|
88
|
+
"""Ensure that fermi_velocity and bare_mass are not both set."""
|
|
89
|
+
if (self._fermi_velocity is not None) and (self._bare_mass is not None):
|
|
90
|
+
raise ValueError(
|
|
91
|
+
"Cannot set both `fermi_velocity` and `bare_mass`: choose one "
|
|
92
|
+
"physical parametrization (SpectralLinear or SpectralQuadratic)."
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# ---------------- core read-only axes ----------------
|
|
96
|
+
@property
|
|
97
|
+
def ekin_range(self):
|
|
98
|
+
return self._ekin_range
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def enel_range(self):
|
|
102
|
+
if self._ekin_range is None:
|
|
103
|
+
return None
|
|
104
|
+
hnp = 0.0 if self._hnuminPhi is None else self._hnuminPhi
|
|
105
|
+
return np.asarray(self._ekin_range) - hnp
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def hnuminPhi(self):
|
|
109
|
+
return self._hnuminPhi
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def energy_resolution(self):
|
|
113
|
+
"""Energy resolution associated with the self-energy."""
|
|
114
|
+
return self._energy_resolution
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def temperature(self):
|
|
118
|
+
"""Temperature associated with the self-energy [K]."""
|
|
119
|
+
return self._temperature
|
|
120
|
+
|
|
121
|
+
# ---------------- identifiers ----------------
|
|
122
|
+
@property
|
|
123
|
+
def label(self):
|
|
124
|
+
return self._label
|
|
125
|
+
|
|
126
|
+
@label.setter
|
|
127
|
+
def label(self, x):
|
|
128
|
+
self._label = x
|
|
129
|
+
|
|
130
|
+
# ---------------- exported user parameters ----------------
|
|
131
|
+
@property
|
|
132
|
+
def parameters(self):
|
|
133
|
+
"""Dictionary with user-supplied parameters (read-only view)."""
|
|
134
|
+
return self._parameters
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def side(self):
|
|
138
|
+
"""Optional side selector: 'left' or 'right'."""
|
|
139
|
+
return self._side
|
|
140
|
+
|
|
141
|
+
@side.setter
|
|
142
|
+
def side(self, x):
|
|
143
|
+
if x is not None and x not in ("left", "right"):
|
|
144
|
+
raise ValueError("`side` must be 'left' or 'right' if provided.")
|
|
145
|
+
self._side = x
|
|
146
|
+
if x is not None:
|
|
147
|
+
self._parameters["side"] = x
|
|
148
|
+
else:
|
|
149
|
+
self._parameters.pop("side", None)
|
|
150
|
+
# affects sign of peak_positions and thus `real`
|
|
151
|
+
self._peak_positions = None
|
|
152
|
+
self._real = None
|
|
153
|
+
self._real_sigma = None
|
|
154
|
+
self._mdc_maxima = None
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def fermi_wavevector(self):
|
|
158
|
+
"""Optional k_F; can be set later."""
|
|
159
|
+
return self._fermi_wavevector
|
|
160
|
+
|
|
161
|
+
@fermi_wavevector.setter
|
|
162
|
+
def fermi_wavevector(self, x):
|
|
163
|
+
self._fermi_wavevector = x
|
|
164
|
+
self._parameters["fermi_wavevector"] = x
|
|
165
|
+
# invalidate dependent cache
|
|
166
|
+
self._real = None
|
|
167
|
+
self._real_sigma = None
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def fermi_velocity(self):
|
|
171
|
+
"""Optional v_F; can be set later."""
|
|
172
|
+
return self._fermi_velocity
|
|
173
|
+
|
|
174
|
+
@fermi_velocity.setter
|
|
175
|
+
def fermi_velocity(self, x):
|
|
176
|
+
if self._class == "SpectralQuadratic":
|
|
177
|
+
raise ValueError(
|
|
178
|
+
"`fermi_velocity` cannot be set for SpectralQuadratic."
|
|
179
|
+
)
|
|
180
|
+
self._fermi_velocity = x
|
|
181
|
+
self._parameters["fermi_velocity"] = x
|
|
182
|
+
# invalidate dependents
|
|
183
|
+
self._imag = None; self._imag_sigma = None
|
|
184
|
+
self._real = None; self._real_sigma = None
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def bare_mass(self):
|
|
188
|
+
"""Optional bare mass; used by SpectralQuadratic formulas."""
|
|
189
|
+
return self._bare_mass
|
|
190
|
+
|
|
191
|
+
@bare_mass.setter
|
|
192
|
+
def bare_mass(self, x):
|
|
193
|
+
if self._class == "SpectralLinear":
|
|
194
|
+
raise ValueError(
|
|
195
|
+
"`bare_mass` cannot be set for SpectralLinear."
|
|
196
|
+
)
|
|
197
|
+
self._bare_mass = x
|
|
198
|
+
self._parameters["bare_mass"] = x
|
|
199
|
+
# invalidate dependents
|
|
200
|
+
self._imag = None; self._imag_sigma = None
|
|
201
|
+
self._real = None; self._real_sigma = None
|
|
202
|
+
|
|
203
|
+
# ---------------- optional fit parameters (convenience) ----------------
|
|
204
|
+
@property
|
|
205
|
+
def amplitude(self):
|
|
206
|
+
return self._amplitude
|
|
207
|
+
|
|
208
|
+
@amplitude.setter
|
|
209
|
+
def amplitude(self, x):
|
|
210
|
+
self._amplitude = x
|
|
211
|
+
self._properties["amplitude"] = x
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def amplitude_sigma(self):
|
|
215
|
+
return self._amplitude_sigma
|
|
216
|
+
|
|
217
|
+
@amplitude_sigma.setter
|
|
218
|
+
def amplitude_sigma(self, x):
|
|
219
|
+
self._amplitude_sigma = x
|
|
220
|
+
self._properties["amplitude_sigma"] = x
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def peak(self):
|
|
224
|
+
return self._peak
|
|
225
|
+
|
|
226
|
+
@peak.setter
|
|
227
|
+
def peak(self, x):
|
|
228
|
+
self._peak = x
|
|
229
|
+
self._properties["peak"] = x
|
|
230
|
+
# invalidate dependent cache
|
|
231
|
+
self._peak_positions = None
|
|
232
|
+
self._real = None
|
|
233
|
+
self._mdc_maxima = None
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def peak_sigma(self):
|
|
237
|
+
return self._peak_sigma
|
|
238
|
+
|
|
239
|
+
@peak_sigma.setter
|
|
240
|
+
def peak_sigma(self, x):
|
|
241
|
+
self._peak_sigma = x
|
|
242
|
+
self._properties["peak_sigma"] = x
|
|
243
|
+
self._peak_positions_sigma = None
|
|
244
|
+
self._real_sigma = None
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def broadening(self):
|
|
248
|
+
return self._broadening
|
|
249
|
+
|
|
250
|
+
@broadening.setter
|
|
251
|
+
def broadening(self, x):
|
|
252
|
+
self._broadening = x
|
|
253
|
+
self._properties["broadening"] = x
|
|
254
|
+
self._imag = None
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def broadening_sigma(self):
|
|
258
|
+
return self._broadening_sigma
|
|
259
|
+
|
|
260
|
+
@broadening_sigma.setter
|
|
261
|
+
def broadening_sigma(self, x):
|
|
262
|
+
self._broadening_sigma = x
|
|
263
|
+
self._properties["broadening_sigma"] = x
|
|
264
|
+
self._imag_sigma = None
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def center_wavevector(self):
|
|
268
|
+
"""Read-only center wavevector (SpectralQuadratic, if present)."""
|
|
269
|
+
return self._center_wavevector
|
|
270
|
+
|
|
271
|
+
# ---------------- derived outputs ----------------
|
|
272
|
+
@property
|
|
273
|
+
def peak_positions(self):
|
|
274
|
+
r"""k_parallel = peak * dtor * sqrt(ekin_range / PREF) (lazy)."""
|
|
275
|
+
if self._peak_positions is None:
|
|
276
|
+
if self._peak is None or self._ekin_range is None:
|
|
277
|
+
return None
|
|
278
|
+
if self._class == "SpectralQuadratic":
|
|
279
|
+
if self._side is None:
|
|
280
|
+
raise AttributeError(
|
|
281
|
+
"For SpectralQuadratic, set `side` ('left'/'right') "
|
|
282
|
+
"before accessing peak_positions and quantities that "
|
|
283
|
+
"depend on the latter."
|
|
284
|
+
)
|
|
285
|
+
kpar_mag = (
|
|
286
|
+
np.sqrt(self._ekin_range / PREF)
|
|
287
|
+
* np.sin(np.deg2rad(np.abs(self._peak)))
|
|
288
|
+
)
|
|
289
|
+
self._peak_positions = ((-1.0 if self._side == "left"
|
|
290
|
+
else 1.0) * kpar_mag)
|
|
291
|
+
else:
|
|
292
|
+
self._peak_positions = (np.sqrt(self._ekin_range / PREF)
|
|
293
|
+
* np.sin(np.deg2rad(self._peak)))
|
|
294
|
+
return self._peak_positions
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def peak_positions_sigma(self):
|
|
299
|
+
r"""Std. dev. of k_parallel (lazy)."""
|
|
300
|
+
if self._peak_positions_sigma is None:
|
|
301
|
+
if self._peak_sigma is None or self._ekin_range is None:
|
|
302
|
+
return None
|
|
303
|
+
self._peak_positions_sigma = (
|
|
304
|
+
np.sqrt(self._ekin_range / PREF)
|
|
305
|
+
* np.abs(np.cos(np.deg2rad(self._peak)))
|
|
306
|
+
* np.deg2rad(self._peak_sigma)
|
|
307
|
+
)
|
|
308
|
+
return self._peak_positions_sigma
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@property
|
|
312
|
+
def imag(self):
|
|
313
|
+
r"""-Σ'' (lazy)."""
|
|
314
|
+
if self._imag is None:
|
|
315
|
+
if self._broadening is None or self._ekin_range is None:
|
|
316
|
+
return None
|
|
317
|
+
self._imag = self._compute_imag()
|
|
318
|
+
return self._imag
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def imag_sigma(self):
|
|
323
|
+
r"""Std. dev. of -Σ'' (lazy)."""
|
|
324
|
+
if self._imag_sigma is None:
|
|
325
|
+
if self._broadening_sigma is None or self._ekin_range is None:
|
|
326
|
+
return None
|
|
327
|
+
self._imag_sigma = self._compute_imag_sigma()
|
|
328
|
+
return self._imag_sigma
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def real(self):
|
|
333
|
+
r"""Σ' (lazy)."""
|
|
334
|
+
if self._real is None:
|
|
335
|
+
if self._peak is None or self._ekin_range is None:
|
|
336
|
+
return None
|
|
337
|
+
self._real = self._compute_real()
|
|
338
|
+
return self._real
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def real_sigma(self):
|
|
343
|
+
r"""Std. dev. of Σ' (lazy)."""
|
|
344
|
+
if self._real_sigma is None:
|
|
345
|
+
if self._peak_sigma is None or self._ekin_range is None:
|
|
346
|
+
return None
|
|
347
|
+
self._real_sigma = self._compute_real_sigma()
|
|
348
|
+
return self._real_sigma
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _compute_imag(self, fermi_velocity=None, bare_mass=None):
|
|
352
|
+
r"""Compute -Σ'' without touching caches."""
|
|
353
|
+
if self._broadening is None or self._ekin_range is None:
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
ekin = np.asarray(self._ekin_range)
|
|
357
|
+
broad = self._broadening
|
|
358
|
+
|
|
359
|
+
if self._class == "SpectralLinear":
|
|
360
|
+
vF = self._fermi_velocity if fermi_velocity is None else fermi_velocity
|
|
361
|
+
if vF is None:
|
|
362
|
+
raise AttributeError(
|
|
363
|
+
"Cannot compute `imag` (SpectralLinear): set `fermi_velocity` "
|
|
364
|
+
"first."
|
|
365
|
+
)
|
|
366
|
+
return np.abs(vF) * np.sqrt(ekin / PREF) * broad
|
|
367
|
+
|
|
368
|
+
if self._class == "SpectralQuadratic":
|
|
369
|
+
mb = self._bare_mass if bare_mass is None else bare_mass
|
|
370
|
+
if mb is None:
|
|
371
|
+
raise AttributeError(
|
|
372
|
+
"Cannot compute `imag` (SpectralQuadratic): set `bare_mass` "
|
|
373
|
+
"first."
|
|
374
|
+
)
|
|
375
|
+
return (ekin * broad) / np.abs(mb)
|
|
376
|
+
|
|
377
|
+
raise NotImplementedError(
|
|
378
|
+
f"_compute_imag is not implemented for spectral class '{self._class}'."
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _compute_imag_sigma(self, fermi_velocity=None, bare_mass=None):
|
|
383
|
+
r"""Compute std. dev. of -Σ'' without touching caches."""
|
|
384
|
+
if self._broadening_sigma is None or self._ekin_range is None:
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
ekin = np.asarray(self._ekin_range)
|
|
388
|
+
broad_sigma = self._broadening_sigma
|
|
389
|
+
|
|
390
|
+
if self._class == "SpectralLinear":
|
|
391
|
+
vF = self._fermi_velocity if fermi_velocity is None else fermi_velocity
|
|
392
|
+
if vF is None:
|
|
393
|
+
raise AttributeError(
|
|
394
|
+
"Cannot compute `imag_sigma` (SpectralLinear): set "
|
|
395
|
+
"`fermi_velocity` first."
|
|
396
|
+
)
|
|
397
|
+
return np.abs(vF) * np.sqrt(ekin / PREF) * broad_sigma
|
|
398
|
+
|
|
399
|
+
if self._class == "SpectralQuadratic":
|
|
400
|
+
mb = self._bare_mass if bare_mass is None else bare_mass
|
|
401
|
+
if mb is None:
|
|
402
|
+
raise AttributeError(
|
|
403
|
+
"Cannot compute `imag_sigma` (SpectralQuadratic): set "
|
|
404
|
+
"`bare_mass` first."
|
|
405
|
+
)
|
|
406
|
+
return (ekin * broad_sigma) / np.abs(mb)
|
|
407
|
+
|
|
408
|
+
raise NotImplementedError(
|
|
409
|
+
f"_compute_imag_sigma is not implemented for spectral class "
|
|
410
|
+
f"'{self._class}'."
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _compute_real(self, fermi_velocity=None, fermi_wavevector=None,
|
|
415
|
+
bare_mass=None):
|
|
416
|
+
r"""Compute Σ' without touching caches."""
|
|
417
|
+
if self._peak is None or self._ekin_range is None:
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
enel = self.enel_range
|
|
421
|
+
kpar = self.peak_positions
|
|
422
|
+
if kpar is None:
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
if self._class == "SpectralLinear":
|
|
426
|
+
vF = self._fermi_velocity if fermi_velocity is None else fermi_velocity
|
|
427
|
+
kF = (self._fermi_wavevector if fermi_wavevector is None
|
|
428
|
+
else fermi_wavevector)
|
|
429
|
+
if vF is None or kF is None:
|
|
430
|
+
raise AttributeError(
|
|
431
|
+
"Cannot compute `real` (SpectralLinear): set `fermi_velocity` "
|
|
432
|
+
"and `fermi_wavevector` first."
|
|
433
|
+
)
|
|
434
|
+
return enel - vF * (kpar - kF)
|
|
435
|
+
|
|
436
|
+
if self._class == "SpectralQuadratic":
|
|
437
|
+
mb = self._bare_mass if bare_mass is None else bare_mass
|
|
438
|
+
kF = (self._fermi_wavevector if fermi_wavevector is None
|
|
439
|
+
else fermi_wavevector)
|
|
440
|
+
if mb is None or kF is None:
|
|
441
|
+
raise AttributeError(
|
|
442
|
+
"Cannot compute `real` (SpectralQuadratic): set `bare_mass` "
|
|
443
|
+
"and `fermi_wavevector` first."
|
|
444
|
+
)
|
|
445
|
+
return enel - (PREF / mb) * (kpar**2 - kF**2)
|
|
446
|
+
|
|
447
|
+
raise NotImplementedError(
|
|
448
|
+
f"_compute_real is not implemented for spectral class '{self._class}'."
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def _compute_real_sigma(self, fermi_velocity=None, fermi_wavevector=None,
|
|
452
|
+
bare_mass=None):
|
|
453
|
+
r"""Compute std. dev. of Σ' without touching caches."""
|
|
454
|
+
if self._peak_sigma is None or self._ekin_range is None:
|
|
455
|
+
return None
|
|
456
|
+
|
|
457
|
+
kpar_sigma = self.peak_positions_sigma
|
|
458
|
+
if kpar_sigma is None:
|
|
459
|
+
return None
|
|
460
|
+
|
|
461
|
+
if self._class == "SpectralLinear":
|
|
462
|
+
vF = self._fermi_velocity if fermi_velocity is None else fermi_velocity
|
|
463
|
+
if vF is None:
|
|
464
|
+
raise AttributeError(
|
|
465
|
+
"Cannot compute `real_sigma` (SpectralLinear): set "
|
|
466
|
+
"`fermi_velocity` first."
|
|
467
|
+
)
|
|
468
|
+
return np.abs(vF) * kpar_sigma
|
|
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_sigma` (SpectralQuadratic): set "
|
|
477
|
+
"`bare_mass` and `fermi_wavevector` first."
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
kpar = self.peak_positions
|
|
481
|
+
if kpar is None:
|
|
482
|
+
return None
|
|
483
|
+
return 2.0 * PREF * kpar_sigma * np.abs(kpar / mb)
|
|
484
|
+
|
|
485
|
+
raise ValueError(
|
|
486
|
+
f"Unsupported spectral class '{self._class}' in "
|
|
487
|
+
"_compute_real_sigma."
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _evaluate_self_energy_arrays(self, fermi_velocity=None, fermi_wavevector=None,
|
|
492
|
+
bare_mass=None):
|
|
493
|
+
r"""Evaluate Σ' / -Σ'' and 1σ uncertainties without mutating caches."""
|
|
494
|
+
real = self._compute_real(
|
|
495
|
+
fermi_velocity=fermi_velocity,
|
|
496
|
+
fermi_wavevector=fermi_wavevector,
|
|
497
|
+
bare_mass=bare_mass,
|
|
498
|
+
)
|
|
499
|
+
real_sigma = self._compute_real_sigma(
|
|
500
|
+
fermi_velocity=fermi_velocity,
|
|
501
|
+
fermi_wavevector=fermi_wavevector,
|
|
502
|
+
bare_mass=bare_mass,
|
|
503
|
+
)
|
|
504
|
+
imag = self._compute_imag(fermi_velocity=fermi_velocity, bare_mass=bare_mass)
|
|
505
|
+
imag_sigma = self._compute_imag_sigma(
|
|
506
|
+
fermi_velocity=fermi_velocity, bare_mass=bare_mass
|
|
507
|
+
)
|
|
508
|
+
return real, real_sigma, imag, imag_sigma
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
@property
|
|
512
|
+
def mdc_maxima(self):
|
|
513
|
+
"""
|
|
514
|
+
MDC maxima (lazy).
|
|
515
|
+
|
|
516
|
+
SpectralLinear:
|
|
517
|
+
identical to peak_positions
|
|
518
|
+
|
|
519
|
+
SpectralQuadratic:
|
|
520
|
+
peak_positions + center_wavevector
|
|
521
|
+
"""
|
|
522
|
+
if getattr(self, "_mdc_maxima", None) is None:
|
|
523
|
+
if self.peak_positions is None:
|
|
524
|
+
return None
|
|
525
|
+
|
|
526
|
+
if self._class == "SpectralLinear":
|
|
527
|
+
self._mdc_maxima = self.peak_positions
|
|
528
|
+
elif self._class == "SpectralQuadratic":
|
|
529
|
+
self._mdc_maxima = (
|
|
530
|
+
self.peak_positions + self._center_wavevector
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
return self._mdc_maxima
|
|
534
|
+
|
|
535
|
+
def _se_legend_labels(self):
|
|
536
|
+
"""Return (real_label, imag_label) for legend with safe subscripts."""
|
|
537
|
+
se_label = getattr(self, "label", None)
|
|
538
|
+
|
|
539
|
+
if se_label is None:
|
|
540
|
+
real_label = r"$\Sigma'(E)$"
|
|
541
|
+
imag_label = r"$-\Sigma''(E)$"
|
|
542
|
+
return real_label, imag_label
|
|
543
|
+
|
|
544
|
+
safe_label = str(se_label).replace("_", r"\_")
|
|
545
|
+
|
|
546
|
+
# If the label is empty after conversion, fall back
|
|
547
|
+
if safe_label == "":
|
|
548
|
+
real_label = r"$\Sigma'(E)$"
|
|
549
|
+
imag_label = r"$-\Sigma''(E)$"
|
|
550
|
+
return real_label, imag_label
|
|
551
|
+
|
|
552
|
+
real_label = rf"$\Sigma_{{\mathrm{{{safe_label}}}}}'(E)$"
|
|
553
|
+
imag_label = rf"$-\Sigma_{{\mathrm{{{safe_label}}}}}''(E)$"
|
|
554
|
+
|
|
555
|
+
return real_label, imag_label
|
|
556
|
+
|
|
557
|
+
@add_fig_kwargs
|
|
558
|
+
def plot_real(self, ax=None, **kwargs):
|
|
559
|
+
r"""Plot the real part Σ' of the self-energy as a function of E-μ.
|
|
560
|
+
|
|
561
|
+
Parameters
|
|
562
|
+
----------
|
|
563
|
+
ax : Matplotlib-Axes or None
|
|
564
|
+
Axis to plot on. Created if not provided by the user.
|
|
565
|
+
**kwargs :
|
|
566
|
+
Additional keyword arguments passed to ``ax.errorbar``.
|
|
567
|
+
|
|
568
|
+
Returns
|
|
569
|
+
-------
|
|
570
|
+
fig : Matplotlib-Figure
|
|
571
|
+
Figure containing the Σ'(E) plot.
|
|
572
|
+
"""
|
|
573
|
+
from . import settings_parameters as xprs
|
|
574
|
+
|
|
575
|
+
ax, fig, plt = get_ax_fig_plt(ax=ax)
|
|
576
|
+
|
|
577
|
+
x = self.enel_range
|
|
578
|
+
y = self.real
|
|
579
|
+
y_sigma = self.real_sigma
|
|
580
|
+
|
|
581
|
+
real_label, _ = self._se_legend_labels()
|
|
582
|
+
kwargs.setdefault("label", real_label)
|
|
583
|
+
|
|
584
|
+
if y_sigma is not None:
|
|
585
|
+
if np.isnan(y_sigma).any():
|
|
586
|
+
print(
|
|
587
|
+
"Warning: some Σ'(E) uncertainty values are missing. "
|
|
588
|
+
"Error bars omitted at those energies."
|
|
589
|
+
)
|
|
590
|
+
kwargs.setdefault("yerr", xprs.sigma_confidence * y_sigma)
|
|
591
|
+
|
|
592
|
+
ax.errorbar(x, y, **kwargs)
|
|
593
|
+
ax.set_xlabel(r"$E-\mu$ (eV)")
|
|
594
|
+
ax.set_ylabel(r"$\Sigma'(E)$ (eV)")
|
|
595
|
+
ax.legend()
|
|
596
|
+
|
|
597
|
+
return fig
|
|
598
|
+
|
|
599
|
+
@add_fig_kwargs
|
|
600
|
+
def plot_imag(self, ax=None, **kwargs):
|
|
601
|
+
r"""Plot the imaginary part -Σ'' of the self-energy vs. E-μ.
|
|
602
|
+
|
|
603
|
+
Parameters
|
|
604
|
+
----------
|
|
605
|
+
ax : Matplotlib-Axes or None
|
|
606
|
+
Axis to plot on. Created if not provided by the user.
|
|
607
|
+
**kwargs :
|
|
608
|
+
Additional keyword arguments passed to ``ax.errorbar``.
|
|
609
|
+
|
|
610
|
+
Returns
|
|
611
|
+
-------
|
|
612
|
+
fig : Matplotlib-Figure
|
|
613
|
+
Figure containing the -Σ''(E) plot.
|
|
614
|
+
"""
|
|
615
|
+
from . import settings_parameters as xprs
|
|
616
|
+
|
|
617
|
+
ax, fig, plt = get_ax_fig_plt(ax=ax)
|
|
618
|
+
|
|
619
|
+
x = self.enel_range
|
|
620
|
+
y = self.imag
|
|
621
|
+
y_sigma = self.imag_sigma
|
|
622
|
+
|
|
623
|
+
_, imag_label = self._se_legend_labels()
|
|
624
|
+
kwargs.setdefault("label", imag_label)
|
|
625
|
+
|
|
626
|
+
if y_sigma is not None:
|
|
627
|
+
if np.isnan(y_sigma).any():
|
|
628
|
+
print(
|
|
629
|
+
"Warning: some -Σ''(E) uncertainty values are missing. "
|
|
630
|
+
"Error bars omitted at those energies."
|
|
631
|
+
)
|
|
632
|
+
kwargs.setdefault("yerr", xprs.sigma_confidence * y_sigma)
|
|
633
|
+
|
|
634
|
+
ax.errorbar(x, y, **kwargs)
|
|
635
|
+
ax.set_xlabel(r"$E-\mu$ (eV)")
|
|
636
|
+
ax.set_ylabel(r"$-\Sigma''(E)$ (eV)")
|
|
637
|
+
ax.legend()
|
|
638
|
+
|
|
639
|
+
return fig
|
|
640
|
+
|
|
641
|
+
@add_fig_kwargs
|
|
642
|
+
def plot_both(self, ax=None, **kwargs):
|
|
643
|
+
r"""Plot Σ'(E) and -Σ''(E) vs. E-μ on the same axis."""
|
|
644
|
+
from . import settings_parameters as xprs
|
|
645
|
+
|
|
646
|
+
ax, fig, plt = get_ax_fig_plt(ax=ax)
|
|
647
|
+
|
|
648
|
+
x = self.enel_range
|
|
649
|
+
real = self.real
|
|
650
|
+
imag = self.imag
|
|
651
|
+
real_sigma = self.real_sigma
|
|
652
|
+
imag_sigma = self.imag_sigma
|
|
653
|
+
|
|
654
|
+
real_label, imag_label = self._se_legend_labels()
|
|
655
|
+
|
|
656
|
+
# --- plot Σ'
|
|
657
|
+
kw_real = dict(kwargs)
|
|
658
|
+
if real_sigma is not None:
|
|
659
|
+
if np.isnan(real_sigma).any():
|
|
660
|
+
print(
|
|
661
|
+
"Warning: some Σ'(E) uncertainty values are missing. "
|
|
662
|
+
"Error bars omitted at those energies."
|
|
663
|
+
)
|
|
664
|
+
kw_real.setdefault("yerr", xprs.sigma_confidence * real_sigma)
|
|
665
|
+
kw_real.setdefault("label", real_label)
|
|
666
|
+
ax.errorbar(x, real, **kw_real)
|
|
667
|
+
|
|
668
|
+
# --- plot -Σ''
|
|
669
|
+
kw_imag = dict(kwargs)
|
|
670
|
+
if imag_sigma is not None:
|
|
671
|
+
if np.isnan(imag_sigma).any():
|
|
672
|
+
print(
|
|
673
|
+
"Warning: some -Σ''(E) uncertainty values are missing. "
|
|
674
|
+
"Error bars omitted at those energies."
|
|
675
|
+
)
|
|
676
|
+
kw_imag.setdefault("yerr", xprs.sigma_confidence * imag_sigma)
|
|
677
|
+
kw_imag.setdefault("label", imag_label)
|
|
678
|
+
ax.errorbar(x, imag, **kw_imag)
|
|
679
|
+
|
|
680
|
+
ax.set_xlabel(r"$E-\mu$ (eV)")
|
|
681
|
+
ax.set_ylabel(r"$\Sigma'(E),\ -\Sigma''(E)$ (eV)")
|
|
682
|
+
ax.legend()
|
|
683
|
+
|
|
684
|
+
return fig
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def extract_a2f(self, *, omega_min, omega_max, omega_num, omega_I, omega_M,
|
|
688
|
+
mem=None, **mem_kwargs):
|
|
689
|
+
r"""
|
|
690
|
+
Extract Eliashberg function α²F(ω) from the self-energy. While working
|
|
691
|
+
with band maps and MDCs is more intuitive in eV, the self-energy
|
|
692
|
+
extraction is performed in eV.
|
|
693
|
+
|
|
694
|
+
"""
|
|
695
|
+
from . import settings_parameters as xprs
|
|
696
|
+
|
|
697
|
+
mem_cfg = self._merge_defaults(xprs.mem_defaults, mem, mem_kwargs)
|
|
698
|
+
|
|
699
|
+
method = mem_cfg["method"]
|
|
700
|
+
parts = mem_cfg["parts"]
|
|
701
|
+
iter_max = int(mem_cfg["iter_max"])
|
|
702
|
+
alpha_min = float(mem_cfg["alpha_min"])
|
|
703
|
+
alpha_max = float(mem_cfg["alpha_max"])
|
|
704
|
+
alpha_num = int(mem_cfg["alpha_num"])
|
|
705
|
+
ecut_left = float(mem_cfg["ecut_left"])
|
|
706
|
+
ecut_right = mem_cfg["ecut_right"]
|
|
707
|
+
omega_S = float(mem_cfg["omega_S"])
|
|
708
|
+
f_chi_squared = mem_cfg["f_chi_squared"]
|
|
709
|
+
sigma_svd = float(mem_cfg["sigma_svd"])
|
|
710
|
+
t_criterion = float(mem_cfg["t_criterion"])
|
|
711
|
+
mu = float(mem_cfg["mu"])
|
|
712
|
+
a_guess = float(mem_cfg["a_guess"])
|
|
713
|
+
b_guess = float(mem_cfg["b_guess"])
|
|
714
|
+
c_guess = float(mem_cfg["c_guess"])
|
|
715
|
+
d_guess = float(mem_cfg["d_guess"])
|
|
716
|
+
power = int(mem_cfg["power"])
|
|
717
|
+
lambda_el = float(mem_cfg["lambda_el"])
|
|
718
|
+
impurity_magnitude = float(mem_cfg["impurity_magnitude"])
|
|
719
|
+
W = mem_cfg.get("W", None)
|
|
720
|
+
|
|
721
|
+
if omega_S < 0.0:
|
|
722
|
+
raise ValueError("omega_S must be >= 0.")
|
|
723
|
+
if f_chi_squared is None:
|
|
724
|
+
f_chi_squared = 2.5 if parts == "both" else 2.0
|
|
725
|
+
else:
|
|
726
|
+
f_chi_squared = float(f_chi_squared)
|
|
727
|
+
|
|
728
|
+
h_n = mem_cfg.get("h_n", None)
|
|
729
|
+
if h_n is None:
|
|
730
|
+
raise ValueError(
|
|
731
|
+
"`h_n` must be provided explicitly (h_n=... or mem={'h_n': ...}). "
|
|
732
|
+
"No default is assumed."
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
from . import (create_model_function, create_kernel_function,
|
|
736
|
+
singular_value_decomposition, MEM_core)
|
|
737
|
+
|
|
738
|
+
omega_range = np.linspace(omega_min, omega_max, omega_num)
|
|
739
|
+
|
|
740
|
+
model = create_model_function(omega_range, omega_I, omega_M, omega_S,
|
|
741
|
+
h_n)
|
|
742
|
+
|
|
743
|
+
delta_omega = (omega_max - omega_min) / (omega_num - 1)
|
|
744
|
+
model_in = model * delta_omega
|
|
745
|
+
|
|
746
|
+
energies_eV = self.enel_range
|
|
747
|
+
|
|
748
|
+
ecut_left_eV = ecut_left / KILO
|
|
749
|
+
if ecut_right is None:
|
|
750
|
+
ecut_right_eV = self.energy_resolution
|
|
751
|
+
else:
|
|
752
|
+
ecut_right_eV = float(ecut_right) / KILO
|
|
753
|
+
|
|
754
|
+
Emin = np.min(energies_eV)
|
|
755
|
+
Elow = Emin + ecut_left_eV
|
|
756
|
+
Ehigh = -ecut_right_eV
|
|
757
|
+
mE = (energies_eV >= Elow) & (energies_eV <= Ehigh)
|
|
758
|
+
|
|
759
|
+
if not np.any(mE):
|
|
760
|
+
raise ValueError(
|
|
761
|
+
"Energy cutoffs removed all points; adjust ecut_left/right."
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
energies_eV_masked = energies_eV[mE]
|
|
765
|
+
energies = energies_eV_masked * KILO
|
|
766
|
+
|
|
767
|
+
k_BT = K_B * self.temperature * KILO
|
|
768
|
+
|
|
769
|
+
kernel = create_kernel_function(energies, omega_range, k_BT)
|
|
770
|
+
|
|
771
|
+
if lambda_el:
|
|
772
|
+
if W is None:
|
|
773
|
+
if self._class == "SpectralQuadratic":
|
|
774
|
+
W = (
|
|
775
|
+
PREF * self._fermi_wavevector**2 / self._bare_mass
|
|
776
|
+
) * KILO
|
|
777
|
+
else:
|
|
778
|
+
raise ValueError(
|
|
779
|
+
"lambda_el was provided, but W is None. For a linearised "
|
|
780
|
+
"band (SpectralLinear), you must also provide W in meV: "
|
|
781
|
+
"the electron–electron interaction scale."
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
energies_el = energies_eV_masked * KILO
|
|
786
|
+
real_el, imag_el = self._el_el_self_energy(
|
|
787
|
+
energies_el, k_BT, lambda_el, W, power
|
|
788
|
+
)
|
|
789
|
+
else:
|
|
790
|
+
real_el = 0.0
|
|
791
|
+
imag_el = 0.0
|
|
792
|
+
|
|
793
|
+
if parts == "both":
|
|
794
|
+
real = self.real[mE] * KILO - real_el
|
|
795
|
+
real_sigma = self.real_sigma[mE] * KILO
|
|
796
|
+
imag = self.imag[mE] * KILO - impurity_magnitude - imag_el
|
|
797
|
+
imag_sigma = self.imag_sigma[mE] * KILO
|
|
798
|
+
dvec = np.concatenate((real, imag))
|
|
799
|
+
wvec = np.concatenate((real_sigma**(-2), imag_sigma**(-2)))
|
|
800
|
+
H = np.concatenate((np.real(kernel), -np.imag(kernel)))
|
|
801
|
+
|
|
802
|
+
elif parts == "real":
|
|
803
|
+
real = self.real[mE] * KILO - real_el
|
|
804
|
+
real_sigma = self.real_sigma[mE] * KILO
|
|
805
|
+
dvec = real
|
|
806
|
+
wvec = real_sigma**(-2)
|
|
807
|
+
H = np.real(kernel)
|
|
808
|
+
|
|
809
|
+
else: # parts == "imag"
|
|
810
|
+
imag = self.imag[mE] * KILO - impurity_magnitude - imag_el
|
|
811
|
+
imag_sigma = self.imag_sigma[mE] * KILO
|
|
812
|
+
dvec = imag
|
|
813
|
+
wvec = imag_sigma**(-2)
|
|
814
|
+
H = -np.imag(kernel)
|
|
815
|
+
|
|
816
|
+
V_Sigma, U, uvec = singular_value_decomposition(H, sigma_svd)
|
|
817
|
+
|
|
818
|
+
if method == "chi2kink":
|
|
819
|
+
spectrum_in, _ = self._chi2kink_a2f(
|
|
820
|
+
dvec, model_in, uvec, mu, wvec, V_Sigma, U, alpha_min,
|
|
821
|
+
alpha_max, alpha_num, a_guess, b_guess, c_guess, d_guess,
|
|
822
|
+
f_chi_squared, t_criterion, iter_max, MEM_core
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
spectrum = spectrum_in * omega_num / omega_max
|
|
826
|
+
return spectrum, model
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def bayesian_loop(self, *, omega_min, omega_max, omega_num, omega_I,
|
|
830
|
+
omega_M, fermi_velocity=None,
|
|
831
|
+
fermi_wavevector=None, bare_mass=None, vary=(),
|
|
832
|
+
opt_method="Nelder-Mead", opt_options=None,
|
|
833
|
+
mem=None, loop=None, **mem_kwargs):
|
|
834
|
+
r"""
|
|
835
|
+
Bayesian outer loop calling `_cost_function()`.
|
|
836
|
+
|
|
837
|
+
If `vary` is non-empty, runs a SciPy optimization over the selected
|
|
838
|
+
parameters in `vary`.
|
|
839
|
+
|
|
840
|
+
Supported entries in `vary` depend on `self._class`:
|
|
841
|
+
|
|
842
|
+
- Common: "fermi_wavevector", "impurity_magnitude", "lambda_el", "h_n"
|
|
843
|
+
- SpectralLinear: additionally "fermi_velocity"
|
|
844
|
+
- SpectralQuadratic: additionally "bare_mass"
|
|
845
|
+
|
|
846
|
+
Notes
|
|
847
|
+
-----
|
|
848
|
+
**Convergence behaviour**
|
|
849
|
+
|
|
850
|
+
By default, convergence is controlled by a *custom patience criterion*:
|
|
851
|
+
the optimization terminates when the absolute difference between the
|
|
852
|
+
current cost and the best cost seen so far is smaller than `tole` for
|
|
853
|
+
`converge_iters` consecutive iterations.
|
|
854
|
+
|
|
855
|
+
To instead rely on SciPy's native convergence criteria (e.g. Nelder–Mead
|
|
856
|
+
`xatol` / `fatol`), disable the custom criterion by setting
|
|
857
|
+
`converge_iters=0` or `tole=None`. In that case, SciPy termination options
|
|
858
|
+
supplied via `opt_options` are used.
|
|
859
|
+
|
|
860
|
+
Parameters
|
|
861
|
+
----------
|
|
862
|
+
opt_options : dict, optional
|
|
863
|
+
Options passed directly to `scipy.optimize.minimize`. These are only
|
|
864
|
+
used for convergence if the custom criterion is disabled (see Notes).
|
|
865
|
+
"""
|
|
866
|
+
|
|
867
|
+
fermi_velocity, fermi_wavevector, bare_mass = self._prepare_bare(
|
|
868
|
+
fermi_velocity, fermi_wavevector, bare_mass)
|
|
869
|
+
|
|
870
|
+
vary = tuple(vary) if vary is not None else ()
|
|
871
|
+
|
|
872
|
+
allowed = {"fermi_wavevector", "impurity_magnitude", "lambda_el", "h_n"}
|
|
873
|
+
|
|
874
|
+
if self._class == "SpectralLinear":
|
|
875
|
+
allowed.add("fermi_velocity")
|
|
876
|
+
elif self._class == "SpectralQuadratic":
|
|
877
|
+
allowed.add("bare_mass")
|
|
878
|
+
else:
|
|
879
|
+
raise NotImplementedError(
|
|
880
|
+
f"bayesian_loop does not support spectral class '{self._class}'."
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
unknown = set(vary).difference(allowed)
|
|
884
|
+
if unknown:
|
|
885
|
+
raise ValueError(
|
|
886
|
+
f"Unsupported entries in vary: {sorted(unknown)}. "
|
|
887
|
+
f"Allowed: {sorted(allowed)}."
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
omega_num = int(omega_num)
|
|
891
|
+
if omega_num < 2:
|
|
892
|
+
raise ValueError("omega_num must be an integer >= 2.")
|
|
893
|
+
|
|
894
|
+
from . import settings_parameters as xprs
|
|
895
|
+
|
|
896
|
+
mem_cfg = self._merge_defaults(xprs.mem_defaults, mem, mem_kwargs)
|
|
897
|
+
|
|
898
|
+
parts = mem_cfg["parts"]
|
|
899
|
+
sigma_svd = float(mem_cfg["sigma_svd"])
|
|
900
|
+
ecut_left = float(mem_cfg["ecut_left"])
|
|
901
|
+
ecut_right = mem_cfg["ecut_right"]
|
|
902
|
+
omega_S = float(mem_cfg["omega_S"])
|
|
903
|
+
imp0 = float(mem_cfg["impurity_magnitude"])
|
|
904
|
+
lae0 = float(mem_cfg["lambda_el"])
|
|
905
|
+
h_n0 = float(mem_cfg["h_n"])
|
|
906
|
+
h_n_min = float(mem_cfg.get("h_n_min", 1e-8))
|
|
907
|
+
|
|
908
|
+
loop_overrides = {
|
|
909
|
+
key: val for key, val in mem_kwargs.items()
|
|
910
|
+
if (val is not None) and (key in xprs.loop_defaults)
|
|
911
|
+
}
|
|
912
|
+
loop_cfg = self._merge_defaults(xprs.loop_defaults, loop, loop_overrides)
|
|
913
|
+
|
|
914
|
+
tole = float(loop_cfg["tole"])
|
|
915
|
+
converge_iters = int(loop_cfg["converge_iters"])
|
|
916
|
+
opt_iter_max = int(loop_cfg["opt_iter_max"])
|
|
917
|
+
scale_vF = float(loop_cfg["scale_vF"])
|
|
918
|
+
scale_mb = float(loop_cfg["scale_mb"])
|
|
919
|
+
scale_imp = float(loop_cfg["scale_imp"])
|
|
920
|
+
scale_kF = float(loop_cfg["scale_kF"])
|
|
921
|
+
scale_lambda_el = float(loop_cfg["scale_lambda_el"])
|
|
922
|
+
scale_hn = float(loop_cfg["scale_hn"])
|
|
923
|
+
|
|
924
|
+
rollback_steps = int(loop_cfg.get("rollback_steps"))
|
|
925
|
+
max_retries = int(loop_cfg.get("max_retries"))
|
|
926
|
+
relative_best = float(loop_cfg.get("relative_best"))
|
|
927
|
+
min_steps_for_regression = int(loop_cfg.get("min_steps_for_regression"))
|
|
928
|
+
|
|
929
|
+
if rollback_steps < 0:
|
|
930
|
+
raise ValueError("rollback_steps must be >= 0.")
|
|
931
|
+
if max_retries < 0:
|
|
932
|
+
raise ValueError("max_retries must be >= 0.")
|
|
933
|
+
if relative_best <= 0.0:
|
|
934
|
+
raise ValueError("relative_best must be > 0.")
|
|
935
|
+
if min_steps_for_regression < 0:
|
|
936
|
+
raise ValueError("min_steps_for_regression must be >= 0.")
|
|
937
|
+
|
|
938
|
+
vF0 = float(fermi_velocity) if fermi_velocity is not None else None
|
|
939
|
+
kF0 = float(fermi_wavevector) if fermi_wavevector is not None else None
|
|
940
|
+
mb0 = float(bare_mass) if bare_mass is not None else None
|
|
941
|
+
|
|
942
|
+
if lae0 < 0.0:
|
|
943
|
+
raise ValueError("Initial lambda_el must be >= 0.")
|
|
944
|
+
if imp0 < 0.0:
|
|
945
|
+
raise ValueError("Initial impurity_magnitude must be >= 0.")
|
|
946
|
+
if omega_S < 0.0:
|
|
947
|
+
raise ValueError("omega_S must be >= 0.")
|
|
948
|
+
if h_n_min <= 0.0:
|
|
949
|
+
raise ValueError("h_n_min must be > 0.")
|
|
950
|
+
if h_n0 < h_n_min:
|
|
951
|
+
raise ValueError(
|
|
952
|
+
f"Initial h_n ({h_n0:g}) must be >= h_n_min ({h_n_min:g})."
|
|
953
|
+
)
|
|
954
|
+
if kF0 is None:
|
|
955
|
+
raise ValueError(
|
|
956
|
+
"bayesian_loop requires an initial fermi_wavevector."
|
|
957
|
+
)
|
|
958
|
+
if self._class == "SpectralLinear" and vF0 is None:
|
|
959
|
+
raise ValueError(
|
|
960
|
+
"bayesian_loop requires an initial fermi_velocity."
|
|
961
|
+
)
|
|
962
|
+
if self._class == "SpectralQuadratic" and mb0 is None:
|
|
963
|
+
raise ValueError("bayesian_loop requires an initial bare_mass.")
|
|
964
|
+
|
|
965
|
+
from scipy.optimize import minimize
|
|
966
|
+
from . import create_kernel_function, singular_value_decomposition
|
|
967
|
+
|
|
968
|
+
ecut_left = float(mem_cfg["ecut_left"])
|
|
969
|
+
ecut_right = mem_cfg["ecut_right"]
|
|
970
|
+
|
|
971
|
+
ecut_left_eV = ecut_left / KILO
|
|
972
|
+
if ecut_right is None:
|
|
973
|
+
ecut_right_eV = self.energy_resolution
|
|
974
|
+
else:
|
|
975
|
+
ecut_right_eV = float(ecut_right) / KILO
|
|
976
|
+
|
|
977
|
+
energies_eV = self.enel_range
|
|
978
|
+
Emin = np.min(energies_eV)
|
|
979
|
+
Elow = Emin + ecut_left_eV
|
|
980
|
+
Ehigh = -ecut_right_eV
|
|
981
|
+
mE = (energies_eV >= Elow) & (energies_eV <= Ehigh)
|
|
982
|
+
|
|
983
|
+
if not np.any(mE):
|
|
984
|
+
raise ValueError(
|
|
985
|
+
"Energy cutoffs removed all points; adjust ecut_left/right."
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
energies_eV_masked = energies_eV[mE]
|
|
989
|
+
energies = energies_eV_masked * KILO
|
|
990
|
+
|
|
991
|
+
k_BT = K_B * self.temperature * KILO
|
|
992
|
+
omega_range = np.linspace(omega_min, omega_max, omega_num)
|
|
993
|
+
|
|
994
|
+
kernel_raw = create_kernel_function(energies, omega_range, k_BT)
|
|
995
|
+
|
|
996
|
+
if parts == "both":
|
|
997
|
+
kernel_used = np.concatenate((np.real(kernel_raw), -np.imag(kernel_raw)))
|
|
998
|
+
elif parts == "real":
|
|
999
|
+
kernel_used = np.real(kernel_raw)
|
|
1000
|
+
else: # parts == "imag"
|
|
1001
|
+
kernel_used = -np.imag(kernel_raw)
|
|
1002
|
+
|
|
1003
|
+
V_Sigma, U, uvec0 = singular_value_decomposition(kernel_used, sigma_svd)
|
|
1004
|
+
|
|
1005
|
+
_precomp = {
|
|
1006
|
+
"omega_range": omega_range,
|
|
1007
|
+
"mE": mE,
|
|
1008
|
+
"energies_eV_masked": energies_eV_masked,
|
|
1009
|
+
"V_Sigma": V_Sigma,
|
|
1010
|
+
"U": U,
|
|
1011
|
+
"uvec0": uvec0,
|
|
1012
|
+
"ecut_left": ecut_left,
|
|
1013
|
+
"ecut_right": ecut_right,
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
def _reflect_min(xi, p0, p_min, scale):
|
|
1017
|
+
"""Map R -> [p_min, +inf) using linear reflection around p_min."""
|
|
1018
|
+
return p_min + np.abs((float(p0) - p_min) + scale * float(xi))
|
|
1019
|
+
|
|
1020
|
+
def _unpack_params(x):
|
|
1021
|
+
params = {}
|
|
1022
|
+
|
|
1023
|
+
i = 0
|
|
1024
|
+
for name in vary:
|
|
1025
|
+
xi = float(x[i])
|
|
1026
|
+
|
|
1027
|
+
if name == "fermi_velocity":
|
|
1028
|
+
if vF0 is None:
|
|
1029
|
+
raise ValueError("Cannot vary fermi_velocity: no "
|
|
1030
|
+
"initial vF provided.")
|
|
1031
|
+
params["fermi_velocity"] = vF0 + scale_vF * xi
|
|
1032
|
+
|
|
1033
|
+
elif name == "bare_mass":
|
|
1034
|
+
if mb0 is None:
|
|
1035
|
+
raise ValueError("Cannot vary bare_mass: no initial "
|
|
1036
|
+
"bare_mass provided.")
|
|
1037
|
+
params["bare_mass"] = mb0 + scale_mb * xi
|
|
1038
|
+
|
|
1039
|
+
elif name == "fermi_wavevector":
|
|
1040
|
+
if kF0 is None:
|
|
1041
|
+
raise ValueError(
|
|
1042
|
+
"Cannot vary fermi_wavevector: no initial kF "
|
|
1043
|
+
"provided."
|
|
1044
|
+
)
|
|
1045
|
+
params["fermi_wavevector"] = kF0 + scale_kF * xi
|
|
1046
|
+
|
|
1047
|
+
elif name == "impurity_magnitude":
|
|
1048
|
+
params["impurity_magnitude"] = _reflect_min(xi, imp0, 0.0, scale_imp)
|
|
1049
|
+
|
|
1050
|
+
elif name == "lambda_el":
|
|
1051
|
+
params["lambda_el"] = _reflect_min(xi, lae0, 0.0, scale_lambda_el)
|
|
1052
|
+
|
|
1053
|
+
elif name == "h_n":
|
|
1054
|
+
params["h_n"] = _reflect_min(xi, h_n0, h_n_min, scale_hn)
|
|
1055
|
+
|
|
1056
|
+
i += 1
|
|
1057
|
+
|
|
1058
|
+
params.setdefault("fermi_wavevector", kF0)
|
|
1059
|
+
params.setdefault("impurity_magnitude", imp0)
|
|
1060
|
+
params.setdefault("lambda_el", lae0)
|
|
1061
|
+
params.setdefault("h_n", h_n0)
|
|
1062
|
+
|
|
1063
|
+
if self._class == "SpectralLinear":
|
|
1064
|
+
params.setdefault("fermi_velocity", vF0)
|
|
1065
|
+
elif self._class == "SpectralQuadratic":
|
|
1066
|
+
params.setdefault("bare_mass", mb0)
|
|
1067
|
+
|
|
1068
|
+
return params
|
|
1069
|
+
|
|
1070
|
+
def _evaluate_cost(params):
|
|
1071
|
+
optimisation_parameters = {
|
|
1072
|
+
"h_n": params["h_n"],
|
|
1073
|
+
"impurity_magnitude": params["impurity_magnitude"],
|
|
1074
|
+
"lambda_el": params["lambda_el"],
|
|
1075
|
+
"fermi_wavevector": params["fermi_wavevector"],
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if self._class == "SpectralLinear":
|
|
1079
|
+
optimisation_parameters["fermi_velocity"] = params["fermi_velocity"]
|
|
1080
|
+
elif self._class == "SpectralQuadratic":
|
|
1081
|
+
optimisation_parameters["bare_mass"] = params["bare_mass"]
|
|
1082
|
+
else:
|
|
1083
|
+
raise NotImplementedError(
|
|
1084
|
+
f"_evaluate_cost does not support class '{self._class}'."
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
return self._cost_function(
|
|
1088
|
+
optimisation_parameters=optimisation_parameters,
|
|
1089
|
+
omega_min=omega_min, omega_max=omega_max, omega_num=omega_num,
|
|
1090
|
+
omega_I=omega_I, omega_M=omega_M, mem_cfg=mem_cfg,
|
|
1091
|
+
_precomp=_precomp
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
last = {"cost": None, "spectrum": None, "model": None, "alpha": None}
|
|
1095
|
+
|
|
1096
|
+
iter_counter = {"n": 0}
|
|
1097
|
+
|
|
1098
|
+
class ConvergenceException(RuntimeError):
|
|
1099
|
+
"""Raised when optimisation has converged successfully."""
|
|
1100
|
+
|
|
1101
|
+
class RegressionException(RuntimeError):
|
|
1102
|
+
"""Raised when optimizer regresses toward the initial guess."""
|
|
1103
|
+
|
|
1104
|
+
if converge_iters is None:
|
|
1105
|
+
converge_iters = 0
|
|
1106
|
+
converge_iters = int(converge_iters)
|
|
1107
|
+
|
|
1108
|
+
if tole is not None:
|
|
1109
|
+
tole = float(tole)
|
|
1110
|
+
if tole < 0.0:
|
|
1111
|
+
raise ValueError("tole must be >= 0.")
|
|
1112
|
+
if converge_iters < 0:
|
|
1113
|
+
raise ValueError("converge_iters must be >= 0.")
|
|
1114
|
+
|
|
1115
|
+
# Track best solution seen across all obj calls (not just last).
|
|
1116
|
+
best_global = {
|
|
1117
|
+
"x": None,
|
|
1118
|
+
"params": None,
|
|
1119
|
+
"cost": np.inf,
|
|
1120
|
+
"spectrum": None,
|
|
1121
|
+
"model": None,
|
|
1122
|
+
"alpha": None,
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
history = []
|
|
1126
|
+
|
|
1127
|
+
# Cache most recent evaluation so the callback can read a cost without
|
|
1128
|
+
# forcing an extra objective evaluation.
|
|
1129
|
+
last_x = {"x": None}
|
|
1130
|
+
last_cost = {"cost": None}
|
|
1131
|
+
initial_cost = {"cost": None}
|
|
1132
|
+
|
|
1133
|
+
iter_counter = {"n": 0}
|
|
1134
|
+
|
|
1135
|
+
def _clean_params(params):
|
|
1136
|
+
"""Convert NumPy scalar values to plain Python scalars."""
|
|
1137
|
+
out = {}
|
|
1138
|
+
for key, val in params.items():
|
|
1139
|
+
if isinstance(val, np.generic):
|
|
1140
|
+
out[key] = float(val)
|
|
1141
|
+
else:
|
|
1142
|
+
out[key] = val
|
|
1143
|
+
return out
|
|
1144
|
+
|
|
1145
|
+
def obj(x):
|
|
1146
|
+
import warnings
|
|
1147
|
+
|
|
1148
|
+
iter_counter["n"] += 1
|
|
1149
|
+
|
|
1150
|
+
params = _unpack_params(x)
|
|
1151
|
+
|
|
1152
|
+
with warnings.catch_warnings():
|
|
1153
|
+
warnings.simplefilter("error", RuntimeWarning)
|
|
1154
|
+
try:
|
|
1155
|
+
cost, spectrum, model, alpha_select = _evaluate_cost(params)
|
|
1156
|
+
except RuntimeWarning as exc:
|
|
1157
|
+
raise ValueError(f"RuntimeWarning during cost eval: {exc}") from exc
|
|
1158
|
+
cost_f = float(cost)
|
|
1159
|
+
|
|
1160
|
+
history.append(
|
|
1161
|
+
{
|
|
1162
|
+
"x": np.array(x, dtype=float, copy=True),
|
|
1163
|
+
"params": _clean_params(params),
|
|
1164
|
+
"cost": cost_f,
|
|
1165
|
+
"spectrum": spectrum,
|
|
1166
|
+
"model": model,
|
|
1167
|
+
"alpha": float(alpha_select),
|
|
1168
|
+
}
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
last["cost"] = cost_f
|
|
1172
|
+
last["spectrum"] = spectrum
|
|
1173
|
+
last["model"] = model
|
|
1174
|
+
last["alpha"] = float(alpha_select)
|
|
1175
|
+
|
|
1176
|
+
last_x["x"] = np.array(x, dtype=float, copy=True)
|
|
1177
|
+
last_cost["cost"] = cost_f
|
|
1178
|
+
|
|
1179
|
+
if initial_cost["cost"] is None:
|
|
1180
|
+
initial_cost["cost"] = cost_f
|
|
1181
|
+
|
|
1182
|
+
if cost_f < best_global["cost"]:
|
|
1183
|
+
best_global["x"] = np.array(x, dtype=float, copy=True)
|
|
1184
|
+
best_global["cost"] = cost_f
|
|
1185
|
+
best_global["params"] = _clean_params(params)
|
|
1186
|
+
best_global["spectrum"] = spectrum
|
|
1187
|
+
best_global["model"] = model
|
|
1188
|
+
best_global["alpha"] = float(alpha_select)
|
|
1189
|
+
|
|
1190
|
+
msg = [f"Iter {iter_counter['n']:4d} | cost = {cost: .4e}"]
|
|
1191
|
+
for key in sorted(params):
|
|
1192
|
+
msg.append(f"{key}={params[key]:.8g}")
|
|
1193
|
+
print(" | ".join(msg))
|
|
1194
|
+
|
|
1195
|
+
return cost_f
|
|
1196
|
+
|
|
1197
|
+
class TerminationCallback:
|
|
1198
|
+
def __init__(self, tole, converge_iters, min_steps_for_regression):
|
|
1199
|
+
self.tole = None if tole is None else float(tole)
|
|
1200
|
+
self.converge_iters = int(converge_iters)
|
|
1201
|
+
self.min_steps_for_regression = int(min_steps_for_regression)
|
|
1202
|
+
self.iter_count = 0
|
|
1203
|
+
self.call_count = 0
|
|
1204
|
+
|
|
1205
|
+
def __call__(self, xk):
|
|
1206
|
+
self.call_count += 1
|
|
1207
|
+
|
|
1208
|
+
if self.tole is None or self.converge_iters <= 0:
|
|
1209
|
+
return
|
|
1210
|
+
|
|
1211
|
+
current = last_cost["cost"]
|
|
1212
|
+
if current is None:
|
|
1213
|
+
return
|
|
1214
|
+
|
|
1215
|
+
best_cost = float(best["cost"])
|
|
1216
|
+
if np.isfinite(best_cost) and abs(current - best_cost) < self.tole:
|
|
1217
|
+
self.iter_count += 1
|
|
1218
|
+
else:
|
|
1219
|
+
self.iter_count = 0
|
|
1220
|
+
|
|
1221
|
+
if self.iter_count >= self.converge_iters:
|
|
1222
|
+
raise ConvergenceException(
|
|
1223
|
+
f"Converged: |cost-best| < {self.tole:g} for "
|
|
1224
|
+
f"{self.converge_iters} iterations."
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
if self.call_count < self.min_steps_for_regression:
|
|
1228
|
+
return
|
|
1229
|
+
|
|
1230
|
+
current = float(current)
|
|
1231
|
+
init = initial_cost["cost"]
|
|
1232
|
+
if init is None:
|
|
1233
|
+
return
|
|
1234
|
+
|
|
1235
|
+
best_cost = float(best["cost"])
|
|
1236
|
+
if not np.isfinite(best_cost):
|
|
1237
|
+
return
|
|
1238
|
+
|
|
1239
|
+
if abs(current - init) * relative_best < abs(current - best_cost):
|
|
1240
|
+
raise RegressionException(
|
|
1241
|
+
"Regression toward initial guess detected."
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
callback = TerminationCallback(
|
|
1246
|
+
tole=tole,
|
|
1247
|
+
converge_iters=converge_iters,
|
|
1248
|
+
min_steps_for_regression=min_steps_for_regression,
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
if not vary:
|
|
1252
|
+
params = _unpack_params(np.zeros(0, dtype=float))
|
|
1253
|
+
cost, spectrum, model, alpha_select = _evaluate_cost(params)
|
|
1254
|
+
return cost, spectrum, model, alpha_select
|
|
1255
|
+
|
|
1256
|
+
x0 = np.zeros(len(vary), dtype=float)
|
|
1257
|
+
|
|
1258
|
+
options = {} if opt_options is None else dict(opt_options)
|
|
1259
|
+
options.setdefault("maxiter", int(opt_iter_max))
|
|
1260
|
+
|
|
1261
|
+
use_patience = (tole is not None) and (int(converge_iters) > 0)
|
|
1262
|
+
if use_patience:
|
|
1263
|
+
options.pop("xatol", None)
|
|
1264
|
+
options.pop("fatol", None)
|
|
1265
|
+
|
|
1266
|
+
retry_count = 0
|
|
1267
|
+
res = None
|
|
1268
|
+
|
|
1269
|
+
while retry_count <= max_retries:
|
|
1270
|
+
best = {
|
|
1271
|
+
"x": None,
|
|
1272
|
+
"params": None,
|
|
1273
|
+
"cost": np.inf,
|
|
1274
|
+
"spectrum": None,
|
|
1275
|
+
"model": None,
|
|
1276
|
+
"alpha": None,
|
|
1277
|
+
}
|
|
1278
|
+
last_x["x"] = None
|
|
1279
|
+
last_cost["cost"] = None
|
|
1280
|
+
initial_cost["cost"] = None
|
|
1281
|
+
iter_counter["n"] = 0
|
|
1282
|
+
history.clear()
|
|
1283
|
+
|
|
1284
|
+
callback = TerminationCallback(
|
|
1285
|
+
tole=tole,
|
|
1286
|
+
converge_iters=converge_iters,
|
|
1287
|
+
min_steps_for_regression=min_steps_for_regression,
|
|
1288
|
+
)
|
|
1289
|
+
|
|
1290
|
+
try:
|
|
1291
|
+
res = minimize(
|
|
1292
|
+
obj,
|
|
1293
|
+
x0,
|
|
1294
|
+
method=opt_method,
|
|
1295
|
+
options=options,
|
|
1296
|
+
callback=callback,
|
|
1297
|
+
)
|
|
1298
|
+
break
|
|
1299
|
+
|
|
1300
|
+
except ConvergenceException as exc:
|
|
1301
|
+
print(str(exc))
|
|
1302
|
+
res = None
|
|
1303
|
+
break
|
|
1304
|
+
|
|
1305
|
+
except RegressionException as exc:
|
|
1306
|
+
print(f"{exc} Rolling back {rollback_steps} steps.")
|
|
1307
|
+
retry_count += 1
|
|
1308
|
+
|
|
1309
|
+
if rollback_steps <= 0 or not history:
|
|
1310
|
+
continue
|
|
1311
|
+
|
|
1312
|
+
back = min(int(rollback_steps), len(history))
|
|
1313
|
+
x0 = np.array(history[-back]["x"], dtype=float, copy=True)
|
|
1314
|
+
continue
|
|
1315
|
+
|
|
1316
|
+
except ValueError as exc:
|
|
1317
|
+
print(f"ValueError encountered: {exc}. Rolling back.")
|
|
1318
|
+
retry_count += 1
|
|
1319
|
+
|
|
1320
|
+
if rollback_steps <= 0 or not history:
|
|
1321
|
+
continue
|
|
1322
|
+
|
|
1323
|
+
back = min(int(rollback_steps), len(history))
|
|
1324
|
+
x0 = np.array(history[-back]["x"], dtype=float, copy=True)
|
|
1325
|
+
continue
|
|
1326
|
+
|
|
1327
|
+
if retry_count > max_retries:
|
|
1328
|
+
print("Max retries reached. Parameters may not be optimal.")
|
|
1329
|
+
|
|
1330
|
+
if best_global["params"] is None:
|
|
1331
|
+
params = _unpack_params(x0)
|
|
1332
|
+
cost, spectrum, model, alpha_select = _evaluate_cost(params)
|
|
1333
|
+
else:
|
|
1334
|
+
params = best_global["params"]
|
|
1335
|
+
cost = best_global["cost"]
|
|
1336
|
+
spectrum = best_global["spectrum"]
|
|
1337
|
+
model = best_global["model"]
|
|
1338
|
+
alpha_select = best_global["alpha"]
|
|
1339
|
+
|
|
1340
|
+
args = ", ".join(
|
|
1341
|
+
f"{key}={params[key]:.10g}" if isinstance(params[key], float)
|
|
1342
|
+
else f"{key}={params[key]}"
|
|
1343
|
+
for key in sorted(params)
|
|
1344
|
+
)
|
|
1345
|
+
print("Optimised parameters:")
|
|
1346
|
+
print(args)
|
|
1347
|
+
|
|
1348
|
+
return cost, spectrum, model, alpha_select, params
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
@staticmethod
|
|
1352
|
+
def _merge_defaults(defaults, override_dict=None, override_kwargs=None):
|
|
1353
|
+
"""Merge defaults with dict + kwargs overrides (kwargs win)."""
|
|
1354
|
+
cfg = dict(defaults)
|
|
1355
|
+
if override_dict:
|
|
1356
|
+
cfg.update(dict(override_dict))
|
|
1357
|
+
if override_kwargs:
|
|
1358
|
+
cfg.update({k: v for k, v in override_kwargs.items() if v is not None})
|
|
1359
|
+
return cfg
|
|
1360
|
+
|
|
1361
|
+
def _prepare_bare(self, fermi_velocity, fermi_wavevector, bare_mass):
|
|
1362
|
+
"""Validate class-compatible band parameters and infer missing defaults.
|
|
1363
|
+
|
|
1364
|
+
Enforces:
|
|
1365
|
+
- SpectralLinear: bare_mass must be None; vF and kF must be available.
|
|
1366
|
+
- SpectralQuadratic: fermi_velocity must be None; bare_mass and kF must
|
|
1367
|
+
be available.
|
|
1368
|
+
|
|
1369
|
+
Returns
|
|
1370
|
+
-------
|
|
1371
|
+
fermi_velocity : float or None
|
|
1372
|
+
Initial vF (Linear) or None (Quadratic).
|
|
1373
|
+
fermi_wavevector : float
|
|
1374
|
+
Initial kF.
|
|
1375
|
+
bare_mass : float or None
|
|
1376
|
+
Initial bare mass (Quadratic) or None (Linear).
|
|
1377
|
+
"""
|
|
1378
|
+
if self._class == "SpectralLinear":
|
|
1379
|
+
if bare_mass is not None:
|
|
1380
|
+
raise ValueError(
|
|
1381
|
+
"SpectralLinear bayesian_loop does not accept "
|
|
1382
|
+
"`bare_mass`. Provide `fermi_velocity` instead."
|
|
1383
|
+
)
|
|
1384
|
+
|
|
1385
|
+
if fermi_velocity is None:
|
|
1386
|
+
fermi_velocity = getattr(self, "fermi_velocity", None)
|
|
1387
|
+
if fermi_velocity is None:
|
|
1388
|
+
raise ValueError(
|
|
1389
|
+
"SpectralLinear optimisation requires an initial "
|
|
1390
|
+
"fermi_velocity to be provided."
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
if fermi_wavevector is None:
|
|
1394
|
+
fermi_wavevector = getattr(self, "fermi_wavevector", None)
|
|
1395
|
+
if fermi_wavevector is None:
|
|
1396
|
+
raise ValueError(
|
|
1397
|
+
"SpectralLinear optimisation requires an initial "
|
|
1398
|
+
"fermi_wavevector to be provided."
|
|
1399
|
+
)
|
|
1400
|
+
|
|
1401
|
+
return float(fermi_velocity), float(fermi_wavevector), None
|
|
1402
|
+
|
|
1403
|
+
elif self._class == "SpectralQuadratic":
|
|
1404
|
+
if fermi_velocity is not None:
|
|
1405
|
+
raise ValueError(
|
|
1406
|
+
"SpectralQuadratic bayesian_loop does not accept "
|
|
1407
|
+
"`fermi_velocity`. Provide `bare_mass` instead."
|
|
1408
|
+
)
|
|
1409
|
+
|
|
1410
|
+
if bare_mass is None:
|
|
1411
|
+
bare_mass = getattr(self, "_bare_mass", None)
|
|
1412
|
+
if bare_mass is None:
|
|
1413
|
+
raise ValueError(
|
|
1414
|
+
"SpectralQuadratic optimisation requires an initial "
|
|
1415
|
+
"bare_mass to be provided."
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
if fermi_wavevector is None:
|
|
1419
|
+
fermi_wavevector = getattr(self, "fermi_wavevector", None)
|
|
1420
|
+
if fermi_wavevector is None:
|
|
1421
|
+
raise ValueError(
|
|
1422
|
+
"SpectralQuadratic optimisation requires an initial "
|
|
1423
|
+
"fermi_wavevector to be provided."
|
|
1424
|
+
)
|
|
1425
|
+
|
|
1426
|
+
return None, float(fermi_wavevector), float(bare_mass)
|
|
1427
|
+
|
|
1428
|
+
else:
|
|
1429
|
+
raise NotImplementedError(
|
|
1430
|
+
f"_prepare_bare is not implemented for spectral class "
|
|
1431
|
+
"'{self._class}'.")
|
|
1432
|
+
|
|
1433
|
+
def _cost_function(self, *, optimisation_parameters, omega_min, omega_max,
|
|
1434
|
+
omega_num, omega_I, omega_M, mem_cfg, _precomp):
|
|
1435
|
+
r"""TBD
|
|
1436
|
+
|
|
1437
|
+
Negative log-posterior cost function for Bayesian optimisation.
|
|
1438
|
+
|
|
1439
|
+
This mirrors `extract_a2f()` but recomputes the self-energy arrays for the
|
|
1440
|
+
candidate optimisation parameters instead of using cached `self.real/imag`.
|
|
1441
|
+
|
|
1442
|
+
Parameters
|
|
1443
|
+
----------
|
|
1444
|
+
optimisation_parameters : dict
|
|
1445
|
+
Must include at least keys: "h_n", "impurity_magnitude", "lambda_el".
|
|
1446
|
+
For SpectralLinear, must also include "fermi_velocity" and
|
|
1447
|
+
"fermi_wavevector". For SpectralQuadratic, "bare_mass" is optional
|
|
1448
|
+
(falls back to `self._bare_mass` if present).
|
|
1449
|
+
|
|
1450
|
+
Returns
|
|
1451
|
+
-------
|
|
1452
|
+
cost : float
|
|
1453
|
+
Negative log-posterior evaluated at the selected alpha.
|
|
1454
|
+
spectrum : ndarray
|
|
1455
|
+
Rescaled α²F(ω) spectrum (same scaling convention as `extract_a2f()`).
|
|
1456
|
+
model : ndarray
|
|
1457
|
+
The model spectrum used by MEM (same as `extract_a2f()`).
|
|
1458
|
+
alpha_select : float
|
|
1459
|
+
The selected alpha returned by `_chi2kink_a2f`.
|
|
1460
|
+
"""
|
|
1461
|
+
|
|
1462
|
+
required = {"h_n", "impurity_magnitude", "lambda_el"}
|
|
1463
|
+
missing = required.difference(optimisation_parameters)
|
|
1464
|
+
if missing:
|
|
1465
|
+
raise ValueError(
|
|
1466
|
+
f"Missing optimisation parameters: {sorted(missing)}"
|
|
1467
|
+
)
|
|
1468
|
+
|
|
1469
|
+
parts = mem_cfg["parts"]
|
|
1470
|
+
method = mem_cfg["method"]
|
|
1471
|
+
alpha_min = float(mem_cfg["alpha_min"])
|
|
1472
|
+
alpha_max = float(mem_cfg["alpha_max"])
|
|
1473
|
+
alpha_num = int(mem_cfg["alpha_num"])
|
|
1474
|
+
omega_S = float(mem_cfg["omega_S"])
|
|
1475
|
+
|
|
1476
|
+
mu = float(mem_cfg["mu"])
|
|
1477
|
+
a_guess = float(mem_cfg["a_guess"])
|
|
1478
|
+
b_guess = float(mem_cfg["b_guess"])
|
|
1479
|
+
c_guess = float(mem_cfg["c_guess"])
|
|
1480
|
+
d_guess = float(mem_cfg["d_guess"])
|
|
1481
|
+
|
|
1482
|
+
f_chi_squared = mem_cfg["f_chi_squared"]
|
|
1483
|
+
power = int(mem_cfg["power"])
|
|
1484
|
+
W = mem_cfg.get("W", None)
|
|
1485
|
+
t_criterion = float(mem_cfg["t_criterion"])
|
|
1486
|
+
iter_max = int(mem_cfg["iter_max"])
|
|
1487
|
+
|
|
1488
|
+
if f_chi_squared is None:
|
|
1489
|
+
f_chi_squared = 2.5 if parts == "both" else 2.0
|
|
1490
|
+
else:
|
|
1491
|
+
f_chi_squared = float(f_chi_squared)
|
|
1492
|
+
|
|
1493
|
+
h_n = mem_cfg.get("h_n", None)
|
|
1494
|
+
if h_n is None:
|
|
1495
|
+
raise ValueError(
|
|
1496
|
+
"`h_n` must be provided explicitly (h_n=... or mem={'h_n': ...}). "
|
|
1497
|
+
"No default is assumed."
|
|
1498
|
+
)
|
|
1499
|
+
|
|
1500
|
+
if parts not in {"both", "real", "imag"}:
|
|
1501
|
+
raise ValueError("parts must be one of {'both', 'real', 'imag'}")
|
|
1502
|
+
|
|
1503
|
+
if method != "chi2kink":
|
|
1504
|
+
raise NotImplementedError(
|
|
1505
|
+
"Only method='chi2kink' is currently implemented."
|
|
1506
|
+
)
|
|
1507
|
+
|
|
1508
|
+
impurity_magnitude = float(optimisation_parameters["impurity_magnitude"])
|
|
1509
|
+
lambda_el = float(optimisation_parameters["lambda_el"])
|
|
1510
|
+
h_n = float(optimisation_parameters["h_n"])
|
|
1511
|
+
|
|
1512
|
+
fermi_velocity = None
|
|
1513
|
+
fermi_wavevector = None
|
|
1514
|
+
bare_mass = None
|
|
1515
|
+
|
|
1516
|
+
if self._class == "SpectralLinear":
|
|
1517
|
+
required_lin = {"fermi_velocity", "fermi_wavevector"}
|
|
1518
|
+
missing_lin = required_lin.difference(optimisation_parameters)
|
|
1519
|
+
if missing_lin:
|
|
1520
|
+
raise ValueError(
|
|
1521
|
+
"SpectralLinear requires optimisation_parameters to include "
|
|
1522
|
+
f"{sorted(missing_lin)}."
|
|
1523
|
+
)
|
|
1524
|
+
fermi_velocity = optimisation_parameters["fermi_velocity"]
|
|
1525
|
+
fermi_wavevector = optimisation_parameters["fermi_wavevector"]
|
|
1526
|
+
|
|
1527
|
+
elif self._class == "SpectralQuadratic":
|
|
1528
|
+
if "fermi_wavevector" not in optimisation_parameters:
|
|
1529
|
+
raise ValueError(
|
|
1530
|
+
"SpectralQuadratic requires optimisation_parameters to include "
|
|
1531
|
+
"'fermi_wavevector'."
|
|
1532
|
+
)
|
|
1533
|
+
fermi_wavevector = optimisation_parameters["fermi_wavevector"]
|
|
1534
|
+
|
|
1535
|
+
bare_mass = optimisation_parameters.get("bare_mass", None)
|
|
1536
|
+
if bare_mass is None:
|
|
1537
|
+
bare_mass = getattr(self, "_bare_mass", None)
|
|
1538
|
+
|
|
1539
|
+
else:
|
|
1540
|
+
raise NotImplementedError(
|
|
1541
|
+
f"_cost_function does not support class '{self._class}'."
|
|
1542
|
+
)
|
|
1543
|
+
|
|
1544
|
+
from . import create_model_function, MEM_core
|
|
1545
|
+
|
|
1546
|
+
if f_chi_squared is None:
|
|
1547
|
+
f_chi_squared = 2.5 if parts == "both" else 2.0
|
|
1548
|
+
|
|
1549
|
+
if _precomp is None:
|
|
1550
|
+
raise ValueError(
|
|
1551
|
+
"_precomp is None in _cost_function. Pass the precomputed"
|
|
1552
|
+
" kernel/SVD bundle from bayesian_loop."
|
|
1553
|
+
)
|
|
1554
|
+
|
|
1555
|
+
omega_range = _precomp["omega_range"]
|
|
1556
|
+
mE = _precomp["mE"]
|
|
1557
|
+
energies_eV_masked = _precomp["energies_eV_masked"]
|
|
1558
|
+
|
|
1559
|
+
V_Sigma = _precomp["V_Sigma"]
|
|
1560
|
+
U = _precomp["U"]
|
|
1561
|
+
uvec = np.array(_precomp["uvec0"], copy=True)
|
|
1562
|
+
|
|
1563
|
+
if f_chi_squared is None:
|
|
1564
|
+
f_chi_squared = 2.5 if parts == "both" else 2.0
|
|
1565
|
+
|
|
1566
|
+
model = create_model_function(omega_range, omega_I, omega_M, omega_S, h_n)
|
|
1567
|
+
|
|
1568
|
+
delta_omega = (omega_max - omega_min) / (omega_num - 1)
|
|
1569
|
+
model_in = model * delta_omega
|
|
1570
|
+
|
|
1571
|
+
k_BT = K_B * self.temperature * KILO
|
|
1572
|
+
|
|
1573
|
+
if lambda_el:
|
|
1574
|
+
if W is None:
|
|
1575
|
+
if self._class == "SpectralQuadratic":
|
|
1576
|
+
if fermi_wavevector is None or bare_mass is None:
|
|
1577
|
+
raise ValueError(
|
|
1578
|
+
"lambda_el is nonzero, but W is None and cannot be "
|
|
1579
|
+
"inferred. Provide W (meV), or pass both "
|
|
1580
|
+
"`fermi_wavevector` and `bare_mass`."
|
|
1581
|
+
)
|
|
1582
|
+
W = (PREF * fermi_wavevector**2 / bare_mass) * KILO
|
|
1583
|
+
else:
|
|
1584
|
+
raise ValueError(
|
|
1585
|
+
"lambda_el was provided, but W is None. For "
|
|
1586
|
+
"SpectralLinear you must provide W in meV."
|
|
1587
|
+
)
|
|
1588
|
+
|
|
1589
|
+
energies_el = energies_eV_masked * KILO
|
|
1590
|
+
real_el, imag_el = self._el_el_self_energy(
|
|
1591
|
+
energies_el, k_BT, lambda_el, W, power
|
|
1592
|
+
)
|
|
1593
|
+
else:
|
|
1594
|
+
real_el = 0.0
|
|
1595
|
+
imag_el = 0.0
|
|
1596
|
+
|
|
1597
|
+
real, real_sigma, imag, imag_sigma = self._evaluate_self_energy_arrays(
|
|
1598
|
+
fermi_velocity=fermi_velocity,
|
|
1599
|
+
fermi_wavevector=fermi_wavevector,
|
|
1600
|
+
bare_mass=bare_mass,
|
|
1601
|
+
)
|
|
1602
|
+
if real is None or imag is None:
|
|
1603
|
+
raise ValueError(
|
|
1604
|
+
"Cannot compute self-energy arrays for cost evaluation. "
|
|
1605
|
+
"Ensure the required band parameters and peak/broadening " \
|
|
1606
|
+
"inputs are set.")
|
|
1607
|
+
|
|
1608
|
+
real_m = real[mE] * KILO - real_el
|
|
1609
|
+
imag_m = imag[mE] * KILO - impurity_magnitude - imag_el
|
|
1610
|
+
|
|
1611
|
+
if parts == "both":
|
|
1612
|
+
real_sig_m = real_sigma[mE] * KILO
|
|
1613
|
+
imag_sig_m = imag_sigma[mE] * KILO
|
|
1614
|
+
dvec = np.concatenate((real_m, imag_m))
|
|
1615
|
+
wvec = np.concatenate((real_sig_m**(-2), imag_sig_m**(-2)))
|
|
1616
|
+
elif parts == "real":
|
|
1617
|
+
real_sig_m = real_sigma[mE] * KILO
|
|
1618
|
+
dvec = real_m
|
|
1619
|
+
wvec = real_sig_m**(-2)
|
|
1620
|
+
else:
|
|
1621
|
+
imag_sig_m = imag_sigma[mE] * KILO
|
|
1622
|
+
dvec = imag_m
|
|
1623
|
+
wvec = imag_sig_m**(-2)
|
|
1624
|
+
|
|
1625
|
+
spectrum_in, alpha_select = self._chi2kink_a2f(
|
|
1626
|
+
dvec, model_in, uvec, mu, wvec, V_Sigma, U, alpha_min, alpha_max,
|
|
1627
|
+
alpha_num, a_guess, b_guess, c_guess, d_guess, f_chi_squared,
|
|
1628
|
+
t_criterion, iter_max, MEM_core,
|
|
1629
|
+
)
|
|
1630
|
+
|
|
1631
|
+
T = V_Sigma @ (U.T @ spectrum_in)
|
|
1632
|
+
chi_squared = wvec @ ((T - dvec) ** 2)
|
|
1633
|
+
|
|
1634
|
+
mask = (spectrum_in > 0.0) & (model_in > 0.0)
|
|
1635
|
+
if not np.any(mask):
|
|
1636
|
+
raise ValueError(
|
|
1637
|
+
"Invalid spectrum/model for entropy: no positive entries "
|
|
1638
|
+
"after MEM."
|
|
1639
|
+
)
|
|
1640
|
+
|
|
1641
|
+
information_entropy = (
|
|
1642
|
+
np.sum(spectrum_in[mask] - model_in[mask])
|
|
1643
|
+
- np.sum(
|
|
1644
|
+
spectrum_in[mask]
|
|
1645
|
+
* np.log(spectrum_in[mask] / model_in[mask])
|
|
1646
|
+
)
|
|
1647
|
+
)
|
|
1648
|
+
|
|
1649
|
+
cost = (0.5 * chi_squared
|
|
1650
|
+
- alpha_select * information_entropy
|
|
1651
|
+
+ 0.5 * np.sum(np.log(2.0 * np.pi / wvec))
|
|
1652
|
+
- 0.5 * spectrum_in.size * np.log(alpha_select))
|
|
1653
|
+
|
|
1654
|
+
spectrum = spectrum_in * omega_num / omega_max
|
|
1655
|
+
|
|
1656
|
+
return (cost, spectrum, model, alpha_select)
|
|
1657
|
+
|
|
1658
|
+
|
|
1659
|
+
@staticmethod
|
|
1660
|
+
def _chi2kink_a2f(dvec, model_in, uvec, mu, wvec, V_Sigma, U,
|
|
1661
|
+
alpha_min, alpha_max, alpha_num, a_guess, b_guess,
|
|
1662
|
+
c_guess, d_guess, f_chi_squared, t_criterion,
|
|
1663
|
+
iter_max, MEM_core):
|
|
1664
|
+
r"""Compute MEM spectrum using the chi2kink alpha-selection procedure.
|
|
1665
|
+
|
|
1666
|
+
Returns
|
|
1667
|
+
-------
|
|
1668
|
+
spectrum_in : ndarray
|
|
1669
|
+
Selected spectrum from MEM_core evaluated at the chi2kink alpha.
|
|
1670
|
+
"""
|
|
1671
|
+
from . import (fit_leastsq, chi2kink_logistic)
|
|
1672
|
+
|
|
1673
|
+
alpha_range = np.logspace(alpha_min, alpha_max, alpha_num)
|
|
1674
|
+
chi_squared = np.empty_like(alpha_range, dtype=float)
|
|
1675
|
+
|
|
1676
|
+
for i, alpha in enumerate(alpha_range):
|
|
1677
|
+
spectrum_in, uvec = MEM_core(dvec, model_in, uvec, mu, alpha,
|
|
1678
|
+
wvec, V_Sigma, U, t_criterion, iter_max)
|
|
1679
|
+
|
|
1680
|
+
T = V_Sigma @ (U.T @ spectrum_in)
|
|
1681
|
+
chi_squared[i] = wvec @ ((T - dvec) ** 2)
|
|
1682
|
+
|
|
1683
|
+
if (not np.all(np.isfinite(chi_squared))) or np.any(chi_squared <= 0.0):
|
|
1684
|
+
raise ValueError(
|
|
1685
|
+
"chi_squared contains non-finite or non-positive values."
|
|
1686
|
+
)
|
|
1687
|
+
|
|
1688
|
+
log_alpha = np.log10(alpha_range)
|
|
1689
|
+
log_chi_squared = np.log10(chi_squared)
|
|
1690
|
+
|
|
1691
|
+
p0 = np.array([a_guess, b_guess, c_guess, d_guess], dtype=float)
|
|
1692
|
+
pfit, pcov = fit_leastsq(
|
|
1693
|
+
p0, log_alpha, log_chi_squared, chi2kink_logistic
|
|
1694
|
+
)
|
|
1695
|
+
|
|
1696
|
+
cout = pfit[2]
|
|
1697
|
+
dout = pfit[3]
|
|
1698
|
+
alpha_select = 10 ** (cout - f_chi_squared / dout)
|
|
1699
|
+
|
|
1700
|
+
spectrum_in, uvec = MEM_core(dvec, model_in, uvec, mu, alpha_select,
|
|
1701
|
+
wvec, V_Sigma, U, t_criterion, iter_max)
|
|
1702
|
+
|
|
1703
|
+
return spectrum_in, alpha_select
|
|
1704
|
+
|
|
1705
|
+
|
|
1706
|
+
@staticmethod
|
|
1707
|
+
def _el_el_self_energy(enel_range, k_BT, lambda_el, W, power):
|
|
1708
|
+
"""Electron–electron contribution to the self-energy."""
|
|
1709
|
+
x = enel_range / W
|
|
1710
|
+
denom = 1.0 - (np.pi * k_BT / W) ** 2
|
|
1711
|
+
|
|
1712
|
+
if denom == 0.0:
|
|
1713
|
+
raise ZeroDivisionError(
|
|
1714
|
+
"Invalid parameters: 1 - (π k_BT / W)^2 = 0."
|
|
1715
|
+
)
|
|
1716
|
+
|
|
1717
|
+
pref = lambda_el / (W * denom)
|
|
1718
|
+
|
|
1719
|
+
if power == 2:
|
|
1720
|
+
real_el = pref * x * ((np.pi * k_BT) ** 2 - W ** 2) / (1.0 + x ** 2)
|
|
1721
|
+
imag_el = (pref * (enel_range ** 2 + (np.pi * k_BT) ** 2)
|
|
1722
|
+
/ (1.0 + x ** 2))
|
|
1723
|
+
|
|
1724
|
+
elif power == 4:
|
|
1725
|
+
num = (
|
|
1726
|
+
(np.pi * k_BT) ** 2 * (1.0 + x ** 2)
|
|
1727
|
+
- W ** 2 * (1.0 - x ** 2)
|
|
1728
|
+
)
|
|
1729
|
+
real_el = pref * x * num / (1.0 + x ** 4)
|
|
1730
|
+
imag_el = (pref * np.sqrt(2.0) * (enel_range ** 2 + (np.pi * k_BT)
|
|
1731
|
+
** 2) / ( 1.0 + x ** 4))
|
|
1732
|
+
else:
|
|
1733
|
+
raise ValueError(
|
|
1734
|
+
"El-el coupling has not yet been implemented for the given " \
|
|
1735
|
+
"power."
|
|
1736
|
+
)
|
|
1737
|
+
|
|
1738
|
+
return real_el, imag_el
|
|
1739
|
+
|
|
1740
|
+
|
|
1741
|
+
class CreateSelfEnergies:
|
|
1742
|
+
r"""
|
|
1743
|
+
Thin container for self-energies with leaf-aware utilities.
|
|
1744
|
+
All items are assumed to be leaf self-energy objects with
|
|
1745
|
+
a `.label` attribute for identification.
|
|
1746
|
+
"""
|
|
1747
|
+
|
|
1748
|
+
def __init__(self, self_energies):
|
|
1749
|
+
self.self_energies = self_energies
|
|
1750
|
+
|
|
1751
|
+
# ------ Basic container protocol ------
|
|
1752
|
+
def __call__(self):
|
|
1753
|
+
return self.self_energies
|
|
1754
|
+
|
|
1755
|
+
@property
|
|
1756
|
+
def self_energies(self):
|
|
1757
|
+
return self._self_energies
|
|
1758
|
+
|
|
1759
|
+
@self_energies.setter
|
|
1760
|
+
def self_energies(self, x):
|
|
1761
|
+
self._self_energies = x
|
|
1762
|
+
|
|
1763
|
+
def __iter__(self):
|
|
1764
|
+
return iter(self.self_energies)
|
|
1765
|
+
|
|
1766
|
+
def __getitem__(self, index):
|
|
1767
|
+
return self.self_energies[index]
|
|
1768
|
+
|
|
1769
|
+
def __setitem__(self, index, value):
|
|
1770
|
+
self.self_energies[index] = value
|
|
1771
|
+
|
|
1772
|
+
def __len__(self):
|
|
1773
|
+
return len(self.self_energies)
|
|
1774
|
+
|
|
1775
|
+
def __deepcopy__(self, memo):
|
|
1776
|
+
import copy
|
|
1777
|
+
return type(self)(copy.deepcopy(self.self_energies, memo))
|
|
1778
|
+
|
|
1779
|
+
# ------ Label-based utilities ------
|
|
1780
|
+
def get_by_label(self, label):
|
|
1781
|
+
r"""
|
|
1782
|
+
Return the self-energy object with the given label.
|
|
1783
|
+
|
|
1784
|
+
Parameters
|
|
1785
|
+
----------
|
|
1786
|
+
label : str
|
|
1787
|
+
Label of the self-energy to retrieve.
|
|
1788
|
+
|
|
1789
|
+
Returns
|
|
1790
|
+
-------
|
|
1791
|
+
obj : SelfEnergy
|
|
1792
|
+
The corresponding self-energy instance.
|
|
1793
|
+
|
|
1794
|
+
Raises
|
|
1795
|
+
------
|
|
1796
|
+
KeyError
|
|
1797
|
+
If no self-energy with the given label exists.
|
|
1798
|
+
"""
|
|
1799
|
+
for se in self.self_energies:
|
|
1800
|
+
if getattr(se, "label", None) == label:
|
|
1801
|
+
return se
|
|
1802
|
+
raise KeyError(
|
|
1803
|
+
f"No self-energy with label {label!r} found in container."
|
|
1804
|
+
)
|
|
1805
|
+
|
|
1806
|
+
def labels(self):
|
|
1807
|
+
r"""
|
|
1808
|
+
Return a list of all labels.
|
|
1809
|
+
"""
|
|
1810
|
+
return [getattr(se, "label", None) for se in self.self_energies]
|
|
1811
|
+
|
|
1812
|
+
def as_dict(self):
|
|
1813
|
+
r"""
|
|
1814
|
+
Return a {label: self_energy} dictionary for convenient access.
|
|
1815
|
+
"""
|
|
1816
|
+
return {se.label: se for se in self.self_energies}
|