xarpes 0.2.3__py3-none-any.whl → 0.3.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/distributions.py CHANGED
@@ -1,11 +1,151 @@
1
- # Copyright (C) 2024 xARPES Developers
1
+ # Copyright (C) 2025 xARPES Developers
2
2
  # This program is free software under the terms of the GNU GPLv3 license.
3
3
 
4
4
  """The distributions used throughout the code."""
5
5
 
6
6
  import numpy as np
7
+ from .functions import extend_function
8
+ from .plotting import get_ax_fig_plt, add_fig_kwargs
9
+ from .constants import k_B, pref, dtor
10
+
11
+ class CreateDistributions:
12
+ r"""
13
+ Thin container for distributions with leaf-aware utilities.
14
+ If a distribution implements `components()` -> iterable of leaf
15
+ distributions, we use it to count/flatten; otherwise it's treated as a leaf.
16
+ """
17
+ def __init__(self, distributions):
18
+ # Adds a call method to the distribution list
19
+ self.distributions = distributions
20
+
21
+ def __call__(self):
22
+ r"""
23
+ """
24
+ return self.distributions
25
+
26
+ @property
27
+ def distributions(self):
28
+ r"""
29
+ """
30
+ return self._distributions
31
+
32
+ @distributions.setter
33
+ def distributions(self, x):
34
+ r"""
35
+ """
36
+ self._distributions = x
37
+
38
+ def __iter__(self):
39
+ r"""
40
+ """
41
+ return iter(self.distributions)
42
+
43
+ def __getitem__(self, index):
44
+ r"""
45
+ """
46
+ return self.distributions[index]
47
+
48
+ def __setitem__(self, index, value):
49
+ r"""
50
+ """
51
+ self.distributions[index] = value
52
+
53
+ def __len__(self):
54
+ r"""
55
+ """
56
+ # Fast path: flat list length (may be different from n_individuals
57
+ # if composites exist)
58
+ return len(self.distributions)
59
+
60
+ def __deepcopy__(self, memo):
61
+ r"""
62
+ """
63
+ import copy
64
+ return type(self)(copy.deepcopy(self.distributions, memo))
65
+
66
+ # -------- Leaf-aware helpers --------
67
+ def _iter_leaves(self):
68
+ """
69
+ Yield leaf distributions. If an item has .components(), flatten it;
70
+ else treat the item itself as a leaf.
71
+ """
72
+ for d in self.distributions:
73
+ comps = getattr(d, "components", None)
74
+ if callable(comps):
75
+ for leaf in comps():
76
+ yield leaf
77
+ else:
78
+ yield d
79
+
80
+ @property
81
+ def n_individuals(self) -> int:
82
+ """
83
+ Number of leaf components (i.e., individual curves to plot).
84
+ """
85
+ # If items define __len__ as num_leaves you can use that;
86
+ # but components() is the most explicit/robust.
87
+ return sum(1 for _ in self._iter_leaves())
88
+
89
+ def flatten(self):
90
+ """
91
+ Return a flat list of leaf distributions (useful for plotting/storage).
92
+ """
93
+ return list(self._iter_leaves())
94
+
95
+
96
+ @add_fig_kwargs
97
+ def plot(self, angle_range, angle_resolution, kinetic_energy=None,
98
+ hnuminphi=None, matrix_element=None, matrix_args=None, ax=None,
99
+ **kwargs):
100
+ r"""
101
+ """
102
+ if angle_resolution < 0:
103
+ raise ValueError('Distributions cannot be plotted with negative '
104
+ + 'resolution.')
7
105
 
