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/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
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,17 +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
- 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):
33
208
  r"""Parent class for unique distributions, to be used one at a time, e.g.,
34
209
  during the background of an MDC fit or the Fermi-Dirac distribution.
35
210
 
@@ -55,105 +230,10 @@ class unique_distribution(distribution):
55
230
  """
56
231
  return self._label
57
232
 
58
- class constant(unique_distribution):
59
- r"""Child class for constant distributions, used e.g., during MDC fitting.
60
- The constant class is unique, only one instance should be used per task.
61
-
62
- Parameters
63
- ----------
64
- offset : float
65
- The value of the distribution for the abscissa equal to 0.
66
- """
67
- def __init__(self, offset, name='constant'):
68
- super().__init__(name)
69
- self._offset = offset
70
-
71
- @property
72
- def offset(self):
73
- r"""Returns the offset of the constant distribution.
74
-
75
- Returns
76
- -------
77
- offset : float
78
- The value of the distribution for the abscissa equal to 0.
79
- """
80
- return self._offset
81
-
82
- @offset.setter
83
- def set_offset(self, x):
84
- r"""Sets the offset of the constant distribution.
85
-
86
- Parameters
87
- ----------
88
- offset : float
89
- The value of the distribution for the abscissa equal to 0.
90
- """
91
- self._offset = x
92
-
93
- class linear(unique_distribution):
94
- r"""Child cass for for linear distributions, used e.g., during MDC fitting.
95
- The constant class is unique, only one instance should be used per task.
96
-
97
- Parameters
98
- ----------
99
- offset : float
100
- The value of the distribution for the abscissa equal to 0.
101
- slope : float
102
- The linear slope of the distribution w.r.t. the abscissa.
103
- """
104
- def __init__(self, slope, offset, name='linear'):
105
- super().__init__(name)
106
- self._offset = offset
107
- self._slope = slope
108
-
109
- @property
110
- def offset(self):
111
- r"""Returns the offset of the linear distribution.
112
-
113
- Returns
114
- -------
115
- offset : float
116
- The value of the distribution for the abscissa equal to 0.
117
- """
118
- return self._offset
119
-
120
- @offset.setter
121
- def set_offset(self, x):
122
- r"""Sets the offset of the linear distribution.
123
-
124
- Parameters
125
- ----------
126
- offset : float
127
- The value of the distribution for the abscissa equal to 0.
128
- """
129
- self._offset = x
130
-
131
- @property
132
- def slope(self):
133
- r"""Returns the slope of the linear distribution.
134
-
135
- Returns
136
- -------
137
- slope : float
138
- The linear slope of the distribution w.r.t. the abscissa.
139
- """
140
- return self._slope
141
-
142
- @slope.setter
143
- def set_slope(self, x):
144
- r"""Sets the slope of the linear distribution.
145
-
146
- Parameters
147
- ----------
148
- slope : float
149
- The linear slope of the distribution w.r.t. the abscissa.
150
- """
151
- self._slope = x
152
-
153
- class fermi_dirac(unique_distribution):
154
- r"""Child class for Fermi-Dirac (FD) distributions, used e.g., during Fermi
155
- edge fitting. The FD class is unique, only one instance should be used
156
- 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.
157
237
 
158
238
  The Fermi-Dirac distribution is described by the following formula:
159
239
 
@@ -162,25 +242,25 @@ class fermi_dirac(unique_distribution):
162
242
  \frac{A}{\rm{e}^{\beta(E_{\rm{kin}}-(h\nu-\Phi))}+1} + B
163
243
 
164
244
  with :math:`A` as :attr:`integrated_weight`, :math:`B` as
165
- :attr:`background`, :math:`h\nu-\Phi` as :attr:`hnuminphi`, and
245
+ :attr:`background`, :math:`h\nu-\Phi` as :attr:`hnuminPhi`, and
166
246
  :math:`\beta=1/(k_{\rm{B}}T)` with :math:`T` as :attr:`temperature`.
167
247
 
168
248
  Parameters
169
249
  ----------
170
250
  temperature : float
171
251
  Temperature of the sample [K]
172
- hnuminphi : float
252
+ hnuminPhi : float
173
253
  Kinetic energy minus the work function [eV]
174
254
  background : float
175
255
  Background spectral weight [counts]
176
256
  integrated_weight : float
177
257
  Integrated weight on top of the background [counts]
178
258
  """
