xarpes 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
xarpes/plotting.py CHANGED
@@ -11,54 +11,7 @@
11
11
 
12
12
  """Functions related to plotting."""
13
13
 
14
- from functools import wraps
15
- from IPython import get_ipython
16
14
  import matplotlib.pyplot as plt
17
- import matplotlib as mpl
18
-
19
-
20
- def plot_settings(name='default', register_pre_run=True):
21
- """Configure default plotting style for xARPES."""
22
-
23
- mpl.rc('xtick', labelsize=10, direction='in')
24
- mpl.rc('ytick', labelsize=10, direction='in')
25
- plt.rcParams['legend.frameon'] = False
26
- lw = dict(default=2.0, large=4.0)[name]
27
-
28
- mpl.rcParams.update({
29
- 'lines.linewidth': lw,
30
- 'lines.markersize': 3,
31
- 'xtick.major.size': 4,
32
- 'xtick.minor.size': 2,
33
- 'xtick.major.width': 0.8,
34
- 'font.size': 16,
35
- 'axes.ymargin': 0.15,
36
- })
37
-
38
- if register_pre_run:
39
- _maybe_register_pre_run_close_all()
40
-
41
-
42
- def _maybe_register_pre_run_close_all():
43
- """Register a pre_run_cell hook once, and only inside Jupyter."""
44
-
45
- # Create the function attribute on first call
46
- if not hasattr(_maybe_register_pre_run_close_all, "_registered"):
47
- _maybe_register_pre_run_close_all._registered = False
48
-
49
- if _maybe_register_pre_run_close_all._registered:
50
- return
51
-
52
- ip = get_ipython()
53
- if ip is None or ip.__class__.__name__ != "ZMQInteractiveShell":
54
- return
55
-
56
- def _close_all(_info):
57
- plt.close('all')
58
-
59
- ip.events.register('pre_run_cell', _close_all)
60
- _maybe_register_pre_run_close_all._registered = True
61
-
62
15
 
63
16
  def get_ax_fig_plt(ax=None, **kwargs):
64
17
  r"""Helper function used in plot functions supporting an optional `Axes`
@@ -100,6 +53,7 @@ def add_fig_kwargs(func):
100
53
  first element is a matplotlib figure, or None to signal some sort of
101
54
  error/unexpected event.
102
55
  """
56
+ from functools import wraps
103
57
  @wraps(func)
104
58
  def wrapper(*args, **kwargs):
105
59
  # pop the kwds used by the decorator.