8
- class distribution:
106
+ from scipy.ndimage import gaussian_filter
107
+
108
+ ax, fig, plt = get_ax_fig_plt(ax=ax)
109
+
110
+ ax.set_xlabel('Angle ($\degree$)')
111
+ ax.set_ylabel('Counts (-)')
112
+
113
+
114
+ extend, step, numb = extend_function(angle_range, angle_resolution)
115
+
116
+ total_result = np.zeros(np.shape(extend))
117
+
118
+ for dist in self.distributions:
119
+ if dist.class_name == 'SpectralQuadratic':
120
+ if (dist.center_angle is not None) and (kinetic_energy is
121
+ None or hnuminphi is None):
122
+ raise ValueError('Spectral quadratic function is '
123
+ 'defined in terms of a center angle. Please provide '
124
+ 'a kinetic energy and hnuminphi.')
125
+ extended_result = dist.evaluate(extend,
126
+ kinetic_energy, hnuminphi)
127
+ else:
128
+ extended_result = dist.evaluate(extend)
129
+
130
+ if matrix_element is not None:
131
+ extended_result *= matrix_element(extend, **matrix_args)
132
+
133
+ total_result += extended_result
134
+
135
+ individual_result = gaussian_filter(extended_result, sigma=step
136
+ )[numb:-numb if numb else None]
137
+ ax.plot(angle_range, individual_result, label=dist.label)
138
+
139
+ final_result = gaussian_filter(total_result, sigma=step
140
+ )[numb:-numb if numb else None]
141
+
142
+ ax.plot(angle_range, final_result, label=str('Distribution sum'))
143
+
144
+ ax.legend()
145
+
146
+ return fig
147
+
148
+ class Distribution:
9
149
  r"""Parent class for distributions. The class cannot be used on its own,
10
150
  but is used to instantiate unique and non-unique distributions.
11
151
 
@@ -19,18 +159,52 @@ class distribution:
19
159
 
20
160
  @property
21
161
  def name(self):
22
- r"""Returns the name of the class instance.
23
-
24
- Returns
25
- -------
26
- name : str
27
- Non-unique name for instances, not to be modified after
28
- instantiation.
162
+ r"""
29
163
  """
30
164
  return self._name
31
165
 
32
-
33
- class unique_distribution(distribution):
166
+ @property
167
+ def class_name(self):
168
+ r"""TBD
169
+ """
170
+ return self.__class__.__name__
171
+
172
+ @add_fig_kwargs
173
+ def plot(self, angle_range, angle_resolution, kinetic_energy=None,
174
+ hnuminphi=None, matrix_element=None, matrix_args=None,
175
+ ax=None, **kwargs):
176
+ r"""Overwritten for FermiDirac distribution.
177
+ """
178
+ if angle_resolution < 0:
179
+ raise ValueError('Distribution cannot be plotted with negative '
180
+ + 'resolution.')
181
+ from scipy.ndimage import gaussian_filter
182
+
183
+ ax, fig, plt = get_ax_fig_plt(ax=ax)
184
+
185
+ ax.set_xlabel('Angle ($\degree$)')
186
+ ax.set_ylabel('Counts (-)')
187
+
188
+ extend, step, numb = extend_function(angle_range, angle_resolution)
189
+
190
+ if self.class_name == 'SpectralQuadratic':
191
+ extended_result = self.evaluate(extend, kinetic_energy, hnuminphi)
192
+ else:
193
+ extended_result = self.evaluate(extend)
194
+
195
+ if matrix_element is not None:
196
+ extended_result *= matrix_element(extend, **matrix_args)
197
+
198
+ final_result = gaussian_filter(extended_result, sigma=step)\
199
+ [numb:-numb if numb else None]
200
+
201
+ ax.plot(angle_range, final_result, label=self.label)
202
+
203
+ ax.legend()
204
+
205
+ return fig
206
+
207
+ class UniqueDistribution(Distribution):
34
208
  r"""Parent class for unique distributions, to be used one at a time, e.g.,
35
209
  during the background of an MDC fit or the Fermi-Dirac distribution.
36
210
 
@@ -56,108 +230,10 @@ class unique_distribution(distribution):
56
230
  """
57
231
  return self._label
58
232
 