179
- def __init__(self, temperature, hnuminphi, background=0,
180
- integrated_weight=1, name='fermi_dirac'):
259
+ def __init__(self, temperature, hnuminPhi, background=0,
260
+ integrated_weight=1, name='FermiDirac'):
181
261
  super().__init__(name)
182
262
  self.temperature = temperature
183
- self.hnuminphi = hnuminphi
263
+ self.hnuminPhi = hnuminPhi
184
264
  self.background = background
185
265
  self.integrated_weight = integrated_weight
186
266
 
@@ -196,7 +276,7 @@ class fermi_dirac(unique_distribution):
196
276
  return self._temperature
197
277
 
198
278
  @temperature.setter
199
- def set_temperature(self, x):
279
+ def temperature(self, x):
200
280
  r"""Sets the temperature of the FD distribution.
201
281
 
202
282
  Parameters
@@ -207,28 +287,28 @@ class fermi_dirac(unique_distribution):
207
287
  self._temperature = x
208
288
 
209
289
  @property
210
- def hnuminphi(self):
290
+ def hnuminPhi(self):
211
291
  r"""Returns the photon energy minus the work function of the FD
212
292
  distribution.
213
293
 
214
294
  Returns
215
295
  -------
216
- hnuminphi: float
296
+ hnuminPhi: float
217
297
  Kinetic energy minus the work function [eV]
218
298
  """
219
- return self._hnuminphi
299
+ return self._hnuminPhi
220
300
 
221
- @hnuminphi.setter
222
- def set_hnuminphi(self, x):
301
+ @hnuminPhi.setter
302
+ def hnuminPhi(self, x):
223
303
  r"""Sets the photon energy minus the work function of the FD
224
304
  distribution.
225
305
 
226
306
  Parameters
227
307
  ----------
228
- hnuminphi : float
308
+ hnuminPhi : float
229
309
  Kinetic energy minus the work function [eV]
230
310
  """
231
- self._hnuminphi = x
311
+ self._hnuminPhi = x
232
312
 
233
313
  @property
234
314
  def background(self):
@@ -242,7 +322,7 @@ class fermi_dirac(unique_distribution):
242
322
  return self._background
243
323
 
244
324
  @background.setter
245
- def set_background(self, x):
325
+ def background(self, x):
246
326
  r"""Sets the background intensity of the FD distribution.
247
327
 
248
328
  Parameters
@@ -264,7 +344,7 @@ class fermi_dirac(unique_distribution):
264
344
  return self._integrated_weight
265
345
 
266
346
  @integrated_weight.setter
267
- def set_integrated_weight(self, x):
347
+ def integrated_weight(self, x):
268
348
  r"""Sets the integrated weight of the FD distribution.
269
349
 
270
350
  Parameters
@@ -274,8 +354,8 @@ class fermi_dirac(unique_distribution):
274
354
  """
275
355
  self._integrated_weight = x
276
356
 
277
- def __call__(self, energy_range, hnuminphi, background, integrated_weight,
278
- energy_resolution):
357
+ def __call__(self, energy_range, hnuminPhi, background, integrated_weight,
358
+ temperature):
279
359
  """Call method to directly evaluate a FD distribution without having to
280
360
  instantiate a class instance.
281
361
 
@@ -283,7 +363,7 @@ class fermi_dirac(unique_distribution):
283
363
  ----------
284
364
  energy_range : ndarray
285
365
  1D array on which to evaluate the FD distribution [eV]
286
- hnuminphi : float
366
+ hnuminPhi : float
287
367
  Kinetic energy minus the work function [eV]
288
368
  background : float
289
369
  Background spectral weight [counts]
@@ -297,23 +377,10 @@ class fermi_dirac(unique_distribution):
297
377
  evalf : ndarray
298
378
  1D array of the energy-convolved FD distribution [counts]
299
379
  """
300
- from scipy.ndimage import gaussian_filter
380
+ k_BT = temperature * K_B
301
381
 
