Skip to content

convolution

Convolution

Bases: NumericalConvolutionBase

Convolution class that combines analytical and numerical convolution methods to efficiently perform convolutions of ComponentCollections with ResolutionComponents.

Supports analytical convolution for pairs of analytical model components (DeltaFunction, Gaussian, Lorentzian, Voigt), while using numerical convolution for other components. If temperature is provided, detailed balance correction is applied to the sample model. In this case, all convolutions are handled numerically. Includes a setting to normalize the detailed balance correction. Includes optional upsampling and extended range to improve accuracy of the numerical convolutions. Also warns about numerical instabilities if peaks are very wide or very narrow.

Source code in src/easydynamics/convolution/convolution.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
class Convolution(NumericalConvolutionBase):
    """
    Convolution class that combines analytical and numerical convolution methods to efficiently
    perform convolutions of ComponentCollections with ResolutionComponents.

    Supports analytical convolution for pairs of analytical model components (DeltaFunction,
    Gaussian, Lorentzian, Voigt), while using numerical convolution for other components. If
    temperature is provided, detailed balance correction is applied to the sample model. In this
    case, all convolutions are handled numerically. Includes a setting to normalize the detailed
    balance correction. Includes optional upsampling and extended range to improve accuracy of the
    numerical convolutions. Also warns about numerical instabilities if peaks are very wide or very
    narrow.
    """

    # When these attributes are changed, the convolution plan
    # needs to be rebuilt
    _invalidate_plan_on_change: ClassVar[dict[str, object]] = {
        'energy',
        '_energy',
        '_energy_grid',
        '_sample_components',
        '_resolution_components',
        '_temperature',
        '_energy_unit',
        '_normalize_detailed_balance',
        '_detailed_balance_settings',
    }

    def __init__(
        self,
        energy: np.ndarray | sc.Variable,
        sample_components: ComponentCollection | ModelComponent,
        resolution_components: ComponentCollection | ModelComponent,
        energy_offset: Numeric | Parameter = 0.0,
        convolution_settings: ConvolutionSettings | None = None,
        temperature: Parameter | Numeric | None = None,
        temperature_unit: str | sc.Unit = 'K',
        detailed_balance_settings: DetailedBalanceSettings | None = None,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'MyConvolution',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the Convolution class.

        Parameters
        ----------
        energy : np.ndarray | sc.Variable
            1D array of energy values where the convolution is evaluated.
        sample_components : ComponentCollection | ModelComponent
            The  sample components to be convolved.
        resolution_components : ComponentCollection | ModelComponent
            The resolution components to convolve with.
        energy_offset : Numeric | Parameter, default=0.0
            An energy offset to apply to the energy values before convolution.
        convolution_settings : ConvolutionSettings | None, default=None
            The settings for the convolution. If None, default settings will be used.
        temperature : Parameter | Numeric | None, default=None
            The temperature to use for detailed balance correction.
        temperature_unit : str | sc.Unit, default='K'
            The unit of the temperature parameter.
        detailed_balance_settings : DetailedBalanceSettings | None, default=None
            The settings for detailed balance. If None, default settings will be used.
        unit : str | sc.Unit, default='meV'
            The unit of the energy.
        display_name : str | None, default='MyConvolution'
            Display name of the model.
        unique_name : str | None, default=None
            Unique name of the model. If None, a unique name will be generated.
        """

        self._reactions_enabled = False
        super().__init__(
            energy=energy,
            sample_components=sample_components,
            resolution_components=resolution_components,
            energy_offset=energy_offset,
            convolution_settings=convolution_settings,
            temperature=temperature,
            temperature_unit=temperature_unit,
            detailed_balance_settings=detailed_balance_settings,
            unit=unit,
            display_name=display_name,
            unique_name=unique_name,
        )

        self._reactions_enabled = True
        # Separate sample model components into pairs that can be
        # handled analytically, delta functions, and the rest
        # Also initialize analytical and numerical convolvers based on
        # sample model component
        self._build_convolution_plan()

    def convolution(
        self,
    ) -> np.ndarray:
        """
        Perform convolution using analytical convolutions where possible, and numerical
        convolutions for the remaining components.

        Returns
        -------
        np.ndarray
            The convolved values evaluated at energy.
        """
        if not self.convolution_settings.convolution_plan_is_valid:
            self._build_convolution_plan()
        total = np.zeros_like(self.energy.values, dtype=float)

        # Analytical convolution
        if self._analytical_convolver is not None:
            total += self._analytical_convolver.convolution()

        # Numerical convolution
        if self._numerical_convolver is not None:
            total += self._numerical_convolver.convolution()

        # Delta function components
        if self._delta_sample_components.components:
            total += self._convolve_delta_functions()

        return total

    def _convolve_delta_functions(self) -> np.ndarray:
        """
        Convolve delta function components of the sample model with the resolution components. No
        detailed balance correction is applied to delta functions.

        Returns
        -------
        np.ndarray
            The convolved values of the delta function c components evaluated at energy.
        """
        return sum(
            delta.area.value
            * self._resolution_components.evaluate(
                self.energy_with_offset.values - delta.center.value
            )
            for delta in self._delta_sample_components.components
        )

    def _check_if_pair_is_analytic(
        self,
        sample_component: ModelComponent,
        resolution_component: ModelComponent,
    ) -> bool:
        """
        Check if the convolution of the given component pair can be handled analytically.

        Parameters
        ----------
        sample_component : ModelComponent
            The sample component to be convolved.
        resolution_component : ModelComponent
            The resolution component to convolve with.

        Raises
        ------
        TypeError
            If either component is not a ModelComponent, or if the resolution component is a
            DeltaFunction.

        Returns
        -------
        bool
            True if the component pair can be handled analytically, False otherwise.
        """

        if not isinstance(sample_component, ModelComponent):
            raise TypeError(
                f'`sample_component` is an instance of {type(sample_component).__name__}, \
                but must be a ModelComponent.'
            )

        if not isinstance(resolution_component, ModelComponent):
            raise TypeError(
                f'`resolution_component` is an instance of {type(resolution_component).__name__}, \
                    but must be a ModelComponent.'
            )

        if isinstance(resolution_component, DeltaFunction):
            raise TypeError(
                'resolution components contains delta functions. This is not supported.'
            )

        analytical_types = (Gaussian, Lorentzian, Voigt)
        return bool(
            isinstance(sample_component, analytical_types)
            and isinstance(resolution_component, analytical_types)
        )

    def _build_convolution_plan(self) -> None:
        """
        Separate sample model components into analytical pairs, delta functions, and the rest.
        """

        analytical_sample_components = ComponentCollection()
        delta_sample_components = ComponentCollection()
        numerical_sample_components = ComponentCollection()

        for sample_component in self._sample_components.components:
            # If delta function, put in delta sample model and go to the
            # next component
            if isinstance(sample_component, DeltaFunction):
                delta_sample_components.append_component(sample_component)
                continue

            # If temperature is set, all other components go to
            # numerical sample model
            if (
                self.temperature is not None
                and self.detailed_balance_settings.use_detailed_balance
            ):
                numerical_sample_components.append_component(sample_component)
                continue

            # If temperature is not set, check if all
            # resolution components can be convolved analytically with
            # this sample component
            pair_is_analytic = [
                self._check_if_pair_is_analytic(sample_component, resolution_component)
                for resolution_component in self._resolution_components.components
            ]
            # If all resolution components can be convolved analytically
            # with this sample component, add it to analytical
            # sample model. If not, it goes to numerical sample model.
            if all(pair_is_analytic):
                analytical_sample_components.append_component(sample_component)
            else:
                numerical_sample_components.append_component(sample_component)

        self._analytical_sample_components = analytical_sample_components
        self._delta_sample_components = delta_sample_components
        self._numerical_sample_components = numerical_sample_components

        # Update convolvers
        self._set_convolvers()
        self.convolution_settings.convolution_plan_is_valid = True

    def _set_convolvers(self) -> None:
        """
        Initialize analytical and numerical convolvers based on sample model components.

        There is no delta function convolver, as delta functions are handled directly in the
        convolution method.
        """

        if self._analytical_sample_components.components:
            self._analytical_convolver = AnalyticalConvolution(
                energy=self.energy,
                energy_offset=self.energy_offset,
                sample_components=self._analytical_sample_components,
                resolution_components=self._resolution_components,
            )
        else:
            self._analytical_convolver = None

        if self._numerical_sample_components.components:
            self._numerical_convolver = NumericalConvolution(
                energy=self.energy,
                energy_offset=self.energy_offset,
                sample_components=self._numerical_sample_components,
                resolution_components=self._resolution_components,
                convolution_settings=self.convolution_settings,
                temperature=self.temperature,
                temperature_unit=self._temperature_unit,
                detailed_balance_settings=self.detailed_balance_settings,
                unit=self.unit,
            )
        else:
            self._numerical_convolver = None

    # Update some setters so the internal sample models are updated
    def __setattr__(self, name: str, value: any) -> None:
        """
        Custom setattr to invalidate convolution plan on relevant attribute changes, and build a
        new plan.

        The new plan is only built after initialization (when _reactions_enabled is True) to avoid
        issues during __init__.

        Parameters
        ----------
        name : str
            The name of the attribute to set.
        value : any
            The value to set the attribute to.
        """
        super().__setattr__(name, value)

        # Only rebuild the convolution plan if reactions are enabled, to
        # avoid issues during __init__
        if getattr(self, '_reactions_enabled', False) and name in self._invalidate_plan_on_change:
            self.convolution_settings.convolution_plan_is_valid = False

__init__(energy, sample_components, resolution_components, energy_offset=0.0, convolution_settings=None, temperature=None, temperature_unit='K', detailed_balance_settings=None, unit='meV', display_name='MyConvolution', unique_name=None)

Initialize the Convolution class.

Parameters:

Name Type Description Default
energy ndarray | Variable

1D array of energy values where the convolution is evaluated.

required
sample_components ComponentCollection | ModelComponent

The sample components to be convolved.

required
resolution_components ComponentCollection | ModelComponent

The resolution components to convolve with.

required
energy_offset Numeric | Parameter

An energy offset to apply to the energy values before convolution.

0.0
convolution_settings ConvolutionSettings | None

The settings for the convolution. If None, default settings will be used.

None
temperature Parameter | Numeric | None

The temperature to use for detailed balance correction.

None
temperature_unit str | Unit

The unit of the temperature parameter.

'K'
detailed_balance_settings DetailedBalanceSettings | None

The settings for detailed balance. If None, default settings will be used.

None
unit str | Unit

The unit of the energy.

'meV'
display_name str | None

Display name of the model.

'MyConvolution'
unique_name str | None

Unique name of the model. If None, a unique name will be generated.

None
Source code in src/easydynamics/convolution/convolution.py
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def __init__(
    self,
    energy: np.ndarray | sc.Variable,
    sample_components: ComponentCollection | ModelComponent,
    resolution_components: ComponentCollection | ModelComponent,
    energy_offset: Numeric | Parameter = 0.0,
    convolution_settings: ConvolutionSettings | None = None,
    temperature: Parameter | Numeric | None = None,
    temperature_unit: str | sc.Unit = 'K',
    detailed_balance_settings: DetailedBalanceSettings | None = None,
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'MyConvolution',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the Convolution class.

    Parameters
    ----------
    energy : np.ndarray | sc.Variable
        1D array of energy values where the convolution is evaluated.
    sample_components : ComponentCollection | ModelComponent
        The  sample components to be convolved.
    resolution_components : ComponentCollection | ModelComponent
        The resolution components to convolve with.
    energy_offset : Numeric | Parameter, default=0.0
        An energy offset to apply to the energy values before convolution.
    convolution_settings : ConvolutionSettings | None, default=None
        The settings for the convolution. If None, default settings will be used.
    temperature : Parameter | Numeric | None, default=None
        The temperature to use for detailed balance correction.
    temperature_unit : str | sc.Unit, default='K'
        The unit of the temperature parameter.
    detailed_balance_settings : DetailedBalanceSettings | None, default=None
        The settings for detailed balance. If None, default settings will be used.
    unit : str | sc.Unit, default='meV'
        The unit of the energy.
    display_name : str | None, default='MyConvolution'
        Display name of the model.
    unique_name : str | None, default=None
        Unique name of the model. If None, a unique name will be generated.
    """

    self._reactions_enabled = False
    super().__init__(
        energy=energy,
        sample_components=sample_components,
        resolution_components=resolution_components,
        energy_offset=energy_offset,
        convolution_settings=convolution_settings,
        temperature=temperature,
        temperature_unit=temperature_unit,
        detailed_balance_settings=detailed_balance_settings,
        unit=unit,
        display_name=display_name,
        unique_name=unique_name,
    )

    self._reactions_enabled = True
    # Separate sample model components into pairs that can be
    # handled analytically, delta functions, and the rest
    # Also initialize analytical and numerical convolvers based on
    # sample model component
    self._build_convolution_plan()

__setattr__(name, value)

Custom setattr to invalidate convolution plan on relevant attribute changes, and build a new plan.

The new plan is only built after initialization (when _reactions_enabled is True) to avoid issues during init.

Parameters:

Name Type Description Default
name str

The name of the attribute to set.

required
value any

The value to set the attribute to.

required
Source code in src/easydynamics/convolution/convolution.py
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
def __setattr__(self, name: str, value: any) -> None:
    """
    Custom setattr to invalidate convolution plan on relevant attribute changes, and build a
    new plan.

    The new plan is only built after initialization (when _reactions_enabled is True) to avoid
    issues during __init__.

    Parameters
    ----------
    name : str
        The name of the attribute to set.
    value : any
        The value to set the attribute to.
    """
    super().__setattr__(name, value)

    # Only rebuild the convolution plan if reactions are enabled, to
    # avoid issues during __init__
    if getattr(self, '_reactions_enabled', False) and name in self._invalidate_plan_on_change:
        self.convolution_settings.convolution_plan_is_valid = False

convolution()

Perform convolution using analytical convolutions where possible, and numerical convolutions for the remaining components.

Returns:

Type Description
ndarray

The convolved values evaluated at energy.

Source code in src/easydynamics/convolution/convolution.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def convolution(
    self,
) -> np.ndarray:
    """
    Perform convolution using analytical convolutions where possible, and numerical
    convolutions for the remaining components.

    Returns
    -------
    np.ndarray
        The convolved values evaluated at energy.
    """
    if not self.convolution_settings.convolution_plan_is_valid:
        self._build_convolution_plan()
    total = np.zeros_like(self.energy.values, dtype=float)

    # Analytical convolution
    if self._analytical_convolver is not None:
        total += self._analytical_convolver.convolution()

    # Numerical convolution
    if self._numerical_convolver is not None:
        total += self._numerical_convolver.convolution()

    # Delta function components
    if self._delta_sample_components.components:
        total += self._convolve_delta_functions()

    return total

analytical_convolution

AnalyticalConvolution

Bases: ConvolutionBase

Analytical convolution of a ModelComponent or ComponentCollection with a ResolutionModel.

Possible analytical convolutions are any combination of delta functions, Gaussians, Lorentzians and Voigt profiles.

Source code in src/easydynamics/convolution/analytical_convolution.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
class AnalyticalConvolution(ConvolutionBase):
    """
    Analytical convolution of a ModelComponent or ComponentCollection with a ResolutionModel.

    Possible analytical convolutions are any combination of delta functions, Gaussians, Lorentzians
    and Voigt profiles.
    """

    # Mapping of supported component type pairs to convolution methods.
    # Delta functions are handled separately.
    _CONVOLUTIONS: ClassVar[dict[str, object]] = {
        ('Gaussian', 'Gaussian'): '_convolute_gaussian_gaussian',
        ('Gaussian', 'Lorentzian'): '_convolute_gaussian_lorentzian',
        ('Gaussian', 'Voigt'): '_convolute_gaussian_voigt',
        ('Lorentzian', 'Lorentzian'): '_convolute_lorentzian_lorentzian',
        ('Lorentzian', 'Voigt'): '_convolute_lorentzian_voigt',
        ('Voigt', 'Voigt'): '_convolute_voigt_voigt',
    }

    def __init__(
        self,
        energy: np.ndarray | sc.Variable,
        unit: str | sc.Unit = 'meV',
        sample_components: ComponentCollection | ModelComponent | None = None,
        resolution_components: ComponentCollection | ModelComponent | None = None,
        energy_offset: Numeric | Parameter = 0.0,
        display_name: str | None = 'MyConvolution',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize an AnalyticalConvolution.

        Parameters
        ----------
        energy : np.ndarray | sc.Variable
            1D array of energy values where the convolution is evaluated.
        unit : str | sc.Unit, default='meV'
            The unit of the energy.
        sample_components : ComponentCollection | ModelComponent | None, default=None
            The sample model to be convolved.
        resolution_components : ComponentCollection | ModelComponent | None, default=None
            The resolution model to convolve with.
        energy_offset : Numeric | Parameter, default=0.0
            An offset to shift the energy values by.
        display_name : str | None, default='MyConvolution'
            Display name of the model.
        unique_name : str | None, default=None
            Unique name of the model. If None, a unique name will be generated.
        """
        super().__init__(
            energy=energy,
            unit=unit,
            sample_components=sample_components,
            resolution_components=resolution_components,
            energy_offset=energy_offset,
            display_name=display_name,
            unique_name=unique_name,
        )

    def convolution(
        self,
    ) -> np.ndarray:
        """
        Convolve sample with resolution analytically if possible.

        Accepts ComponentCollection or single ModelComponent for each. Possible analytical
        convolutions are any combination of delta functions, Gaussians, Lorentzians and Voigt
        profiles.

        Returns
        -------
        np.ndarray
            The convolution of the sample_components and resolution_components values evaluated at
            self.energy.
        """

        sample_components = self.sample_components.components
        resolution_components = self.resolution_components.components

        total = np.zeros_like(self.energy.values, dtype=float)

        for sample_component in sample_components:
            # Go through resolution components,
            # adding analytical contributions
            for resolution_component in resolution_components:
                contrib = self._convolute_analytic_pair(
                    sample_component=sample_component,
                    resolution_component=resolution_component,
                )
                total += contrib

        return total

    def _convolute_analytic_pair(
        self,
        sample_component: ModelComponent,
        resolution_component: ModelComponent,
    ) -> np.ndarray:
        r"""
        Analytic convolution for component pair (sample_component, resolution_component).

        The convolution of two Gaussian components results in another Gaussian component with width
        $\sqrt{w_1^2 + w_2^2}$.

        The convolution of two Lorentzian components results in another Lorentzian component with
        width $w_1 + w_2$.

        The convolution of a Gaussian and a Lorentzian results in a Voigt profile.

        The convolution of a Gaussian and a Voigt profile results in another Voigt profile, with
        the Lorentzian width unchanged and the Gaussian widths summed in quadrature.

        The convolution of a Lorentzian and a Voigt profile results in another Voigt profile, with
        the Gaussian width unchanged and the Lorentzian widths summed.

        The convolution of two Voigt profiles results in another Voigt profile, with the Gaussian
        widths summed in quadrature and the Lorentzian widths summed.

        The convolution of a delta function with any component or ComponentCollection results in
        the same component or ComponentCollection shifted by the delta center.

        All areas are multiplied in the convolution.

        Parameters
        ----------
        sample_component : ModelComponent
            The sample component to be convolved.
        resolution_component : ModelComponent
            The resolution component to convolve with.

        Raises
        ------
        ValueError
            If the component pair cannot be handled analytically.

        Returns
        -------
        np.ndarray
            The convolution result.
        """

        if isinstance(resolution_component, DeltaFunction):
            raise ValueError(
                'Analytical convolution with a delta function \
                    in the resolution model is not supported.'
            )

        # Delta function + anything -->
        # anything, shifted by delta center with area A1 * A2
        if isinstance(sample_component, DeltaFunction):
            return self._convolute_delta_any(
                sample_component,
                resolution_component,
            )

        pair = (type(sample_component).__name__, type(resolution_component).__name__)
        swapped = False

        if pair not in self._CONVOLUTIONS:
            # Try reversing the pair
            pair = (
                type(resolution_component).__name__,
                type(sample_component).__name__,
            )
            swapped = True

        func_name = self._CONVOLUTIONS.get(pair)

        if func_name is None:
            raise ValueError(
                f'Analytical convolution not supported for component pair: '
                f'{type(sample_component).__name__}, {type(resolution_component).__name__}'
            )

        # Call the corresponding method
        if swapped:
            return getattr(self, func_name)(resolution_component, sample_component)
        return getattr(self, func_name)(sample_component, resolution_component)

    def _convolute_delta_any(
        self,
        sample_component: DeltaFunction,
        resolution_components: ComponentCollection | ModelComponent,
    ) -> np.ndarray:
        """
        Convolution of delta function with any ModelComponent or ComponentCollection results in the
        same component or ComponentCollection shifted by the delta center. The areas are
        multiplied.

        Parameters
        ----------
        sample_component : DeltaFunction
            The sample component to be convolved.
        resolution_components : ComponentCollection | ModelComponent
            : The resolution model to convolve with.

        Returns
        -------
        np.ndarray
            The evaluated convolution values at self.energy.
        """
        return sample_component.area.value * resolution_components.evaluate(
            self.energy_with_offset.values - sample_component.center.value
        )

    def _convolute_gaussian_gaussian(
        self,
        sample_component: Gaussian,
        resolution_component: Gaussian,
    ) -> np.ndarray:
        r"""
        Convolution of two Gaussian components results in another Gaussian component with width
        $\sqrt{w_1^2 + w_2^2}$. The areas are multiplied.

        Parameters
        ----------
        sample_component : Gaussian
            The sample Gaussian component to be convolved.
        resolution_component : Gaussian
            The resolution Gaussian component to convolve with.

        Returns
        -------
        np.ndarray
            The evaluated convolution values at self.energy.
        """

        width = np.sqrt(sample_component.width.value**2 + resolution_component.width.value**2)

        area = sample_component.area.value * resolution_component.area.value

        center = sample_component.center.value + resolution_component.center.value

        return self._gaussian_eval(area=area, center=center, width=width)

    def _convolute_gaussian_lorentzian(
        self,
        sample_component: Gaussian,
        resolution_component: Lorentzian,
    ) -> np.ndarray:
        """
        Convolution of a Gaussian and a Lorentzian results in a Voigt profile. The areas are
        multiplied.

        Parameters
        ----------
        sample_component : Gaussian
            The sample Gaussian component to be convolved.
        resolution_component : Lorentzian
            The resolution Lorentzian component to convolve with.

        Returns
        -------
        np.ndarray
            The evaluated convolution values at self.energy.
        """
        center = sample_component.center.value + resolution_component.center.value
        area = sample_component.area.value * resolution_component.area.value

        return self._voigt_eval(
            area=area,
            center=center,
            gaussian_width=sample_component.width.value,
            lorentzian_width=resolution_component.width.value,
        )

    def _convolute_gaussian_voigt(
        self,
        sample_component: Gaussian,
        resolution_component: Voigt,
    ) -> np.ndarray:
        """
        Convolution of a Gaussian and a Voigt profile results in another Voigt profile. The
        Lorentzian width remains unchanged, while the Gaussian widths are summed in quadrature. The
        areas are multiplied.

        Parameters
        ----------
        sample_component : Gaussian
            The sample Gaussian component to be convolved.
        resolution_component : Voigt
            The resolution Voigt component to convolve with.

        Returns
        -------
        np.ndarray
            The evaluated convolution values at self.energy.
        """
        area = sample_component.area.value * resolution_component.area.value

        center = sample_component.center.value + resolution_component.center.value

        gaussian_width = np.sqrt(
            sample_component.width.value**2 + resolution_component.gaussian_width.value**2
        )

        lorentzian_width = resolution_component.lorentzian_width.value

        return self._voigt_eval(
            area=area,
            center=center,
            gaussian_width=gaussian_width,
            lorentzian_width=lorentzian_width,
        )

    def _convolute_lorentzian_lorentzian(
        self,
        sample_component: Lorentzian,
        resolution_component: Lorentzian,
    ) -> np.ndarray:
        r"""
        Convolution of two Lorentzian components results in another Lorentzian component with width
        $w_1 + w_2$. The areas are multiplied.

        Parameters
        ----------
        sample_component : Lorentzian
            The sample Lorentzian component to be convolved.
        resolution_component : Lorentzian
            The resolution Lorentzian component to convolve with.

        Returns
        -------
        np.ndarray
            The evaluated convolution values at self.energy.
        """
        area = sample_component.area.value * resolution_component.area.value

        center = sample_component.center.value + resolution_component.center.value

        width = sample_component.width.value + resolution_component.width.value

        return self._lorentzian_eval(area=area, center=center, width=width)

    def _convolute_lorentzian_voigt(
        self,
        sample_component: Lorentzian,
        resolution_component: Voigt,
    ) -> np.ndarray:
        """
        Convolution of a Lorentzian and a Voigt profile results in another Voigt profile.

        The Gaussian width remains unchanged, while the Lorentzian widths are summed.

        The areas are multiplied.

        Parameters
        ----------
        sample_component : Lorentzian
            The sample Lorentzian component to be convolved.
        resolution_component : Voigt
            The resolution Voigt component to convolve with.

        Returns
        -------
        np.ndarray
            The evaluated convolution values at self.energy.
        """
        area = sample_component.area.value * resolution_component.area.value

        center = sample_component.center.value + resolution_component.center.value

        gaussian_width = resolution_component.gaussian_width.value

        lorentzian_width = (
            sample_component.width.value + resolution_component.lorentzian_width.value
        )

        return self._voigt_eval(
            area=area,
            center=center,
            gaussian_width=gaussian_width,
            lorentzian_width=lorentzian_width,
        )

    def _convolute_voigt_voigt(
        self,
        sample_component: Voigt,
        resolution_component: Voigt,
    ) -> np.ndarray:
        """
        Convolution of two Voigt profiles results in another Voigt profile.

        The Gaussian widths are summed in quadrature, while the Lorentzian widths are summed. The
        areas are multiplied.

        Parameters
        ----------
        sample_component : Voigt
            The sample Voigt component to be convolved.
        resolution_component : Voigt
            The resolution Voigt component to convolve with.

        Returns
        -------
        np.ndarray
            The evaluated convolution values at self.energy.
        """
        area = sample_component.area.value * resolution_component.area.value

        center = sample_component.center.value + resolution_component.center.value

        gaussian_width = np.sqrt(
            sample_component.gaussian_width.value**2 + resolution_component.gaussian_width.value**2
        )

        lorentzian_width = (
            sample_component.lorentzian_width.value + resolution_component.lorentzian_width.value
        )
        return self._voigt_eval(
            area=area,
            center=center,
            gaussian_width=gaussian_width,
            lorentzian_width=lorentzian_width,
        )

    def _gaussian_eval(
        self,
        area: float,
        center: float,
        width: float,
    ) -> np.ndarray:
        r"""
        Evaluate a Gaussian function.

        $$ I(x) = \frac{A}{\sigma \sqrt{2\pi}} \exp\left( -\frac{1}{2} \left(\frac{x -
        x_0}{\sigma}\right)^2 \right) $$

        where $A$ is the area, $x_0$ is the center, and $\sigma$ is the width.

        All checks are handled in the calling function.

        Parameters
        ----------
        area : float
            The area under the Gaussian curve.
        center : float
            The center of the Gaussian.
        width : float
            The width (sigma) of the Gaussian.

        Returns
        -------
        np.ndarray
            The evaluated Gaussian values at self.energy.
        """

        normalization = 1 / (np.sqrt(2 * np.pi) * width)
        exponent = -0.5 * ((self.energy_with_offset.values - center) / width) ** 2

        return area * normalization * np.exp(exponent)

    def _lorentzian_eval(self, area: float, center: float, width: float) -> np.ndarray:
        r"""
        Evaluate a Lorentzian function.

        $$ I(x) = \frac{A}{\\pi} \frac{\Gamma}{(x - x_0)^2 + \Gamma^2}, $$

        where $A$ is the area, $x_0$ is the center, and $\\Gamma$ is the half width at half maximum
        (HWHM).

        All checks are handled in the calling function.

        Parameters
        ----------
        area : float
            The area under the Lorentzian.
        center : float
            The center of the Lorentzian.
        width : float
            The width (HWHM) of the Lorentzian.

        Returns
        -------
        np.ndarray
            The evaluated Lorentzian values at self.energy.
        """

        normalization = width / np.pi
        denominator = (self.energy_with_offset.values - center) ** 2 + width**2

        return area * normalization / denominator

    def _voigt_eval(
        self,
        area: float,
        center: float,
        gaussian_width: float,
        lorentzian_width: float,
    ) -> np.ndarray:
        """
        Evaluate a Voigt profile function using scipy's voigt_profile.

        Parameters
        ----------
        area : float
            The area under the Voigt profile.
        center : float
            The center of the Voigt profile.
        gaussian_width : float
            The Gaussian width (sigma) of the Voigt profile.
        lorentzian_width : float
            The Lorentzian width (HWHM) of the Voigt profile.

        Returns
        -------
        np.ndarray
            The evaluated Voigt profile values at self.energy.
        """

        return area * voigt_profile(
            self.energy_with_offset.values - center, gaussian_width, lorentzian_width
        )

__init__(energy, unit='meV', sample_components=None, resolution_components=None, energy_offset=0.0, display_name='MyConvolution', unique_name=None)

Initialize an AnalyticalConvolution.

Parameters:

Name Type Description Default
energy ndarray | Variable

1D array of energy values where the convolution is evaluated.

required
unit str | Unit

The unit of the energy.

'meV'
sample_components ComponentCollection | ModelComponent | None

The sample model to be convolved.

None
resolution_components ComponentCollection | ModelComponent | None

The resolution model to convolve with.

None
energy_offset Numeric | Parameter

An offset to shift the energy values by.

0.0
display_name str | None

Display name of the model.

'MyConvolution'
unique_name str | None

Unique name of the model. If None, a unique name will be generated.

None
Source code in src/easydynamics/convolution/analytical_convolution.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def __init__(
    self,
    energy: np.ndarray | sc.Variable,
    unit: str | sc.Unit = 'meV',
    sample_components: ComponentCollection | ModelComponent | None = None,
    resolution_components: ComponentCollection | ModelComponent | None = None,
    energy_offset: Numeric | Parameter = 0.0,
    display_name: str | None = 'MyConvolution',
    unique_name: str | None = None,
) -> None:
    """
    Initialize an AnalyticalConvolution.

    Parameters
    ----------
    energy : np.ndarray | sc.Variable
        1D array of energy values where the convolution is evaluated.
    unit : str | sc.Unit, default='meV'
        The unit of the energy.
    sample_components : ComponentCollection | ModelComponent | None, default=None
        The sample model to be convolved.
    resolution_components : ComponentCollection | ModelComponent | None, default=None
        The resolution model to convolve with.
    energy_offset : Numeric | Parameter, default=0.0
        An offset to shift the energy values by.
    display_name : str | None, default='MyConvolution'
        Display name of the model.
    unique_name : str | None, default=None
        Unique name of the model. If None, a unique name will be generated.
    """
    super().__init__(
        energy=energy,
        unit=unit,
        sample_components=sample_components,
        resolution_components=resolution_components,
        energy_offset=energy_offset,
        display_name=display_name,
        unique_name=unique_name,
    )

convolution()

Convolve sample with resolution analytically if possible.

Accepts ComponentCollection or single ModelComponent for each. Possible analytical convolutions are any combination of delta functions, Gaussians, Lorentzians and Voigt profiles.

Returns:

Type Description
ndarray

The convolution of the sample_components and resolution_components values evaluated at self.energy.

Source code in src/easydynamics/convolution/analytical_convolution.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def convolution(
    self,
) -> np.ndarray:
    """
    Convolve sample with resolution analytically if possible.

    Accepts ComponentCollection or single ModelComponent for each. Possible analytical
    convolutions are any combination of delta functions, Gaussians, Lorentzians and Voigt
    profiles.

    Returns
    -------
    np.ndarray
        The convolution of the sample_components and resolution_components values evaluated at
        self.energy.
    """

    sample_components = self.sample_components.components
    resolution_components = self.resolution_components.components

    total = np.zeros_like(self.energy.values, dtype=float)

    for sample_component in sample_components:
        # Go through resolution components,
        # adding analytical contributions
        for resolution_component in resolution_components:
            contrib = self._convolute_analytic_pair(
                sample_component=sample_component,
                resolution_component=resolution_component,
            )
            total += contrib

    return total

convolution

Convolution

Bases: NumericalConvolutionBase

Convolution class that combines analytical and numerical convolution methods to efficiently perform convolutions of ComponentCollections with ResolutionComponents.

Supports analytical convolution for pairs of analytical model components (DeltaFunction, Gaussian, Lorentzian, Voigt), while using numerical convolution for other components. If temperature is provided, detailed balance correction is applied to the sample model. In this case, all convolutions are handled numerically. Includes a setting to normalize the detailed balance correction. Includes optional upsampling and extended range to improve accuracy of the numerical convolutions. Also warns about numerical instabilities if peaks are very wide or very narrow.

Source code in src/easydynamics/convolution/convolution.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
class Convolution(NumericalConvolutionBase):
    """
    Convolution class that combines analytical and numerical convolution methods to efficiently
    perform convolutions of ComponentCollections with ResolutionComponents.

    Supports analytical convolution for pairs of analytical model components (DeltaFunction,
    Gaussian, Lorentzian, Voigt), while using numerical convolution for other components. If
    temperature is provided, detailed balance correction is applied to the sample model. In this
    case, all convolutions are handled numerically. Includes a setting to normalize the detailed
    balance correction. Includes optional upsampling and extended range to improve accuracy of the
    numerical convolutions. Also warns about numerical instabilities if peaks are very wide or very
    narrow.
    """

    # When these attributes are changed, the convolution plan
    # needs to be rebuilt
    _invalidate_plan_on_change: ClassVar[dict[str, object]] = {
        'energy',
        '_energy',
        '_energy_grid',
        '_sample_components',
        '_resolution_components',
        '_temperature',
        '_energy_unit',
        '_normalize_detailed_balance',
        '_detailed_balance_settings',
    }

    def __init__(
        self,
        energy: np.ndarray | sc.Variable,
        sample_components: ComponentCollection | ModelComponent,
        resolution_components: ComponentCollection | ModelComponent,
        energy_offset: Numeric | Parameter = 0.0,
        convolution_settings: ConvolutionSettings | None = None,
        temperature: Parameter | Numeric | None = None,
        temperature_unit: str | sc.Unit = 'K',
        detailed_balance_settings: DetailedBalanceSettings | None = None,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'MyConvolution',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the Convolution class.

        Parameters
        ----------
        energy : np.ndarray | sc.Variable
            1D array of energy values where the convolution is evaluated.
        sample_components : ComponentCollection | ModelComponent
            The  sample components to be convolved.
        resolution_components : ComponentCollection | ModelComponent
            The resolution components to convolve with.
        energy_offset : Numeric | Parameter, default=0.0
            An energy offset to apply to the energy values before convolution.
        convolution_settings : ConvolutionSettings | None, default=None
            The settings for the convolution. If None, default settings will be used.
        temperature : Parameter | Numeric | None, default=None
            The temperature to use for detailed balance correction.
        temperature_unit : str | sc.Unit, default='K'
            The unit of the temperature parameter.
        detailed_balance_settings : DetailedBalanceSettings | None, default=None
            The settings for detailed balance. If None, default settings will be used.
        unit : str | sc.Unit, default='meV'
            The unit of the energy.
        display_name : str | None, default='MyConvolution'
            Display name of the model.
        unique_name : str | None, default=None
            Unique name of the model. If None, a unique name will be generated.
        """

        self._reactions_enabled = False
        super().__init__(
            energy=energy,
            sample_components=sample_components,
            resolution_components=resolution_components,
            energy_offset=energy_offset,
            convolution_settings=convolution_settings,
            temperature=temperature,
            temperature_unit=temperature_unit,
            detailed_balance_settings=detailed_balance_settings,
            unit=unit,
            display_name=display_name,
            unique_name=unique_name,
        )

        self._reactions_enabled = True
        # Separate sample model components into pairs that can be
        # handled analytically, delta functions, and the rest
        # Also initialize analytical and numerical convolvers based on
        # sample model component
        self._build_convolution_plan()

    def convolution(
        self,
    ) -> np.ndarray:
        """
        Perform convolution using analytical convolutions where possible, and numerical
        convolutions for the remaining components.

        Returns
        -------
        np.ndarray
            The convolved values evaluated at energy.
        """
        if not self.convolution_settings.convolution_plan_is_valid:
            self._build_convolution_plan()
        total = np.zeros_like(self.energy.values, dtype=float)

        # Analytical convolution
        if self._analytical_convolver is not None:
            total += self._analytical_convolver.convolution()

        # Numerical convolution
        if self._numerical_convolver is not None:
            total += self._numerical_convolver.convolution()

        # Delta function components
        if self._delta_sample_components.components:
            total += self._convolve_delta_functions()

        return total

    def _convolve_delta_functions(self) -> np.ndarray:
        """
        Convolve delta function components of the sample model with the resolution components. No
        detailed balance correction is applied to delta functions.

        Returns
        -------
        np.ndarray
            The convolved values of the delta function c components evaluated at energy.
        """
        return sum(
            delta.area.value
            * self._resolution_components.evaluate(
                self.energy_with_offset.values - delta.center.value
            )
            for delta in self._delta_sample_components.components
        )

    def _check_if_pair_is_analytic(
        self,
        sample_component: ModelComponent,
        resolution_component: ModelComponent,
    ) -> bool:
        """
        Check if the convolution of the given component pair can be handled analytically.

        Parameters
        ----------
        sample_component : ModelComponent
            The sample component to be convolved.
        resolution_component : ModelComponent
            The resolution component to convolve with.

        Raises
        ------
        TypeError
            If either component is not a ModelComponent, or if the resolution component is a
            DeltaFunction.

        Returns
        -------
        bool
            True if the component pair can be handled analytically, False otherwise.
        """

        if not isinstance(sample_component, ModelComponent):
            raise TypeError(
                f'`sample_component` is an instance of {type(sample_component).__name__}, \
                but must be a ModelComponent.'
            )

        if not isinstance(resolution_component, ModelComponent):
            raise TypeError(
                f'`resolution_component` is an instance of {type(resolution_component).__name__}, \
                    but must be a ModelComponent.'
            )

        if isinstance(resolution_component, DeltaFunction):
            raise TypeError(
                'resolution components contains delta functions. This is not supported.'
            )

        analytical_types = (Gaussian, Lorentzian, Voigt)
        return bool(
            isinstance(sample_component, analytical_types)
            and isinstance(resolution_component, analytical_types)
        )

    def _build_convolution_plan(self) -> None:
        """
        Separate sample model components into analytical pairs, delta functions, and the rest.
        """

        analytical_sample_components = ComponentCollection()
        delta_sample_components = ComponentCollection()
        numerical_sample_components = ComponentCollection()

        for sample_component in self._sample_components.components:
            # If delta function, put in delta sample model and go to the
            # next component
            if isinstance(sample_component, DeltaFunction):
                delta_sample_components.append_component(sample_component)
                continue

            # If temperature is set, all other components go to
            # numerical sample model
            if (
                self.temperature is not None
                and self.detailed_balance_settings.use_detailed_balance
            ):
                numerical_sample_components.append_component(sample_component)
                continue

            # If temperature is not set, check if all
            # resolution components can be convolved analytically with
            # this sample component
            pair_is_analytic = [
                self._check_if_pair_is_analytic(sample_component, resolution_component)
                for resolution_component in self._resolution_components.components
            ]
            # If all resolution components can be convolved analytically
            # with this sample component, add it to analytical
            # sample model. If not, it goes to numerical sample model.
            if all(pair_is_analytic):
                analytical_sample_components.append_component(sample_component)
            else:
                numerical_sample_components.append_component(sample_component)

        self._analytical_sample_components = analytical_sample_components
        self._delta_sample_components = delta_sample_components
        self._numerical_sample_components = numerical_sample_components

        # Update convolvers
        self._set_convolvers()
        self.convolution_settings.convolution_plan_is_valid = True

    def _set_convolvers(self) -> None:
        """
        Initialize analytical and numerical convolvers based on sample model components.

        There is no delta function convolver, as delta functions are handled directly in the
        convolution method.
        """

        if self._analytical_sample_components.components:
            self._analytical_convolver = AnalyticalConvolution(
                energy=self.energy,
                energy_offset=self.energy_offset,
                sample_components=self._analytical_sample_components,
                resolution_components=self._resolution_components,
            )
        else:
            self._analytical_convolver = None

        if self._numerical_sample_components.components:
            self._numerical_convolver = NumericalConvolution(
                energy=self.energy,
                energy_offset=self.energy_offset,
                sample_components=self._numerical_sample_components,
                resolution_components=self._resolution_components,
                convolution_settings=self.convolution_settings,
                temperature=self.temperature,
                temperature_unit=self._temperature_unit,
                detailed_balance_settings=self.detailed_balance_settings,
                unit=self.unit,
            )
        else:
            self._numerical_convolver = None

    # Update some setters so the internal sample models are updated
    def __setattr__(self, name: str, value: any) -> None:
        """
        Custom setattr to invalidate convolution plan on relevant attribute changes, and build a
        new plan.

        The new plan is only built after initialization (when _reactions_enabled is True) to avoid
        issues during __init__.

        Parameters
        ----------
        name : str
            The name of the attribute to set.
        value : any
            The value to set the attribute to.
        """
        super().__setattr__(name, value)

        # Only rebuild the convolution plan if reactions are enabled, to
        # avoid issues during __init__
        if getattr(self, '_reactions_enabled', False) and name in self._invalidate_plan_on_change:
            self.convolution_settings.convolution_plan_is_valid = False

__init__(energy, sample_components, resolution_components, energy_offset=0.0, convolution_settings=None, temperature=None, temperature_unit='K', detailed_balance_settings=None, unit='meV', display_name='MyConvolution', unique_name=None)

Initialize the Convolution class.

Parameters:

Name Type Description Default
energy ndarray | Variable

1D array of energy values where the convolution is evaluated.

required
sample_components ComponentCollection | ModelComponent

The sample components to be convolved.

required
resolution_components ComponentCollection | ModelComponent

The resolution components to convolve with.

required
energy_offset Numeric | Parameter

An energy offset to apply to the energy values before convolution.

0.0
convolution_settings ConvolutionSettings | None

The settings for the convolution. If None, default settings will be used.

None
temperature Parameter | Numeric | None

The temperature to use for detailed balance correction.

None
temperature_unit str | Unit

The unit of the temperature parameter.

'K'
detailed_balance_settings DetailedBalanceSettings | None

The settings for detailed balance. If None, default settings will be used.

None
unit str | Unit

The unit of the energy.

'meV'
display_name str | None

Display name of the model.

'MyConvolution'
unique_name str | None

Unique name of the model. If None, a unique name will be generated.

None
Source code in src/easydynamics/convolution/convolution.py
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def __init__(
    self,
    energy: np.ndarray | sc.Variable,
    sample_components: ComponentCollection | ModelComponent,
    resolution_components: ComponentCollection | ModelComponent,
    energy_offset: Numeric | Parameter = 0.0,
    convolution_settings: ConvolutionSettings | None = None,
    temperature: Parameter | Numeric | None = None,
    temperature_unit: str | sc.Unit = 'K',
    detailed_balance_settings: DetailedBalanceSettings | None = None,
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'MyConvolution',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the Convolution class.

    Parameters
    ----------
    energy : np.ndarray | sc.Variable
        1D array of energy values where the convolution is evaluated.
    sample_components : ComponentCollection | ModelComponent
        The  sample components to be convolved.
    resolution_components : ComponentCollection | ModelComponent
        The resolution components to convolve with.
    energy_offset : Numeric | Parameter, default=0.0
        An energy offset to apply to the energy values before convolution.
    convolution_settings : ConvolutionSettings | None, default=None
        The settings for the convolution. If None, default settings will be used.
    temperature : Parameter | Numeric | None, default=None
        The temperature to use for detailed balance correction.
    temperature_unit : str | sc.Unit, default='K'
        The unit of the temperature parameter.
    detailed_balance_settings : DetailedBalanceSettings | None, default=None
        The settings for detailed balance. If None, default settings will be used.
    unit : str | sc.Unit, default='meV'
        The unit of the energy.
    display_name : str | None, default='MyConvolution'
        Display name of the model.
    unique_name : str | None, default=None
        Unique name of the model. If None, a unique name will be generated.
    """

    self._reactions_enabled = False
    super().__init__(
        energy=energy,
        sample_components=sample_components,
        resolution_components=resolution_components,
        energy_offset=energy_offset,
        convolution_settings=convolution_settings,
        temperature=temperature,
        temperature_unit=temperature_unit,
        detailed_balance_settings=detailed_balance_settings,
        unit=unit,
        display_name=display_name,
        unique_name=unique_name,
    )

    self._reactions_enabled = True
    # Separate sample model components into pairs that can be
    # handled analytically, delta functions, and the rest
    # Also initialize analytical and numerical convolvers based on
    # sample model component
    self._build_convolution_plan()

__setattr__(name, value)

Custom setattr to invalidate convolution plan on relevant attribute changes, and build a new plan.

The new plan is only built after initialization (when _reactions_enabled is True) to avoid issues during init.

Parameters:

Name Type Description Default
name str

The name of the attribute to set.

required
value any

The value to set the attribute to.

required
Source code in src/easydynamics/convolution/convolution.py
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
def __setattr__(self, name: str, value: any) -> None:
    """
    Custom setattr to invalidate convolution plan on relevant attribute changes, and build a
    new plan.

    The new plan is only built after initialization (when _reactions_enabled is True) to avoid
    issues during __init__.

    Parameters
    ----------
    name : str
        The name of the attribute to set.
    value : any
        The value to set the attribute to.
    """
    super().__setattr__(name, value)

    # Only rebuild the convolution plan if reactions are enabled, to
    # avoid issues during __init__
    if getattr(self, '_reactions_enabled', False) and name in self._invalidate_plan_on_change:
        self.convolution_settings.convolution_plan_is_valid = False

convolution()

Perform convolution using analytical convolutions where possible, and numerical convolutions for the remaining components.

Returns:

Type Description
ndarray

The convolved values evaluated at energy.

Source code in src/easydynamics/convolution/convolution.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def convolution(
    self,
) -> np.ndarray:
    """
    Perform convolution using analytical convolutions where possible, and numerical
    convolutions for the remaining components.

    Returns
    -------
    np.ndarray
        The convolved values evaluated at energy.
    """
    if not self.convolution_settings.convolution_plan_is_valid:
        self._build_convolution_plan()
    total = np.zeros_like(self.energy.values, dtype=float)

    # Analytical convolution
    if self._analytical_convolver is not None:
        total += self._analytical_convolver.convolution()

    # Numerical convolution
    if self._numerical_convolver is not None:
        total += self._numerical_convolver.convolution()

    # Delta function components
    if self._delta_sample_components.components:
        total += self._convolve_delta_functions()

    return total

convolution_base

ConvolutionBase

Bases: EasyDynamicsModelBase

Base class for convolutions of sample and resolution models.

This base class has no convolution functionality.

Source code in src/easydynamics/convolution/convolution_base.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
class ConvolutionBase(EasyDynamicsModelBase):
    """
    Base class for convolutions of sample and resolution models.

    This base class has no convolution functionality.
    """

    def __init__(
        self,
        energy: np.ndarray | sc.Variable,
        sample_components: ComponentCollection | ModelComponent | None = None,
        resolution_components: ComponentCollection | ModelComponent | None = None,
        unit: str | sc.Unit = 'meV',
        energy_offset: Numeric | Parameter = 0.0,
        display_name: str | None = 'MyConvolution',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the ConvolutionBase.

        Parameters
        ----------
        energy : np.ndarray | sc.Variable
            1D array of energy values where the convolution is evaluated.
        sample_components : ComponentCollection | ModelComponent | None, default=None
            The sample model to be convolved.
        resolution_components : ComponentCollection | ModelComponent | None, default=None
            The resolution model to convolve with.
        unit : str | sc.Unit, default='meV'
            The unit of the energy.
        energy_offset : Numeric | Parameter, default=0.0
            The energy offset applied to the convolution.
        display_name : str | None, default='MyConvolution'
            Display name of the model.
        unique_name : str | None, default=None
            Unique name of the model. If None, a unique name will be generated.

        Raises
        ------
        TypeError
            If energy is not a numpy ndarray or a scipp Variable or if energy_unit is not a string
            or scipp unit, or if energy_offset is not a number or a Parameter, or if
            sample_components is not a ComponentCollection or ModelComponent, or if
            resolution_components is not a ComponentCollection or ModelComponent.
        """

        super().__init__(
            unit=unit,
            display_name=display_name,
            unique_name=unique_name,
        )

        if isinstance(energy, Numeric):
            energy = np.array([float(energy)])

        if not isinstance(energy, (np.ndarray, sc.Variable)):
            raise TypeError(f'Energy must be a numpy ndarray or a scipp Variable. Got {energy}')

        if isinstance(energy, np.ndarray):
            energy = sc.array(dims=['energy'], values=energy, unit=unit)

        if isinstance(energy_offset, Numeric):
            energy_offset = Parameter(name='energy_offset', value=float(energy_offset), unit=unit)

        if not isinstance(energy_offset, Parameter):
            raise TypeError('Energy_offset must be a number or a Parameter.')

        self._energy = energy
        self._energy_offset = energy_offset

        if sample_components is not None and not (
            isinstance(sample_components, (ComponentCollection, ModelComponent))
        ):
            raise TypeError(
                f'`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent.'  # noqa: E501
            )
        if isinstance(sample_components, ModelComponent):
            sample_components = ComponentCollection(components=[sample_components])
        self._sample_components = sample_components

        if resolution_components is not None and not (
            isinstance(resolution_components, (ComponentCollection, ModelComponent))
        ):
            raise TypeError(
                f'`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent.'  # noqa: E501
            )
        if isinstance(resolution_components, ModelComponent):
            resolution_components = ComponentCollection(components=[resolution_components])
        self._resolution_components = resolution_components

    @property
    def energy_offset(self) -> Parameter:
        """
        Get the energy offset.

        Returns
        -------
        Parameter
            The energy offset applied to the convolution.
        """
        return self._energy_offset

    @energy_offset.setter
    def energy_offset(self, energy_offset: Numeric | Parameter) -> None:
        """
        Set the energy offset.

        Parameters
        ----------
        energy_offset : Numeric | Parameter
            The energy offset to apply to the convolution.

        Raises
        ------
        TypeError
            If energy_offset is not a number or a Parameter.
        """
        if not isinstance(energy_offset, Parameter | Numeric):
            raise TypeError('Energy_offset must be a number or a Parameter.')

        if isinstance(energy_offset, Numeric):
            self._energy_offset.value = float(energy_offset)

        if isinstance(energy_offset, Parameter):
            self._energy_offset = energy_offset

    @property
    def energy_with_offset(self) -> sc.Variable:
        """
        Get the energy with the offset applied.

        Returns
        -------
        sc.Variable
            The energy values with the offset applied.
        """
        energy_with_offset = self.energy.copy()
        energy_with_offset.values = self.energy.values - self.energy_offset.value
        return energy_with_offset

    @energy_with_offset.setter
    def energy_with_offset(self, _value: sc.Variable) -> None:
        """
        Energy with offset is a read-only property derived from energy and energy_offset.

        Parameters
        ----------
        _value : sc.Variable
            The value to set (ignored).

        Raises
        ------
        AttributeError
            Always raised since energy_with_offset is read-only.
        """
        raise AttributeError(
            'Energy with offset is a read-only property derived from energy and energy_offset.'
        )

    @property
    def energy(self) -> sc.Variable:
        """
        Get the energy.

        Returns
        -------
        sc.Variable
            The energy values where the convolution is evaluated.
        """

        return self._energy

    @energy.setter
    def energy(self, energy: np.ndarray | sc.Variable) -> None:
        """
        Set the energy.

        Parameters
        ----------
        energy : np.ndarray | sc.Variable
            1D array of energy values where the convolution is evaluated.

        Raises
        ------
        TypeError
            If energy is not a numpy ndarray or a scipp Variable.
        """

        if isinstance(energy, Numeric):
            energy = np.array([float(energy)])

        if not isinstance(energy, (np.ndarray, sc.Variable)):
            raise TypeError('Energy must be a Number, a numpy ndarray or a scipp Variable.')

        if isinstance(energy, np.ndarray):
            self._energy = sc.array(dims=['energy'], values=energy, unit=self._energy.unit)

        if isinstance(energy, sc.Variable):
            self._energy = energy
            self._unit = energy.unit

    def convert_unit(self, unit: str | sc.Unit) -> None:
        """
        Convert the energy and energy_offset to the specified unit.

        Parameters
        ----------
        unit : str | sc.Unit
            The unit of the energy.

        Raises
        ------
        TypeError
            If unit is not a string or scipp unit.
        Exception
            If energy cannot be converted to the specified unit.
        """
        if not isinstance(unit, (str, sc.Unit)):
            raise TypeError('Energy unit must be a string or scipp unit.')

        old_energy = self.energy.copy()
        try:
            self.energy = sc.to_unit(self.energy, unit)
        except Exception as e:
            self.energy = old_energy
            raise e

        old_energy_offset = self.energy_offset
        try:
            self.energy_offset.convert_unit(unit)
        except Exception as e:
            self.energy_offset = old_energy_offset
            raise e

        self._unit = unit

    @property
    def sample_components(self) -> ComponentCollection | ModelComponent:
        """
        Get the sample model.

        Returns
        -------
        ComponentCollection | ModelComponent
            The sample model to be convolved.
        """
        return self._sample_components

    @sample_components.setter
    def sample_components(self, sample_components: ComponentCollection | ModelComponent) -> None:
        """
        Set the sample model.

        Parameters
        ----------
        sample_components : ComponentCollection | ModelComponent
            The sample model to be convolved.

        Raises
        ------
        TypeError
            If sample_components is not a ComponentCollection or ModelComponent.
        """
        if not isinstance(sample_components, (ComponentCollection, ModelComponent)):
            raise TypeError(
                f'`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent.'  # noqa: E501
            )

        if isinstance(sample_components, ModelComponent):
            sample_components = ComponentCollection(components=[sample_components])
        self._sample_components = sample_components

    @property
    def resolution_components(self) -> ComponentCollection | ModelComponent:
        """
        Get the resolution model.

        Returns
        -------
        ComponentCollection | ModelComponent
            The resolution model to be convolved.
        """
        return self._resolution_components

    @resolution_components.setter
    def resolution_components(
        self, resolution_components: ComponentCollection | ModelComponent
    ) -> None:
        """
        Set the resolution model.

        Parameters
        ----------
        resolution_components : ComponentCollection | ModelComponent
            The resolution model to be convolved. Can be a ComponentCollection or a single
            ModelComponent.

        Raises
        ------
        TypeError
            If resolution_components is not a ComponentCollection or ModelComponent.
        """
        if not isinstance(resolution_components, (ComponentCollection, ModelComponent)):
            raise TypeError(
                f'`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent.'  # noqa: E501
            )

        if isinstance(resolution_components, ModelComponent):
            resolution_components = ComponentCollection(components=[resolution_components])
        self._resolution_components = resolution_components

__init__(energy, sample_components=None, resolution_components=None, unit='meV', energy_offset=0.0, display_name='MyConvolution', unique_name=None)

Initialize the ConvolutionBase.

Parameters:

Name Type Description Default
energy ndarray | Variable

1D array of energy values where the convolution is evaluated.

required
sample_components ComponentCollection | ModelComponent | None

The sample model to be convolved.

None
resolution_components ComponentCollection | ModelComponent | None

The resolution model to convolve with.

None
unit str | Unit

The unit of the energy.

'meV'
energy_offset Numeric | Parameter

The energy offset applied to the convolution.

0.0
display_name str | None

Display name of the model.

'MyConvolution'
unique_name str | None

Unique name of the model. If None, a unique name will be generated.

None

Raises:

Type Description
TypeError

If energy is not a numpy ndarray or a scipp Variable or if energy_unit is not a string or scipp unit, or if energy_offset is not a number or a Parameter, or if sample_components is not a ComponentCollection or ModelComponent, or if resolution_components is not a ComponentCollection or ModelComponent.

Source code in src/easydynamics/convolution/convolution_base.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def __init__(
    self,
    energy: np.ndarray | sc.Variable,
    sample_components: ComponentCollection | ModelComponent | None = None,
    resolution_components: ComponentCollection | ModelComponent | None = None,
    unit: str | sc.Unit = 'meV',
    energy_offset: Numeric | Parameter = 0.0,
    display_name: str | None = 'MyConvolution',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the ConvolutionBase.

    Parameters
    ----------
    energy : np.ndarray | sc.Variable
        1D array of energy values where the convolution is evaluated.
    sample_components : ComponentCollection | ModelComponent | None, default=None
        The sample model to be convolved.
    resolution_components : ComponentCollection | ModelComponent | None, default=None
        The resolution model to convolve with.
    unit : str | sc.Unit, default='meV'
        The unit of the energy.
    energy_offset : Numeric | Parameter, default=0.0
        The energy offset applied to the convolution.
    display_name : str | None, default='MyConvolution'
        Display name of the model.
    unique_name : str | None, default=None
        Unique name of the model. If None, a unique name will be generated.

    Raises
    ------
    TypeError
        If energy is not a numpy ndarray or a scipp Variable or if energy_unit is not a string
        or scipp unit, or if energy_offset is not a number or a Parameter, or if
        sample_components is not a ComponentCollection or ModelComponent, or if
        resolution_components is not a ComponentCollection or ModelComponent.
    """

    super().__init__(
        unit=unit,
        display_name=display_name,
        unique_name=unique_name,
    )

    if isinstance(energy, Numeric):
        energy = np.array([float(energy)])

    if not isinstance(energy, (np.ndarray, sc.Variable)):
        raise TypeError(f'Energy must be a numpy ndarray or a scipp Variable. Got {energy}')

    if isinstance(energy, np.ndarray):
        energy = sc.array(dims=['energy'], values=energy, unit=unit)

    if isinstance(energy_offset, Numeric):
        energy_offset = Parameter(name='energy_offset', value=float(energy_offset), unit=unit)

    if not isinstance(energy_offset, Parameter):
        raise TypeError('Energy_offset must be a number or a Parameter.')

    self._energy = energy
    self._energy_offset = energy_offset

    if sample_components is not None and not (
        isinstance(sample_components, (ComponentCollection, ModelComponent))
    ):
        raise TypeError(
            f'`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent.'  # noqa: E501
        )
    if isinstance(sample_components, ModelComponent):
        sample_components = ComponentCollection(components=[sample_components])
    self._sample_components = sample_components

    if resolution_components is not None and not (
        isinstance(resolution_components, (ComponentCollection, ModelComponent))
    ):
        raise TypeError(
            f'`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent.'  # noqa: E501
        )
    if isinstance(resolution_components, ModelComponent):
        resolution_components = ComponentCollection(components=[resolution_components])
    self._resolution_components = resolution_components

convert_unit(unit)

Convert the energy and energy_offset to the specified unit.

Parameters:

Name Type Description Default
unit str | Unit

The unit of the energy.

required

Raises:

Type Description
TypeError

If unit is not a string or scipp unit.

Exception

If energy cannot be converted to the specified unit.

Source code in src/easydynamics/convolution/convolution_base.py
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def convert_unit(self, unit: str | sc.Unit) -> None:
    """
    Convert the energy and energy_offset to the specified unit.

    Parameters
    ----------
    unit : str | sc.Unit
        The unit of the energy.

    Raises
    ------
    TypeError
        If unit is not a string or scipp unit.
    Exception
        If energy cannot be converted to the specified unit.
    """
    if not isinstance(unit, (str, sc.Unit)):
        raise TypeError('Energy unit must be a string or scipp unit.')

    old_energy = self.energy.copy()
    try:
        self.energy = sc.to_unit(self.energy, unit)
    except Exception as e:
        self.energy = old_energy
        raise e

    old_energy_offset = self.energy_offset
    try:
        self.energy_offset.convert_unit(unit)
    except Exception as e:
        self.energy_offset = old_energy_offset
        raise e

    self._unit = unit

energy property writable

Get the energy.

Returns:

Type Description
Variable

The energy values where the convolution is evaluated.

energy_offset property writable

Get the energy offset.

Returns:

Type Description
Parameter

The energy offset applied to the convolution.

energy_with_offset property writable

Get the energy with the offset applied.

Returns:

Type Description
Variable

The energy values with the offset applied.

resolution_components property writable

Get the resolution model.

Returns:

Type Description
ComponentCollection | ModelComponent

The resolution model to be convolved.

sample_components property writable

Get the sample model.

Returns:

Type Description
ComponentCollection | ModelComponent

The sample model to be convolved.

energy_grid

EnergyGrid dataclass

Container for the dense energy grid and related metadata.

Attributes:

Name Type Description
energy_dense ndarray

The upsampled and extended energy array.

energy_dense_centered ndarray

The centered version of energy_dense (used for resolution evaluation).

energy_dense_step float

The spacing of energy_dense (used for width checks and normalization).

energy_span_dense float

The total span of energy_dense. (used for width checks).

energy_even_length_offset float

The offset to apply if energy_dense has even length (used for convolution alignment).

Source code in src/easydynamics/convolution/energy_grid.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@dataclass(frozen=True)
class EnergyGrid:
    """
    Container for the dense energy grid and related metadata.

    Attributes
    ----------
    energy_dense : np.ndarray
        The upsampled and extended energy array.
    energy_dense_centered : np.ndarray
        The centered version of energy_dense (used for resolution evaluation).
    energy_dense_step : float
        The spacing of energy_dense (used for width checks and normalization).
    energy_span_dense : float
        The total span of energy_dense. (used for width checks).
    energy_even_length_offset : float
        The offset to apply if energy_dense has even length (used for convolution alignment).
    """

    energy_dense: np.ndarray
    energy_dense_centered: np.ndarray
    energy_dense_step: float
    energy_span_dense: float
    energy_even_length_offset: float

numerical_convolution

NumericalConvolution

Bases: NumericalConvolutionBase

Numerical convolution of a ComponentCollection with a ComponentCollection using FFT.

Includes optional upsampling and extended range to improve accuracy. Warns about very wide or very narrow peaks in the models. If temperature is provided, detailed balance correction is applied to the sample model.

Source code in src/easydynamics/convolution/numerical_convolution.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
class NumericalConvolution(NumericalConvolutionBase):
    """
    Numerical convolution of a ComponentCollection with a ComponentCollection using FFT.

    Includes optional upsampling and extended range to improve accuracy. Warns about very wide or
    very narrow peaks in the models. If temperature is provided, detailed balance correction is
    applied to the sample model.
    """

    def __init__(
        self,
        energy: np.ndarray | sc.Variable,
        sample_components: ComponentCollection | ModelComponent,
        resolution_components: ComponentCollection | ModelComponent,
        energy_offset: Numeric | Parameter = 0.0,
        convolution_settings: ConvolutionSettings | None = None,
        temperature: Parameter | Numeric | None = None,
        temperature_unit: str | sc.Unit = 'K',
        detailed_balance_settings: DetailedBalanceSettings | None = None,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'MyConvolution',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the NumericalConvolution object.

        Parameters
        ----------
        energy : np.ndarray | sc.Variable
            1D array of energy values where the convolution is evaluated.
        sample_components : ComponentCollection | ModelComponent
            The sample model to be convolved.
        resolution_components : ComponentCollection | ModelComponent
            The resolution model to convolve with.
        energy_offset : Numeric | Parameter, default=0.0
            An energy offset to apply to the energy values before convolution.
        convolution_settings : ConvolutionSettings | None, default=None
            The settings for the convolution.
        temperature : Parameter | Numeric | None, default=None
            The temperature to use for detailed balance correction.
        temperature_unit : str | sc.Unit, default='K'
            The unit of the temperature parameter.
        detailed_balance_settings : DetailedBalanceSettings | None, default=None
            The settings for detailed balance. If None, default settings will be used.
        unit : str | sc.Unit, default='meV'
            The unit of the energy.
        display_name : str | None, default='MyConvolution'
            Display name of the model.
        unique_name : str | None, default=None
            Unique name of the model. If None, a unique name will be generated.
        """
        super().__init__(
            energy=energy,
            sample_components=sample_components,
            resolution_components=resolution_components,
            energy_offset=energy_offset,
            convolution_settings=convolution_settings,
            temperature=temperature,
            temperature_unit=temperature_unit,
            detailed_balance_settings=detailed_balance_settings,
            unit=unit,
            display_name=display_name,
            unique_name=unique_name,
        )

    def convolution(
        self,
    ) -> np.ndarray:
        """
        Calculate the convolution of the sample and resolution models at the values given in
        energy. Includes detailed balance correction if temperature is provided.

        Returns
        -------
        np.ndarray
            The convolved values evaluated at energy.
        """
        # Make sure the convolver is updated with the latest convolution
        # settings before convolution.
        if not self.convolution_settings.convolution_plan_is_valid:
            self._energy_grid = self._create_energy_grid()

        # Give warnings if peaks are very wide or very narrow
        self._check_width_thresholds(
            model=self.sample_components,
            model_name='sample model',
        )
        self._check_width_thresholds(
            model=self.resolution_components,
            model_name='resolution model',
        )

        # Evaluate sample model. If called via the Convolution class,
        # delta functions are already filtered out.
        sample_vals = self.sample_components.evaluate(
            self._energy_grid.energy_dense
            - self._energy_grid.energy_even_length_offset
            - self.energy_offset.value
        )

        # Detailed balance correction
        if self.temperature is not None and self.detailed_balance_settings.use_detailed_balance:
            detailed_balance_factor_correction = detailed_balance_factor(
                energy=self._energy_grid.energy_dense - self.energy_offset.value,
                temperature=self.temperature,
                energy_unit=self.energy.unit,
                divide_by_temperature=self.detailed_balance_settings.normalize_detailed_balance,
            )
            sample_vals *= detailed_balance_factor_correction

        # Evaluate resolution model
        resolution_vals = self.resolution_components.evaluate(
            self._energy_grid.energy_dense_centered
        )

        # Convolution
        convolved = fftconvolve(sample_vals, resolution_vals, mode='same')
        convolved *= self._energy_grid.energy_dense_step  # normalize

        if self.upsample_factor is not None:
            # interpolate back to original energy grid
            convolved = np.interp(
                self.energy.values,
                self._energy_grid.energy_dense,
                convolved,
                left=0.0,
                right=0.0,
            )

        return convolved

__init__(energy, sample_components, resolution_components, energy_offset=0.0, convolution_settings=None, temperature=None, temperature_unit='K', detailed_balance_settings=None, unit='meV', display_name='MyConvolution', unique_name=None)

Initialize the NumericalConvolution object.

Parameters:

Name Type Description Default
energy ndarray | Variable

1D array of energy values where the convolution is evaluated.

required
sample_components ComponentCollection | ModelComponent

The sample model to be convolved.

required
resolution_components ComponentCollection | ModelComponent

The resolution model to convolve with.

required
energy_offset Numeric | Parameter

An energy offset to apply to the energy values before convolution.

0.0
convolution_settings ConvolutionSettings | None

The settings for the convolution.

None
temperature Parameter | Numeric | None

The temperature to use for detailed balance correction.

None
temperature_unit str | Unit

The unit of the temperature parameter.

'K'
detailed_balance_settings DetailedBalanceSettings | None

The settings for detailed balance. If None, default settings will be used.

None
unit str | Unit

The unit of the energy.

'meV'
display_name str | None

Display name of the model.

'MyConvolution'
unique_name str | None

Unique name of the model. If None, a unique name will be generated.

None
Source code in src/easydynamics/convolution/numerical_convolution.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def __init__(
    self,
    energy: np.ndarray | sc.Variable,
    sample_components: ComponentCollection | ModelComponent,
    resolution_components: ComponentCollection | ModelComponent,
    energy_offset: Numeric | Parameter = 0.0,
    convolution_settings: ConvolutionSettings | None = None,
    temperature: Parameter | Numeric | None = None,
    temperature_unit: str | sc.Unit = 'K',
    detailed_balance_settings: DetailedBalanceSettings | None = None,
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'MyConvolution',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the NumericalConvolution object.

    Parameters
    ----------
    energy : np.ndarray | sc.Variable
        1D array of energy values where the convolution is evaluated.
    sample_components : ComponentCollection | ModelComponent
        The sample model to be convolved.
    resolution_components : ComponentCollection | ModelComponent
        The resolution model to convolve with.
    energy_offset : Numeric | Parameter, default=0.0
        An energy offset to apply to the energy values before convolution.
    convolution_settings : ConvolutionSettings | None, default=None
        The settings for the convolution.
    temperature : Parameter | Numeric | None, default=None
        The temperature to use for detailed balance correction.
    temperature_unit : str | sc.Unit, default='K'
        The unit of the temperature parameter.
    detailed_balance_settings : DetailedBalanceSettings | None, default=None
        The settings for detailed balance. If None, default settings will be used.
    unit : str | sc.Unit, default='meV'
        The unit of the energy.
    display_name : str | None, default='MyConvolution'
        Display name of the model.
    unique_name : str | None, default=None
        Unique name of the model. If None, a unique name will be generated.
    """
    super().__init__(
        energy=energy,
        sample_components=sample_components,
        resolution_components=resolution_components,
        energy_offset=energy_offset,
        convolution_settings=convolution_settings,
        temperature=temperature,
        temperature_unit=temperature_unit,
        detailed_balance_settings=detailed_balance_settings,
        unit=unit,
        display_name=display_name,
        unique_name=unique_name,
    )

convolution()

Calculate the convolution of the sample and resolution models at the values given in energy. Includes detailed balance correction if temperature is provided.

Returns:

Type Description
ndarray

The convolved values evaluated at energy.

Source code in src/easydynamics/convolution/numerical_convolution.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def convolution(
    self,
) -> np.ndarray:
    """
    Calculate the convolution of the sample and resolution models at the values given in
    energy. Includes detailed balance correction if temperature is provided.

    Returns
    -------
    np.ndarray
        The convolved values evaluated at energy.
    """
    # Make sure the convolver is updated with the latest convolution
    # settings before convolution.
    if not self.convolution_settings.convolution_plan_is_valid:
        self._energy_grid = self._create_energy_grid()

    # Give warnings if peaks are very wide or very narrow
    self._check_width_thresholds(
        model=self.sample_components,
        model_name='sample model',
    )
    self._check_width_thresholds(
        model=self.resolution_components,
        model_name='resolution model',
    )

    # Evaluate sample model. If called via the Convolution class,
    # delta functions are already filtered out.
    sample_vals = self.sample_components.evaluate(
        self._energy_grid.energy_dense
        - self._energy_grid.energy_even_length_offset
        - self.energy_offset.value
    )

    # Detailed balance correction
    if self.temperature is not None and self.detailed_balance_settings.use_detailed_balance:
        detailed_balance_factor_correction = detailed_balance_factor(
            energy=self._energy_grid.energy_dense - self.energy_offset.value,
            temperature=self.temperature,
            energy_unit=self.energy.unit,
            divide_by_temperature=self.detailed_balance_settings.normalize_detailed_balance,
        )
        sample_vals *= detailed_balance_factor_correction

    # Evaluate resolution model
    resolution_vals = self.resolution_components.evaluate(
        self._energy_grid.energy_dense_centered
    )

    # Convolution
    convolved = fftconvolve(sample_vals, resolution_vals, mode='same')
    convolved *= self._energy_grid.energy_dense_step  # normalize

    if self.upsample_factor is not None:
        # interpolate back to original energy grid
        convolved = np.interp(
            self.energy.values,
            self._energy_grid.energy_dense,
            convolved,
            left=0.0,
            right=0.0,
        )

    return convolved

numerical_convolution_base

NumericalConvolutionBase

Bases: ConvolutionBase

Base class for numerical convolutions of sample and resolution models.

Provides methods to handle upsampling, extension, and detailed balance correction. This base class has no convolution functionality.

Source code in src/easydynamics/convolution/numerical_convolution_base.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
class NumericalConvolutionBase(ConvolutionBase):
    """
    Base class for numerical convolutions of sample and resolution models.

    Provides methods to handle upsampling, extension, and detailed balance correction. This base
    class has no convolution functionality.
    """

    def __init__(
        self,
        energy: np.ndarray | sc.Variable,
        sample_components: ComponentCollection | ModelComponent,
        resolution_components: ComponentCollection | ModelComponent,
        energy_offset: Numeric | Parameter = 0.0,
        convolution_settings: ConvolutionSettings | None = None,
        temperature: Parameter | Numeric | None = None,
        temperature_unit: str | sc.Unit = 'K',
        detailed_balance_settings: DetailedBalanceSettings | None = None,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'MyConvolution',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the NumericalConvolutionBase.

        Parameters
        ----------
        energy : np.ndarray | sc.Variable
            1D array of energy values where the convolution is evaluated.
        sample_components : ComponentCollection | ModelComponent
            The components to be convolved.
        resolution_components : ComponentCollection | ModelComponent
            The resolution components to convolve with.
        energy_offset : Numeric | Parameter, default=0.0
            An energy offset to apply to the energy values before convolution.
        convolution_settings : ConvolutionSettings | None, default=None
             The settings for the convolution. If None, default settings will be used.
        temperature : Parameter | Numeric | None, default=None
            The temperature to use for detailed balance correction.
        temperature_unit : str | sc.Unit, default='K'
            The unit of the temperature parameter.
        detailed_balance_settings : DetailedBalanceSettings | None, default=None
            The settings for detailed balance. If None, default settings will be used.
        unit : str | sc.Unit, default='meV'
            The unit of the energy.
        display_name : str | None, default='MyConvolution'
            Display name of the model.
        unique_name : str | None, default=None
            Unique name of the model. If None, a unique name will be generated.

        Raises
        ------
        TypeError
            If temperature is not None, a number, or a Parameter, or if temperature_unit is not a
            string or sc.Unit.
        """
        super().__init__(
            energy=energy,
            sample_components=sample_components,
            resolution_components=resolution_components,
            unit=unit,
            energy_offset=energy_offset,
            display_name=display_name,
            unique_name=unique_name,
        )

        if temperature is not None and not isinstance(temperature, (Numeric, Parameter)):
            raise TypeError('Temperature must be None, a number or a Parameter.')

        if not isinstance(temperature_unit, (str, sc.Unit)):
            raise TypeError('Temperature_unit must be a string or sc.Unit.')
        self._temperature_unit = temperature_unit
        self._temperature = None
        self.temperature = temperature

        if convolution_settings is None:
            convolution_settings = ConvolutionSettings()
        self._convolution_settings = convolution_settings

        if detailed_balance_settings is None:
            detailed_balance_settings = DetailedBalanceSettings()
        if not isinstance(detailed_balance_settings, DetailedBalanceSettings):
            raise TypeError(
                'detailed_balance_settings must be a DetailedBalanceSettings instance.'
            )
        self._detailed_balance_settings = detailed_balance_settings

        # Create a dense grid to improve accuracy.
        # When upsample_factor>1, we evaluate on this grid and
        # interpolate back to the original values at the end
        self._energy_grid = self._create_energy_grid()

    @property
    def convolution_settings(self) -> ConvolutionSettings:
        """
        Get the convolution settings.

        Returns
        -------
        ConvolutionSettings
            The convolution settings.
        """

        return self._convolution_settings

    @convolution_settings.setter
    def convolution_settings(self, settings: ConvolutionSettings) -> None:
        """
        Set the convolution settings and recreate the dense grid.

        Parameters
        ----------
        settings : ConvolutionSettings
            The new convolution settings.

        Raises
        ------
        TypeError
            If settings is not a ConvolutionSettings instance.
        """
        if not isinstance(settings, ConvolutionSettings):
            raise TypeError('settings must be a ConvolutionSettings instance.')
        self._convolution_settings = settings
        self._convolution_settings.convolution_plan_is_valid = False

    @ConvolutionBase.energy.setter
    def energy(self, energy: np.ndarray) -> None:
        """
        Set the energy array and recreate the dense grid.

        Parameters
        ----------
        energy : np.ndarray
            The new energy array.
        """
        ConvolutionBase.energy.fset(self, energy)
        self.convolution_settings.convolution_plan_is_valid = False

    @property
    def upsample_factor(self) -> Numeric | None:
        """
        Get the upsample factor.

        Returns
        -------
        Numeric | None
            The upsample factor.
        """

        return self.convolution_settings.upsample_factor

    @upsample_factor.setter
    def upsample_factor(self, factor: Numeric | None) -> None:
        """
        Set the upsample factor and recreate the dense grid.

        Parameters
        ----------
        factor : Numeric | None
            The new upsample factor.

        Raises
        ------
        TypeError
            If factor is not a number or None.
        ValueError
            If factor is not greater than 1.
        """
        if factor is None:
            self.convolution_settings.upsample_factor = factor
            return

        if not isinstance(factor, Numeric):
            raise TypeError('Upsample factor must be a numerical value or None.')
        factor = float(factor)
        if factor <= 1.0:
            raise ValueError('Upsample factor must be greater than 1.')

        self.convolution_settings.upsample_factor = factor

    @property
    def extension_factor(self) -> float:
        """
        Get the extension factor.

        The extension factor determines how much the energy range is extended on both sides before
        convolution. 0.2 means extending by 20% of the original energy span on each side

        Returns
        -------
        float
            The extension factor.
        """

        return self.convolution_settings.extension_factor

    @extension_factor.setter
    def extension_factor(self, factor: Numeric) -> None:
        """
        Set the extension factor and recreate the dense grid.

        The extension factor determines how much the energy range is extended on both sides before
        convolution. 0.2 means extending by 20% of the original energy span on each side.

        Parameters
        ----------
        factor : Numeric
            The new extension factor.

        Raises
        ------
        TypeError
            If factor is not a number.
        ValueError
            If factor is negative.
        """

        if not isinstance(factor, Numeric):
            raise TypeError('Extension factor must be a number.')
        if factor < 0.0:
            raise ValueError('Extension factor must be non-negative.')

        self.convolution_settings.extension_factor = float(factor)

    @property
    def temperature(self) -> Parameter | None:
        """
        Get the temperature.

        Returns
        -------
        Parameter | None
            The temperature parameter, or None if detailed balance correction is disabled.
        """

        return self._temperature

    @temperature.setter
    def temperature(self, temp: Parameter | Numeric | None) -> None:
        """
        Set the temperature.

        If None, disables detailed balance correction and removes the temperature parameter.

        Parameters
        ----------
        temp : Parameter | Numeric | None
            The temperature to set. The unit will be the same as the existing temperature parameter
            if it exists, otherwise 'K'.

        Raises
        ------
        TypeError
            If temp is not a Numeric, Parameter, or None.
        """

        if temp is None:
            self._temperature = None
        elif isinstance(temp, Numeric):
            if self._temperature is not None:
                self._temperature.value = float(temp)
            else:
                self._temperature = Parameter(
                    name='temperature',
                    value=float(temp),
                    unit=self._temperature_unit,
                    fixed=True,
                )
        elif isinstance(temp, Parameter):
            self._temperature = temp
        else:
            raise TypeError('Temperature must be None, a float or a Parameter.')

    @property
    def detailed_balance_settings(self) -> DetailedBalanceSettings:
        """
        Get the DetailedBalanceSettings of the Convolution.

        Returns
        -------
        DetailedBalanceSettings
            The DetailedBalanceSettings of the Convolution.
        """
        return self._detailed_balance_settings

    @detailed_balance_settings.setter
    def detailed_balance_settings(self, value: DetailedBalanceSettings) -> None:
        """
        Set the DetailedBalanceSettings of the Convolution.

        Parameters
        ----------
        value : DetailedBalanceSettings
            The DetailedBalanceSettings to set.

        Raises
        ------
        TypeError
            If value is not a DetailedBalanceSettings.
        """
        if not isinstance(value, DetailedBalanceSettings):
            raise TypeError('detailed_balance_settings must be a DetailedBalanceSettings')
        self._detailed_balance_settings = value

    def _create_energy_grid(
        self,
    ) -> EnergyGrid:
        """
        Create a dense grid by upsampling and extending the energy array.

        If upsample_factor is None, no upsampling or extension is performed. This dense grid is
        used for convolution to improve accuracy.

        Raises
        ------
        ValueError
            If energy array is not uniformly spaced when upsample_factor is None, or if energy
            array has less than 2 points.

        Returns
        -------
        EnergyGrid
            The dense grid created by upsampling and extending energy.
        """
        if self.upsample_factor is None:
            # Check if the array is uniformly spaced.
            energy_diff = np.diff(self.energy.values)
            is_uniform = np.allclose(energy_diff, energy_diff[0])
            if not is_uniform:
                raise ValueError(
                    'Input array `energy` must be uniformly spaced if upsample_factor is not given.'  # noqa: E501
                )
            energy_dense = self.energy.values

            energy_span_dense = self.energy.values.max() - self.energy.values.min()
        else:
            # Create an extended and upsampled energy grid
            energy_min, energy_max = self.energy.values.min(), self.energy.values.max()
            energy_span_original = energy_max - energy_min
            extra = self.extension_factor / 2 * energy_span_original
            extended_min = energy_min - extra
            extended_max = energy_max + extra
            num_points = round(len(self.energy.values) * self.upsample_factor)
            energy_dense = np.linspace(extended_min, extended_max, num_points)
            energy_span_dense = extended_max - extended_min

        if len(energy_dense) < 2:
            raise ValueError('Energy array must have at least two points.')
        energy_dense_step = energy_dense[1] - energy_dense[0]

        # Handle offset for even length of energy_dense in convolution.
        # The convolution of two arrays of length N is of length 2N-1.
        #  When using 'same' mode, only the central N points are kept,
        # so the output has the same length as the input.
        # However, if N is even, the center falls between two points,
        # leading to a half-bin offset.
        # For example, if N=4, the convolution has length 7, and when we
        # select the 4 central points we either get
        # indices [2,3,4,5] or [1,2,3,4], both of which are offset by
        # 0.5*dx from the true center at index 3.5.
        energy_even_length_offset = -0.5 * energy_dense_step if len(energy_dense) % 2 == 0 else 0.0

        # Handle the case when energy_dense is not symmetric around 0.
        # The resolution is still centered around zero (or close to it),
        # so it needs to be evaluated there.
        if not np.isclose(energy_dense.mean(), 0.0):
            energy_dense_centered = np.linspace(
                -0.5 * energy_span_dense, 0.5 * energy_span_dense, len(energy_dense)
            )
        else:
            energy_dense_centered = energy_dense

        energy_grid = EnergyGrid(
            energy_dense=energy_dense,
            energy_dense_centered=energy_dense_centered,
            energy_dense_step=energy_dense_step,
            energy_span_dense=energy_span_dense,
            energy_even_length_offset=energy_even_length_offset,
        )
        self._energy_grid = energy_grid
        self.convolution_settings.convolution_plan_is_valid = True
        return energy_grid

    def _check_width_thresholds(
        self,
        model: ComponentCollection | ModelComponent,
        model_name: str,
    ) -> None:
        """
        Helper function to check and warn if components are wide compared to the span of the data,
        or narrow compared to the spacing.

        In both cases, the convolution accuracy may be compromised.

        Parameters
        ----------
        model : ComponentCollection | ModelComponent
            The model to check.
        model_name : str
            A string indicating whether the model is a 'sample model' or 'resolution model' for
            warning messages.
        """

        # Handle ComponentCollection or ModelComponent
        components = model.components if isinstance(model, ComponentCollection) else [model]

        for comp in components:
            if hasattr(comp, 'width'):
                if comp.width.value > LARGE_WIDTH_THRESHOLD * self._energy_grid.energy_span_dense:
                    warnings.warn(
                        (
                            f"The width of the {model_name} component '{comp.unique_name}' "
                            f'({comp.width.value}) is large compared to the span of the input '
                            f'array ({self._energy_grid.energy_span_dense}). '
                            f'This may lead to inaccuracies in the convolution. '
                            f'Increase extension_factor to improve accuracy.'
                        ),
                        UserWarning,
                        stacklevel=3,
                    )
                if comp.width.value < SMALL_WIDTH_THRESHOLD * self._energy_grid.energy_dense_step:
                    warnings.warn(
                        (
                            f"The width of the {model_name} component '{comp.unique_name}' "
                            f'({comp.width.value}) is small compared to the spacing of the input '
                            f'array ({self._energy_grid.energy_dense_step}). '
                            f'This may lead to inaccuracies in the convolution. '
                            f'Increase upsample_factor to improve accuracy.'
                        ),
                        UserWarning,
                        stacklevel=3,
                    )

    def __repr__(self) -> str:
        """
        Return a string representation of the NumericalConvolutionBase.

        Returns
        -------
        str
            A string representation of the NumericalConvolutionBase.
        """
        return (
            f'{self.__class__.__name__}('
            f'energy=array of shape {self.energy.values.shape},\n '
            f'sample_components={self.sample_components!r}, \n'
            f'resolution_components={self.resolution_components!r},\n '
            f'unit={self.unit}, '
            f'upsample_factor={self.upsample_factor}, '
            f'extension_factor={self.extension_factor}, '
            f'temperature={self.temperature}, '
            f'detailed_balance={self.detailed_balance_settings!r})'
        )

__init__(energy, sample_components, resolution_components, energy_offset=0.0, convolution_settings=None, temperature=None, temperature_unit='K', detailed_balance_settings=None, unit='meV', display_name='MyConvolution', unique_name=None)

Initialize the NumericalConvolutionBase.

Parameters:

Name Type Description Default
energy ndarray | Variable

1D array of energy values where the convolution is evaluated.

required
sample_components ComponentCollection | ModelComponent

The components to be convolved.

required
resolution_components ComponentCollection | ModelComponent

The resolution components to convolve with.

required
energy_offset Numeric | Parameter

An energy offset to apply to the energy values before convolution.

0.0
convolution_settings ConvolutionSettings | None

The settings for the convolution. If None, default settings will be used.

None
temperature Parameter | Numeric | None

The temperature to use for detailed balance correction.

None
temperature_unit str | Unit

The unit of the temperature parameter.

'K'
detailed_balance_settings DetailedBalanceSettings | None

The settings for detailed balance. If None, default settings will be used.

None
unit str | Unit

The unit of the energy.

'meV'
display_name str | None

Display name of the model.

'MyConvolution'
unique_name str | None

Unique name of the model. If None, a unique name will be generated.

None

Raises:

Type Description
TypeError

If temperature is not None, a number, or a Parameter, or if temperature_unit is not a string or sc.Unit.

Source code in src/easydynamics/convolution/numerical_convolution_base.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def __init__(
    self,
    energy: np.ndarray | sc.Variable,
    sample_components: ComponentCollection | ModelComponent,
    resolution_components: ComponentCollection | ModelComponent,
    energy_offset: Numeric | Parameter = 0.0,
    convolution_settings: ConvolutionSettings | None = None,
    temperature: Parameter | Numeric | None = None,
    temperature_unit: str | sc.Unit = 'K',
    detailed_balance_settings: DetailedBalanceSettings | None = None,
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'MyConvolution',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the NumericalConvolutionBase.

    Parameters
    ----------
    energy : np.ndarray | sc.Variable
        1D array of energy values where the convolution is evaluated.
    sample_components : ComponentCollection | ModelComponent
        The components to be convolved.
    resolution_components : ComponentCollection | ModelComponent
        The resolution components to convolve with.
    energy_offset : Numeric | Parameter, default=0.0
        An energy offset to apply to the energy values before convolution.
    convolution_settings : ConvolutionSettings | None, default=None
         The settings for the convolution. If None, default settings will be used.
    temperature : Parameter | Numeric | None, default=None
        The temperature to use for detailed balance correction.
    temperature_unit : str | sc.Unit, default='K'
        The unit of the temperature parameter.
    detailed_balance_settings : DetailedBalanceSettings | None, default=None
        The settings for detailed balance. If None, default settings will be used.
    unit : str | sc.Unit, default='meV'
        The unit of the energy.
    display_name : str | None, default='MyConvolution'
        Display name of the model.
    unique_name : str | None, default=None
        Unique name of the model. If None, a unique name will be generated.

    Raises
    ------
    TypeError
        If temperature is not None, a number, or a Parameter, or if temperature_unit is not a
        string or sc.Unit.
    """
    super().__init__(
        energy=energy,
        sample_components=sample_components,
        resolution_components=resolution_components,
        unit=unit,
        energy_offset=energy_offset,
        display_name=display_name,
        unique_name=unique_name,
    )

    if temperature is not None and not isinstance(temperature, (Numeric, Parameter)):
        raise TypeError('Temperature must be None, a number or a Parameter.')

    if not isinstance(temperature_unit, (str, sc.Unit)):
        raise TypeError('Temperature_unit must be a string or sc.Unit.')
    self._temperature_unit = temperature_unit
    self._temperature = None
    self.temperature = temperature

    if convolution_settings is None:
        convolution_settings = ConvolutionSettings()
    self._convolution_settings = convolution_settings

    if detailed_balance_settings is None:
        detailed_balance_settings = DetailedBalanceSettings()
    if not isinstance(detailed_balance_settings, DetailedBalanceSettings):
        raise TypeError(
            'detailed_balance_settings must be a DetailedBalanceSettings instance.'
        )
    self._detailed_balance_settings = detailed_balance_settings

    # Create a dense grid to improve accuracy.
    # When upsample_factor>1, we evaluate on this grid and
    # interpolate back to the original values at the end
    self._energy_grid = self._create_energy_grid()

__repr__()

Return a string representation of the NumericalConvolutionBase.

Returns:

Type Description
str

A string representation of the NumericalConvolutionBase.

Source code in src/easydynamics/convolution/numerical_convolution_base.py
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
def __repr__(self) -> str:
    """
    Return a string representation of the NumericalConvolutionBase.

    Returns
    -------
    str
        A string representation of the NumericalConvolutionBase.
    """
    return (
        f'{self.__class__.__name__}('
        f'energy=array of shape {self.energy.values.shape},\n '
        f'sample_components={self.sample_components!r}, \n'
        f'resolution_components={self.resolution_components!r},\n '
        f'unit={self.unit}, '
        f'upsample_factor={self.upsample_factor}, '
        f'extension_factor={self.extension_factor}, '
        f'temperature={self.temperature}, '
        f'detailed_balance={self.detailed_balance_settings!r})'
    )

convolution_settings property writable

Get the convolution settings.

Returns:

Type Description
ConvolutionSettings

The convolution settings.

detailed_balance_settings property writable

Get the DetailedBalanceSettings of the Convolution.

Returns:

Type Description
DetailedBalanceSettings

The DetailedBalanceSettings of the Convolution.

energy(energy)

Set the energy array and recreate the dense grid.

Parameters:

Name Type Description Default
energy ndarray

The new energy array.

required
Source code in src/easydynamics/convolution/numerical_convolution_base.py
153
154
155
156
157
158
159
160
161
162
163
164
@ConvolutionBase.energy.setter
def energy(self, energy: np.ndarray) -> None:
    """
    Set the energy array and recreate the dense grid.

    Parameters
    ----------
    energy : np.ndarray
        The new energy array.
    """
    ConvolutionBase.energy.fset(self, energy)
    self.convolution_settings.convolution_plan_is_valid = False

extension_factor property writable

Get the extension factor.

The extension factor determines how much the energy range is extended on both sides before convolution. 0.2 means extending by 20% of the original energy span on each side

Returns:

Type Description
float

The extension factor.

temperature property writable

Get the temperature.

Returns:

Type Description
Parameter | None

The temperature parameter, or None if detailed balance correction is disabled.

upsample_factor property writable

Get the upsample factor.

Returns:

Type Description
Numeric | None

The upsample factor.