59
-
60
- class constant(unique_distribution):
61
- r"""Child class for constant distributions, used e.g., during MDC fitting.
62
- The constant class is unique, only one instance should be used per task.
63
-
64
- Parameters
65
- ----------
66
- offset : float
67
- The value of the distribution for the abscissa equal to 0.
68
- """
69
- def __init__(self, offset, name='constant'):
70
- super().__init__(name)
71
- self._offset = offset
72
-
73
- @property
74
- def offset(self):
75
- r"""Returns the offset of the constant distribution.
76
-
77
- Returns
78
- -------
79
- offset : float
80
- The value of the distribution for the abscissa equal to 0.
81
- """
82
- return self._offset
83
-
84
- @offset.setter
85
- def set_offset(self, x):
86
- r"""Sets the offset of the constant distribution.
87
-
88
- Parameters
89
- ----------
90
- offset : float
91
- The value of the distribution for the abscissa equal to 0.
92
- """
93
- self._offset = x
94
-
95
-
96
- class linear(unique_distribution):
97
- r"""Child cass for for linear distributions, used e.g., during MDC fitting.
98
- The constant class is unique, only one instance should be used per task.
99
-
100
- Parameters
101
- ----------
102
- offset : float
103
- The value of the distribution for the abscissa equal to 0.
104
- slope : float
105
- The linear slope of the distribution w.r.t. the abscissa.
106
- """
107
- def __init__(self, slope, offset, name='linear'):
108
- super().__init__(name)
109
- self._offset = offset
110
- self._slope = slope
111
-
112
- @property
113
- def offset(self):
114
- r"""Returns the offset of the linear distribution.
115
-
116
- Returns
117
- -------
118
- offset : float
119
- The value of the distribution for the abscissa equal to 0.
120
- """
121
- return self._offset
122
-
123
- @offset.setter
124
- def set_offset(self, x):
125
- r"""Sets the offset of the linear distribution.
126
-
127
- Parameters
128
- ----------
129
- offset : float
130
- The value of the distribution for the abscissa equal to 0.
131
- """
132
- self._offset = x
133
-
134
- @property
135
- def slope(self):
136
- r"""Returns the slope of the linear distribution.
137
-
138
- Returns
139
- -------
140
- slope : float
141
- The linear slope of the distribution w.r.t. the abscissa.
142
- """
143
- return self._slope
144
-
145
- @slope.setter
146
- def set_slope(self, x):
147
- r"""Sets the slope of the linear distribution.
148
-
149
- Parameters
150
- ----------
151
- slope : float
152
- The linear slope of the distribution w.r.t. the abscissa.
153
- """
154
- self._slope = x
155
-
156
-
157
- class fermi_dirac(unique_distribution):
158
- r"""Child class for Fermi-Dirac (FD) distributions, used e.g., during Fermi
159
- edge fitting. The FD class is unique, only one instance should be used
160
- per task.
233
+ class FermiDirac(UniqueDistribution):
234
+ r"""Child class for Fermi-Dirac (FD) distributions, used e.g., during
235
+ Fermi-edge fitting. The FD class is unique, only one instance should be
236
+ used per task.
161
237
 
162
238
  The Fermi-Dirac distribution is described by the following formula:
163
239
 
@@ -181,7 +257,7 @@ class fermi_dirac(unique_distribution):
181
257
  Integrated weight on top of the background [counts]
182
258
  """
183
259
  def __init__(self, temperature, hnuminphi, background=0,
184
- integrated_weight=1, name='fermi_dirac'):
260
+ integrated_weight=1, name='FermiDirac'):
185
261
  super().__init__(name)
186
262
  self.temperature = temperature
187
263
  self.hnuminphi = hnuminphi
@@ -200,7 +276,7 @@ class fermi_dirac(unique_distribution):
200
276
  return self._temperature
201
277
 
202
278
  @temperature.setter
203
- def set_temperature(self, x):
279
+ def temperature(self, x):
204
280
  r"""Sets the temperature of the FD distribution.
205
281
 
206
282
  Parameters
@@ -223,7 +299,7 @@ class fermi_dirac(unique_distribution):
223
299
  return self._hnuminphi
224
300
 
