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

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