302
- sigma_extend = 5 # Extend data range by "5 sigma"
303
- # Conversion from FWHM to standard deviation [-]
304
- fwhm_to_std = np.sqrt(8 * np.log(2))
305
- k_B = 8.617e-5 # Boltzmann constant [eV/K]
306
- k_BT = self.temperature * k_B
307
- step_size = np.abs(energy_range[1] - energy_range[0])
308
- estep = energy_resolution / (step_size * fwhm_to_std)
309
- enumb = int(sigma_extend * estep)
310
- extend = np.linspace(energy_range[0] - enumb * step_size,
311
- energy_range[-1] + enumb * step_size,
312
- len(energy_range) + 2 * enumb)
313
- result = (integrated_weight / (1 + np.exp((extend - hnuminphi) / k_BT))
314
- + background)
315
- evalf = gaussian_filter(result, sigma=estep)[enumb:-enumb]
316
- return evalf
382
+ return (integrated_weight / (1 + np.exp((energy_range - hnuminPhi)
383
+ / k_BT)) + background)
317
384
 
318
385
  def evaluate(self, energy_range):
319
386
  r"""Evaluates the FD distribution for a given class instance.
@@ -329,47 +396,157 @@ class fermi_dirac(unique_distribution):
329
396
  evalf : ndarray
330
397
  1D array of the evaluated FD distribution [counts]
331
398
  """
332
- k_B = 8.617e-5 # Boltzmann constant [eV/K]
333
- k_BT = self.temperature * k_B
334
- evalf = (self.integrated_weight
335
- / (1 + np.exp((energy_range - self.hnuminphi) / k_BT))
399
+ k_BT = self.temperature * K_B
400
+
401
+ return (self.integrated_weight
402
+ / (1 + np.exp((energy_range - self.hnuminPhi) / k_BT))
336
403
  + self.background)
337
- return evalf
338
404
 
339
- def convolve(self, energy_range, energy_resolution):
340
- r"""Evaluates the FD distribution for a given class instance and
341
- performs the energy convolution with the given resolution. The
342
- convolution is performed with an expanded abscissa range of 5
343
- 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.
344
460
 
345
461
  Parameters
346
462
  ----------
347
- energy_range : ndarray
348
- 1D array on which to evaluate and convolve FD distribution [eV]
349
- energy_resolution : float
350
- 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.
351
498
 
352
499
  Returns
353
500
  -------
354
- evalf : ndarray
355
- 1D array of the energy-convolved FD distribution [counts]
501
+ offset : float
502
+ The value of the distribution for the abscissa equal to 0.
356
503
  """
357
- from scipy.ndimage import gaussian_filter
504
+ return self._offset
358
505
 
359
- sigma_extend = 5 # Extend data range by "5 sigma"
360
- # Conversion from FWHM to standard deviation [-]
361
- fwhm_to_std = np.sqrt(8 * np.log(2))
362
- step_size = np.abs(energy_range[1] - energy_range[0])
363
- estep = energy_resolution / (step_size * fwhm_to_std)
364
- enumb = int(sigma_extend * estep)
365
- extend = np.linspace(energy_range[0] - enumb * step_size,
366
- energy_range[-1] + enumb * step_size,
367
- len(energy_range) + 2 * enumb)
368
- evalf = gaussian_filter(self.evaluate(extend),
369
- sigma=estep)[enumb:-enumb]
370
- return evalf
371
-
372
- class non_unique_distribution(distribution):
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):
373
550
  r"""Parent class for unique distributions, to be used one at a time, e.g.,
374
551
  during the background of an MDC fit or the Fermi-Dirac distribution.
375
552
 
@@ -381,7 +558,8 @@ class non_unique_distribution(distribution):
381
558
  """
382
559
  def __init__(self, name, index):
383
560
  super().__init__(name)
384
- self._label = name + index
561
+ self._label = name + '_' + index
562
+ self._index = index
385
563
 
386
564
  @property
387
565
  def label(self):
@@ -390,26 +568,32 @@ class non_unique_distribution(distribution):
390
568
  Returns
391
569
  -------
392
570
  label : str