225
301
  @hnuminphi.setter
226
- def set_hnuminphi(self, x):
302
+ def hnuminphi(self, x):
227
303
  r"""Sets the photon energy minus the work function of the FD
228
304
  distribution.
229
305
 
@@ -246,7 +322,7 @@ class fermi_dirac(unique_distribution):
246
322
  return self._background
247
323
 
248
324
  @background.setter
249
- def set_background(self, x):
325
+ def background(self, x):
250
326
  r"""Sets the background intensity of the FD distribution.
251
327
 
252
328
  Parameters
@@ -268,7 +344,7 @@ class fermi_dirac(unique_distribution):
268
344
  return self._integrated_weight
269
345
 
270
346
  @integrated_weight.setter
271
- def set_integrated_weight(self, x):
347
+ def integrated_weight(self, x):
272
348
  r"""Sets the integrated weight of the FD distribution.
273
349
 
274
350
  Parameters
@@ -279,7 +355,7 @@ class fermi_dirac(unique_distribution):
279
355
  self._integrated_weight = x
280
356
 
281
357
  def __call__(self, energy_range, hnuminphi, background, integrated_weight,
282
- energy_resolution):
358
+ temperature):
283
359
  """Call method to directly evaluate a FD distribution without having to
284
360
  instantiate a class instance.
285
361
 
@@ -301,23 +377,10 @@ class fermi_dirac(unique_distribution):
301
377
  evalf : ndarray
302
378
  1D array of the energy-convolved FD distribution [counts]
303
379
  """
304
- from scipy.ndimage import gaussian_filter
305
-
306
- sigma_extend = 5 # Extend data range by "5 sigma"
307
- # Conversion from FWHM to standard deviation [-]
308
- fwhm_to_std = np.sqrt(8 * np.log(2))
309
- k_B = 8.617e-5 # Boltzmann constant [eV/K]
310
- k_BT = self.temperature * k_B
311
- step_size = np.abs(energy_range[1] - energy_range[0])
312
- estep = energy_resolution / (step_size * fwhm_to_std)
313
- enumb = int(sigma_extend * estep)
314
- extend = np.linspace(energy_range[0] - enumb * step_size,
315
- energy_range[-1] + enumb * step_size,
316
- len(energy_range) + 2 * enumb)
317
- result = (integrated_weight / (1 + np.exp((extend - hnuminphi) / k_BT))
318
- + background)
319
- evalf = gaussian_filter(result, sigma=estep)[enumb:-enumb]
320
- return evalf
380
+ k_BT = temperature * k_B
381
+
382
+ return (integrated_weight / (1 + np.exp((energy_range - hnuminphi)
383
+ / k_BT)) + background)
321
384
 
322
385
  def evaluate(self, energy_range):
323
386
  r"""Evaluates the FD distribution for a given class instance.
@@ -332,49 +395,158 @@ class fermi_dirac(unique_distribution):
332
395
  -------
333
396
  evalf : ndarray
334
397
  1D array of the evaluated FD distribution [counts]
335
- """
336
- k_B = 8.617e-5 # Boltzmann constant [eV/K]
398
+ """
337
399
  k_BT = self.temperature * k_B