xarpes/selfenergies.py ADDED
@@ -0,0 +1,621 @@
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
17
+
18
+ class SelfEnergy:
19
+ r"""Self-energy (ekin-leading; hnuminPhi/ekin are read-only)."""
20
+
21
+ def __init__(self, ekin_range, hnuminPhi, label, properties, parameters):
22
+ # core read-only state
23
+ self._ekin_range = ekin_range
24
+ self._hnuminPhi = hnuminPhi
25
+ self._label = label
26
+
27
+ # accept either a dict or a single-element list of dicts
28
+ if isinstance(properties, list):
29
+ if len(properties) == 1:
30
+ properties = properties[0]
31
+ else:
32
+ raise ValueError("`properties` must be a dict or a single " \
33
+ "dict in a list.")
34
+
35
+ # single source of truth for all params (+ their *_sigma)
36
+ self._properties = dict(properties or {})
37
+ self._class = self._properties.get("_class", None)
38
+
39
+ # ---- enforce supported classes at construction
40
+ if self._class not in ("SpectralLinear", "SpectralQuadratic"):
41
+ raise ValueError(
42
+ f"Unsupported spectral class '{self._class}'. "
43
+ "Only 'SpectralLinear' or 'SpectralQuadratic' are allowed."
44
+ )
45
+
46
+ # grab user parameters
47
+ self._parameters = dict(parameters or {})
48
+ self._fermi_wavevector = self._parameters.get("fermi_wavevector")
49
+ self._fermi_velocity = self._parameters.get("fermi_velocity")
50
+ self._bare_mass = self._parameters.get("bare_mass")
51
+ self._side = self._parameters.get("side", None)
52
+
53
+ # ---- class-specific parameter constraints
54
+ if self._class == "SpectralLinear" and (self._bare_mass is not None):
55
+ raise ValueError("`bare_mass` cannot be set for SpectralLinear.")
56
+ if self._class == "SpectralQuadratic" and (self._fermi_velocity is not None):
57
+ raise ValueError("`fermi_velocity` cannot be set for SpectralQuadratic.")
58
+
59
+ if self._side is not None and self._side not in ("left", "right"):
60
+ raise ValueError("`side` must be 'left' or 'right' if provided.")
61
+ if self._side is not None:
62
+ self._parameters["side"] = self._side
63
+
64
+ # convenience attributes (read from properties)
65
+ self._amplitude = self._properties.get("amplitude")
66
+ self._amplitude_sigma = self._properties.get("amplitude_sigma")
67
+ self._peak = self._properties.get("peak")
68
+ self._peak_sigma = self._properties.get("peak_sigma")
69
+ self._broadening = self._properties.get("broadening")
70
+ self._broadening_sigma = self._properties.get("broadening_sigma")
71
+ self._center_wavevector = self._properties.get("center_wavevector")
72
+
73
+ # lazy caches
74
+ self._peak_positions = None
75
+ self._peak_positions_sigma = None
76
+ self._real = None
77
+ self._real_sigma = None
78
+ self._imag = None
79
+ self._imag_sigma = None
80
+
81
+ def _check_mass_velocity_exclusivity(self):
82
+ """Ensure that fermi_velocity and bare_mass are not both set."""
83
+ if (self._fermi_velocity is not None) and (self._bare_mass is not None):
84
+ raise ValueError(
85
+ "Cannot set both `fermi_velocity` and `bare_mass`: "
86
+ "choose one physical parametrization (SpectralLinear or SpectralQuadratic)."
87
+ )
88
+
89
+ # ---------------- core read-only axes ----------------
90
+ @property
91
+ def ekin_range(self):
92
+ return self._ekin_range
93
+
94
+ @property
95
+ def enel_range(self):
96
+ if self._ekin_range is None:
97
+ return None
98
+ hnp = 0.0 if self._hnuminPhi is None else self._hnuminPhi
99
+ return np.asarray(self._ekin_range) - hnp
100
+
101
+ @property
102
+ def hnuminPhi(self):
103
+ return self._hnuminPhi
104
+
105
+ # ---------------- identifiers ----------------
106
+ @property
107
+ def label(self):
108
+ return self._label
109
+
110
+ @label.setter
111
+ def label(self, x):
112
+ self._label = x
113
+
114
+ # ---------------- exported user parameters ----------------
115
+ @property
116
+ def parameters(self):
117
+ """Dictionary with user-supplied parameters (read-only view)."""
118
+ return self._parameters
119
+
120
+ @property
121
+ def side(self):
122
+ """Optional side selector: 'left' or 'right'."""
123
+ return self._side
124
+
125
+ @side.setter
126
+ def side(self, x):
127
+ if x is not None and x not in ("left", "right"):
128
+ raise ValueError("`side` must be 'left' or 'right' if provided.")
129
+ self._side = x
130
+ if x is not None:
131
+ self._parameters["side"] = x
132
+ else:
133
+ self._parameters.pop("side", None)
134
+ # affects sign of peak_positions and thus `real`
135
+ self._peak_positions = None
136
+ self._real = None
137
+ self._real_sigma = None
138
+ self._mdc_maxima = None
139
+
140
+ @property
141
+ def fermi_wavevector(self):
142
+ """Optional k_F; can be set later."""
143
+ return self._fermi_wavevector
144
+
145
+ @fermi_wavevector.setter
146
+ def fermi_wavevector(self, x):
147
+ self._fermi_wavevector = x
148
+ self._parameters["fermi_wavevector"] = x
149
+ # invalidate dependent cache
150
+ self._real = None
151
+ self._real_sigma = None
152
+
153
+ @property
154
+ def fermi_velocity(self):
155
+ """Optional v_F; can be set later."""
156
+ return self._fermi_velocity
157
+
158
+ @fermi_velocity.setter
159
+ def fermi_velocity(self, x):
160
+ if self._class == "SpectralQuadratic":
161
+ raise ValueError("`fermi_velocity` cannot be set for" \
162
+ " SpectralQuadratic.")
163
+ self._fermi_velocity = x
164
+ self._parameters["fermi_velocity"] = x
165
+ # invalidate dependents
166
+ self._imag = None; self._imag_sigma = None
167
+ self._real = None; self._real_sigma = None
168
+
169
+ @property
170
+ def bare_mass(self):
171
+ """Optional bare mass; used by SpectralQuadratic formulas."""
172
+ return self._bare_mass
173
+
174
+ @bare_mass.setter
175
+ def bare_mass(self, x):
176
+ if self._class == "SpectralLinear":
177
+ raise ValueError("`bare_mass` cannot be set for SpectralLinear.")
178
+ self._bare_mass = x
179
+ self._parameters["bare_mass"] = x
180
+ # invalidate dependents
181
+ self._imag = None; self._imag_sigma = None
182
+ self._real = None; self._real_sigma = None
183
+
184
+ # ---------------- optional fit parameters (convenience) ----------------
185
+ @property
186
+ def amplitude(self):
187
+ return self._amplitude
188
+
189
+ @amplitude.setter
190
+ def amplitude(self, x):
191
+ self._amplitude = x
192
+ self._properties["amplitude"] = x
193
+
194
+ @property
195
+ def amplitude_sigma(self):
196
+ return self._amplitude_sigma
197
+
198
+ @amplitude_sigma.setter
199
+ def amplitude_sigma(self, x):
200
+ self._amplitude_sigma = x
201
+ self._properties["amplitude_sigma"] = x
202
+
203
+ @property
204
+ def peak(self):
205
+ return self._peak
206
+
207
+ @peak.setter
208
+ def peak(self, x):
209
+ self._peak = x
210
+ self._properties["peak"] = x
211
+ # invalidate dependent cache
212
+ self._peak_positions = None
213
+ self._real = None
214
+ self._mdc_maxima = None
215
+
216
+ @property
217
+ def peak_sigma(self):
218
+ return self._peak_sigma
219
+
220
+ @peak_sigma.setter
221
+ def peak_sigma(self, x):
222
+ self._peak_sigma = x
223
+ self._properties["peak_sigma"] = x
224
+ self._peak_positions_sigma = None
225
+ self._real_sigma = None
226
+
227
+ @property
228
+ def broadening(self):
229
+ return self._broadening
230
+
231
+ @broadening.setter
232
+ def broadening(self, x):
233
+ self._broadening = x
234
+ self._properties["broadening"] = x
235
+ self._imag = None
236
+
237
+ @property
238
+ def broadening_sigma(self):
239
+ return self._broadening_sigma
240
+
241
+ @broadening_sigma.setter
242
+ def broadening_sigma(self, x):
243
+ self._broadening_sigma = x
244
+ self._properties["broadening_sigma"] = x
245
+ self._imag_sigma = None
246
+
247
+ @property
248
+ def center_wavevector(self):
249
+ """Read-only center wavevector (SpectralQuadratic, if present)."""
250
+ return self._center_wavevector
251
+
252
+ # ---------------- derived outputs ----------------
253
+ @property
254
+ def peak_positions(self):
255
+ r"""k_parallel = peak * dtor * sqrt(ekin_range / pref) (lazy)."""
256
+ if self._peak_positions is None:
257
+ if self._peak is None or self._ekin_range is None:
258
+ return None
259
+ if self._class == "SpectralQuadratic":
260
+ if self._side is None:
261
+ raise AttributeError(
262
+ "For SpectralQuadratic, set `side` ('left'/'right') "
263
+ "before accessing peak_positions and quantities that "
264
+ "depend on the latter."
265
+ )
266
+ kpar_mag = np.sqrt(self._ekin_range / PREF) * \
267
+ np.sin(np.deg2rad(np.abs(self._peak)))
268
+ self._peak_positions = (-1.0 if self._side == "left" \
269
+ else 1.0) * kpar_mag
270
+ else:
271
+ self._peak_positions = np.sqrt(self._ekin_range / PREF) \
272
+ * np.sin(np.deg2rad(self._peak))
273
+ return self._peak_positions
274
+
275
+
276
+ @property
277
+ def peak_positions_sigma(self):
278
+ r"""Std. dev. of k_parallel (lazy)."""
279
+ if self._peak_positions_sigma is None:
280
+ if self._peak_sigma is None or self._ekin_range is None:
281
+ return None
282
+ self._peak_positions_sigma = (np.sqrt(self._ekin_range / PREF) \
283
+ * np.abs(np.cos(np.deg2rad(self._peak))) \
284
+ * np.deg2rad(self._peak_sigma))
285
+ return self._peak_positions_sigma
286
+
287
+ @property
288
+ def imag(self):
289
+ r"""-Σ'' (lazy)."""
290
+ if self._imag is None:
291
+ if self._broadening is None or self._ekin_range is None:
292
+ return None
293
+ if self._class == "SpectralLinear":
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)
305
+ return self._imag
306
+
307
+ @property
308
+ def imag_sigma(self):
309
+ r"""Std. dev. of -Σ'' (lazy)."""
310
+ if self._imag_sigma is None:
311
+ if self._broadening_sigma is None or self._ekin_range is None:
312
+ return None
313
+ if self._class == "SpectralLinear":
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)
325
+ return self._imag_sigma
326
+
327
+ @property
328
+ def real(self):
329
+ r"""Σ' (lazy)."""
330
+ if self._real is None:
331
+ if self._peak is None or self._ekin_range is None:
332
+ return None
333
+ if self._class == "SpectralLinear":
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)
348
+ return self._real
349
+
350
+ @property
351
+ def real_sigma(self):
352
+ r"""Std. dev. of Σ' (lazy)."""
353
+ if self._real_sigma is None:
354
+ if self._peak_sigma is None or self._ekin_range is None:
355
+ return None
356
+ if self._class == "SpectralLinear":
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)
368
+ return self._real_sigma
369
+
370
+ @property
371
+ def mdc_maxima(self):
372
+ """
373
+ MDC maxima (lazy).
374
+
375
+ SpectralLinear:
376
+ identical to peak_positions
377
+
378
+ SpectralQuadratic:
379
+ peak_positions + center_wavevector
380
+ """
381
+ if getattr(self, "_mdc_maxima", None) is None:
382
+ if self.peak_positions is None:
383
+ return None
384
+
385
+ if self._class == "SpectralLinear":
386
+ self._mdc_maxima = self.peak_positions
387
+ elif self._class == "SpectralQuadratic":
388
+ self._mdc_maxima = (
389
+ self.peak_positions + self._center_wavevector
390
+ )
391
+
392
+ return self._mdc_maxima
393
+
394
+ def _se_legend_labels(self):
395
+ """Return (real_label, imag_label) for legend with safe subscripts."""
396
+ se_label = getattr(self, "label", None)
397
+
398
+ if se_label is None:
399
+ real_label = r"$\Sigma'(E)$"
400
+ imag_label = r"$-\Sigma''(E)$"
401
+ return real_label, imag_label
402
+
403
+ safe_label = str(se_label).replace("_", r"\_")
404
+
405
+ # If the label is empty after conversion, fall back
406
+ if safe_label == "":
407
+ real_label = r"$\Sigma'(E)$"
408
+ imag_label = r"$-\Sigma''(E)$"
409
+ return real_label, imag_label
410
+
411
+ real_label = rf"$\Sigma_{{\mathrm{{{safe_label}}}}}'(E)$"
412
+ imag_label = rf"$-\Sigma_{{\mathrm{{{safe_label}}}}}''(E)$"
413
+
414
+ return real_label, imag_label
415
+
416
+ @add_fig_kwargs
417
+ def plot_real(self, ax=None, **kwargs):
418
+ r"""Plot the real part Σ' of the self-energy as a function of E-μ.
419
+
420
+ Parameters
421
+ ----------
422
+ ax : Matplotlib-Axes or None
423
+ Axis to plot on. Created if not provided by the user.
424
+ **kwargs :
425
+ Additional keyword arguments passed to ``ax.errorbar``.
426
+
427
+ Returns
428
+ -------
429
+ fig : Matplotlib-Figure
430
+ Figure containing the Σ'(E) plot.
431
+ """
432
+ from . import settings_parameters as xprs
433
+
434
+ ax, fig, plt = get_ax_fig_plt(ax=ax)
435
+
436
+ x = self.enel_range
437
+ y = self.real
438
+ y_sigma = self.real_sigma
439
+
440
+ real_label, _ = self._se_legend_labels()
441
+ kwargs.setdefault("label", real_label)
442
+
443
+ if y_sigma is not None:
444
+ if np.isnan(y_sigma).any():
445
+ print(
446
+ "Warning: some Σ'(E) uncertainty values are missing. "
447
+ "Error bars omitted at those energies."
448
+ )
449
+ kwargs.setdefault("yerr", xprs.sigma_confidence * y_sigma)
450
+
451
+ ax.errorbar(x, y, **kwargs)
452
+ ax.set_xlabel(r"$E-\mu$ (eV)")
453
+ ax.set_ylabel(r"$\Sigma'(E)$ (eV)")
454
+ ax.legend()
455
+
456
+ return fig
457
+
458
+ @add_fig_kwargs
459
+ def plot_imag(self, ax=None, **kwargs):
460
+ r"""Plot the imaginary part -Σ'' of the self-energy vs. E-μ.
461
+
462
+ Parameters
463
+ ----------
464
+ ax : Matplotlib-Axes or None
465
+ Axis to plot on. Created if not provided by the user.
466
+ **kwargs :
467
+ Additional keyword arguments passed to ``ax.errorbar``.
468
+
469
+ Returns
470
+ -------
471
+ fig : Matplotlib-Figure
472
+ Figure containing the -Σ''(E) plot.
473
+ """
474
+ from . import settings_parameters as xprs
475
+
476
+ ax, fig, plt = get_ax_fig_plt(ax=ax)
477
+
478
+ x = self.enel_range
479
+ y = self.imag
480
+ y_sigma = self.imag_sigma
481
+
482
+ _, imag_label = self._se_legend_labels()
483
+ kwargs.setdefault("label", imag_label)
484
+
485
+ if y_sigma is not None:
486
+ if np.isnan(y_sigma).any():
487
+ print(
488
+ "Warning: some -Σ''(E) uncertainty values are missing. "
489
+ "Error bars omitted at those energies."
490
+ )
491
+ kwargs.setdefault("yerr", xprs.sigma_confidence * y_sigma)
492
+
493
+ ax.errorbar(x, y, **kwargs)
494
+ ax.set_xlabel(r"$E-\mu$ (eV)")
495
+ ax.set_ylabel(r"$-\Sigma''(E)$ (eV)")
496
+ ax.legend()
497
+
498
+ return fig
499
+
500
+ @add_fig_kwargs
501
+ def plot_both(self, ax=None, **kwargs):
502
+ r"""Plot Σ'(E) and -Σ''(E) vs. E-μ on the same axis."""
503
+ from . import settings_parameters as xprs
504
+
505
+ ax, fig, plt = get_ax_fig_plt(ax=ax)
506
+
507
+ x = self.enel_range
508
+ real = self.real
509
+ imag = self.imag
510
+ real_sigma = self.real_sigma
511
+ imag_sigma = self.imag_sigma
512
+
513
+ real_label, imag_label = self._se_legend_labels()
514
+
515
+ # --- plot Σ'
516
+ kw_real = dict(kwargs)
517
+ if real_sigma is not None:
518
+ if np.isnan(real_sigma).any():
519
+ print(
520
+ "Warning: some Σ'(E) uncertainty values are missing. "
521
+ "Error bars omitted at those energies."
522
+ )
523
+ kw_real.setdefault("yerr", xprs.sigma_confidence * real_sigma)
524
+ kw_real.setdefault("label", real_label)
525
+ ax.errorbar(x, real, **kw_real)
526
+
527
+ # --- plot -Σ''
528
+ kw_imag = dict(kwargs)
529
+ if imag_sigma is not None:
530
+ if np.isnan(imag_sigma).any():
531
+ print(
532
+ "Warning: some -Σ''(E) uncertainty values are missing. "
533
+ "Error bars omitted at those energies."
534
+ )
535
+ kw_imag.setdefault("yerr", xprs.sigma_confidence * imag_sigma)
536
+ kw_imag.setdefault("label", imag_label)
537
+ ax.errorbar(x, imag, **kw_imag)
538
+
539
+ ax.set_xlabel(r"$E-\mu$ (eV)")
540
+ ax.set_ylabel(r"$\Sigma'(E),\ -\Sigma''(E)$ (eV)")
541
+ ax.legend()
542
+
543
+ return fig
544
+
545
+
546
+ class CreateSelfEnergies:
547
+ r"""
548
+ Thin container for self-energies with leaf-aware utilities.
549
+ All items are assumed to be leaf self-energy objects with
550
+ a `.label` attribute for identification.
551
+ """
552
+
553
+ def __init__(self, self_energies):
554
+ self.self_energies = self_energies
555
+
556
+ # ------ Basic container protocol ------
557
+ def __call__(self):
558
+ return self.self_energies
559
+
560
+ @property
561
+ def self_energies(self):
562
+ return self._self_energies
563
+
564
+ @self_energies.setter
565
+ def self_energies(self, x):
566
+ self._self_energies = x
567
+
568
+ def __iter__(self):
569
+ return iter(self.self_energies)
570
+
571
+ def __getitem__(self, index):
572
+ return self.self_energies[index]
573
+
574
+ def __setitem__(self, index, value):
575
+ self.self_energies[index] = value
576
+
577
+ def __len__(self):
578
+ return len(self.self_energies)
579
+
580
+ def __deepcopy__(self, memo):
581
+ import copy
582
+ return type(self)(copy.deepcopy(self.self_energies, memo))
583
+
584
+ # ------ Label-based utilities ------
585
+ def get_by_label(self, label):
586
+ r"""
587
+ Return the self-energy object with the given label.
588
+
589
+ Parameters
590
+ ----------
591
+ label : str
592
+ Label of the self-energy to retrieve.
593
+
594
+ Returns
595
+ -------
596
+ obj : SelfEnergy
597
+ The corresponding self-energy instance.
598
+
599
+ Raises
600
+ ------
601
+ KeyError
602
+ If no self-energy with the given label exists.
603
+ """
604
+ for se in self.self_energies:
605
+ if getattr(se, "label", None) == label:
606
+ return se
607
+ raise KeyError(
608
+ f"No self-energy with label {label!r} found in container."
609
+ )
610
+
611
+ def labels(self):
612
+ r"""
613
+ Return a list of all labels.
614
+ """
615
+ return [getattr(se, "label", None) for se in self.self_energies]
616
+
617
+ def as_dict(self):
618
+ r"""
619
+ Return a {label: self_energy} dictionary for convenient access.
620
+ """
621
+ return {se.label: se for se in self.self_energies}