393
- Unique label for instances, identical to the name for unique
394
- 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.
395
574
  """
396
575
  return self._label
397
576
 
398
- class dispersion(distribution):
399
- r"""Dispersions are assumed to be unique, so they need an index.
400
- """
401
- def __init__(self, amplitude, center, broadening, name, index):
402
- super().__init__(name)
403
- self._amplitude = amplitude
404
- self._center = center
405
- self._broadening = broadening
406
- self._label = name + index
407
-
408
577
  @property
409
- def label(self):
410
- r"""
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.
411
586
  """
412
- return self._label
587
+ return self._index
588
+
589
+ class Dispersion(NonUniqueDistribution):
590
+ r"""Dispersions are assumed to be unique, so they need an index.
591
+ """
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
413
597
 
414
598
  @property
415
599
  def amplitude(self):
@@ -418,22 +602,22 @@ class dispersion(distribution):
418
602
  return self._amplitude
419
603
 
420
604
  @amplitude.setter
421
- def set_amplitude(self, x):
605
+ def amplitude(self, x):
422
606
  r"""
423
607
  """
424
608
  self._amplitude = x
425
609
 
426
610
  @property
427
- def center(self):
611
+ def peak(self):
428
612
  r"""
429
613
  """
430
- return self._center
614
+ return self._peak
431
615
 
432
- @amplitude.setter
433
- def set_center(self, x):
616
+ @peak.setter
617
+ def peak(self, x):
434
618
  r"""
435
619
  """
436
- self._center = x
620
+ self._peak = x
437
621
 
438
622
  @property
439
623
  def broadening(self):
@@ -442,56 +626,143 @@ class dispersion(distribution):
442
626
  return self._broadening
443
627
 
444
628
  @broadening.setter
445
- def set_broadening(self, x):
629
+ def broadening(self, x):
446
630
  r"""