338
- evalf = (self.integrated_weight
400
+
401
+ return (self.integrated_weight
339
402
  / (1 + np.exp((energy_range - self.hnuminphi) / k_BT))
340
403
  + self.background)
341
- return evalf
342
404
 
343
- def convolve(self, energy_range, energy_resolution):
344
- r"""Evaluates the FD distribution for a given class instance and
345
- performs the energy convolution with the given resolution. The
346
- convolution is performed with an expanded abscissa range of 5
347
- times the standard deviation.
405
+ @add_fig_kwargs
406
+ def plot(self, energy_range, energy_resolution, ax=None, **kwargs):
407
+ r"""TBD
408
+ """
409
+ if energy_resolution < 0:
410
+ raise ValueError('Distribution cannot be plotted with negative '
411
+ + 'resolution.')
412
+ from scipy.ndimage import gaussian_filter
413
+
414
+ ax, fig, plt = get_ax_fig_plt(ax=ax)
415
+
416
+ ax.set_xlabel(r'$E_{\mathrm{kin}}$ (-)')
417
+ ax.set_ylabel('Counts (-)')
418
+
419
+ extend, step, numb = extend_function(energy_range, \
420
+ energy_resolution)
421
+
422
+ extended_result = self.evaluate(extend)
423
+
424
+ final_result = gaussian_filter(extended_result, sigma=step)\
425
+ [numb:-numb if numb else None]
426
+
427
+ ax.plot(energy_range, final_result, label=self.label)
428
+
429
+ ax.legend()
430
+
431
+ return fig
432
+
433
+ class Constant(UniqueDistribution):
434
+ r"""Child class for constant distributions, used e.g., during MDC fitting.
435
+ The constant class is unique, only one instance should be used per task.
436
+
437
+ Parameters
438
+ ----------
439
+ offset : float
440
+ The value of the distribution for the abscissa equal to 0.
441
+ """
442
+ def __init__(self, offset, name='Constant'):
443
+ super().__init__(name)
444
+ self.offset = offset
445
+
446
+ @property
447
+ def offset(self):
448
+ r"""Returns the offset of the constant distribution.
449
+
450
+ Returns
451
+ -------
452
+ offset : float
453
+ The value of the distribution for the abscissa equal to 0.
454
+ """
455
+ return self._offset
456
+
457
+ @offset.setter
458
+ def offset(self, x):
459
+ r"""Sets the offset of the constant distribution.
348
460
 
349
461
  Parameters
350
462
  ----------
351
- energy_range : ndarray
352
- 1D array on which to evaluate and convolve FD distribution [eV]
353
- energy_resolution : float
354
- Energy resolution of the detector for the convolution [eV]
463
+ offset : float
464
+ The value of the distribution for the abscissa equal to 0.
465
+ """
466
+ self._offset = x
467
+
468
+ def __call__(self, angle_range, offset):
469
+ r"""TBD
470
+ """
471
+ return np.full(np.shape(angle_range), offset)
472
+
473
+ def evaluate(self, angle_range):
474
+ r"""TBD
475
+ """
476
+ return np.full(np.shape(angle_range), self.offset)
477
+
478
+ class Linear(UniqueDistribution):
479
+ r"""Child cass for for linear distributions, used e.g., during MDC
480
+ fitting. The linear class is unique, only one instance should be used per
481
+ task.
482
+
483
+ Parameters
484
+ ----------
485
+ offset : float
486
+ The value of the distribution for the abscissa equal to 0.
487
+ slope : float
488
+ The linear slope of the distribution w.r.t. the abscissa.
489
+ """
490
+ def __init__(self, slope, offset, name='Linear'):
491
+ super().__init__(name)
492
+ self.offset = offset
493
+ self.slope = slope
494
+
495
+ @property
496
+ def offset(self):
497
+ r"""Returns the offset of the linear distribution.
355
498
 
356
499
  Returns
357
500
  -------
358
- evalf : ndarray
359
- 1D array of the energy-convolved FD distribution [counts]
501
+ offset : float
502
+ The value of the distribution for the abscissa equal to 0.
360
503
  """
361
- from scipy.ndimage import gaussian_filter
362
-
363
- sigma_extend = 5 # Extend data range by "5 sigma"
364
- # Conversion from FWHM to standard deviation [-]
365
- fwhm_to_std = np.sqrt(8 * np.log(2))
366
- step_size = np.abs(energy_range[1] - energy_range[0])
367
- estep = energy_resolution / (step_size * fwhm_to_std)
368
- enumb = int(sigma_extend * estep)
369
- extend = np.linspace(energy_range[0] - enumb * step_size,
370
- energy_range[-1] + enumb * step_size,
371
- len(energy_range) + 2 * enumb)
372
- evalf = gaussian_filter(self.evaluate(extend),
373
- sigma=estep)[enumb:-enumb]
374
- return evalf
375
-
376
-
377
- class non_unique_distribution(distribution):
504
+ return self._offset
505
+
506
+ @offset.setter
507
+ def offset(self, x):
508
+ r"""Sets the offset of the linear distribution.
509
+
510
+ Parameters
511
+ ----------
512
+ offset : float
513
+ The value of the distribution for the abscissa equal to 0.
514
+ """
515
+ self._offset = x
516
+
517
+ @property
518
+ def slope(self):
519
+ r"""Returns the slope of the linear distribution.
520
+
521
+ Returns
522
+ -------
523
+ slope : float
524
+ The linear slope of the distribution w.r.t. the abscissa.
525
+ """
526
+ return self._slope
527
+
528
+ @slope.setter
529
+ def slope(self, x):
530
+ r"""Sets the slope of the linear distribution.
531
+
532
+ Parameters
533
+ ----------
534
+ slope : float
535
+ The linear slope of the distribution w.r.t. the abscissa.
536
+ """
537
+ self._slope = x
538
+
539
+ def __call__(self, angle_range, offset, slope):
540
+ r"""For a linear slope, convolution changes something.
541
+ """
542
+ return offset + slope * angle_range
543
+
544
+ def evaluate(self, angle_range):
545
+ r"""No energy convolution is performed with evaluate.
546
+ """
547
+ return self.offset + self.slope * angle_range
548
+
549
+ class NonUniqueDistribution(Distribution):
378
550
  r"""Parent class for unique distributions, to be used one at a time, e.g.,
379
551
  during the background of an MDC fit or the Fermi-Dirac distribution.
380
552
 
@@ -386,7 +558,8 @@ class non_unique_distribution(distribution):
386
558
  """
387
559
  def __init__(self, name, index):
388
560
  super().__init__(name)
389
- self._label = name + index
561
+ self._label = name + '_' + index
562
+ self._index = index
390
563
 
391
564
  @property
392
565
  def label(self):
@@ -395,27 +568,32 @@ class non_unique_distribution(distribution):
395
568
  Returns
396
569
  -------
397
570
  label : str
398
- Unique label for instances, identical to the name for unique
399
- distributions. Not to be modified after instantiation.
571
+ Unique label for instances, consisting of the label and the index
572
+ for non-unique distributions. Not to be modified after
573
+ instantiation.
400
574
  """
401
575
  return self._label
402
576
 
577
+ @property
578
+ def index(self):
579
+ r"""Returns the unique class index.
580
+
581
+ Returns
582
+ -------
583
+ index : str
584
+ Unique index for instances. Not to be modified after
585
+ instantiation.
586
+ """
587
+ return self._index
403
588
 
404
- class dispersion(distribution):
589
+ class Dispersion(NonUniqueDistribution):
405
590
  r"""Dispersions are assumed to be unique, so they need an index.
406
591
  """
407
- def __init__(self, amplitude, center, broadening, name, index):
408
- super().__init__(name)
409
- self._amplitude = amplitude
410
- self._center = center
411
- self._broadening = broadening
412
- self._label = name + index
413
-
414
- @property
415
- def label(self):
416
- r"""
417
- """
418
- return self._label
592
+ def __init__(self, amplitude, peak, broadening, name, index):
593
+ super().__init__(name, index)
594
+ self.amplitude = amplitude
595
+ self.peak = peak
596
+ self.broadening = broadening
419
597
 
420
598
  @property
421
599
  def amplitude(self):
@@ -424,22 +602,22 @@ class dispersion(distribution):
424
602
  return self._amplitude
425
603
 
426
604
  @amplitude.setter
427
- def set_amplitude(self, x):
605
+ def amplitude(self, x):
428
606
  r"""
429
607
  """
430
608
  self._amplitude = x
431
609
 
432
610
  @property
433
- def center(self):
611
+ def peak(self):
434
612
  r"""
435
613
  """
436
- return self._center
614
+ return self._peak
437
615
 
438
- @amplitude.setter
439
- def set_center(self, x):
616
+ @peak.setter
617
+ def peak(self, x):
440
618
  r"""
441
619
  """
442
- self._center = x
620
+ self._peak = x
443
621
 
444
622
  @property
445
623
  def broadening(self):
@@ -448,58 +626,141 @@ class dispersion(distribution):
448
626
  return self._broadening
449
627
 
450
628
  @broadening.setter
451
- def set_broadening(self, x):
629
+ def broadening(self, x):
452
630
  r"""
453
631
  """
454
632
  self._broadening = x
455
633
 
634
+ class SpectralLinear(Dispersion):
635
+ r"""Class for the linear dispersion spectral function"""
636
+ def __init__(self, amplitude, peak, broadening, name, index):
637
+ super().__init__(amplitude=amplitude, peak=peak,
638
+ broadening=broadening, name=name, index=index)
639
+
640
+ def __call__(self, angle_range, amplitude, broadening,
641
+ peak):
642
+ r"""
643
+ """
644
+ result = amplitude / np.pi * broadening / ((np.sin(angle_range * dtor)
645
+ - np.sin(peak * dtor)) ** 2 + broadening ** 2)
646
+ return result
647
+
648
+ def evaluate(self, angle_range):
649
+ r"""
650
+ """
651
+ return self.amplitude / np.pi * self.broadening / ((np.sin(
652
+ angle_range * dtor) - np.sin(self.peak * dtor)) ** 2 +
653
+ self.broadening ** 2)
456
654
 
457
- # class spectral_linear(dispersion):
458
- # r"""Class for the linear dispersion spectral function"""
459
- # def __init__(self, amplitude, center, broadening, name, index):
460
- # super().__init__(amplitude=amplitude, center=center,
461
- # broadening=broadening, name=name, index=index)
462
-
463
- # def result(self, x):
464
- # r"""
465
- # """
466
- # dtor = np.pi/180
467
- # evalf = self.amplitude / np.pi * self.broadening / ((np.sin(x*dtor)
468
- # - np.sin(self.center*dtor))**2 + self.broadening**2)
469
- # return evalf
470
-
471
-
472
- # class spectral_linear(dispersion):
473
- # r"""Class for the linear dispersion spectral function"""
474
- # def __init__(self, amplitude, center, broadening, bottom, side, name,
475
- # index):
476
- # super()__init__(amplitude=amplitude, center=center,
477
- # broadening=broadening, name=name, index=index)
478
- # self._bottom = bottom
479
- # self._side = side
480
-
481
- # @property
482
- # def bottom(self):
483
- # r"""
484
- # """
485
- # return self._bottom
486
-
487
- # @bottom.setter
488
- # def set_bottom(self, x):
489
- # r"""
490
- # """
491
- # self._bottom = x
492
-
493
- # @property
494
- # def side(self):
495
- # r"""
496
- # """
497
- # return self._side
498
655
 
499
- # def result(self, x):
500
- # r"""
501
- # """
502
- # dtor = np.pi/180
503
- # evalf = self.amplitude / np.pi * self.broadening / (((np.sin(x*dtor)
504
- # - np.sin(self.bottom*dtor))**2 - np.sin(self.center*dtor)**2)**2
505
- # + self.broadening**2)
656
+ class SpectralQuadratic(Dispersion):
657
+ r"""Class for the quadratic dispersion spectral function"""
658
+ def __init__(self, amplitude, peak, broadening, name, index,
659
+ center_wavevector=None, center_angle=None):
660
+ self.check_center_coordinates(center_wavevector, center_angle)
661
+ super().__init__(amplitude=amplitude, peak=peak,
662
+ broadening=broadening, name=name, index=index)
663
+ self.center_wavevector = center_wavevector
664
+ self.center_angle = center_angle
665
+
666
+ @property
667
+ def center_angle(self):
668
+ r"""TBD
669
+ """
670
+ return self._center_angle
671
+
672
+ @center_angle.setter
673
+ def center_angle(self, x):
674
+ r"""TBD
675
+ """
676
+ self._center_angle = x
677
+
678
+ @property
679
+ def center_wavevector(self):
680
+ r"""TBD
681
+ """
682
+ return self._center_wavevector
683
+
684
+ @center_wavevector.setter
685
+ def center_wavevector(self, x):
686
+ r"""TBD
687
+ """
688
+ self._center_wavevector = x
689
+
690
+ def check_center_coordinates(self, center_wavevector, center_angle):
691
+ r"""TBD
692
+ """
693
+ if (center_wavevector is None and center_angle is None) \
694
+ or (center_wavevector is not None and center_angle is not None):
695
+ raise ValueError('Please specify exactly one of '
696
+ 'center_wavevector and center_angle.')
697
+
698
+ def check_binding_angle(self, binding_angle):
699
+ r"""TBD
700
+ """
701
+ if np.isnan(binding_angle):
702
+ raise ValueError('The provided wavevector cannot be reached '
703
+ 'with the available range of kinetic '
704
+ 'energies. Please check again.')
705
+
706
+ def __call__(self, angle_range, amplitude, broadening,
707
+ peak, kinetic_energy, hnuminphi, center_wavevector=None,
708
+ center_angle=None):
709
+ r"""TBD
710
+ """
711
+ self.check_center_coordinates(center_wavevector, center_angle)
712
+
713
+ if center_wavevector is not None:
714
+ binding_angle = np.arcsin(np.sqrt(pref / kinetic_energy)
715
+ * center_wavevector) / dtor
716
+ self.check_binding_angle(binding_angle)
717
+ elif center_angle is not None:
718
+ binding_angle = self.center_angle * np.sqrt(hnuminphi /
719
+ kinetic_energy)
720
+
721
+ return amplitude / np.pi * broadening / (((np.sin(angle_range * dtor)
722
+ - np.sin(binding_angle * dtor)) ** 2 - np.sin(peak * dtor) ** 2)
723
+ ** 2 + broadening ** 2)
724
+
725
+ def evaluate(self, angle_range, kinetic_energy, hnuminphi):
726
+ r"""TBD
727
+ """
728
+ if self.center_wavevector is not None:
729
+ binding_angle = np.arcsin(np.sqrt(pref / kinetic_energy)
730
+ * self.center_wavevector) / dtor
731
+ self.check_binding_angle(binding_angle)
732
+ elif self.center_angle is not None:
733
+ binding_angle = self.center_angle * np.sqrt(hnuminphi /
734
+ kinetic_energy)
735
+
736
+ return self.amplitude / np.pi * self.broadening / (((np.sin(
737
+ angle_range * dtor) - np.sin(binding_angle * dtor)) ** 2 - np.sin(
738
+ self.peak * dtor) ** 2) ** 2 + self.broadening ** 2)
739
+
740
+ @add_fig_kwargs
741
+ def plot(self, angle_range, angle_resolution, kinetic_energy, hnuminphi,
742
+ matrix_element=None, matrix_args=None, ax=None, **kwargs):
743
+ r"""Overwrites generic class plotting method.
744
+ """
745
+ from scipy.ndimage import gaussian_filter
746
+
747
+ ax, fig, plt = get_ax_fig_plt(ax=ax)
748
+
749
+ ax.set_xlabel('Angle ($\degree$)')
750
+ ax.set_ylabel('Counts (-)')
751
+
752
+ extend, step, numb = extend_function(angle_range, angle_resolution)
753
+
754
+ extended_result = self.evaluate(extend, kinetic_energy, hnuminphi)
755
+
756
+ if matrix_element is not None:
757
+ extended_result *= matrix_element(extend, **matrix_args)
758
+
759
+ final_result = gaussian_filter(extended_result, sigma=step)[
760
+ numb:-numb if numb else None]
761
+
762
+ ax.plot(angle_range, final_result, label=self.label)
763
+
764
+ ax.legend()
765
+
766
+ return fig