447
631
  """
448
632
  self._broadening = x
449
633
 
450
- # class spectral_linear(dispersion):
451
- # r"""Class for the linear dispersion spectral function"""
452
- # def __init__(self, amplitude, center, broadening, name, index):
453
- # super().__init__(amplitude=amplitude, center=center,
454
- # broadening=broadening, name=name, index=index)
455
-
456
- # def result(self, x):
457
- # r"""
458
- # """
459
- # dtor = np.pi/180
460
- # evalf = self.amplitude / np.pi * self.broadening / ((np.sin(x*dtor)
461
- # - np.sin(self.center*dtor))**2 + self.broadening**2)
462
- # return evalf
463
-
464
- # class spectral_linear(dispersion):
465
- # r"""Class for the linear dispersion spectral function"""
466
- # def __init__(self, amplitude, center, broadening, bottom, side, name,
467
- # index):
468
- # super()__init__(amplitude=amplitude, center=center,
469
- # broadening=broadening, name=name, index=index)
470
- # self._bottom = bottom
471
- # self._side = side
472
-
473
- # @property
474
- # def bottom(self):
475
- # r"""
476
- # """
477
- # return self._bottom
478
-
479
- # @bottom.setter
480
- # def set_bottom(self, x):
481
- # r"""
482
- # """
483
- # self._bottom = x
484
-
485
- # @property
486
- # def side(self):
487
- # r"""
488
- # """
489
- # return self._side
490
-
491
- # def result(self, x):
492
- # r"""
493
- # """
494
- # dtor = np.pi/180
495
- # evalf = self.amplitude / np.pi * self.broadening / (((np.sin(x*dtor)
496
- # - np.sin(self.bottom*dtor))**2 - np.sin(self.center*dtor)**2)**2
497
- # + self.broadening**2)
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 / \
645
+ ((np.sin(np.deg2rad(angle_range)) - np.sin(np.deg2rad(peak)))**2 \
646
+ + broadening ** 2)
647
+ return result
648
+
649
+ def evaluate(self, angle_range):
650
+ r"""
651
+ """
652
+ return self.amplitude / np.pi * self.broadening / ((np.sin(
653
+ np.deg2rad(angle_range)) - np.sin(np.deg2rad(self.peak)))** 2 +
654
+ self.broadening** 2)
655
+
656
+
657
+ class SpectralQuadratic(Dispersion):
658
+ r"""Class for the quadratic dispersion spectral function"""
659
+ def __init__(self, amplitude, peak, broadening, name, index,
660
+ center_wavevector=None, center_angle=None):
661
+ self.check_center_coordinates(center_wavevector, center_angle)
662
+ super().__init__(amplitude=amplitude, peak=peak,
663
+ broadening=broadening, name=name, index=index)
664
+ self.center_wavevector = center_wavevector
665
+ self.center_angle = center_angle
666
+
667
+ @property
668
+ def center_angle(self):
669
+ r"""TBD
670
+ """
671
+ return self._center_angle
672
+
673
+ @center_angle.setter
674
+ def center_angle(self, x):
675
+ r"""TBD
676
+ """
677
+ self._center_angle = x
678
+
679
+ @property
680
+ def center_wavevector(self):
681
+ r"""TBD
682
+ """
683
+ return self._center_wavevector
684
+
685
+ @center_wavevector.setter
686
+ def center_wavevector(self, x):
687
+ r"""TBD
688
+ """
689
+ self._center_wavevector = x
690
+
691
+ def check_center_coordinates(self, center_wavevector, center_angle):
692
+ r"""TBD
693
+ """
694
+ if (center_wavevector is None and center_angle is None) \
695
+ or (center_wavevector is not None and center_angle is not None):
696
+ raise ValueError('Please specify exactly one of '
697
+ 'center_wavevector and center_angle.')
698
+
699
+ def check_binding_angle(self, binding_angle):
700
+ r"""TBD
701
+ """
702
+ if np.isnan(binding_angle):
703
+ raise ValueError('The provided wavevector cannot be reached '
704
+ 'with the available range of kinetic '
705
+ 'energies. Please check again.')
706
+
707
+ def __call__(self, angle_range, amplitude, broadening,
708
+ peak, kinetic_energy, hnuminPhi, center_wavevector=None,
709
+ center_angle=None):
710
+ r"""TBD
711
+ """
712
+ self.check_center_coordinates(center_wavevector, center_angle)
713
+
714
+ if center_wavevector is not None:
715
+ binding_angle = np.rad2deg(np.arcsin(np.sqrt(PREF / kinetic_energy)
716
+ * center_wavevector))
717
+ self.check_binding_angle(binding_angle)
718
+ elif center_angle is not None:
719
+ binding_angle = self.center_angle * np.sqrt(hnuminPhi /
720
+ kinetic_energy)
721
+
722
+ return amplitude / np.pi * broadening / (((np.sin(np.deg2rad(angle_range))
723
+ - np.sin(np.deg2rad(binding_angle)))** 2 - np.sin(np.deg2rad(peak))** 2)
724
+ ** 2 + broadening ** 2)
725
+
726
+ def evaluate(self, angle_range, kinetic_energy, hnuminPhi):
727
+ r"""TBD
728
+ """
729
+ if self.center_wavevector is not None:
730
+ binding_angle = np.rad2deg(np.arcsin(np.sqrt(PREF / kinetic_energy)
731
+ * self.center_wavevector))
732
+ self.check_binding_angle(binding_angle)
733
+ elif self.center_angle is not None:
734
+ binding_angle = self.center_angle * np.sqrt(hnuminPhi /
735
+ kinetic_energy)
736
+
737
+ return self.amplitude / np.pi * self.broadening / \
738
+ (((np.sin(np.deg2rad(angle_range)) - \
739
+ np.sin(np.deg2rad(binding_angle)))**2 - \
740
+ np.sin(np.deg2rad(self.peak))**2)**2 + self.broadening**2)
741
+
742
+ @add_fig_kwargs
743
+ def plot(self, angle_range, angle_resolution, kinetic_energy, hnuminPhi,
744
+ matrix_element=None, matrix_args=None, ax=None, **kwargs):
745
+ r"""Overwrites generic class plotting method.
746
+ """
747
+ from scipy.ndimage import gaussian_filter
748
+
749
+ ax, fig, plt = get_ax_fig_plt(ax=ax)
750
+
751
+ ax.set_xlabel('Angle ($\degree$)')
752
+ ax.set_ylabel('Counts (-)')
753
+
754
+ extend, step, numb = extend_function(angle_range, angle_resolution)
755
+
756
+ extended_result = self.evaluate(extend, kinetic_energy, hnuminPhi)
757
+
758
+ if matrix_element is not None:
759
+ extended_result *= matrix_element(extend, **matrix_args)
760
+
761
+ final_result = gaussian_filter(extended_result, sigma=step)[
762
+ numb:-numb if numb else None]
763
+
764
+ ax.plot(angle_range, final_result, label=self.label)
765
+
766
+ ax.legend()
767
+
768
+ return fig