Skip to content

sample_model

BrownianTranslationalDiffusion

Bases: DiffusionModelBase

Model of Brownian translational diffusion, consisting of a Lorentzian function for each Q-value, where the width is given by :math:DQ^2. Q is assumed to have units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given Q-values.

Example usage: Q=np.linspace(0.5,2,7) energy=np.linspace(-2, 2, 501) scale=1.0 diffusion_coefficient = 2.4e-9 # m^2/s diffusion_model=BrownianTranslationalDiffusion(display_name="DiffusionModel", scale=scale, diffusion_coefficient= diffusion_coefficient) component_collections=diffusion_model.create_component_collections(Q) See also the examples.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
 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
class BrownianTranslationalDiffusion(DiffusionModelBase):
    """Model of Brownian translational diffusion, consisting of a
    Lorentzian function for each Q-value, where the width is given by
    :math:`DQ^2`. Q is assumed to have units of 1/angstrom. Creates
    ComponentCollections with Lorentzian components for given Q-values.

    Example usage:
    Q=np.linspace(0.5,2,7)
    energy=np.linspace(-2, 2, 501)
    scale=1.0
    diffusion_coefficient = 2.4e-9  # m^2/s
    diffusion_model=BrownianTranslationalDiffusion(display_name="DiffusionModel",
    scale=scale, diffusion_coefficient= diffusion_coefficient)
    component_collections=diffusion_model.create_component_collections(Q)
    See also the examples.
    """

    def __init__(
        self,
        display_name: str | None = 'BrownianTranslationalDiffusion',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
        scale: Numeric = 1.0,
        diffusion_coefficient: Numeric = 1.0,
        diffusion_unit: str = 'm**2/s',
    ):
        """Initialize a new BrownianTranslationalDiffusion model.

        Parameters
        ----------
        display_name : str
            Display name of the diffusion model.
        unique_name : str or None
            Unique name of the diffusion model. If None, a unique name
            is automatically generated.
        unit : str or sc.Unit, optional
            Energy unit for the underlying Lorentzian components.
            Defaults to "meV".
        scale : float or Parameter, optional
            Scale factor for the diffusion model.
        diffusion_coefficient : float or Parameter, optional
            Diffusion coefficient D. If a number is provided,
            it is assumed to be in the unit given by diffusion_unit.
            Defaults to 1.0.
        diffusion_unit : str, optional
            Unit for the diffusion coefficient D. Default is m**2/s.
            Options are 'meV*Å**2' or 'm**2/s'
        """
        if not isinstance(scale, (Parameter, Numeric)):
            raise TypeError('scale must be a number.')

        if not isinstance(diffusion_coefficient, (Parameter, Numeric)):
            raise TypeError('diffusion_coefficient must be a number.')

        if not isinstance(diffusion_unit, str):
            raise TypeError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.")

        if diffusion_unit == 'meV*Å**2' or diffusion_unit == 'meV*angstrom**2':
            # In this case, hbar is absorbed in the unit of D
            self._hbar = DescriptorNumber('hbar', 1.0)
        elif diffusion_unit == 'm**2/s' or diffusion_unit == 'm^2/s':
            self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar)
        else:
            raise ValueError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.")

        scale = Parameter(name='scale', value=float(scale), fixed=False, min=0.0)

        diffusion_coefficient = Parameter(
            name='diffusion_coefficient',
            value=float(diffusion_coefficient),
            fixed=False,
            unit=diffusion_unit,
        )
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
            unit=unit,
        )
        self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
        self._scale = scale
        self._diffusion_coefficient = diffusion_coefficient

    @property
    def scale(self) -> Parameter:
        """Get the scale parameter of the diffusion model.

        Returns
        -------
        Parameter
            Scale parameter.
        """
        return self._scale

    @scale.setter
    def scale(self, scale: Numeric) -> None:
        """Set the scale parameter of the diffusion model."""
        if not isinstance(scale, (Numeric)):
            raise TypeError('scale must be a number.')
        self._scale.value = scale

    @property
    def diffusion_coefficient(self) -> Parameter:
        """Get the diffusion coefficient parameter D.

        Returns
        -------
        Parameter
            Diffusion coefficient D.
        """
        return self._diffusion_coefficient

    @diffusion_coefficient.setter
    def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None:
        """Set the diffusion coefficient parameter D."""
        if not isinstance(diffusion_coefficient, (Numeric)):
            raise TypeError('diffusion_coefficient must be a number.')
        self._diffusion_coefficient.value = diffusion_coefficient

    def calculate_width(self, Q: Q_type) -> np.ndarray:
        """Calculate the half-width at half-maximum (HWHM) for the
        diffusion model.

        Parameters
        ----------
        Q : np.ndarray | Numeric | list | ArrayLike
            Scattering vector in 1/angstrom

        Returns
        -------
        np.ndarray
            HWHM values in the unit of the model (e.g., meV).
        """

        Q = _validate_and_convert_Q(Q)

        unit_conversion_factor = self._hbar * self.diffusion_coefficient / (self._angstrom**2)
        unit_conversion_factor.convert_unit(self.unit)
        width = Q**2 * unit_conversion_factor.value

        return width

    def calculate_EISF(self, Q: Q_type) -> np.ndarray:
        """Calculate the Elastic Incoherent Structure Factor (EISF) for
        the Brownian translational diffusion model.

        Parameters
        ----------
        Q : np.ndarray | Numeric | list | ArrayLike
            Scattering vector in 1/angstrom

        Returns
        -------
        np.ndarray
            EISF values (dimensionless).
        """
        Q = _validate_and_convert_Q(Q)
        EISF = np.zeros_like(Q)
        return EISF

    def calculate_QISF(self, Q: Q_type) -> np.ndarray:
        """Calculate the Quasi-Elastic Incoherent Structure Factor
        (QISF).

        Parameters
        ----------
        Q : np.ndarray | Numeric | list | ArrayLike
            Scattering vector in 1/angstrom

        Returns
        -------
        np.ndarray
            QISF values (dimensionless).
        """

        Q = _validate_and_convert_Q(Q)
        QISF = np.ones_like(Q)
        return QISF

    def create_component_collections(
        self,
        Q: Q_type,
        component_display_name: str = 'Brownian translational diffusion',
    ) -> List[ComponentCollection]:
        """Create ComponentCollection components for the Brownian
        translational diffusion model at given Q values.

        Args:
        ----------
        Q : Number, list, or np.ndarray
            Scattering vector values.
        component_display_name : str
            Name of the Lorentzian component.
        Returns
        -------
        List[ComponentCollection]
            List of ComponentCollections with Lorentzian components.
        """
        Q = _validate_and_convert_Q(Q)

        if not isinstance(component_display_name, str):
            raise TypeError('component_name must be a string.')

        component_collection_list = [None] * len(Q)
        # In more complex models, this is used to scale the area of the
        # Lorentzians and the delta function.
        QISF = self.calculate_QISF(Q)

        # Create a Lorentzian component for each Q-value, with
        # width D*Q^2 and area equal to scale.
        # No delta function, as the EISF is 0.
        for i, Q_value in enumerate(Q):
            component_collection_list[i] = ComponentCollection(
                display_name=f'{self.display_name}_Q{Q_value:.2f}', unit=self.unit
            )

            lorentzian_component = Lorentzian(
                display_name=component_display_name,
                unit=self.unit,
            )

            # Make the width dependent on Q
            dependency_expression = self._write_width_dependency_expression(Q[i])
            dependency_map = self._write_width_dependency_map_expression()

            lorentzian_component.width.make_dependent_on(
                dependency_expression=dependency_expression,
                dependency_map=dependency_map,
            )

            # Make the area dependent on Q
            area_dependency_map = self._write_area_dependency_map_expression()
            lorentzian_component.area.make_dependent_on(
                dependency_expression=self._write_area_dependency_expression(QISF[i]),
                dependency_map=area_dependency_map,
            )

            # Resolving the dependency can do weird things to the units,
            # so we make sure it's correct.
            lorentzian_component.width.convert_unit(self.unit)
            component_collection_list[i].append_component(lorentzian_component)

        return component_collection_list

    def _write_width_dependency_expression(self, Q: float) -> str:
        """Write the dependency expression for the width as a function
        of Q to make dependent Parameters.

        Parameters
        ----------
        Q : float
            Scattering vector in 1/angstrom
        Returns
        -------
        str
            Dependency expression for the width.
        """
        if not isinstance(Q, (float)):
            raise TypeError('Q must be a float.')

        # Q is given as a float, so we need to add the units
        return f'hbar * D* {Q} **2*1/(angstrom**2)'

    def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]:
        """Write the dependency map expression to make dependent
        Parameters.
        """
        return {
            'D': self.diffusion_coefficient,
            'hbar': self._hbar,
            'angstrom': self._angstrom,
        }

    def _write_area_dependency_expression(self, QISF: float) -> str:
        """Write the dependency expression for the area to make
        dependent Parameters.

        Returns
        -------
        str
            Dependency expression for the area.
        """
        if not isinstance(QISF, (float)):
            raise TypeError('QISF must be a float.')

        return f'{QISF} * scale'

    def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]:
        """Write the dependency map expression to make dependent
        Parameters.
        """
        return {
            'scale': self.scale,
        }

    def __repr__(self):
        """String representation of the BrownianTranslationalDiffusion
        model.
        """
        return (
            f'BrownianTranslationalDiffusion(display_name={self.display_name},'
            f'diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})'
        )

__init__(display_name='BrownianTranslationalDiffusion', unique_name=None, unit='meV', scale=1.0, diffusion_coefficient=1.0, diffusion_unit='m**2/s')

Initialize a new BrownianTranslationalDiffusion model.

Parameters

display_name : str Display name of the diffusion model. unique_name : str or None Unique name of the diffusion model. If None, a unique name is automatically generated. unit : str or sc.Unit, optional Energy unit for the underlying Lorentzian components. Defaults to "meV". scale : float or Parameter, optional Scale factor for the diffusion model. diffusion_coefficient : float or Parameter, optional Diffusion coefficient D. If a number is provided, it is assumed to be in the unit given by diffusion_unit. Defaults to 1.0. diffusion_unit : str, optional Unit for the diffusion coefficient D. Default is m2/s. Options are 'meV*Å2' or 'm**2/s'

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
 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
def __init__(
    self,
    display_name: str | None = 'BrownianTranslationalDiffusion',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
    scale: Numeric = 1.0,
    diffusion_coefficient: Numeric = 1.0,
    diffusion_unit: str = 'm**2/s',
):
    """Initialize a new BrownianTranslationalDiffusion model.

    Parameters
    ----------
    display_name : str
        Display name of the diffusion model.
    unique_name : str or None
        Unique name of the diffusion model. If None, a unique name
        is automatically generated.
    unit : str or sc.Unit, optional
        Energy unit for the underlying Lorentzian components.
        Defaults to "meV".
    scale : float or Parameter, optional
        Scale factor for the diffusion model.
    diffusion_coefficient : float or Parameter, optional
        Diffusion coefficient D. If a number is provided,
        it is assumed to be in the unit given by diffusion_unit.
        Defaults to 1.0.
    diffusion_unit : str, optional
        Unit for the diffusion coefficient D. Default is m**2/s.
        Options are 'meV*Å**2' or 'm**2/s'
    """
    if not isinstance(scale, (Parameter, Numeric)):
        raise TypeError('scale must be a number.')

    if not isinstance(diffusion_coefficient, (Parameter, Numeric)):
        raise TypeError('diffusion_coefficient must be a number.')

    if not isinstance(diffusion_unit, str):
        raise TypeError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.")

    if diffusion_unit == 'meV*Å**2' or diffusion_unit == 'meV*angstrom**2':
        # In this case, hbar is absorbed in the unit of D
        self._hbar = DescriptorNumber('hbar', 1.0)
    elif diffusion_unit == 'm**2/s' or diffusion_unit == 'm^2/s':
        self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar)
    else:
        raise ValueError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.")

    scale = Parameter(name='scale', value=float(scale), fixed=False, min=0.0)

    diffusion_coefficient = Parameter(
        name='diffusion_coefficient',
        value=float(diffusion_coefficient),
        fixed=False,
        unit=diffusion_unit,
    )
    super().__init__(
        display_name=display_name,
        unique_name=unique_name,
        unit=unit,
    )
    self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
    self._scale = scale
    self._diffusion_coefficient = diffusion_coefficient

__repr__()

String representation of the BrownianTranslationalDiffusion model.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
319
320
321
322
323
324
325
326
def __repr__(self):
    """String representation of the BrownianTranslationalDiffusion
    model.
    """
    return (
        f'BrownianTranslationalDiffusion(display_name={self.display_name},'
        f'diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})'
    )

calculate_EISF(Q)

Calculate the Elastic Incoherent Structure Factor (EISF) for the Brownian translational diffusion model.

Parameters

Q : np.ndarray | Numeric | list | ArrayLike Scattering vector in 1/angstrom

Returns

np.ndarray EISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def calculate_EISF(self, Q: Q_type) -> np.ndarray:
    """Calculate the Elastic Incoherent Structure Factor (EISF) for
    the Brownian translational diffusion model.

    Parameters
    ----------
    Q : np.ndarray | Numeric | list | ArrayLike
        Scattering vector in 1/angstrom

    Returns
    -------
    np.ndarray
        EISF values (dimensionless).
    """
    Q = _validate_and_convert_Q(Q)
    EISF = np.zeros_like(Q)
    return EISF

calculate_QISF(Q)

Calculate the Quasi-Elastic Incoherent Structure Factor (QISF).

Parameters

Q : np.ndarray | Numeric | list | ArrayLike Scattering vector in 1/angstrom

Returns

np.ndarray QISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def calculate_QISF(self, Q: Q_type) -> np.ndarray:
    """Calculate the Quasi-Elastic Incoherent Structure Factor
    (QISF).

    Parameters
    ----------
    Q : np.ndarray | Numeric | list | ArrayLike
        Scattering vector in 1/angstrom

    Returns
    -------
    np.ndarray
        QISF values (dimensionless).
    """

    Q = _validate_and_convert_Q(Q)
    QISF = np.ones_like(Q)
    return QISF

calculate_width(Q)

Calculate the half-width at half-maximum (HWHM) for the diffusion model.

Parameters

Q : np.ndarray | Numeric | list | ArrayLike Scattering vector in 1/angstrom

Returns

np.ndarray HWHM values in the unit of the model (e.g., meV).

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def calculate_width(self, Q: Q_type) -> np.ndarray:
    """Calculate the half-width at half-maximum (HWHM) for the
    diffusion model.

    Parameters
    ----------
    Q : np.ndarray | Numeric | list | ArrayLike
        Scattering vector in 1/angstrom

    Returns
    -------
    np.ndarray
        HWHM values in the unit of the model (e.g., meV).
    """

    Q = _validate_and_convert_Q(Q)

    unit_conversion_factor = self._hbar * self.diffusion_coefficient / (self._angstrom**2)
    unit_conversion_factor.convert_unit(self.unit)
    width = Q**2 * unit_conversion_factor.value

    return width

create_component_collections(Q, component_display_name='Brownian translational diffusion')

Create ComponentCollection components for the Brownian translational diffusion model at given Q values.

Args:

Q : Number, list, or np.ndarray Scattering vector values. component_display_name : str Name of the Lorentzian component. Returns


List[ComponentCollection] List of ComponentCollections with Lorentzian components.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
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
def create_component_collections(
    self,
    Q: Q_type,
    component_display_name: str = 'Brownian translational diffusion',
) -> List[ComponentCollection]:
    """Create ComponentCollection components for the Brownian
    translational diffusion model at given Q values.

    Args:
    ----------
    Q : Number, list, or np.ndarray
        Scattering vector values.
    component_display_name : str
        Name of the Lorentzian component.
    Returns
    -------
    List[ComponentCollection]
        List of ComponentCollections with Lorentzian components.
    """
    Q = _validate_and_convert_Q(Q)

    if not isinstance(component_display_name, str):
        raise TypeError('component_name must be a string.')

    component_collection_list = [None] * len(Q)
    # In more complex models, this is used to scale the area of the
    # Lorentzians and the delta function.
    QISF = self.calculate_QISF(Q)

    # Create a Lorentzian component for each Q-value, with
    # width D*Q^2 and area equal to scale.
    # No delta function, as the EISF is 0.
    for i, Q_value in enumerate(Q):
        component_collection_list[i] = ComponentCollection(
            display_name=f'{self.display_name}_Q{Q_value:.2f}', unit=self.unit
        )

        lorentzian_component = Lorentzian(
            display_name=component_display_name,
            unit=self.unit,
        )

        # Make the width dependent on Q
        dependency_expression = self._write_width_dependency_expression(Q[i])
        dependency_map = self._write_width_dependency_map_expression()

        lorentzian_component.width.make_dependent_on(
            dependency_expression=dependency_expression,
            dependency_map=dependency_map,
        )

        # Make the area dependent on Q
        area_dependency_map = self._write_area_dependency_map_expression()
        lorentzian_component.area.make_dependent_on(
            dependency_expression=self._write_area_dependency_expression(QISF[i]),
            dependency_map=area_dependency_map,
        )

        # Resolving the dependency can do weird things to the units,
        # so we make sure it's correct.
        lorentzian_component.width.convert_unit(self.unit)
        component_collection_list[i].append_component(lorentzian_component)

    return component_collection_list

diffusion_coefficient property writable

Get the diffusion coefficient parameter D.

Returns

Parameter Diffusion coefficient D.

scale property writable

Get the scale parameter of the diffusion model.

Returns

Parameter Scale parameter.

ComponentCollection

Bases: ModelBase

A model of the scattering from a sample, combining multiple model components.

Attributes

display_name : str Display name of the ComponentCollection. unit : str or sc.Unit Unit of the ComponentCollection.

Source code in src/easydynamics/sample_model/component_collection.py
 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
class ComponentCollection(ModelBase):
    """A model of the scattering from a sample, combining multiple model
    components.

    Attributes
    ----------
    display_name : str
        Display name of the ComponentCollection.
    unit : str or sc.Unit
        Unit of the ComponentCollection.
    """

    def __init__(
        self,
        unit: str | sc.Unit = 'meV',
        display_name: str = 'MyComponentCollection',
        unique_name: str | None = None,
        components: List[ModelComponent] | None = None,
    ):
        """Initialize a new ComponentCollection.

        Parameters
        ----------
        unit : str or sc.Unit, optional
            Unit of the sample model. Defaults to "meV".
        display_name : str
            Display name of the sample model.
        unique_name : str or None, optional
            Unique name of the sample model. Defaults to None.
        components : List[ModelComponent], optional
            Initial model components to add to the ComponentCollection.
        """

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

        if unit is not None and not isinstance(unit, (str, sc.Unit)):
            raise TypeError(
                f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}'
            )
        self._unit = unit
        self._components = []

        # Add initial components if provided. Used for serialization.
        if components is not None:
            if not isinstance(components, list):
                raise TypeError('components must be a list of ModelComponent instances.')
            for comp in components:
                self.append_component(comp)

    def append_component(self, component: ModelComponent | 'ComponentCollection') -> None:
        match component:
            case ModelComponent():
                components = (component,)
            case ComponentCollection(components=components):
                pass
            case _:
                raise TypeError('Component must be a ModelComponent or ComponentCollection.')

        for comp in components:
            if comp in self._components:
                raise ValueError(
                    f"Component '{comp.unique_name}' is already in the collection. "
                    f'Existing components: {self.list_component_names()}'
                )

            self._components.append(comp)

    def remove_component(self, unique_name: str) -> None:
        if not isinstance(unique_name, str):
            raise TypeError('Component name must be a string.')

        for comp in self._components:
            if comp.unique_name == unique_name:
                self._components.remove(comp)
                return

        raise KeyError(
            f"No component named '{unique_name}' exists. "
            f'Did you accidentally use the display_name? '
            f'Here is a list of the components in the collection: {self.list_component_names()}'
        )

    @property
    def components(self) -> list[ModelComponent]:
        return list(self._components)

    @components.setter
    def components(self, components: List[ModelComponent]) -> None:
        if not isinstance(components, list):
            raise TypeError('components must be a list of ModelComponent instances.')
        for comp in components:
            if not isinstance(comp, ModelComponent):
                raise TypeError(
                    'All items in components must be instances of ModelComponent. '
                    f'Got {type(comp).__name__} instead.'
                )

        self._components = components

    def list_component_names(self) -> List[str]:
        """List the names of all components in the model.

        Returns
        -------
        List[str]
            Component names.
        """

        return [component.unique_name for component in self._components]

    def clear_components(self) -> None:
        """Remove all components."""
        self._components.clear()

    def normalize_area(self) -> None:
        # Useful for convolutions.
        """Normalize the areas of all components so they sum to 1."""
        if not self.components:
            raise ValueError('No components in the model to normalize.')

        area_params = []
        total_area = Parameter(name='total_area', value=0.0, unit=self._unit)

        for component in self.components:
            if hasattr(component, 'area'):
                area_params.append(component.area)
                total_area += component.area
            else:
                warnings.warn(
                    f"Component '{component.unique_name}' does not have an 'area' attribute "
                    f'and will be skipped in normalization.',
                    UserWarning,
                )

        if total_area.value == 0:
            raise ValueError('Total area is zero; cannot normalize.')

        if not np.isfinite(total_area.value):
            raise ValueError('Total area is not finite; cannot normalize.')

        for param in area_params:
            param.value /= total_area.value

    def get_all_variables(self) -> list[DescriptorBase]:
        """Get all parameters from the model component.

        Returns:
        List[Parameter]: List of parameters in the component.
        """

        return [var for component in self.components for var in component.get_all_variables()]

    @property
    def unit(self) -> str | sc.Unit:
        """Get the unit of the ComponentCollection.

        Returns
        -------
        str or sc.Unit or None
        """
        return self._unit

    @unit.setter
    def unit(self, unit_str: str) -> None:
        raise AttributeError(
            (
                f'Unit is read-only. Use convert_unit to change the unit between allowed types '
                f'or create a new {self.__class__.__name__} with the desired unit.'
            )
        )  # noqa: E501

    def convert_unit(self, unit: str | sc.Unit) -> None:
        """Convert the unit of the ComponentCollection and all its
        components.
        """

        old_unit = self._unit

        try:
            for component in self.components:
                component.convert_unit(unit)
            self._unit = unit
        except Exception as e:
            # Attempt to rollback on failure
            try:
                for component in self.components:
                    component.convert_unit(old_unit)
            except Exception:  # noqa: S110
                pass  # Best effort rollback
            raise e

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the sum of all components.

        Parameters
        ----------
        x : Number, list, np.ndarray, sc.Variable, or sc.DataArray
            Energy axis.

        Returns
        -------
        np.ndarray
            Evaluated model values.
        """

        if not self.components:
            raise ValueError('No components in the model to evaluate.')
        return sum(component.evaluate(x) for component in self.components)

    def evaluate_component(
        self,
        x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray,
        unique_name: str,
    ) -> np.ndarray:
        """Evaluate a single component by name.

        Parameters
        ----------
        x : Number, list, np.ndarray, sc.Variable, or sc.DataArray
            Energy axis.
        unique_name : str
            Component unique name.

        Returns
        -------
        np.ndarray
            Evaluated values for the specified component.
        """
        if not self.components:
            raise ValueError('No components in the model to evaluate.')

        if not isinstance(unique_name, str):
            raise TypeError(
                (f'Component unique name must be a string, got {type(unique_name)} instead.')
            )

        matches = [comp for comp in self.components if comp.unique_name == unique_name]
        if not matches:
            raise KeyError(f"No component named '{unique_name}' exists.")

        component = matches[0]

        result = component.evaluate(x)

        return result

    def fix_all_parameters(self) -> None:
        """Fix all free parameters in the model."""
        for param in self.get_fittable_parameters():
            param.fixed = True

    def free_all_parameters(self) -> None:
        """Free all fixed parameters in the model."""
        for param in self.get_fittable_parameters():
            param.fixed = False

    def __contains__(self, item: str | ModelComponent) -> bool:
        """Check if a component with the given name or instance exists
        in the ComponentCollection.

        Args:
        ----------
        item : str or ModelComponent
            The component name or instance to check for.
        Returns
        -------
        bool
            True if the component exists, False otherwise.
        """

        if isinstance(item, str):
            # Check by component unique name
            return any(comp.unique_name == item for comp in self.components)
        elif isinstance(item, ModelComponent):
            # Check by component instance
            return any(comp is item for comp in self.components)
        else:
            return False

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

        Returns
        -------
        str
        """
        comp_names = ', '.join(c.unique_name for c in self.components) or 'No components'

        return f"<ComponentCollection unique_name='{self.unique_name}' | Components: {comp_names}>"

__contains__(item)

Check if a component with the given name or instance exists in the ComponentCollection.

Args:

item : str or ModelComponent The component name or instance to check for. Returns


bool True if the component exists, False otherwise.

Source code in src/easydynamics/sample_model/component_collection.py
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
def __contains__(self, item: str | ModelComponent) -> bool:
    """Check if a component with the given name or instance exists
    in the ComponentCollection.

    Args:
    ----------
    item : str or ModelComponent
        The component name or instance to check for.
    Returns
    -------
    bool
        True if the component exists, False otherwise.
    """

    if isinstance(item, str):
        # Check by component unique name
        return any(comp.unique_name == item for comp in self.components)
    elif isinstance(item, ModelComponent):
        # Check by component instance
        return any(comp is item for comp in self.components)
    else:
        return False

__init__(unit='meV', display_name='MyComponentCollection', unique_name=None, components=None)

Initialize a new ComponentCollection.

Parameters

unit : str or sc.Unit, optional Unit of the sample model. Defaults to "meV". display_name : str Display name of the sample model. unique_name : str or None, optional Unique name of the sample model. Defaults to None. components : List[ModelComponent], optional Initial model components to add to the ComponentCollection.

Source code in src/easydynamics/sample_model/component_collection.py
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
def __init__(
    self,
    unit: str | sc.Unit = 'meV',
    display_name: str = 'MyComponentCollection',
    unique_name: str | None = None,
    components: List[ModelComponent] | None = None,
):
    """Initialize a new ComponentCollection.

    Parameters
    ----------
    unit : str or sc.Unit, optional
        Unit of the sample model. Defaults to "meV".
    display_name : str
        Display name of the sample model.
    unique_name : str or None, optional
        Unique name of the sample model. Defaults to None.
    components : List[ModelComponent], optional
        Initial model components to add to the ComponentCollection.
    """

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

    if unit is not None and not isinstance(unit, (str, sc.Unit)):
        raise TypeError(
            f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}'
        )
    self._unit = unit
    self._components = []

    # Add initial components if provided. Used for serialization.
    if components is not None:
        if not isinstance(components, list):
            raise TypeError('components must be a list of ModelComponent instances.')
        for comp in components:
            self.append_component(comp)

__repr__()

Return a string representation of the ComponentCollection.

Returns

str

Source code in src/easydynamics/sample_model/component_collection.py
299
300
301
302
303
304
305
306
307
308
def __repr__(self) -> str:
    """Return a string representation of the ComponentCollection.

    Returns
    -------
    str
    """
    comp_names = ', '.join(c.unique_name for c in self.components) or 'No components'

    return f"<ComponentCollection unique_name='{self.unique_name}' | Components: {comp_names}>"

clear_components()

Remove all components.

Source code in src/easydynamics/sample_model/component_collection.py
130
131
132
def clear_components(self) -> None:
    """Remove all components."""
    self._components.clear()

convert_unit(unit)

Convert the unit of the ComponentCollection and all its components.

Source code in src/easydynamics/sample_model/component_collection.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
def convert_unit(self, unit: str | sc.Unit) -> None:
    """Convert the unit of the ComponentCollection and all its
    components.
    """

    old_unit = self._unit

    try:
        for component in self.components:
            component.convert_unit(unit)
        self._unit = unit
    except Exception as e:
        # Attempt to rollback on failure
        try:
            for component in self.components:
                component.convert_unit(old_unit)
        except Exception:  # noqa: S110
            pass  # Best effort rollback
        raise e

evaluate(x)

Evaluate the sum of all components.

Parameters

x : Number, list, np.ndarray, sc.Variable, or sc.DataArray Energy axis.

Returns

np.ndarray Evaluated model values.

Source code in src/easydynamics/sample_model/component_collection.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the sum of all components.

    Parameters
    ----------
    x : Number, list, np.ndarray, sc.Variable, or sc.DataArray
        Energy axis.

    Returns
    -------
    np.ndarray
        Evaluated model values.
    """

    if not self.components:
        raise ValueError('No components in the model to evaluate.')
    return sum(component.evaluate(x) for component in self.components)

evaluate_component(x, unique_name)

Evaluate a single component by name.

Parameters

x : Number, list, np.ndarray, sc.Variable, or sc.DataArray Energy axis. unique_name : str Component unique name.

Returns

np.ndarray Evaluated values for the specified component.

Source code in src/easydynamics/sample_model/component_collection.py
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
def evaluate_component(
    self,
    x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray,
    unique_name: str,
) -> np.ndarray:
    """Evaluate a single component by name.

    Parameters
    ----------
    x : Number, list, np.ndarray, sc.Variable, or sc.DataArray
        Energy axis.
    unique_name : str
        Component unique name.

    Returns
    -------
    np.ndarray
        Evaluated values for the specified component.
    """
    if not self.components:
        raise ValueError('No components in the model to evaluate.')

    if not isinstance(unique_name, str):
        raise TypeError(
            (f'Component unique name must be a string, got {type(unique_name)} instead.')
        )

    matches = [comp for comp in self.components if comp.unique_name == unique_name]
    if not matches:
        raise KeyError(f"No component named '{unique_name}' exists.")

    component = matches[0]

    result = component.evaluate(x)

    return result

fix_all_parameters()

Fix all free parameters in the model.

Source code in src/easydynamics/sample_model/component_collection.py
266
267
268
269
def fix_all_parameters(self) -> None:
    """Fix all free parameters in the model."""
    for param in self.get_fittable_parameters():
        param.fixed = True

free_all_parameters()

Free all fixed parameters in the model.

Source code in src/easydynamics/sample_model/component_collection.py
271
272
273
274
def free_all_parameters(self) -> None:
    """Free all fixed parameters in the model."""
    for param in self.get_fittable_parameters():
        param.fixed = False

get_all_variables()

Get all parameters from the model component.

Returns: List[Parameter]: List of parameters in the component.

Source code in src/easydynamics/sample_model/component_collection.py
163
164
165
166
167
168
169
170
def get_all_variables(self) -> list[DescriptorBase]:
    """Get all parameters from the model component.

    Returns:
    List[Parameter]: List of parameters in the component.
    """

    return [var for component in self.components for var in component.get_all_variables()]

list_component_names()

List the names of all components in the model.

Returns

List[str] Component names.

Source code in src/easydynamics/sample_model/component_collection.py
119
120
121
122
123
124
125
126
127
128
def list_component_names(self) -> List[str]:
    """List the names of all components in the model.

    Returns
    -------
    List[str]
        Component names.
    """

    return [component.unique_name for component in self._components]

normalize_area()

Normalize the areas of all components so they sum to 1.

Source code in src/easydynamics/sample_model/component_collection.py
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
def normalize_area(self) -> None:
    # Useful for convolutions.
    """Normalize the areas of all components so they sum to 1."""
    if not self.components:
        raise ValueError('No components in the model to normalize.')

    area_params = []
    total_area = Parameter(name='total_area', value=0.0, unit=self._unit)

    for component in self.components:
        if hasattr(component, 'area'):
            area_params.append(component.area)
            total_area += component.area
        else:
            warnings.warn(
                f"Component '{component.unique_name}' does not have an 'area' attribute "
                f'and will be skipped in normalization.',
                UserWarning,
            )

    if total_area.value == 0:
        raise ValueError('Total area is zero; cannot normalize.')

    if not np.isfinite(total_area.value):
        raise ValueError('Total area is not finite; cannot normalize.')

    for param in area_params:
        param.value /= total_area.value

unit property writable

Get the unit of the ComponentCollection.

Returns

str or sc.Unit or None

DampedHarmonicOscillator

Bases: CreateParametersMixin, ModelComponent

Damped Harmonic Oscillator (DHO). 2areacenter^2width/pi / ( (x^2 - center^2)^2 + (2width*x)^2 )

Parameters:

Name Type Description Default
display_name str

Display name of the component.

'DampedHarmonicOscillator'
center Int or float

Resonance frequency, approximately the

1.0
width Int or float

Damping constant, approximately the

1.0
area Int or float

Area under the curve.

1.0
unit str or Unit

Unit of the parameters.

'meV'
Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
 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
class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent):
    """
    Damped Harmonic Oscillator (DHO).
    2*area*center^2*width/pi / ( (x^2 - center^2)^2 + (2*width*x)^2 )

    Args:
        display_name (str): Display name of the component.
        center (Int or float): Resonance frequency, approximately the
        peak position.
        width (Int or float): Damping constant, approximately the
        half width at half max (HWHM) of the peaks.
        area (Int or float): Area under the curve.
        unit (str or sc.Unit): Unit of the parameters.
        Defaults to "meV".
    """

    def __init__(
        self,
        area: Numeric | Parameter = 1.0,
        center: Numeric | Parameter = 1.0,
        width: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'DampedHarmonicOscillator',
        unique_name: str | None = None,
    ):
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
            unit=unit,
        )

        # These methods live in ValidationMixin
        area = self._create_area_parameter(area=area, name=display_name, unit=self._unit)
        center = self._create_center_parameter(
            center=center,
            name=display_name,
            fix_if_none=False,
            unit=self._unit,
            enforce_minimum_center=True,
        )

        width = self._create_width_parameter(width=width, name=display_name, unit=self._unit)

        self._area = area
        self._center = center
        self._width = width

    @property
    def area(self) -> Parameter:
        """Get the area parameter."""
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """Set the area parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('area must be a number')
        self._area.value = value

    @property
    def center(self) -> Parameter:
        """Get the center parameter."""
        return self._center

    @center.setter
    def center(self, value: Numeric) -> None:
        """Set the center parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')

        if value <= 0:
            raise ValueError('center must be positive')
        self._center.value = value

    @property
    def width(self) -> Parameter:
        """Get the width parameter."""
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """Set the width parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')
        self._width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Damped Harmonic Oscillator at the given x
        values.

        If x is a scipp Variable, the unit of the DHO will be converted
        to match x. The DHO evaluates to
        2*area*center^2*width/pi / ((x^2 - center^2)^2 + (2*width*x)^2)
        """

        x = self._prepare_x_for_evaluate(x)

        normalization = 2 * self.center.value**2 * self.width.value / np.pi
        # No division by zero here, width>0 enforced in setter
        denominator = (x**2 - self.center.value**2) ** 2 + (2 * self.width.value * x) ** 2

        return self.area.value * normalization / (denominator)

    def __repr__(self):
        return (
            f'DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \
        area = {self.area},\n center = {self.center},\n width = {self.width})'
        )

area property writable

Get the area parameter.

center property writable

Get the center parameter.

evaluate(x)

Evaluate the Damped Harmonic Oscillator at the given x values.

If x is a scipp Variable, the unit of the DHO will be converted to match x. The DHO evaluates to 2areacenter^2width/pi / ((x^2 - center^2)^2 + (2width*x)^2)

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Damped Harmonic Oscillator at the given x
    values.

    If x is a scipp Variable, the unit of the DHO will be converted
    to match x. The DHO evaluates to
    2*area*center^2*width/pi / ((x^2 - center^2)^2 + (2*width*x)^2)
    """

    x = self._prepare_x_for_evaluate(x)

    normalization = 2 * self.center.value**2 * self.width.value / np.pi
    # No division by zero here, width>0 enforced in setter
    denominator = (x**2 - self.center.value**2) ** 2 + (2 * self.width.value * x) ** 2

    return self.area.value * normalization / (denominator)

width property writable

Get the width parameter.

DeltaFunction

Bases: CreateParametersMixin, ModelComponent

Delta function. Evaluates to zero everywhere, except in convolutions, where it acts as an identity. This is handled in the ResolutionHandler. If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Parameters:

Name Type Description Default
center Int or float or None

Center of the delta function.

None
area Int or float

Total area under the curve.

1.0
unit str or Unit

Unit of the parameters.

'meV'
display_name str

Name of the component.

'DeltaFunction'
unique_name str or None

Unique name of the component.

None
Source code in src/easydynamics/sample_model/components/delta_function.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
class DeltaFunction(CreateParametersMixin, ModelComponent):
    """Delta function. Evaluates to zero everywhere, except in
    convolutions, where it acts as an identity. This is handled in the
    ResolutionHandler. If the center is not provided, it will be
    centered at 0 and fixed, which is typically what you want in QENS.

    Args:
        center (Int or float or None): Center of the delta function.
        If None, defaults to 0 and is fixed.
        area (Int or float): Total area under the curve.
        unit (str or sc.Unit): Unit of the parameters.
        Defaults to "meV".
        display_name (str): Name of the component.
        unique_name (str or None): Unique name of the component.
        If None, a unique_name is automatically generated.
    """

    def __init__(
        self,
        center: None | Numeric | Parameter = None,
        area: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'DeltaFunction',
        unique_name: str | None = None,
    ):
        # Validate inputs and create Parameters if not given
        super().__init__(
            display_name=display_name,
            unit=unit,
            unique_name=unique_name,
        )

        # These methods live in ValidationMixin
        area = self._create_area_parameter(area=area, name=display_name, unit=self._unit)
        center = self._create_center_parameter(
            center=center, name=display_name, fix_if_none=True, unit=self._unit
        )

        self._area = area
        self._center = center

    @property
    def area(self) -> Parameter:
        """Get the area parameter."""
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """Set the area parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('area must be a number')
        self._area.value = value

    @property
    def center(self) -> Parameter:
        """Get the center parameter."""
        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """Set the center parameter value."""
        if value is None:
            value = 0.0
            self._center.fixed = True
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')
        self._center.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Delta function at the given x values.

        The Delta function evaluates to zero everywhere, except at the
        center. Its numerical integral is equal to the area. It acts as
        an identity in convolutions.
        """

        # x assumed sorted, 1D numpy array
        x = self._prepare_x_for_evaluate(x)
        model = np.zeros_like(x, dtype=float)
        center = self.center.value
        area = self.area.value

        if x.min() - EPSILON <= center <= x.max() + EPSILON:
            # nearest index
            i = np.argmin(np.abs(x - center))

            # left half-width
            if i == 0:
                left = x[1] - x[0] if x.size > 1 else 0.5
            else:
                left = x[i] - x[i - 1]

            # right half-width
            if i == x.size - 1:
                right = x[-1] - x[-2] if x.size > 1 else 0.5
            else:
                right = x[i + 1] - x[i]

            # effective bin width: half left + half right
            bin_width = 0.5 * (left + right)

            model[i] = area / bin_width

        return model

    def __repr__(self):
        return f'DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n \
        area = {self.area},\n center = {self.center}'

area property writable

Get the area parameter.

center property writable

Get the center parameter.

evaluate(x)

Evaluate the Delta function at the given x values.

The Delta function evaluates to zero everywhere, except at the center. Its numerical integral is equal to the area. It acts as an identity in convolutions.

Source code in src/easydynamics/sample_model/components/delta_function.py
 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
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Delta function at the given x values.

    The Delta function evaluates to zero everywhere, except at the
    center. Its numerical integral is equal to the area. It acts as
    an identity in convolutions.
    """

    # x assumed sorted, 1D numpy array
    x = self._prepare_x_for_evaluate(x)
    model = np.zeros_like(x, dtype=float)
    center = self.center.value
    area = self.area.value

    if x.min() - EPSILON <= center <= x.max() + EPSILON:
        # nearest index
        i = np.argmin(np.abs(x - center))

        # left half-width
        if i == 0:
            left = x[1] - x[0] if x.size > 1 else 0.5
        else:
            left = x[i] - x[i - 1]

        # right half-width
        if i == x.size - 1:
            right = x[-1] - x[-2] if x.size > 1 else 0.5
        else:
            right = x[i + 1] - x[i]

        # effective bin width: half left + half right
        bin_width = 0.5 * (left + right)

        model[i] = area / bin_width

    return model

Gaussian

Bases: CreateParametersMixin, ModelComponent

Gaussian function: area/(widthsqrt(2pi)) * exp(-0.5((x - center)/width)^2) If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Parameters:

Name Type Description Default
area (Int, float or Parameter)

Area of the Gaussian.

1.0
center (Int, float, None or Parameter)

Center of the Gaussian.

None
width (Int, float or Parameter)

Standard deviation.

1.0
unit str or Unit

Unit of the parameters.

'meV'
display_name str

Name of the component.

'Gaussian'
unique_name str or None

Unique name of the component.

None
Source code in src/easydynamics/sample_model/components/gaussian.py
 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
class Gaussian(CreateParametersMixin, ModelComponent):
    """
    Gaussian function:
    area/(width*sqrt(2pi)) * exp(-0.5*((x - center)/width)^2)
    If the center is not provided, it will be centered at 0 and fixed,
    which is typically what you want in QENS.

    Args:
        area (Int, float or Parameter): Area of the Gaussian.
        center (Int, float, None or Parameter): Center of the Gaussian.
        If None, defaults to 0 and is fixed
        width (Int, float or Parameter): Standard deviation.
        unit (str or sc.Unit): Unit of the parameters.
        Defaults to "meV".
        display_name (str): Name of the component.
        unique_name (str or None): Unique name of the component.
        If None, a unique_name is automatically generated.
    """

    def __init__(
        self,
        area: Numeric | Parameter = 1.0,
        center: Numeric | Parameter | None = None,
        width: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Gaussian',
        unique_name: str | None = None,
    ):
        # Validate inputs and create Parameters if not given
        super().__init__(
            display_name=display_name,
            unit=unit,
            unique_name=unique_name,
        )

        # These methods live in ValidationMixin
        area = self._create_area_parameter(area=area, name=display_name, unit=self._unit)
        center = self._create_center_parameter(
            center=center, name=display_name, fix_if_none=True, unit=self._unit
        )
        width = self._create_width_parameter(width=width, name=display_name, unit=self._unit)

        self._area = area
        self._center = center
        self._width = width

    @property
    def area(self) -> Parameter:
        """Get the area parameter."""
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """Set the area parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('area must be a number')
        self._area.value = value

    @property
    def center(self) -> Parameter:
        """Get the center parameter."""
        return self._center

    @center.setter
    def center(self, value: Numeric) -> None:
        """Set the center parameter value."""
        if value is None:
            value = 0.0
            self._center.fixed = True
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')
        self._center.value = value

    @property
    def width(self) -> Parameter:
        """Get the width parameter."""
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """Set the width parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')
        self._width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Gaussian at the given x values.

        If x is a scipp Variable, the unit of the Gaussian will be
        converted to match x.
        The Gaussian evaluates to
        area/(width*sqrt(2pi)) * exp(-0.5*((x - center)/width)^2)
        """

        x = self._prepare_x_for_evaluate(x)

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

        return self.area.value * normalization * np.exp(exponent)

    def __repr__(self):
        return f'Gaussian(unique_name = {self.unique_name}, unit = {self._unit},\n \
            area = {self.area},\n center = {self.center},\n width = {self.width})'

area property writable

Get the area parameter.

center property writable

Get the center parameter.

evaluate(x)

Evaluate the Gaussian at the given x values.

If x is a scipp Variable, the unit of the Gaussian will be converted to match x. The Gaussian evaluates to area/(widthsqrt(2pi)) * exp(-0.5((x - center)/width)^2)

Source code in src/easydynamics/sample_model/components/gaussian.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Gaussian at the given x values.

    If x is a scipp Variable, the unit of the Gaussian will be
    converted to match x.
    The Gaussian evaluates to
    area/(width*sqrt(2pi)) * exp(-0.5*((x - center)/width)^2)
    """

    x = self._prepare_x_for_evaluate(x)

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

    return self.area.value * normalization * np.exp(exponent)

width property writable

Get the width parameter.

Lorentzian

Bases: CreateParametersMixin, ModelComponent

Lorentzian function: area*width / (pi * ( (x - center)^2 + width^2 ) ) If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Parameters:

Name Type Description Default
area (Int, float or Parameter)

Area of the Lorentzian.

1.0
center (Int, float, None or Parameter)

Peak center.

None
width (Int, float or Parameter)
1.0
unit str or Unit

Unit of the parameters. Defaults to "meV"

'meV'
display_name str

Display name of the component.

'Lorentzian'
unique_name str or None

Unique name of the component.

None
Source code in src/easydynamics/sample_model/components/lorentzian.py
 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
class Lorentzian(CreateParametersMixin, ModelComponent):
    """
    Lorentzian function:
    area*width / (pi * ( (x - center)^2 + width^2 ) )
    If the center is not provided, it will be centered at 0 and fixed,
    which is typically what you want in QENS.

    Args:
        area (Int, float or Parameter): Area of the Lorentzian.
        center (Int, float, None or Parameter): Peak center.
        If None, defaults to 0 and is fixed.
        width (Int, float or Parameter):
        Half Width at Half Maximum (HWHM)
        unit (str or sc.Unit): Unit of the parameters. Defaults to "meV"
        display_name (str): Display name of the component.
        unique_name (str or None): Unique name of the component.
        If None, a unique_name is automatically generated.
    """

    def __init__(
        self,
        area: Numeric | Parameter = 1.0,
        center: Numeric | Parameter | None = None,
        width: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Lorentzian',
        unique_name: str | None = None,
    ):
        super().__init__(
            display_name=display_name,
            unit=unit,
            unique_name=unique_name,
        )

        # These methods live in ValidationMixin
        area = self._create_area_parameter(area=area, name=display_name, unit=self._unit)
        center = self._create_center_parameter(
            center=center, name=display_name, fix_if_none=True, unit=self._unit
        )
        width = self._create_width_parameter(width=width, name=display_name, unit=self._unit)

        self._area = area
        self._center = center
        self._width = width

    @property
    def area(self) -> Parameter:
        """Get the area parameter."""
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """Set the area parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('area must be a number')
        self._area.value = value

    @property
    def center(self) -> Parameter:
        """Get the center parameter."""
        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """Set the center parameter value."""
        if value is None:
            value = 0.0
            self._center.fixed = True
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')
        self._center.value = value

    @property
    def width(self) -> Parameter:
        """Get the width parameter."""
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """Set the width parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')
        self._width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Lorentzian at the given x values.

        If x is a scipp Variable, the unit of the Lorentzian will be
        converted to match x.
        The Lorentzian evaluates to
        area*width / (pi * ( (x - center)^2 + width^2 ) )
        """

        x = self._prepare_x_for_evaluate(x)

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

        return self.area.value * normalization / denominator

    def __repr__(self):
        return f'Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n \
            area = {self.area},\n center = {self.center},\n width = {self.width})'

area property writable

Get the area parameter.

center property writable

Get the center parameter.

evaluate(x)

Evaluate the Lorentzian at the given x values.

If x is a scipp Variable, the unit of the Lorentzian will be converted to match x. The Lorentzian evaluates to area*width / (pi * ( (x - center)^2 + width^2 ) )

Source code in src/easydynamics/sample_model/components/lorentzian.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Lorentzian at the given x values.

    If x is a scipp Variable, the unit of the Lorentzian will be
    converted to match x.
    The Lorentzian evaluates to
    area*width / (pi * ( (x - center)^2 + width^2 ) )
    """

    x = self._prepare_x_for_evaluate(x)

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

    return self.area.value * normalization / denominator

width property writable

Get the width parameter.

Polynomial

Bases: ModelComponent

Polynomial function component. c0 + c1x + c2x^2 + ... + cN*x^N.

Parameters:

Name Type Description Default
coefficients list or tuple

Coefficients c0, c1, ..., cN

(0.0,)
unit str or Unit

Unit of the Polynomial component.

'meV'
display_name str

Display name of the Polynomial component.

'Polynomial'
unique_name str or None

Unique name of the component. If None, a unique_name is automatically generated.

None
Source code in src/easydynamics/sample_model/components/polynomial.py
 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
class Polynomial(ModelComponent):
    """Polynomial function component. c0 + c1*x + c2*x^2 + ... + cN*x^N.

    Args:
        coefficients (list or tuple): Coefficients c0, c1, ..., cN
        representing f(x) = c0 + c1*x + c2*x^2 + ... + cN*x^N
        unit (str or sc.Unit): Unit of the Polynomial component.
        display_name (str): Display name of the Polynomial component.
        unique_name (str or None): Unique name of the component.
            If None, a unique_name is automatically generated.
    """

    def __init__(
        self,
        coefficients: Sequence[Numeric | Parameter] = (0.0,),
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Polynomial',
        unique_name: str | None = None,
    ):
        super().__init__(display_name=display_name, unit=unit, unique_name=unique_name)

        if not isinstance(coefficients, (list, tuple, np.ndarray)):
            raise TypeError(
                'coefficients must be a sequence (list/tuple/ndarray) \
                    of numbers or Parameter objects.'
            )

        if len(coefficients) == 0:
            raise ValueError('At least one coefficient must be provided.')

        # Internal storage of Parameter objects
        self._coefficients: list[Parameter] = []

        # Coefficients are treated as dimensionless Parameters
        for i, coef in enumerate(coefficients):
            if isinstance(coef, Parameter):
                param = coef
            elif isinstance(coef, Numeric):
                param = Parameter(name=f'{display_name}_c{i}', value=float(coef))
            else:
                raise TypeError('Each coefficient must be either a numeric value or a Parameter.')
            self._coefficients.append(param)

        # Helper scipp scalar to track unit conversions
        # (value initialized to 1 with provided unit)
        self._unit_conversion_helper = sc.scalar(value=1.0, unit=unit)

    @property
    def coefficients(self) -> list[Parameter]:
        """Get the coefficients of the polynomial as a list of
        Parameters.
        """
        return list(self._coefficients)

    @coefficients.setter
    def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None:
        """Replace the coefficients.

        Length must match current number of coefficients.
        """
        if not isinstance(coeffs, (list, tuple, np.ndarray)):
            raise TypeError(
                'coefficients must be a sequence (list/tuple/ndarray) of numbers or Parameter .'
            )
        if len(coeffs) != len(self._coefficients):
            raise ValueError(
                'Number of coefficients must match the existing number of coefficients.'
            )
        for i, coef in enumerate(coeffs):
            if isinstance(coef, Parameter):
                # replace parameter
                self._coefficients[i] = coef
            elif isinstance(coef, Numeric):
                self._coefficients[i].value = float(coef)
            else:
                raise TypeError('Each coefficient must be either a numeric value or a Parameter.')

    def coefficient_values(self) -> list[float]:
        """Get the coefficients of the polynomial as a list."""
        coefficient_list = [param.value for param in self._coefficients]
        return coefficient_list

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Polynomial at the given x values.

        The Polynomial evaluates to c0 + c1*x + c2*x^2 + ... + cN*x^N
        """

        x = self._prepare_x_for_evaluate(x)

        result = np.zeros_like(x, dtype=float)
        for i, param in enumerate(self._coefficients):
            result += param.value * np.power(x, i)

        if any(result < 0):
            warnings.warn(
                f'The Polynomial with unique_name {self.unique_name} has negative values, '
                'which may not be physically meaningful.',
                UserWarning,
            )
        return result

    @property
    def degree(self) -> int:
        """Return the degree of the polynomial."""
        return len(self._coefficients) - 1

    @degree.setter
    def degree(self, value: int) -> None:
        raise AttributeError(
            'The degree of the polynomial is determined by the number of coefficients \
                and cannot be set directly.'
        )

    def get_all_variables(self) -> list[DescriptorBase]:
        """Get all parameters from the model component.

        Returns:
        List[Parameter]: List of parameters in the component.
        """
        return list(self._coefficients)

    def convert_unit(self, unit: str | sc.Unit):
        """Convert the unit of the polynomial.

        Args:
            unit (str or sc.Unit): The target unit to convert to.
        """

        if not isinstance(unit, (str, sc.Unit)):
            raise UnitError('unit must be a string or a scipp unit.')

        # Find out how much the unit changes
        # by converting a helper variable
        conversion_value_before = self._unit_conversion_helper.value
        self._unit_conversion_helper = sc.to_unit(self._unit_conversion_helper, unit=unit)
        conversion_value_after = self._unit_conversion_helper.value
        for i, param in enumerate(self._coefficients):
            param.value *= (
                conversion_value_before / conversion_value_after
            ) ** i  # set the values directly to the appropriate power

        self._unit = unit

    def __repr__(self) -> str:
        coeffs_str = ', '.join(f'{param.name}={param.value}' for param in self._coefficients)
        return f'Polynomial(unique_name = {self.unique_name}, \
            unit = {self._unit},\n coefficients = [{coeffs_str}])'

coefficient_values()

Get the coefficients of the polynomial as a list.

Source code in src/easydynamics/sample_model/components/polynomial.py
 97
 98
 99
100
def coefficient_values(self) -> list[float]:
    """Get the coefficients of the polynomial as a list."""
    coefficient_list = [param.value for param in self._coefficients]
    return coefficient_list

coefficients property writable

Get the coefficients of the polynomial as a list of Parameters.

convert_unit(unit)

Convert the unit of the polynomial.

Parameters:

Name Type Description Default
unit str or Unit

The target unit to convert to.

required
Source code in src/easydynamics/sample_model/components/polynomial.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def convert_unit(self, unit: str | sc.Unit):
    """Convert the unit of the polynomial.

    Args:
        unit (str or sc.Unit): The target unit to convert to.
    """

    if not isinstance(unit, (str, sc.Unit)):
        raise UnitError('unit must be a string or a scipp unit.')

    # Find out how much the unit changes
    # by converting a helper variable
    conversion_value_before = self._unit_conversion_helper.value
    self._unit_conversion_helper = sc.to_unit(self._unit_conversion_helper, unit=unit)
    conversion_value_after = self._unit_conversion_helper.value
    for i, param in enumerate(self._coefficients):
        param.value *= (
            conversion_value_before / conversion_value_after
        ) ** i  # set the values directly to the appropriate power

    self._unit = unit

degree property writable

Return the degree of the polynomial.

evaluate(x)

Evaluate the Polynomial at the given x values.

The Polynomial evaluates to c0 + c1x + c2x^2 + ... + cN*x^N

Source code in src/easydynamics/sample_model/components/polynomial.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Polynomial at the given x values.

    The Polynomial evaluates to c0 + c1*x + c2*x^2 + ... + cN*x^N
    """

    x = self._prepare_x_for_evaluate(x)

    result = np.zeros_like(x, dtype=float)
    for i, param in enumerate(self._coefficients):
        result += param.value * np.power(x, i)

    if any(result < 0):
        warnings.warn(
            f'The Polynomial with unique_name {self.unique_name} has negative values, '
            'which may not be physically meaningful.',
            UserWarning,
        )
    return result

get_all_variables()

Get all parameters from the model component.

Returns: List[Parameter]: List of parameters in the component.

Source code in src/easydynamics/sample_model/components/polynomial.py
134
135
136
137
138
139
140
def get_all_variables(self) -> list[DescriptorBase]:
    """Get all parameters from the model component.

    Returns:
    List[Parameter]: List of parameters in the component.
    """
    return list(self._coefficients)

Voigt

Bases: CreateParametersMixin, ModelComponent

Voigt profile, a convolution of Gaussian and Lorentzian. If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Parameters:

Name Type Description Default
area Int or float

Total area under the curve.

1.0
center Int or float or None

Center of the Voigt profile.

None
gaussian_width Int or float

Standard deviation of the

1.0
lorentzian_width Int or float

Half width at half max (HWHM)

1.0
unit str or Unit

Unit of the parameters. Defaults to "meV"

'meV'
display_name str

Display name of the component.

'Voigt'
unique_name str or None

Unique name of the component.

None
Source code in src/easydynamics/sample_model/components/voigt.py
 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
class Voigt(CreateParametersMixin, ModelComponent):
    """Voigt profile, a convolution of Gaussian and Lorentzian. If the
    center is not provided, it will be centered at 0 and fixed, which is
    typically what you want in QENS.

    Args:
        area (Int or float): Total area under the curve.
        center (Int or float or None): Center of the Voigt profile.
        gaussian_width (Int or float): Standard deviation of the
        Gaussian part.
        lorentzian_width (Int or float): Half width at half max (HWHM)
        of the Lorentzian part.
        unit (str or sc.Unit): Unit of the parameters. Defaults to "meV"
        display_name (str): Display name of the component.
        unique_name (str or None): Unique name of the component.
        If None, a unique_name is automatically generated.
    """

    def __init__(
        self,
        area: Numeric | Parameter = 1.0,
        center: Numeric | Parameter | None = None,
        gaussian_width: Numeric | Parameter = 1.0,
        lorentzian_width: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Voigt',
        unique_name: str | None = None,
    ):
        super().__init__(
            display_name=display_name,
            unit=unit,
            unique_name=unique_name,
        )

        # These methods live in ValidationMixin
        area = self._create_area_parameter(area=area, name=display_name, unit=self._unit)
        center = self._create_center_parameter(
            center=center, name=display_name, fix_if_none=True, unit=self._unit
        )
        gaussian_width = self._create_width_parameter(
            width=gaussian_width,
            name=display_name,
            param_name='gaussian_width',
            unit=self._unit,
        )
        lorentzian_width = self._create_width_parameter(
            width=lorentzian_width,
            name=display_name,
            param_name='lorentzian_width',
            unit=self._unit,
        )

        self._area = area
        self._center = center
        self._gaussian_width = gaussian_width
        self._lorentzian_width = lorentzian_width

    @property
    def area(self) -> Parameter:
        """Get the area parameter."""
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """Set the area parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('area must be a number')
        self._area.value = value

    @property
    def center(self) -> Parameter:
        """Get the center parameter."""
        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """Set the center parameter value."""
        if value is None:
            value = 0.0
            self._center.fixed = True
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')
        self._center.value = value

    @property
    def gaussian_width(self) -> Parameter:
        """Get the width parameter."""
        return self._gaussian_width

    @gaussian_width.setter
    def gaussian_width(self, value: Numeric) -> None:
        """Set the width parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('gaussian_width must be a number')
        self._gaussian_width.value = value

    @property
    def lorentzian_width(self) -> Parameter:
        """Get the width parameter."""
        return self._lorentzian_width

    @lorentzian_width.setter
    def lorentzian_width(self, value: Numeric) -> None:
        """Set the width parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('lorentzian_width must be a number')
        self._lorentzian_width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Voigt at the given x values.

        If x is a scipp Variable, the unit of the Voigt will be
        converted to match x. The Voigt evaluates to the convolution of
        a Gaussian with sigma gaussian_width and a Lorentzian with half
        width at half max lorentzian_width, centered at center, with
        area equal to area.
        """

        x = self._prepare_x_for_evaluate(x)

        return self.area.value * voigt_profile(
            x - self.center.value,
            self.gaussian_width.value,
            self.lorentzian_width.value,
        )

    def __repr__(self):
        return f'Voigt(unique_name = {self.unique_name}, unit = {self._unit},\n \
        area = {self.area},\n \
        center = {self.center},\n \
        gaussian_width = {self.gaussian_width},\n \
        lorentzian_width = {self.lorentzian_width})'

area property writable

Get the area parameter.

center property writable

Get the center parameter.

evaluate(x)

Evaluate the Voigt at the given x values.

If x is a scipp Variable, the unit of the Voigt will be converted to match x. The Voigt evaluates to the convolution of a Gaussian with sigma gaussian_width and a Lorentzian with half width at half max lorentzian_width, centered at center, with area equal to area.

Source code in src/easydynamics/sample_model/components/voigt.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Voigt at the given x values.

    If x is a scipp Variable, the unit of the Voigt will be
    converted to match x. The Voigt evaluates to the convolution of
    a Gaussian with sigma gaussian_width and a Lorentzian with half
    width at half max lorentzian_width, centered at center, with
    area equal to area.
    """

    x = self._prepare_x_for_evaluate(x)

    return self.area.value * voigt_profile(
        x - self.center.value,
        self.gaussian_width.value,
        self.lorentzian_width.value,
    )

gaussian_width property writable

Get the width parameter.

lorentzian_width property writable

Get the width parameter.

background_model

BackgroundModel

Bases: ModelBase

BackgroundModel represents a model of the background in an experiment at various Q.

Parameters

display_name : str Display name of the model. unique_name : str | None Unique name of the model. If None, a unique name will be generated. unit : str | sc.Unit | None Unit of the model. If None, unitless. components : ModelComponent | ComponentCollection | None Template components of the model. If None, no components are added. These components are copied into ComponentCollections for each Q value. Q : Q_type | None Q values for the model. If None, Q is not set.

Source code in src/easydynamics/sample_model/background_model.py
13
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
class BackgroundModel(ModelBase):
    """BackgroundModel represents a model of the background in an
    experiment at various Q.

    Parameters
    ----------
    display_name : str
        Display name of the model.
    unique_name : str | None
        Unique name of the model. If None, a unique name will be
        generated.
    unit : str | sc.Unit | None
        Unit of the model. If None, unitless.
    components : ModelComponent | ComponentCollection | None
        Template components of the model. If None, no components are
        added.
        These components are copied into ComponentCollections for each
        Q value.
    Q : Q_type | None
        Q values for the model. If None, Q is not set.
    """

    def __init__(
        self,
        display_name: str = 'MyBackgroundModel',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
        components: ComponentCollection | ModelComponent | None = None,
        Q: Q_type | None = None,
    ):
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
            unit=unit,
            components=components,
            Q=Q,
        )

component_collection

ComponentCollection

Bases: ModelBase

A model of the scattering from a sample, combining multiple model components.

Attributes

display_name : str Display name of the ComponentCollection. unit : str or sc.Unit Unit of the ComponentCollection.

Source code in src/easydynamics/sample_model/component_collection.py
 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
class ComponentCollection(ModelBase):
    """A model of the scattering from a sample, combining multiple model
    components.

    Attributes
    ----------
    display_name : str
        Display name of the ComponentCollection.
    unit : str or sc.Unit
        Unit of the ComponentCollection.
    """

    def __init__(
        self,
        unit: str | sc.Unit = 'meV',
        display_name: str = 'MyComponentCollection',
        unique_name: str | None = None,
        components: List[ModelComponent] | None = None,
    ):
        """Initialize a new ComponentCollection.

        Parameters
        ----------
        unit : str or sc.Unit, optional
            Unit of the sample model. Defaults to "meV".
        display_name : str
            Display name of the sample model.
        unique_name : str or None, optional
            Unique name of the sample model. Defaults to None.
        components : List[ModelComponent], optional
            Initial model components to add to the ComponentCollection.
        """

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

        if unit is not None and not isinstance(unit, (str, sc.Unit)):
            raise TypeError(
                f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}'
            )
        self._unit = unit
        self._components = []

        # Add initial components if provided. Used for serialization.
        if components is not None:
            if not isinstance(components, list):
                raise TypeError('components must be a list of ModelComponent instances.')
            for comp in components:
                self.append_component(comp)

    def append_component(self, component: ModelComponent | 'ComponentCollection') -> None:
        match component:
            case ModelComponent():
                components = (component,)
            case ComponentCollection(components=components):
                pass
            case _:
                raise TypeError('Component must be a ModelComponent or ComponentCollection.')

        for comp in components:
            if comp in self._components:
                raise ValueError(
                    f"Component '{comp.unique_name}' is already in the collection. "
                    f'Existing components: {self.list_component_names()}'
                )

            self._components.append(comp)

    def remove_component(self, unique_name: str) -> None:
        if not isinstance(unique_name, str):
            raise TypeError('Component name must be a string.')

        for comp in self._components:
            if comp.unique_name == unique_name:
                self._components.remove(comp)
                return

        raise KeyError(
            f"No component named '{unique_name}' exists. "
            f'Did you accidentally use the display_name? '
            f'Here is a list of the components in the collection: {self.list_component_names()}'
        )

    @property
    def components(self) -> list[ModelComponent]:
        return list(self._components)

    @components.setter
    def components(self, components: List[ModelComponent]) -> None:
        if not isinstance(components, list):
            raise TypeError('components must be a list of ModelComponent instances.')
        for comp in components:
            if not isinstance(comp, ModelComponent):
                raise TypeError(
                    'All items in components must be instances of ModelComponent. '
                    f'Got {type(comp).__name__} instead.'
                )

        self._components = components

    def list_component_names(self) -> List[str]:
        """List the names of all components in the model.

        Returns
        -------
        List[str]
            Component names.
        """

        return [component.unique_name for component in self._components]

    def clear_components(self) -> None:
        """Remove all components."""
        self._components.clear()

    def normalize_area(self) -> None:
        # Useful for convolutions.
        """Normalize the areas of all components so they sum to 1."""
        if not self.components:
            raise ValueError('No components in the model to normalize.')

        area_params = []
        total_area = Parameter(name='total_area', value=0.0, unit=self._unit)

        for component in self.components:
            if hasattr(component, 'area'):
                area_params.append(component.area)
                total_area += component.area
            else:
                warnings.warn(
                    f"Component '{component.unique_name}' does not have an 'area' attribute "
                    f'and will be skipped in normalization.',
                    UserWarning,
                )

        if total_area.value == 0:
            raise ValueError('Total area is zero; cannot normalize.')

        if not np.isfinite(total_area.value):
            raise ValueError('Total area is not finite; cannot normalize.')

        for param in area_params:
            param.value /= total_area.value

    def get_all_variables(self) -> list[DescriptorBase]:
        """Get all parameters from the model component.

        Returns:
        List[Parameter]: List of parameters in the component.
        """

        return [var for component in self.components for var in component.get_all_variables()]

    @property
    def unit(self) -> str | sc.Unit:
        """Get the unit of the ComponentCollection.

        Returns
        -------
        str or sc.Unit or None
        """
        return self._unit

    @unit.setter
    def unit(self, unit_str: str) -> None:
        raise AttributeError(
            (
                f'Unit is read-only. Use convert_unit to change the unit between allowed types '
                f'or create a new {self.__class__.__name__} with the desired unit.'
            )
        )  # noqa: E501

    def convert_unit(self, unit: str | sc.Unit) -> None:
        """Convert the unit of the ComponentCollection and all its
        components.
        """

        old_unit = self._unit

        try:
            for component in self.components:
                component.convert_unit(unit)
            self._unit = unit
        except Exception as e:
            # Attempt to rollback on failure
            try:
                for component in self.components:
                    component.convert_unit(old_unit)
            except Exception:  # noqa: S110
                pass  # Best effort rollback
            raise e

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the sum of all components.

        Parameters
        ----------
        x : Number, list, np.ndarray, sc.Variable, or sc.DataArray
            Energy axis.

        Returns
        -------
        np.ndarray
            Evaluated model values.
        """

        if not self.components:
            raise ValueError('No components in the model to evaluate.')
        return sum(component.evaluate(x) for component in self.components)

    def evaluate_component(
        self,
        x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray,
        unique_name: str,
    ) -> np.ndarray:
        """Evaluate a single component by name.

        Parameters
        ----------
        x : Number, list, np.ndarray, sc.Variable, or sc.DataArray
            Energy axis.
        unique_name : str
            Component unique name.

        Returns
        -------
        np.ndarray
            Evaluated values for the specified component.
        """
        if not self.components:
            raise ValueError('No components in the model to evaluate.')

        if not isinstance(unique_name, str):
            raise TypeError(
                (f'Component unique name must be a string, got {type(unique_name)} instead.')
            )

        matches = [comp for comp in self.components if comp.unique_name == unique_name]
        if not matches:
            raise KeyError(f"No component named '{unique_name}' exists.")

        component = matches[0]

        result = component.evaluate(x)

        return result

    def fix_all_parameters(self) -> None:
        """Fix all free parameters in the model."""
        for param in self.get_fittable_parameters():
            param.fixed = True

    def free_all_parameters(self) -> None:
        """Free all fixed parameters in the model."""
        for param in self.get_fittable_parameters():
            param.fixed = False

    def __contains__(self, item: str | ModelComponent) -> bool:
        """Check if a component with the given name or instance exists
        in the ComponentCollection.

        Args:
        ----------
        item : str or ModelComponent
            The component name or instance to check for.
        Returns
        -------
        bool
            True if the component exists, False otherwise.
        """

        if isinstance(item, str):
            # Check by component unique name
            return any(comp.unique_name == item for comp in self.components)
        elif isinstance(item, ModelComponent):
            # Check by component instance
            return any(comp is item for comp in self.components)
        else:
            return False

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

        Returns
        -------
        str
        """
        comp_names = ', '.join(c.unique_name for c in self.components) or 'No components'

        return f"<ComponentCollection unique_name='{self.unique_name}' | Components: {comp_names}>"

__contains__(item)

Check if a component with the given name or instance exists in the ComponentCollection.

Args:

item : str or ModelComponent The component name or instance to check for. Returns


bool True if the component exists, False otherwise.

Source code in src/easydynamics/sample_model/component_collection.py
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
def __contains__(self, item: str | ModelComponent) -> bool:
    """Check if a component with the given name or instance exists
    in the ComponentCollection.

    Args:
    ----------
    item : str or ModelComponent
        The component name or instance to check for.
    Returns
    -------
    bool
        True if the component exists, False otherwise.
    """

    if isinstance(item, str):
        # Check by component unique name
        return any(comp.unique_name == item for comp in self.components)
    elif isinstance(item, ModelComponent):
        # Check by component instance
        return any(comp is item for comp in self.components)
    else:
        return False

__init__(unit='meV', display_name='MyComponentCollection', unique_name=None, components=None)

Initialize a new ComponentCollection.

Parameters

unit : str or sc.Unit, optional Unit of the sample model. Defaults to "meV". display_name : str Display name of the sample model. unique_name : str or None, optional Unique name of the sample model. Defaults to None. components : List[ModelComponent], optional Initial model components to add to the ComponentCollection.

Source code in src/easydynamics/sample_model/component_collection.py
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
def __init__(
    self,
    unit: str | sc.Unit = 'meV',
    display_name: str = 'MyComponentCollection',
    unique_name: str | None = None,
    components: List[ModelComponent] | None = None,
):
    """Initialize a new ComponentCollection.

    Parameters
    ----------
    unit : str or sc.Unit, optional
        Unit of the sample model. Defaults to "meV".
    display_name : str
        Display name of the sample model.
    unique_name : str or None, optional
        Unique name of the sample model. Defaults to None.
    components : List[ModelComponent], optional
        Initial model components to add to the ComponentCollection.
    """

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

    if unit is not None and not isinstance(unit, (str, sc.Unit)):
        raise TypeError(
            f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}'
        )
    self._unit = unit
    self._components = []

    # Add initial components if provided. Used for serialization.
    if components is not None:
        if not isinstance(components, list):
            raise TypeError('components must be a list of ModelComponent instances.')
        for comp in components:
            self.append_component(comp)

__repr__()

Return a string representation of the ComponentCollection.

Returns

str

Source code in src/easydynamics/sample_model/component_collection.py
299
300
301
302
303
304
305
306
307
308
def __repr__(self) -> str:
    """Return a string representation of the ComponentCollection.

    Returns
    -------
    str
    """
    comp_names = ', '.join(c.unique_name for c in self.components) or 'No components'

    return f"<ComponentCollection unique_name='{self.unique_name}' | Components: {comp_names}>"

clear_components()

Remove all components.

Source code in src/easydynamics/sample_model/component_collection.py
130
131
132
def clear_components(self) -> None:
    """Remove all components."""
    self._components.clear()

convert_unit(unit)

Convert the unit of the ComponentCollection and all its components.

Source code in src/easydynamics/sample_model/component_collection.py
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
def convert_unit(self, unit: str | sc.Unit) -> None:
    """Convert the unit of the ComponentCollection and all its
    components.
    """

    old_unit = self._unit

    try:
        for component in self.components:
            component.convert_unit(unit)
        self._unit = unit
    except Exception as e:
        # Attempt to rollback on failure
        try:
            for component in self.components:
                component.convert_unit(old_unit)
        except Exception:  # noqa: S110
            pass  # Best effort rollback
        raise e

evaluate(x)

Evaluate the sum of all components.

Parameters

x : Number, list, np.ndarray, sc.Variable, or sc.DataArray Energy axis.

Returns

np.ndarray Evaluated model values.

Source code in src/easydynamics/sample_model/component_collection.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the sum of all components.

    Parameters
    ----------
    x : Number, list, np.ndarray, sc.Variable, or sc.DataArray
        Energy axis.

    Returns
    -------
    np.ndarray
        Evaluated model values.
    """

    if not self.components:
        raise ValueError('No components in the model to evaluate.')
    return sum(component.evaluate(x) for component in self.components)

evaluate_component(x, unique_name)

Evaluate a single component by name.

Parameters

x : Number, list, np.ndarray, sc.Variable, or sc.DataArray Energy axis. unique_name : str Component unique name.

Returns

np.ndarray Evaluated values for the specified component.

Source code in src/easydynamics/sample_model/component_collection.py
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
def evaluate_component(
    self,
    x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray,
    unique_name: str,
) -> np.ndarray:
    """Evaluate a single component by name.

    Parameters
    ----------
    x : Number, list, np.ndarray, sc.Variable, or sc.DataArray
        Energy axis.
    unique_name : str
        Component unique name.

    Returns
    -------
    np.ndarray
        Evaluated values for the specified component.
    """
    if not self.components:
        raise ValueError('No components in the model to evaluate.')

    if not isinstance(unique_name, str):
        raise TypeError(
            (f'Component unique name must be a string, got {type(unique_name)} instead.')
        )

    matches = [comp for comp in self.components if comp.unique_name == unique_name]
    if not matches:
        raise KeyError(f"No component named '{unique_name}' exists.")

    component = matches[0]

    result = component.evaluate(x)

    return result

fix_all_parameters()

Fix all free parameters in the model.

Source code in src/easydynamics/sample_model/component_collection.py
266
267
268
269
def fix_all_parameters(self) -> None:
    """Fix all free parameters in the model."""
    for param in self.get_fittable_parameters():
        param.fixed = True

free_all_parameters()

Free all fixed parameters in the model.

Source code in src/easydynamics/sample_model/component_collection.py
271
272
273
274
def free_all_parameters(self) -> None:
    """Free all fixed parameters in the model."""
    for param in self.get_fittable_parameters():
        param.fixed = False

get_all_variables()

Get all parameters from the model component.

Returns: List[Parameter]: List of parameters in the component.

Source code in src/easydynamics/sample_model/component_collection.py
163
164
165
166
167
168
169
170
def get_all_variables(self) -> list[DescriptorBase]:
    """Get all parameters from the model component.

    Returns:
    List[Parameter]: List of parameters in the component.
    """

    return [var for component in self.components for var in component.get_all_variables()]

list_component_names()

List the names of all components in the model.

Returns

List[str] Component names.

Source code in src/easydynamics/sample_model/component_collection.py
119
120
121
122
123
124
125
126
127
128
def list_component_names(self) -> List[str]:
    """List the names of all components in the model.

    Returns
    -------
    List[str]
        Component names.
    """

    return [component.unique_name for component in self._components]

normalize_area()

Normalize the areas of all components so they sum to 1.

Source code in src/easydynamics/sample_model/component_collection.py
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
def normalize_area(self) -> None:
    # Useful for convolutions.
    """Normalize the areas of all components so they sum to 1."""
    if not self.components:
        raise ValueError('No components in the model to normalize.')

    area_params = []
    total_area = Parameter(name='total_area', value=0.0, unit=self._unit)

    for component in self.components:
        if hasattr(component, 'area'):
            area_params.append(component.area)
            total_area += component.area
        else:
            warnings.warn(
                f"Component '{component.unique_name}' does not have an 'area' attribute "
                f'and will be skipped in normalization.',
                UserWarning,
            )

    if total_area.value == 0:
        raise ValueError('Total area is zero; cannot normalize.')

    if not np.isfinite(total_area.value):
        raise ValueError('Total area is not finite; cannot normalize.')

    for param in area_params:
        param.value /= total_area.value

unit property writable

Get the unit of the ComponentCollection.

Returns

str or sc.Unit or None

components

DampedHarmonicOscillator

Bases: CreateParametersMixin, ModelComponent

Damped Harmonic Oscillator (DHO). 2areacenter^2width/pi / ( (x^2 - center^2)^2 + (2width*x)^2 )

Parameters:

Name Type Description Default
display_name str

Display name of the component.

'DampedHarmonicOscillator'
center Int or float

Resonance frequency, approximately the

1.0
width Int or float

Damping constant, approximately the

1.0
area Int or float

Area under the curve.

1.0
unit str or Unit

Unit of the parameters.

'meV'
Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
 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
class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent):
    """
    Damped Harmonic Oscillator (DHO).
    2*area*center^2*width/pi / ( (x^2 - center^2)^2 + (2*width*x)^2 )

    Args:
        display_name (str): Display name of the component.
        center (Int or float): Resonance frequency, approximately the
        peak position.
        width (Int or float): Damping constant, approximately the
        half width at half max (HWHM) of the peaks.
        area (Int or float): Area under the curve.
        unit (str or sc.Unit): Unit of the parameters.
        Defaults to "meV".
    """

    def __init__(
        self,
        area: Numeric | Parameter = 1.0,
        center: Numeric | Parameter = 1.0,
        width: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'DampedHarmonicOscillator',
        unique_name: str | None = None,
    ):
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
            unit=unit,
        )

        # These methods live in ValidationMixin
        area = self._create_area_parameter(area=area, name=display_name, unit=self._unit)
        center = self._create_center_parameter(
            center=center,
            name=display_name,
            fix_if_none=False,
            unit=self._unit,
            enforce_minimum_center=True,
        )

        width = self._create_width_parameter(width=width, name=display_name, unit=self._unit)

        self._area = area
        self._center = center
        self._width = width

    @property
    def area(self) -> Parameter:
        """Get the area parameter."""
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """Set the area parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('area must be a number')
        self._area.value = value

    @property
    def center(self) -> Parameter:
        """Get the center parameter."""
        return self._center

    @center.setter
    def center(self, value: Numeric) -> None:
        """Set the center parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')

        if value <= 0:
            raise ValueError('center must be positive')
        self._center.value = value

    @property
    def width(self) -> Parameter:
        """Get the width parameter."""
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """Set the width parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')
        self._width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Damped Harmonic Oscillator at the given x
        values.

        If x is a scipp Variable, the unit of the DHO will be converted
        to match x. The DHO evaluates to
        2*area*center^2*width/pi / ((x^2 - center^2)^2 + (2*width*x)^2)
        """

        x = self._prepare_x_for_evaluate(x)

        normalization = 2 * self.center.value**2 * self.width.value / np.pi
        # No division by zero here, width>0 enforced in setter
        denominator = (x**2 - self.center.value**2) ** 2 + (2 * self.width.value * x) ** 2

        return self.area.value * normalization / (denominator)

    def __repr__(self):
        return (
            f'DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \
        area = {self.area},\n center = {self.center},\n width = {self.width})'
        )

area property writable

Get the area parameter.

center property writable

Get the center parameter.

evaluate(x)

Evaluate the Damped Harmonic Oscillator at the given x values.

If x is a scipp Variable, the unit of the DHO will be converted to match x. The DHO evaluates to 2areacenter^2width/pi / ((x^2 - center^2)^2 + (2width*x)^2)

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Damped Harmonic Oscillator at the given x
    values.

    If x is a scipp Variable, the unit of the DHO will be converted
    to match x. The DHO evaluates to
    2*area*center^2*width/pi / ((x^2 - center^2)^2 + (2*width*x)^2)
    """

    x = self._prepare_x_for_evaluate(x)

    normalization = 2 * self.center.value**2 * self.width.value / np.pi
    # No division by zero here, width>0 enforced in setter
    denominator = (x**2 - self.center.value**2) ** 2 + (2 * self.width.value * x) ** 2

    return self.area.value * normalization / (denominator)

width property writable

Get the width parameter.

DeltaFunction

Bases: CreateParametersMixin, ModelComponent

Delta function. Evaluates to zero everywhere, except in convolutions, where it acts as an identity. This is handled in the ResolutionHandler. If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Parameters:

Name Type Description Default
center Int or float or None

Center of the delta function.

None
area Int or float

Total area under the curve.

1.0
unit str or Unit

Unit of the parameters.

'meV'
display_name str

Name of the component.

'DeltaFunction'
unique_name str or None

Unique name of the component.

None
Source code in src/easydynamics/sample_model/components/delta_function.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
class DeltaFunction(CreateParametersMixin, ModelComponent):
    """Delta function. Evaluates to zero everywhere, except in
    convolutions, where it acts as an identity. This is handled in the
    ResolutionHandler. If the center is not provided, it will be
    centered at 0 and fixed, which is typically what you want in QENS.

    Args:
        center (Int or float or None): Center of the delta function.
        If None, defaults to 0 and is fixed.
        area (Int or float): Total area under the curve.
        unit (str or sc.Unit): Unit of the parameters.
        Defaults to "meV".
        display_name (str): Name of the component.
        unique_name (str or None): Unique name of the component.
        If None, a unique_name is automatically generated.
    """

    def __init__(
        self,
        center: None | Numeric | Parameter = None,
        area: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'DeltaFunction',
        unique_name: str | None = None,
    ):
        # Validate inputs and create Parameters if not given
        super().__init__(
            display_name=display_name,
            unit=unit,
            unique_name=unique_name,
        )

        # These methods live in ValidationMixin
        area = self._create_area_parameter(area=area, name=display_name, unit=self._unit)
        center = self._create_center_parameter(
            center=center, name=display_name, fix_if_none=True, unit=self._unit
        )

        self._area = area
        self._center = center

    @property
    def area(self) -> Parameter:
        """Get the area parameter."""
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """Set the area parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('area must be a number')
        self._area.value = value

    @property
    def center(self) -> Parameter:
        """Get the center parameter."""
        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """Set the center parameter value."""
        if value is None:
            value = 0.0
            self._center.fixed = True
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')
        self._center.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Delta function at the given x values.

        The Delta function evaluates to zero everywhere, except at the
        center. Its numerical integral is equal to the area. It acts as
        an identity in convolutions.
        """

        # x assumed sorted, 1D numpy array
        x = self._prepare_x_for_evaluate(x)
        model = np.zeros_like(x, dtype=float)
        center = self.center.value
        area = self.area.value

        if x.min() - EPSILON <= center <= x.max() + EPSILON:
            # nearest index
            i = np.argmin(np.abs(x - center))

            # left half-width
            if i == 0:
                left = x[1] - x[0] if x.size > 1 else 0.5
            else:
                left = x[i] - x[i - 1]

            # right half-width
            if i == x.size - 1:
                right = x[-1] - x[-2] if x.size > 1 else 0.5
            else:
                right = x[i + 1] - x[i]

            # effective bin width: half left + half right
            bin_width = 0.5 * (left + right)

            model[i] = area / bin_width

        return model

    def __repr__(self):
        return f'DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n \
        area = {self.area},\n center = {self.center}'

area property writable

Get the area parameter.

center property writable

Get the center parameter.

evaluate(x)

Evaluate the Delta function at the given x values.

The Delta function evaluates to zero everywhere, except at the center. Its numerical integral is equal to the area. It acts as an identity in convolutions.

Source code in src/easydynamics/sample_model/components/delta_function.py
 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
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Delta function at the given x values.

    The Delta function evaluates to zero everywhere, except at the
    center. Its numerical integral is equal to the area. It acts as
    an identity in convolutions.
    """

    # x assumed sorted, 1D numpy array
    x = self._prepare_x_for_evaluate(x)
    model = np.zeros_like(x, dtype=float)
    center = self.center.value
    area = self.area.value

    if x.min() - EPSILON <= center <= x.max() + EPSILON:
        # nearest index
        i = np.argmin(np.abs(x - center))

        # left half-width
        if i == 0:
            left = x[1] - x[0] if x.size > 1 else 0.5
        else:
            left = x[i] - x[i - 1]

        # right half-width
        if i == x.size - 1:
            right = x[-1] - x[-2] if x.size > 1 else 0.5
        else:
            right = x[i + 1] - x[i]

        # effective bin width: half left + half right
        bin_width = 0.5 * (left + right)

        model[i] = area / bin_width

    return model

Gaussian

Bases: CreateParametersMixin, ModelComponent

Gaussian function: area/(widthsqrt(2pi)) * exp(-0.5((x - center)/width)^2) If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Parameters:

Name Type Description Default
area (Int, float or Parameter)

Area of the Gaussian.

1.0
center (Int, float, None or Parameter)

Center of the Gaussian.

None
width (Int, float or Parameter)

Standard deviation.

1.0
unit str or Unit

Unit of the parameters.

'meV'
display_name str

Name of the component.

'Gaussian'
unique_name str or None

Unique name of the component.

None
Source code in src/easydynamics/sample_model/components/gaussian.py
 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
class Gaussian(CreateParametersMixin, ModelComponent):
    """
    Gaussian function:
    area/(width*sqrt(2pi)) * exp(-0.5*((x - center)/width)^2)
    If the center is not provided, it will be centered at 0 and fixed,
    which is typically what you want in QENS.

    Args:
        area (Int, float or Parameter): Area of the Gaussian.
        center (Int, float, None or Parameter): Center of the Gaussian.
        If None, defaults to 0 and is fixed
        width (Int, float or Parameter): Standard deviation.
        unit (str or sc.Unit): Unit of the parameters.
        Defaults to "meV".
        display_name (str): Name of the component.
        unique_name (str or None): Unique name of the component.
        If None, a unique_name is automatically generated.
    """

    def __init__(
        self,
        area: Numeric | Parameter = 1.0,
        center: Numeric | Parameter | None = None,
        width: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Gaussian',
        unique_name: str | None = None,
    ):
        # Validate inputs and create Parameters if not given
        super().__init__(
            display_name=display_name,
            unit=unit,
            unique_name=unique_name,
        )

        # These methods live in ValidationMixin
        area = self._create_area_parameter(area=area, name=display_name, unit=self._unit)
        center = self._create_center_parameter(
            center=center, name=display_name, fix_if_none=True, unit=self._unit
        )
        width = self._create_width_parameter(width=width, name=display_name, unit=self._unit)

        self._area = area
        self._center = center
        self._width = width

    @property
    def area(self) -> Parameter:
        """Get the area parameter."""
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """Set the area parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('area must be a number')
        self._area.value = value

    @property
    def center(self) -> Parameter:
        """Get the center parameter."""
        return self._center

    @center.setter
    def center(self, value: Numeric) -> None:
        """Set the center parameter value."""
        if value is None:
            value = 0.0
            self._center.fixed = True
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')
        self._center.value = value

    @property
    def width(self) -> Parameter:
        """Get the width parameter."""
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """Set the width parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')
        self._width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Gaussian at the given x values.

        If x is a scipp Variable, the unit of the Gaussian will be
        converted to match x.
        The Gaussian evaluates to
        area/(width*sqrt(2pi)) * exp(-0.5*((x - center)/width)^2)
        """

        x = self._prepare_x_for_evaluate(x)

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

        return self.area.value * normalization * np.exp(exponent)

    def __repr__(self):
        return f'Gaussian(unique_name = {self.unique_name}, unit = {self._unit},\n \
            area = {self.area},\n center = {self.center},\n width = {self.width})'

area property writable

Get the area parameter.

center property writable

Get the center parameter.

evaluate(x)

Evaluate the Gaussian at the given x values.

If x is a scipp Variable, the unit of the Gaussian will be converted to match x. The Gaussian evaluates to area/(widthsqrt(2pi)) * exp(-0.5((x - center)/width)^2)

Source code in src/easydynamics/sample_model/components/gaussian.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Gaussian at the given x values.

    If x is a scipp Variable, the unit of the Gaussian will be
    converted to match x.
    The Gaussian evaluates to
    area/(width*sqrt(2pi)) * exp(-0.5*((x - center)/width)^2)
    """

    x = self._prepare_x_for_evaluate(x)

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

    return self.area.value * normalization * np.exp(exponent)

width property writable

Get the width parameter.

Lorentzian

Bases: CreateParametersMixin, ModelComponent

Lorentzian function: area*width / (pi * ( (x - center)^2 + width^2 ) ) If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Parameters:

Name Type Description Default
area (Int, float or Parameter)

Area of the Lorentzian.

1.0
center (Int, float, None or Parameter)

Peak center.

None
width (Int, float or Parameter)
1.0
unit str or Unit

Unit of the parameters. Defaults to "meV"

'meV'
display_name str

Display name of the component.

'Lorentzian'
unique_name str or None

Unique name of the component.

None
Source code in src/easydynamics/sample_model/components/lorentzian.py
 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
class Lorentzian(CreateParametersMixin, ModelComponent):
    """
    Lorentzian function:
    area*width / (pi * ( (x - center)^2 + width^2 ) )
    If the center is not provided, it will be centered at 0 and fixed,
    which is typically what you want in QENS.

    Args:
        area (Int, float or Parameter): Area of the Lorentzian.
        center (Int, float, None or Parameter): Peak center.
        If None, defaults to 0 and is fixed.
        width (Int, float or Parameter):
        Half Width at Half Maximum (HWHM)
        unit (str or sc.Unit): Unit of the parameters. Defaults to "meV"
        display_name (str): Display name of the component.
        unique_name (str or None): Unique name of the component.
        If None, a unique_name is automatically generated.
    """

    def __init__(
        self,
        area: Numeric | Parameter = 1.0,
        center: Numeric | Parameter | None = None,
        width: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Lorentzian',
        unique_name: str | None = None,
    ):
        super().__init__(
            display_name=display_name,
            unit=unit,
            unique_name=unique_name,
        )

        # These methods live in ValidationMixin
        area = self._create_area_parameter(area=area, name=display_name, unit=self._unit)
        center = self._create_center_parameter(
            center=center, name=display_name, fix_if_none=True, unit=self._unit
        )
        width = self._create_width_parameter(width=width, name=display_name, unit=self._unit)

        self._area = area
        self._center = center
        self._width = width

    @property
    def area(self) -> Parameter:
        """Get the area parameter."""
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """Set the area parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('area must be a number')
        self._area.value = value

    @property
    def center(self) -> Parameter:
        """Get the center parameter."""
        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """Set the center parameter value."""
        if value is None:
            value = 0.0
            self._center.fixed = True
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')
        self._center.value = value

    @property
    def width(self) -> Parameter:
        """Get the width parameter."""
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """Set the width parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')
        self._width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Lorentzian at the given x values.

        If x is a scipp Variable, the unit of the Lorentzian will be
        converted to match x.
        The Lorentzian evaluates to
        area*width / (pi * ( (x - center)^2 + width^2 ) )
        """

        x = self._prepare_x_for_evaluate(x)

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

        return self.area.value * normalization / denominator

    def __repr__(self):
        return f'Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n \
            area = {self.area},\n center = {self.center},\n width = {self.width})'

area property writable

Get the area parameter.

center property writable

Get the center parameter.

evaluate(x)

Evaluate the Lorentzian at the given x values.

If x is a scipp Variable, the unit of the Lorentzian will be converted to match x. The Lorentzian evaluates to area*width / (pi * ( (x - center)^2 + width^2 ) )

Source code in src/easydynamics/sample_model/components/lorentzian.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Lorentzian at the given x values.

    If x is a scipp Variable, the unit of the Lorentzian will be
    converted to match x.
    The Lorentzian evaluates to
    area*width / (pi * ( (x - center)^2 + width^2 ) )
    """

    x = self._prepare_x_for_evaluate(x)

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

    return self.area.value * normalization / denominator

width property writable

Get the width parameter.

Polynomial

Bases: ModelComponent

Polynomial function component. c0 + c1x + c2x^2 + ... + cN*x^N.

Parameters:

Name Type Description Default
coefficients list or tuple

Coefficients c0, c1, ..., cN

(0.0,)
unit str or Unit

Unit of the Polynomial component.

'meV'
display_name str

Display name of the Polynomial component.

'Polynomial'
unique_name str or None

Unique name of the component. If None, a unique_name is automatically generated.

None
Source code in src/easydynamics/sample_model/components/polynomial.py
 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
class Polynomial(ModelComponent):
    """Polynomial function component. c0 + c1*x + c2*x^2 + ... + cN*x^N.

    Args:
        coefficients (list or tuple): Coefficients c0, c1, ..., cN
        representing f(x) = c0 + c1*x + c2*x^2 + ... + cN*x^N
        unit (str or sc.Unit): Unit of the Polynomial component.
        display_name (str): Display name of the Polynomial component.
        unique_name (str or None): Unique name of the component.
            If None, a unique_name is automatically generated.
    """

    def __init__(
        self,
        coefficients: Sequence[Numeric | Parameter] = (0.0,),
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Polynomial',
        unique_name: str | None = None,
    ):
        super().__init__(display_name=display_name, unit=unit, unique_name=unique_name)

        if not isinstance(coefficients, (list, tuple, np.ndarray)):
            raise TypeError(
                'coefficients must be a sequence (list/tuple/ndarray) \
                    of numbers or Parameter objects.'
            )

        if len(coefficients) == 0:
            raise ValueError('At least one coefficient must be provided.')

        # Internal storage of Parameter objects
        self._coefficients: list[Parameter] = []

        # Coefficients are treated as dimensionless Parameters
        for i, coef in enumerate(coefficients):
            if isinstance(coef, Parameter):
                param = coef
            elif isinstance(coef, Numeric):
                param = Parameter(name=f'{display_name}_c{i}', value=float(coef))
            else:
                raise TypeError('Each coefficient must be either a numeric value or a Parameter.')
            self._coefficients.append(param)

        # Helper scipp scalar to track unit conversions
        # (value initialized to 1 with provided unit)
        self._unit_conversion_helper = sc.scalar(value=1.0, unit=unit)

    @property
    def coefficients(self) -> list[Parameter]:
        """Get the coefficients of the polynomial as a list of
        Parameters.
        """
        return list(self._coefficients)

    @coefficients.setter
    def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None:
        """Replace the coefficients.

        Length must match current number of coefficients.
        """
        if not isinstance(coeffs, (list, tuple, np.ndarray)):
            raise TypeError(
                'coefficients must be a sequence (list/tuple/ndarray) of numbers or Parameter .'
            )
        if len(coeffs) != len(self._coefficients):
            raise ValueError(
                'Number of coefficients must match the existing number of coefficients.'
            )
        for i, coef in enumerate(coeffs):
            if isinstance(coef, Parameter):
                # replace parameter
                self._coefficients[i] = coef
            elif isinstance(coef, Numeric):
                self._coefficients[i].value = float(coef)
            else:
                raise TypeError('Each coefficient must be either a numeric value or a Parameter.')

    def coefficient_values(self) -> list[float]:
        """Get the coefficients of the polynomial as a list."""
        coefficient_list = [param.value for param in self._coefficients]
        return coefficient_list

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Polynomial at the given x values.

        The Polynomial evaluates to c0 + c1*x + c2*x^2 + ... + cN*x^N
        """

        x = self._prepare_x_for_evaluate(x)

        result = np.zeros_like(x, dtype=float)
        for i, param in enumerate(self._coefficients):
            result += param.value * np.power(x, i)

        if any(result < 0):
            warnings.warn(
                f'The Polynomial with unique_name {self.unique_name} has negative values, '
                'which may not be physically meaningful.',
                UserWarning,
            )
        return result

    @property
    def degree(self) -> int:
        """Return the degree of the polynomial."""
        return len(self._coefficients) - 1

    @degree.setter
    def degree(self, value: int) -> None:
        raise AttributeError(
            'The degree of the polynomial is determined by the number of coefficients \
                and cannot be set directly.'
        )

    def get_all_variables(self) -> list[DescriptorBase]:
        """Get all parameters from the model component.

        Returns:
        List[Parameter]: List of parameters in the component.
        """
        return list(self._coefficients)

    def convert_unit(self, unit: str | sc.Unit):
        """Convert the unit of the polynomial.

        Args:
            unit (str or sc.Unit): The target unit to convert to.
        """

        if not isinstance(unit, (str, sc.Unit)):
            raise UnitError('unit must be a string or a scipp unit.')

        # Find out how much the unit changes
        # by converting a helper variable
        conversion_value_before = self._unit_conversion_helper.value
        self._unit_conversion_helper = sc.to_unit(self._unit_conversion_helper, unit=unit)
        conversion_value_after = self._unit_conversion_helper.value
        for i, param in enumerate(self._coefficients):
            param.value *= (
                conversion_value_before / conversion_value_after
            ) ** i  # set the values directly to the appropriate power

        self._unit = unit

    def __repr__(self) -> str:
        coeffs_str = ', '.join(f'{param.name}={param.value}' for param in self._coefficients)
        return f'Polynomial(unique_name = {self.unique_name}, \
            unit = {self._unit},\n coefficients = [{coeffs_str}])'

coefficient_values()

Get the coefficients of the polynomial as a list.

Source code in src/easydynamics/sample_model/components/polynomial.py
 97
 98
 99
100
def coefficient_values(self) -> list[float]:
    """Get the coefficients of the polynomial as a list."""
    coefficient_list = [param.value for param in self._coefficients]
    return coefficient_list

coefficients property writable

Get the coefficients of the polynomial as a list of Parameters.

convert_unit(unit)

Convert the unit of the polynomial.

Parameters:

Name Type Description Default
unit str or Unit

The target unit to convert to.

required
Source code in src/easydynamics/sample_model/components/polynomial.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def convert_unit(self, unit: str | sc.Unit):
    """Convert the unit of the polynomial.

    Args:
        unit (str or sc.Unit): The target unit to convert to.
    """

    if not isinstance(unit, (str, sc.Unit)):
        raise UnitError('unit must be a string or a scipp unit.')

    # Find out how much the unit changes
    # by converting a helper variable
    conversion_value_before = self._unit_conversion_helper.value
    self._unit_conversion_helper = sc.to_unit(self._unit_conversion_helper, unit=unit)
    conversion_value_after = self._unit_conversion_helper.value
    for i, param in enumerate(self._coefficients):
        param.value *= (
            conversion_value_before / conversion_value_after
        ) ** i  # set the values directly to the appropriate power

    self._unit = unit

degree property writable

Return the degree of the polynomial.

evaluate(x)

Evaluate the Polynomial at the given x values.

The Polynomial evaluates to c0 + c1x + c2x^2 + ... + cN*x^N

Source code in src/easydynamics/sample_model/components/polynomial.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Polynomial at the given x values.

    The Polynomial evaluates to c0 + c1*x + c2*x^2 + ... + cN*x^N
    """

    x = self._prepare_x_for_evaluate(x)

    result = np.zeros_like(x, dtype=float)
    for i, param in enumerate(self._coefficients):
        result += param.value * np.power(x, i)

    if any(result < 0):
        warnings.warn(
            f'The Polynomial with unique_name {self.unique_name} has negative values, '
            'which may not be physically meaningful.',
            UserWarning,
        )
    return result

get_all_variables()

Get all parameters from the model component.

Returns: List[Parameter]: List of parameters in the component.

Source code in src/easydynamics/sample_model/components/polynomial.py
134
135
136
137
138
139
140
def get_all_variables(self) -> list[DescriptorBase]:
    """Get all parameters from the model component.

    Returns:
    List[Parameter]: List of parameters in the component.
    """
    return list(self._coefficients)

Voigt

Bases: CreateParametersMixin, ModelComponent

Voigt profile, a convolution of Gaussian and Lorentzian. If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Parameters:

Name Type Description Default
area Int or float

Total area under the curve.

1.0
center Int or float or None

Center of the Voigt profile.

None
gaussian_width Int or float

Standard deviation of the

1.0
lorentzian_width Int or float

Half width at half max (HWHM)

1.0
unit str or Unit

Unit of the parameters. Defaults to "meV"

'meV'
display_name str

Display name of the component.

'Voigt'
unique_name str or None

Unique name of the component.

None
Source code in src/easydynamics/sample_model/components/voigt.py
 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
class Voigt(CreateParametersMixin, ModelComponent):
    """Voigt profile, a convolution of Gaussian and Lorentzian. If the
    center is not provided, it will be centered at 0 and fixed, which is
    typically what you want in QENS.

    Args:
        area (Int or float): Total area under the curve.
        center (Int or float or None): Center of the Voigt profile.
        gaussian_width (Int or float): Standard deviation of the
        Gaussian part.
        lorentzian_width (Int or float): Half width at half max (HWHM)
        of the Lorentzian part.
        unit (str or sc.Unit): Unit of the parameters. Defaults to "meV"
        display_name (str): Display name of the component.
        unique_name (str or None): Unique name of the component.
        If None, a unique_name is automatically generated.
    """

    def __init__(
        self,
        area: Numeric | Parameter = 1.0,
        center: Numeric | Parameter | None = None,
        gaussian_width: Numeric | Parameter = 1.0,
        lorentzian_width: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Voigt',
        unique_name: str | None = None,
    ):
        super().__init__(
            display_name=display_name,
            unit=unit,
            unique_name=unique_name,
        )

        # These methods live in ValidationMixin
        area = self._create_area_parameter(area=area, name=display_name, unit=self._unit)
        center = self._create_center_parameter(
            center=center, name=display_name, fix_if_none=True, unit=self._unit
        )
        gaussian_width = self._create_width_parameter(
            width=gaussian_width,
            name=display_name,
            param_name='gaussian_width',
            unit=self._unit,
        )
        lorentzian_width = self._create_width_parameter(
            width=lorentzian_width,
            name=display_name,
            param_name='lorentzian_width',
            unit=self._unit,
        )

        self._area = area
        self._center = center
        self._gaussian_width = gaussian_width
        self._lorentzian_width = lorentzian_width

    @property
    def area(self) -> Parameter:
        """Get the area parameter."""
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """Set the area parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('area must be a number')
        self._area.value = value

    @property
    def center(self) -> Parameter:
        """Get the center parameter."""
        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """Set the center parameter value."""
        if value is None:
            value = 0.0
            self._center.fixed = True
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')
        self._center.value = value

    @property
    def gaussian_width(self) -> Parameter:
        """Get the width parameter."""
        return self._gaussian_width

    @gaussian_width.setter
    def gaussian_width(self, value: Numeric) -> None:
        """Set the width parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('gaussian_width must be a number')
        self._gaussian_width.value = value

    @property
    def lorentzian_width(self) -> Parameter:
        """Get the width parameter."""
        return self._lorentzian_width

    @lorentzian_width.setter
    def lorentzian_width(self, value: Numeric) -> None:
        """Set the width parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('lorentzian_width must be a number')
        self._lorentzian_width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Voigt at the given x values.

        If x is a scipp Variable, the unit of the Voigt will be
        converted to match x. The Voigt evaluates to the convolution of
        a Gaussian with sigma gaussian_width and a Lorentzian with half
        width at half max lorentzian_width, centered at center, with
        area equal to area.
        """

        x = self._prepare_x_for_evaluate(x)

        return self.area.value * voigt_profile(
            x - self.center.value,
            self.gaussian_width.value,
            self.lorentzian_width.value,
        )

    def __repr__(self):
        return f'Voigt(unique_name = {self.unique_name}, unit = {self._unit},\n \
        area = {self.area},\n \
        center = {self.center},\n \
        gaussian_width = {self.gaussian_width},\n \
        lorentzian_width = {self.lorentzian_width})'

area property writable

Get the area parameter.

center property writable

Get the center parameter.

evaluate(x)

Evaluate the Voigt at the given x values.

If x is a scipp Variable, the unit of the Voigt will be converted to match x. The Voigt evaluates to the convolution of a Gaussian with sigma gaussian_width and a Lorentzian with half width at half max lorentzian_width, centered at center, with area equal to area.

Source code in src/easydynamics/sample_model/components/voigt.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Voigt at the given x values.

    If x is a scipp Variable, the unit of the Voigt will be
    converted to match x. The Voigt evaluates to the convolution of
    a Gaussian with sigma gaussian_width and a Lorentzian with half
    width at half max lorentzian_width, centered at center, with
    area equal to area.
    """

    x = self._prepare_x_for_evaluate(x)

    return self.area.value * voigt_profile(
        x - self.center.value,
        self.gaussian_width.value,
        self.lorentzian_width.value,
    )

gaussian_width property writable

Get the width parameter.

lorentzian_width property writable

Get the width parameter.

damped_harmonic_oscillator

DampedHarmonicOscillator

Bases: CreateParametersMixin, ModelComponent

Damped Harmonic Oscillator (DHO). 2areacenter^2width/pi / ( (x^2 - center^2)^2 + (2width*x)^2 )

Parameters:

Name Type Description Default
display_name str

Display name of the component.

'DampedHarmonicOscillator'
center Int or float

Resonance frequency, approximately the

1.0
width Int or float

Damping constant, approximately the

1.0
area Int or float

Area under the curve.

1.0
unit str or Unit

Unit of the parameters.

'meV'
Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
 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
class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent):
    """
    Damped Harmonic Oscillator (DHO).
    2*area*center^2*width/pi / ( (x^2 - center^2)^2 + (2*width*x)^2 )

    Args:
        display_name (str): Display name of the component.
        center (Int or float): Resonance frequency, approximately the
        peak position.
        width (Int or float): Damping constant, approximately the
        half width at half max (HWHM) of the peaks.
        area (Int or float): Area under the curve.
        unit (str or sc.Unit): Unit of the parameters.
        Defaults to "meV".
    """

    def __init__(
        self,
        area: Numeric | Parameter = 1.0,
        center: Numeric | Parameter = 1.0,
        width: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'DampedHarmonicOscillator',
        unique_name: str | None = None,
    ):
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
            unit=unit,
        )

        # These methods live in ValidationMixin
        area = self._create_area_parameter(area=area, name=display_name, unit=self._unit)
        center = self._create_center_parameter(
            center=center,
            name=display_name,
            fix_if_none=False,
            unit=self._unit,
            enforce_minimum_center=True,
        )

        width = self._create_width_parameter(width=width, name=display_name, unit=self._unit)

        self._area = area
        self._center = center
        self._width = width

    @property
    def area(self) -> Parameter:
        """Get the area parameter."""
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """Set the area parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('area must be a number')
        self._area.value = value

    @property
    def center(self) -> Parameter:
        """Get the center parameter."""
        return self._center

    @center.setter
    def center(self, value: Numeric) -> None:
        """Set the center parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')

        if value <= 0:
            raise ValueError('center must be positive')
        self._center.value = value

    @property
    def width(self) -> Parameter:
        """Get the width parameter."""
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """Set the width parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')
        self._width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Damped Harmonic Oscillator at the given x
        values.

        If x is a scipp Variable, the unit of the DHO will be converted
        to match x. The DHO evaluates to
        2*area*center^2*width/pi / ((x^2 - center^2)^2 + (2*width*x)^2)
        """

        x = self._prepare_x_for_evaluate(x)

        normalization = 2 * self.center.value**2 * self.width.value / np.pi
        # No division by zero here, width>0 enforced in setter
        denominator = (x**2 - self.center.value**2) ** 2 + (2 * self.width.value * x) ** 2

        return self.area.value * normalization / (denominator)

    def __repr__(self):
        return (
            f'DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \
        area = {self.area},\n center = {self.center},\n width = {self.width})'
        )
area property writable

Get the area parameter.

center property writable

Get the center parameter.

evaluate(x)

Evaluate the Damped Harmonic Oscillator at the given x values.

If x is a scipp Variable, the unit of the DHO will be converted to match x. The DHO evaluates to 2areacenter^2width/pi / ((x^2 - center^2)^2 + (2width*x)^2)

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Damped Harmonic Oscillator at the given x
    values.

    If x is a scipp Variable, the unit of the DHO will be converted
    to match x. The DHO evaluates to
    2*area*center^2*width/pi / ((x^2 - center^2)^2 + (2*width*x)^2)
    """

    x = self._prepare_x_for_evaluate(x)

    normalization = 2 * self.center.value**2 * self.width.value / np.pi
    # No division by zero here, width>0 enforced in setter
    denominator = (x**2 - self.center.value**2) ** 2 + (2 * self.width.value * x) ** 2

    return self.area.value * normalization / (denominator)
width property writable

Get the width parameter.

delta_function

DeltaFunction

Bases: CreateParametersMixin, ModelComponent

Delta function. Evaluates to zero everywhere, except in convolutions, where it acts as an identity. This is handled in the ResolutionHandler. If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Parameters:

Name Type Description Default
center Int or float or None

Center of the delta function.

None
area Int or float

Total area under the curve.

1.0
unit str or Unit

Unit of the parameters.

'meV'
display_name str

Name of the component.

'DeltaFunction'
unique_name str or None

Unique name of the component.

None
Source code in src/easydynamics/sample_model/components/delta_function.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
class DeltaFunction(CreateParametersMixin, ModelComponent):
    """Delta function. Evaluates to zero everywhere, except in
    convolutions, where it acts as an identity. This is handled in the
    ResolutionHandler. If the center is not provided, it will be
    centered at 0 and fixed, which is typically what you want in QENS.

    Args:
        center (Int or float or None): Center of the delta function.
        If None, defaults to 0 and is fixed.
        area (Int or float): Total area under the curve.
        unit (str or sc.Unit): Unit of the parameters.
        Defaults to "meV".
        display_name (str): Name of the component.
        unique_name (str or None): Unique name of the component.
        If None, a unique_name is automatically generated.
    """

    def __init__(
        self,
        center: None | Numeric | Parameter = None,
        area: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'DeltaFunction',
        unique_name: str | None = None,
    ):
        # Validate inputs and create Parameters if not given
        super().__init__(
            display_name=display_name,
            unit=unit,
            unique_name=unique_name,
        )

        # These methods live in ValidationMixin
        area = self._create_area_parameter(area=area, name=display_name, unit=self._unit)
        center = self._create_center_parameter(
            center=center, name=display_name, fix_if_none=True, unit=self._unit
        )

        self._area = area
        self._center = center

    @property
    def area(self) -> Parameter:
        """Get the area parameter."""
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """Set the area parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('area must be a number')
        self._area.value = value

    @property
    def center(self) -> Parameter:
        """Get the center parameter."""
        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """Set the center parameter value."""
        if value is None:
            value = 0.0
            self._center.fixed = True
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')
        self._center.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Delta function at the given x values.

        The Delta function evaluates to zero everywhere, except at the
        center. Its numerical integral is equal to the area. It acts as
        an identity in convolutions.
        """

        # x assumed sorted, 1D numpy array
        x = self._prepare_x_for_evaluate(x)
        model = np.zeros_like(x, dtype=float)
        center = self.center.value
        area = self.area.value

        if x.min() - EPSILON <= center <= x.max() + EPSILON:
            # nearest index
            i = np.argmin(np.abs(x - center))

            # left half-width
            if i == 0:
                left = x[1] - x[0] if x.size > 1 else 0.5
            else:
                left = x[i] - x[i - 1]

            # right half-width
            if i == x.size - 1:
                right = x[-1] - x[-2] if x.size > 1 else 0.5
            else:
                right = x[i + 1] - x[i]

            # effective bin width: half left + half right
            bin_width = 0.5 * (left + right)

            model[i] = area / bin_width

        return model

    def __repr__(self):
        return f'DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n \
        area = {self.area},\n center = {self.center}'
area property writable

Get the area parameter.

center property writable

Get the center parameter.

evaluate(x)

Evaluate the Delta function at the given x values.

The Delta function evaluates to zero everywhere, except at the center. Its numerical integral is equal to the area. It acts as an identity in convolutions.

Source code in src/easydynamics/sample_model/components/delta_function.py
 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
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Delta function at the given x values.

    The Delta function evaluates to zero everywhere, except at the
    center. Its numerical integral is equal to the area. It acts as
    an identity in convolutions.
    """

    # x assumed sorted, 1D numpy array
    x = self._prepare_x_for_evaluate(x)
    model = np.zeros_like(x, dtype=float)
    center = self.center.value
    area = self.area.value

    if x.min() - EPSILON <= center <= x.max() + EPSILON:
        # nearest index
        i = np.argmin(np.abs(x - center))

        # left half-width
        if i == 0:
            left = x[1] - x[0] if x.size > 1 else 0.5
        else:
            left = x[i] - x[i - 1]

        # right half-width
        if i == x.size - 1:
            right = x[-1] - x[-2] if x.size > 1 else 0.5
        else:
            right = x[i + 1] - x[i]

        # effective bin width: half left + half right
        bin_width = 0.5 * (left + right)

        model[i] = area / bin_width

    return model

gaussian

Gaussian

Bases: CreateParametersMixin, ModelComponent

Gaussian function: area/(widthsqrt(2pi)) * exp(-0.5((x - center)/width)^2) If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Parameters:

Name Type Description Default
area (Int, float or Parameter)

Area of the Gaussian.

1.0
center (Int, float, None or Parameter)

Center of the Gaussian.

None
width (Int, float or Parameter)

Standard deviation.

1.0
unit str or Unit

Unit of the parameters.

'meV'
display_name str

Name of the component.

'Gaussian'
unique_name str or None

Unique name of the component.

None
Source code in src/easydynamics/sample_model/components/gaussian.py
 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
class Gaussian(CreateParametersMixin, ModelComponent):
    """
    Gaussian function:
    area/(width*sqrt(2pi)) * exp(-0.5*((x - center)/width)^2)
    If the center is not provided, it will be centered at 0 and fixed,
    which is typically what you want in QENS.

    Args:
        area (Int, float or Parameter): Area of the Gaussian.
        center (Int, float, None or Parameter): Center of the Gaussian.
        If None, defaults to 0 and is fixed
        width (Int, float or Parameter): Standard deviation.
        unit (str or sc.Unit): Unit of the parameters.
        Defaults to "meV".
        display_name (str): Name of the component.
        unique_name (str or None): Unique name of the component.
        If None, a unique_name is automatically generated.
    """

    def __init__(
        self,
        area: Numeric | Parameter = 1.0,
        center: Numeric | Parameter | None = None,
        width: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Gaussian',
        unique_name: str | None = None,
    ):
        # Validate inputs and create Parameters if not given
        super().__init__(
            display_name=display_name,
            unit=unit,
            unique_name=unique_name,
        )

        # These methods live in ValidationMixin
        area = self._create_area_parameter(area=area, name=display_name, unit=self._unit)
        center = self._create_center_parameter(
            center=center, name=display_name, fix_if_none=True, unit=self._unit
        )
        width = self._create_width_parameter(width=width, name=display_name, unit=self._unit)

        self._area = area
        self._center = center
        self._width = width

    @property
    def area(self) -> Parameter:
        """Get the area parameter."""
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """Set the area parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('area must be a number')
        self._area.value = value

    @property
    def center(self) -> Parameter:
        """Get the center parameter."""
        return self._center

    @center.setter
    def center(self, value: Numeric) -> None:
        """Set the center parameter value."""
        if value is None:
            value = 0.0
            self._center.fixed = True
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')
        self._center.value = value

    @property
    def width(self) -> Parameter:
        """Get the width parameter."""
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """Set the width parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')
        self._width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Gaussian at the given x values.

        If x is a scipp Variable, the unit of the Gaussian will be
        converted to match x.
        The Gaussian evaluates to
        area/(width*sqrt(2pi)) * exp(-0.5*((x - center)/width)^2)
        """

        x = self._prepare_x_for_evaluate(x)

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

        return self.area.value * normalization * np.exp(exponent)

    def __repr__(self):
        return f'Gaussian(unique_name = {self.unique_name}, unit = {self._unit},\n \
            area = {self.area},\n center = {self.center},\n width = {self.width})'
area property writable

Get the area parameter.

center property writable

Get the center parameter.

evaluate(x)

Evaluate the Gaussian at the given x values.

If x is a scipp Variable, the unit of the Gaussian will be converted to match x. The Gaussian evaluates to area/(widthsqrt(2pi)) * exp(-0.5((x - center)/width)^2)

Source code in src/easydynamics/sample_model/components/gaussian.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Gaussian at the given x values.

    If x is a scipp Variable, the unit of the Gaussian will be
    converted to match x.
    The Gaussian evaluates to
    area/(width*sqrt(2pi)) * exp(-0.5*((x - center)/width)^2)
    """

    x = self._prepare_x_for_evaluate(x)

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

    return self.area.value * normalization * np.exp(exponent)
width property writable

Get the width parameter.

lorentzian

Lorentzian

Bases: CreateParametersMixin, ModelComponent

Lorentzian function: area*width / (pi * ( (x - center)^2 + width^2 ) ) If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Parameters:

Name Type Description Default
area (Int, float or Parameter)

Area of the Lorentzian.

1.0
center (Int, float, None or Parameter)

Peak center.

None
width (Int, float or Parameter)
1.0
unit str or Unit

Unit of the parameters. Defaults to "meV"

'meV'
display_name str

Display name of the component.

'Lorentzian'
unique_name str or None

Unique name of the component.

None
Source code in src/easydynamics/sample_model/components/lorentzian.py
 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
class Lorentzian(CreateParametersMixin, ModelComponent):
    """
    Lorentzian function:
    area*width / (pi * ( (x - center)^2 + width^2 ) )
    If the center is not provided, it will be centered at 0 and fixed,
    which is typically what you want in QENS.

    Args:
        area (Int, float or Parameter): Area of the Lorentzian.
        center (Int, float, None or Parameter): Peak center.
        If None, defaults to 0 and is fixed.
        width (Int, float or Parameter):
        Half Width at Half Maximum (HWHM)
        unit (str or sc.Unit): Unit of the parameters. Defaults to "meV"
        display_name (str): Display name of the component.
        unique_name (str or None): Unique name of the component.
        If None, a unique_name is automatically generated.
    """

    def __init__(
        self,
        area: Numeric | Parameter = 1.0,
        center: Numeric | Parameter | None = None,
        width: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Lorentzian',
        unique_name: str | None = None,
    ):
        super().__init__(
            display_name=display_name,
            unit=unit,
            unique_name=unique_name,
        )

        # These methods live in ValidationMixin
        area = self._create_area_parameter(area=area, name=display_name, unit=self._unit)
        center = self._create_center_parameter(
            center=center, name=display_name, fix_if_none=True, unit=self._unit
        )
        width = self._create_width_parameter(width=width, name=display_name, unit=self._unit)

        self._area = area
        self._center = center
        self._width = width

    @property
    def area(self) -> Parameter:
        """Get the area parameter."""
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """Set the area parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('area must be a number')
        self._area.value = value

    @property
    def center(self) -> Parameter:
        """Get the center parameter."""
        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """Set the center parameter value."""
        if value is None:
            value = 0.0
            self._center.fixed = True
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')
        self._center.value = value

    @property
    def width(self) -> Parameter:
        """Get the width parameter."""
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """Set the width parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')
        self._width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Lorentzian at the given x values.

        If x is a scipp Variable, the unit of the Lorentzian will be
        converted to match x.
        The Lorentzian evaluates to
        area*width / (pi * ( (x - center)^2 + width^2 ) )
        """

        x = self._prepare_x_for_evaluate(x)

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

        return self.area.value * normalization / denominator

    def __repr__(self):
        return f'Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n \
            area = {self.area},\n center = {self.center},\n width = {self.width})'
area property writable

Get the area parameter.

center property writable

Get the center parameter.

evaluate(x)

Evaluate the Lorentzian at the given x values.

If x is a scipp Variable, the unit of the Lorentzian will be converted to match x. The Lorentzian evaluates to area*width / (pi * ( (x - center)^2 + width^2 ) )

Source code in src/easydynamics/sample_model/components/lorentzian.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Lorentzian at the given x values.

    If x is a scipp Variable, the unit of the Lorentzian will be
    converted to match x.
    The Lorentzian evaluates to
    area*width / (pi * ( (x - center)^2 + width^2 ) )
    """

    x = self._prepare_x_for_evaluate(x)

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

    return self.area.value * normalization / denominator
width property writable

Get the width parameter.

mixins

CreateParametersMixin

Provides parameter creation and validation methods for model components.

This mixin provides methods to create and validate common physics parameters (area, center, width) with appropriate bounds and type checking.

Source code in src/easydynamics/sample_model/components/mixins.py
 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
class CreateParametersMixin:
    """Provides parameter creation and validation methods for model
    components.

    This mixin provides methods to create and validate common physics
    parameters (area, center, width) with appropriate bounds and type
    checking.
    """

    def _create_area_parameter(
        self,
        area: Numeric | Parameter,
        name: str,
        unit: str | sc.Unit = 'meV',
        minimum_area: float = MINIMUM_AREA,
    ) -> Parameter:
        """Validate and convert a number to a Parameter describing the
        area of a function.

        If the area is negative, a warning is raised.
        If the area is non-negative, its minimum is set to 0 to avoid it
        accidentally becoming negative during fitting.
        args:
            area (Numeric or Parameter): The area value or Parameter.
            name (str): The name of the model component.
            unit (str or sc.Unit): The unit of the area Parameter.
            minimum_area (float): The minimum allowed area.
        returns:
            Parameter: The validated area Parameter.
        raises:
            TypeError: If area is not a number or a Parameter.
            Warning: If area is negative.
        """
        if not isinstance(area, (Parameter, Numeric)):
            raise TypeError('area must be a number or a Parameter.')

        if isinstance(area, Numeric):
            if not np.isfinite(area):
                raise ValueError('area must be a finite number or a Parameter')

            area = Parameter(name=name + ' area', value=float(area), unit=unit)

        if area.value < 0:
            warnings.warn(
                f'The area of {name} is negative, which may not be physically meaningful.'
            )
        else:
            area.min = minimum_area

        return area

    def _create_center_parameter(
        self,
        center: Numeric | Parameter | None,
        name: str,
        fix_if_none: bool,
        unit: str | sc.Unit = 'meV',
        enforce_minimum_center: bool = False,
    ) -> Parameter:
        """Validate and convert a number to a Parameter describing the
        center of a function.

        args:
            center (Numeric, Parameter, or None): The center value or
            Parameter.
            name (str): The name of the model component.
            fix_if_none (bool): Whether to fix the center Parameter
            if center is None.
            unit (str or sc.Unit): The unit of the center Parameter.
        returns:
            Parameter: The validated center Parameter.
        raises:
            TypeError: If center is not None, a number, or a Parameter.
        """
        if center is not None and not isinstance(center, (Numeric, Parameter)):
            raise TypeError('center must be None, a number, or a Parameter.')

        if center is None:
            center = Parameter(
                name=name + ' center',
                value=0.0,
                unit=unit,
                fixed=fix_if_none,
            )
        elif isinstance(center, Numeric):
            if not np.isfinite(center):
                raise ValueError('center must be None, a finite number or a Parameter')

            center = Parameter(name=name + ' center', value=float(center), unit=unit)
        if enforce_minimum_center:
            center.min = DHO_MINIMUM_CENTER
        return center

    def _create_width_parameter(
        self,
        width: Numeric | Parameter,
        name: str,
        param_name: str = 'width',
        unit: str | sc.Unit = 'meV',
        minimum_width: float = MINIMUM_WIDTH,
    ) -> Parameter:
        """Validate and convert a number to a Parameter describing the
        width of a function.

        args:
            width (Numeric or Parameter): The width value or Parameter.
            name (str): The name of the model component.
            param_name (str): The name of the width parameter.
            unit (str or sc.Unit): The unit of the width Parameter.
            minimum_width (float): The minimum allowed width.
        returns:
            Parameter: The validated width Parameter.
        raises:
            TypeError: If width is not a number or a Parameter.
            ValueError: If width is non-positive.
        """
        if not isinstance(width, (Numeric, Parameter)):
            raise TypeError(f'{param_name} must be a number or a Parameter.')

        if isinstance(width, Numeric):
            if not np.isfinite(width):
                raise ValueError(f'{param_name} must be a finite number or a Parameter')

            if float(width) < minimum_width:
                raise ValueError(
                    f'The {param_name} of a {self.__class__.__name__} must be greater than zero.'
                )
            width = Parameter(
                name=name + ' ' + param_name,
                value=float(width),
                unit=unit,
                min=minimum_width,
            )
        else:
            if width.value <= 0:
                raise ValueError(
                    f'The {param_name} of a {self.__class__.__name__} must be greater than zero.'
                )
            width.min = minimum_width

        return width

model_component

ModelComponent

Bases: ModelBase

Abstract base class for all model components.

Source code in src/easydynamics/sample_model/components/model_component.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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
class ModelComponent(ModelBase):
    """Abstract base class for all model components."""

    def __init__(
        self,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = None,
        unique_name: str | None = None,
    ):
        self.validate_unit(unit)
        super().__init__(display_name=display_name, unique_name=unique_name)
        self._unit = unit

    @property
    def unit(self) -> str:
        """Get the unit.

        :return: Unit as a string.
        """
        return str(self._unit)

    @unit.setter
    def unit(self, unit_str: str) -> None:
        raise AttributeError(
            (
                f'Unit is read-only. Use convert_unit to change the unit between allowed types '
                f'or create a new {self.__class__.__name__} with the desired unit.'
            )
        )  # noqa: E501

    def fix_all_parameters(self):
        """Fix all parameters in the model component."""

        pars = self.get_fittable_parameters()
        for p in pars:
            p.fixed = True

    def free_all_parameters(self):
        """Free all parameters in the model component."""
        for p in self.get_fittable_parameters():
            p.fixed = False

    def _prepare_x_for_evaluate(
        self, x: Numeric | List[Numeric] | np.ndarray | sc.Variable | sc.DataArray
    ) -> np.ndarray:
        """Prepare the input x for evaluation by handling units and
        converting to a numpy array.
        """

        # Handle units
        if isinstance(x, sc.DataArray):
            # Check that there's exactly one coordinate
            coords = dict(x.coords)
            ncoords = len(coords)
            if ncoords != 1:
                coord_names = ', '.join(coords.keys())
                raise ValueError(
                    f'scipp.DataArray must have exactly one coordinate to be used as input `x`. '
                    f'Found {ncoords} coordinates: {coord_names}.'
                )
            # get the coordinate, it's a sc.Variable
            coord_name, coord_obj = next(iter(coords.items()))
            x = coord_obj
        if isinstance(x, sc.Variable):
            # Need to check if the units are consistent,
            # and convert if not.
            if x.sizes == {}:  # scalar
                x_in = x.value
            else:  # array
                x_in = x.values
            if self._unit is not None and x.unit != self._unit:
                self_unit_for_warning = self._unit
                try:
                    self.convert_unit(x.unit.name)
                except Exception as e:
                    raise UnitError(
                        f'Input x has unit {x.unit}, but {self.__class__.__name__} component \
                            has unit {self._unit}. \
                                Failed to convert {self.__class__.__name__} to {x.unit}.'
                    ) from e

                warnings.warn(
                    f'Input x has unit {x.unit}, but {self.__class__.__name__} component \
                        has unit {self_unit_for_warning}. \
                            Converting {self.__class__.__name__} to {x.unit}.'
                )
        else:
            x_in = x

        if isinstance(x_in, Numeric):
            x_in = np.array([x_in])
        elif isinstance(x_in, list):
            x_in = np.array(x_in)

        if any(np.isnan(x_in)):
            raise ValueError('Input x contains NaN values.')

        if any(np.isinf(x_in)):
            raise ValueError('Input x contains infinite values.')

        return np.sort(x_in)

    @staticmethod
    def validate_unit(unit) -> None:
        """Raise TypeError if unit is not allowed (string or
        sc.Unit).
        """
        if unit is not None and not isinstance(unit, (str, sc.Unit)):
            raise TypeError(
                f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}'
            )

    def convert_unit(self, unit: str | sc.Unit):
        """Convert the unit of the Parameters in the component.

        Args:
            unit (str or sc.Unit): The new unit to convert to.
        """

        old_unit = self._unit
        pars = self.get_all_parameters()
        try:
            for p in pars:
                p.convert_unit(unit)
            self._unit = unit
        except Exception as e:
            # Attempt to rollback on failure
            try:
                for p in pars:
                    if hasattr(p, 'convert_unit'):
                        p.convert_unit(old_unit)
            except Exception:  # noqa: S110
                pass  # Best effort rollback
            raise e

    @abstractmethod
    def evaluate(self, x: Numeric | sc.Variable) -> np.ndarray:
        """Evaluate the model component at input x.

        Args:
            x (Numeric | sc.Variable): Input values.

        Returns:
            np.ndarray: Evaluated function values.
        """
        pass

    def __repr__(self):
        return f'{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit})'
convert_unit(unit)

Convert the unit of the Parameters in the component.

Parameters:

Name Type Description Default
unit str or Unit

The new unit to convert to.

required
Source code in src/easydynamics/sample_model/components/model_component.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def convert_unit(self, unit: str | sc.Unit):
    """Convert the unit of the Parameters in the component.

    Args:
        unit (str or sc.Unit): The new unit to convert to.
    """

    old_unit = self._unit
    pars = self.get_all_parameters()
    try:
        for p in pars:
            p.convert_unit(unit)
        self._unit = unit
    except Exception as e:
        # Attempt to rollback on failure
        try:
            for p in pars:
                if hasattr(p, 'convert_unit'):
                    p.convert_unit(old_unit)
        except Exception:  # noqa: S110
            pass  # Best effort rollback
        raise e
evaluate(x) abstractmethod

Evaluate the model component at input x.

Parameters:

Name Type Description Default
x Numeric | Variable

Input values.

required

Returns:

Type Description
ndarray

np.ndarray: Evaluated function values.

Source code in src/easydynamics/sample_model/components/model_component.py
153
154
155
156
157
158
159
160
161
162
163
@abstractmethod
def evaluate(self, x: Numeric | sc.Variable) -> np.ndarray:
    """Evaluate the model component at input x.

    Args:
        x (Numeric | sc.Variable): Input values.

    Returns:
        np.ndarray: Evaluated function values.
    """
    pass
fix_all_parameters()

Fix all parameters in the model component.

Source code in src/easydynamics/sample_model/components/model_component.py
48
49
50
51
52
53
def fix_all_parameters(self):
    """Fix all parameters in the model component."""

    pars = self.get_fittable_parameters()
    for p in pars:
        p.fixed = True
free_all_parameters()

Free all parameters in the model component.

Source code in src/easydynamics/sample_model/components/model_component.py
55
56
57
58
def free_all_parameters(self):
    """Free all parameters in the model component."""
    for p in self.get_fittable_parameters():
        p.fixed = False
unit property writable

Get the unit.

:return: Unit as a string.

validate_unit(unit) staticmethod

Raise TypeError if unit is not allowed (string or sc.Unit).

Source code in src/easydynamics/sample_model/components/model_component.py
120
121
122
123
124
125
126
127
128
@staticmethod
def validate_unit(unit) -> None:
    """Raise TypeError if unit is not allowed (string or
    sc.Unit).
    """
    if unit is not None and not isinstance(unit, (str, sc.Unit)):
        raise TypeError(
            f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}'
        )

polynomial

Polynomial

Bases: ModelComponent

Polynomial function component. c0 + c1x + c2x^2 + ... + cN*x^N.

Parameters:

Name Type Description Default
coefficients list or tuple

Coefficients c0, c1, ..., cN

(0.0,)
unit str or Unit

Unit of the Polynomial component.

'meV'
display_name str

Display name of the Polynomial component.

'Polynomial'
unique_name str or None

Unique name of the component. If None, a unique_name is automatically generated.

None
Source code in src/easydynamics/sample_model/components/polynomial.py
 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
class Polynomial(ModelComponent):
    """Polynomial function component. c0 + c1*x + c2*x^2 + ... + cN*x^N.

    Args:
        coefficients (list or tuple): Coefficients c0, c1, ..., cN
        representing f(x) = c0 + c1*x + c2*x^2 + ... + cN*x^N
        unit (str or sc.Unit): Unit of the Polynomial component.
        display_name (str): Display name of the Polynomial component.
        unique_name (str or None): Unique name of the component.
            If None, a unique_name is automatically generated.
    """

    def __init__(
        self,
        coefficients: Sequence[Numeric | Parameter] = (0.0,),
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Polynomial',
        unique_name: str | None = None,
    ):
        super().__init__(display_name=display_name, unit=unit, unique_name=unique_name)

        if not isinstance(coefficients, (list, tuple, np.ndarray)):
            raise TypeError(
                'coefficients must be a sequence (list/tuple/ndarray) \
                    of numbers or Parameter objects.'
            )

        if len(coefficients) == 0:
            raise ValueError('At least one coefficient must be provided.')

        # Internal storage of Parameter objects
        self._coefficients: list[Parameter] = []

        # Coefficients are treated as dimensionless Parameters
        for i, coef in enumerate(coefficients):
            if isinstance(coef, Parameter):
                param = coef
            elif isinstance(coef, Numeric):
                param = Parameter(name=f'{display_name}_c{i}', value=float(coef))
            else:
                raise TypeError('Each coefficient must be either a numeric value or a Parameter.')
            self._coefficients.append(param)

        # Helper scipp scalar to track unit conversions
        # (value initialized to 1 with provided unit)
        self._unit_conversion_helper = sc.scalar(value=1.0, unit=unit)

    @property
    def coefficients(self) -> list[Parameter]:
        """Get the coefficients of the polynomial as a list of
        Parameters.
        """
        return list(self._coefficients)

    @coefficients.setter
    def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None:
        """Replace the coefficients.

        Length must match current number of coefficients.
        """
        if not isinstance(coeffs, (list, tuple, np.ndarray)):
            raise TypeError(
                'coefficients must be a sequence (list/tuple/ndarray) of numbers or Parameter .'
            )
        if len(coeffs) != len(self._coefficients):
            raise ValueError(
                'Number of coefficients must match the existing number of coefficients.'
            )
        for i, coef in enumerate(coeffs):
            if isinstance(coef, Parameter):
                # replace parameter
                self._coefficients[i] = coef
            elif isinstance(coef, Numeric):
                self._coefficients[i].value = float(coef)
            else:
                raise TypeError('Each coefficient must be either a numeric value or a Parameter.')

    def coefficient_values(self) -> list[float]:
        """Get the coefficients of the polynomial as a list."""
        coefficient_list = [param.value for param in self._coefficients]
        return coefficient_list

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Polynomial at the given x values.

        The Polynomial evaluates to c0 + c1*x + c2*x^2 + ... + cN*x^N
        """

        x = self._prepare_x_for_evaluate(x)

        result = np.zeros_like(x, dtype=float)
        for i, param in enumerate(self._coefficients):
            result += param.value * np.power(x, i)

        if any(result < 0):
            warnings.warn(
                f'The Polynomial with unique_name {self.unique_name} has negative values, '
                'which may not be physically meaningful.',
                UserWarning,
            )
        return result

    @property
    def degree(self) -> int:
        """Return the degree of the polynomial."""
        return len(self._coefficients) - 1

    @degree.setter
    def degree(self, value: int) -> None:
        raise AttributeError(
            'The degree of the polynomial is determined by the number of coefficients \
                and cannot be set directly.'
        )

    def get_all_variables(self) -> list[DescriptorBase]:
        """Get all parameters from the model component.

        Returns:
        List[Parameter]: List of parameters in the component.
        """
        return list(self._coefficients)

    def convert_unit(self, unit: str | sc.Unit):
        """Convert the unit of the polynomial.

        Args:
            unit (str or sc.Unit): The target unit to convert to.
        """

        if not isinstance(unit, (str, sc.Unit)):
            raise UnitError('unit must be a string or a scipp unit.')

        # Find out how much the unit changes
        # by converting a helper variable
        conversion_value_before = self._unit_conversion_helper.value
        self._unit_conversion_helper = sc.to_unit(self._unit_conversion_helper, unit=unit)
        conversion_value_after = self._unit_conversion_helper.value
        for i, param in enumerate(self._coefficients):
            param.value *= (
                conversion_value_before / conversion_value_after
            ) ** i  # set the values directly to the appropriate power

        self._unit = unit

    def __repr__(self) -> str:
        coeffs_str = ', '.join(f'{param.name}={param.value}' for param in self._coefficients)
        return f'Polynomial(unique_name = {self.unique_name}, \
            unit = {self._unit},\n coefficients = [{coeffs_str}])'
coefficient_values()

Get the coefficients of the polynomial as a list.

Source code in src/easydynamics/sample_model/components/polynomial.py
 97
 98
 99
100
def coefficient_values(self) -> list[float]:
    """Get the coefficients of the polynomial as a list."""
    coefficient_list = [param.value for param in self._coefficients]
    return coefficient_list
coefficients property writable

Get the coefficients of the polynomial as a list of Parameters.

convert_unit(unit)

Convert the unit of the polynomial.

Parameters:

Name Type Description Default
unit str or Unit

The target unit to convert to.

required
Source code in src/easydynamics/sample_model/components/polynomial.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def convert_unit(self, unit: str | sc.Unit):
    """Convert the unit of the polynomial.

    Args:
        unit (str or sc.Unit): The target unit to convert to.
    """

    if not isinstance(unit, (str, sc.Unit)):
        raise UnitError('unit must be a string or a scipp unit.')

    # Find out how much the unit changes
    # by converting a helper variable
    conversion_value_before = self._unit_conversion_helper.value
    self._unit_conversion_helper = sc.to_unit(self._unit_conversion_helper, unit=unit)
    conversion_value_after = self._unit_conversion_helper.value
    for i, param in enumerate(self._coefficients):
        param.value *= (
            conversion_value_before / conversion_value_after
        ) ** i  # set the values directly to the appropriate power

    self._unit = unit
degree property writable

Return the degree of the polynomial.

evaluate(x)

Evaluate the Polynomial at the given x values.

The Polynomial evaluates to c0 + c1x + c2x^2 + ... + cN*x^N

Source code in src/easydynamics/sample_model/components/polynomial.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Polynomial at the given x values.

    The Polynomial evaluates to c0 + c1*x + c2*x^2 + ... + cN*x^N
    """

    x = self._prepare_x_for_evaluate(x)

    result = np.zeros_like(x, dtype=float)
    for i, param in enumerate(self._coefficients):
        result += param.value * np.power(x, i)

    if any(result < 0):
        warnings.warn(
            f'The Polynomial with unique_name {self.unique_name} has negative values, '
            'which may not be physically meaningful.',
            UserWarning,
        )
    return result
get_all_variables()

Get all parameters from the model component.

Returns: List[Parameter]: List of parameters in the component.

Source code in src/easydynamics/sample_model/components/polynomial.py
134
135
136
137
138
139
140
def get_all_variables(self) -> list[DescriptorBase]:
    """Get all parameters from the model component.

    Returns:
    List[Parameter]: List of parameters in the component.
    """
    return list(self._coefficients)

voigt

Voigt

Bases: CreateParametersMixin, ModelComponent

Voigt profile, a convolution of Gaussian and Lorentzian. If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Parameters:

Name Type Description Default
area Int or float

Total area under the curve.

1.0
center Int or float or None

Center of the Voigt profile.

None
gaussian_width Int or float

Standard deviation of the

1.0
lorentzian_width Int or float

Half width at half max (HWHM)

1.0
unit str or Unit

Unit of the parameters. Defaults to "meV"

'meV'
display_name str

Display name of the component.

'Voigt'
unique_name str or None

Unique name of the component.

None
Source code in src/easydynamics/sample_model/components/voigt.py
 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
class Voigt(CreateParametersMixin, ModelComponent):
    """Voigt profile, a convolution of Gaussian and Lorentzian. If the
    center is not provided, it will be centered at 0 and fixed, which is
    typically what you want in QENS.

    Args:
        area (Int or float): Total area under the curve.
        center (Int or float or None): Center of the Voigt profile.
        gaussian_width (Int or float): Standard deviation of the
        Gaussian part.
        lorentzian_width (Int or float): Half width at half max (HWHM)
        of the Lorentzian part.
        unit (str or sc.Unit): Unit of the parameters. Defaults to "meV"
        display_name (str): Display name of the component.
        unique_name (str or None): Unique name of the component.
        If None, a unique_name is automatically generated.
    """

    def __init__(
        self,
        area: Numeric | Parameter = 1.0,
        center: Numeric | Parameter | None = None,
        gaussian_width: Numeric | Parameter = 1.0,
        lorentzian_width: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Voigt',
        unique_name: str | None = None,
    ):
        super().__init__(
            display_name=display_name,
            unit=unit,
            unique_name=unique_name,
        )

        # These methods live in ValidationMixin
        area = self._create_area_parameter(area=area, name=display_name, unit=self._unit)
        center = self._create_center_parameter(
            center=center, name=display_name, fix_if_none=True, unit=self._unit
        )
        gaussian_width = self._create_width_parameter(
            width=gaussian_width,
            name=display_name,
            param_name='gaussian_width',
            unit=self._unit,
        )
        lorentzian_width = self._create_width_parameter(
            width=lorentzian_width,
            name=display_name,
            param_name='lorentzian_width',
            unit=self._unit,
        )

        self._area = area
        self._center = center
        self._gaussian_width = gaussian_width
        self._lorentzian_width = lorentzian_width

    @property
    def area(self) -> Parameter:
        """Get the area parameter."""
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """Set the area parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('area must be a number')
        self._area.value = value

    @property
    def center(self) -> Parameter:
        """Get the center parameter."""
        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """Set the center parameter value."""
        if value is None:
            value = 0.0
            self._center.fixed = True
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')
        self._center.value = value

    @property
    def gaussian_width(self) -> Parameter:
        """Get the width parameter."""
        return self._gaussian_width

    @gaussian_width.setter
    def gaussian_width(self, value: Numeric) -> None:
        """Set the width parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('gaussian_width must be a number')
        self._gaussian_width.value = value

    @property
    def lorentzian_width(self) -> Parameter:
        """Get the width parameter."""
        return self._lorentzian_width

    @lorentzian_width.setter
    def lorentzian_width(self, value: Numeric) -> None:
        """Set the width parameter value."""
        if not isinstance(value, Numeric):
            raise TypeError('lorentzian_width must be a number')
        self._lorentzian_width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """Evaluate the Voigt at the given x values.

        If x is a scipp Variable, the unit of the Voigt will be
        converted to match x. The Voigt evaluates to the convolution of
        a Gaussian with sigma gaussian_width and a Lorentzian with half
        width at half max lorentzian_width, centered at center, with
        area equal to area.
        """

        x = self._prepare_x_for_evaluate(x)

        return self.area.value * voigt_profile(
            x - self.center.value,
            self.gaussian_width.value,
            self.lorentzian_width.value,
        )

    def __repr__(self):
        return f'Voigt(unique_name = {self.unique_name}, unit = {self._unit},\n \
        area = {self.area},\n \
        center = {self.center},\n \
        gaussian_width = {self.gaussian_width},\n \
        lorentzian_width = {self.lorentzian_width})'
area property writable

Get the area parameter.

center property writable

Get the center parameter.

evaluate(x)

Evaluate the Voigt at the given x values.

If x is a scipp Variable, the unit of the Voigt will be converted to match x. The Voigt evaluates to the convolution of a Gaussian with sigma gaussian_width and a Lorentzian with half width at half max lorentzian_width, centered at center, with area equal to area.

Source code in src/easydynamics/sample_model/components/voigt.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the Voigt at the given x values.

    If x is a scipp Variable, the unit of the Voigt will be
    converted to match x. The Voigt evaluates to the convolution of
    a Gaussian with sigma gaussian_width and a Lorentzian with half
    width at half max lorentzian_width, centered at center, with
    area equal to area.
    """

    x = self._prepare_x_for_evaluate(x)

    return self.area.value * voigt_profile(
        x - self.center.value,
        self.gaussian_width.value,
        self.lorentzian_width.value,
    )
gaussian_width property writable

Get the width parameter.

lorentzian_width property writable

Get the width parameter.

diffusion_model

BrownianTranslationalDiffusion

Bases: DiffusionModelBase

Model of Brownian translational diffusion, consisting of a Lorentzian function for each Q-value, where the width is given by :math:DQ^2. Q is assumed to have units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given Q-values.

Example usage: Q=np.linspace(0.5,2,7) energy=np.linspace(-2, 2, 501) scale=1.0 diffusion_coefficient = 2.4e-9 # m^2/s diffusion_model=BrownianTranslationalDiffusion(display_name="DiffusionModel", scale=scale, diffusion_coefficient= diffusion_coefficient) component_collections=diffusion_model.create_component_collections(Q) See also the examples.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
 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
class BrownianTranslationalDiffusion(DiffusionModelBase):
    """Model of Brownian translational diffusion, consisting of a
    Lorentzian function for each Q-value, where the width is given by
    :math:`DQ^2`. Q is assumed to have units of 1/angstrom. Creates
    ComponentCollections with Lorentzian components for given Q-values.

    Example usage:
    Q=np.linspace(0.5,2,7)
    energy=np.linspace(-2, 2, 501)
    scale=1.0
    diffusion_coefficient = 2.4e-9  # m^2/s
    diffusion_model=BrownianTranslationalDiffusion(display_name="DiffusionModel",
    scale=scale, diffusion_coefficient= diffusion_coefficient)
    component_collections=diffusion_model.create_component_collections(Q)
    See also the examples.
    """

    def __init__(
        self,
        display_name: str | None = 'BrownianTranslationalDiffusion',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
        scale: Numeric = 1.0,
        diffusion_coefficient: Numeric = 1.0,
        diffusion_unit: str = 'm**2/s',
    ):
        """Initialize a new BrownianTranslationalDiffusion model.

        Parameters
        ----------
        display_name : str
            Display name of the diffusion model.
        unique_name : str or None
            Unique name of the diffusion model. If None, a unique name
            is automatically generated.
        unit : str or sc.Unit, optional
            Energy unit for the underlying Lorentzian components.
            Defaults to "meV".
        scale : float or Parameter, optional
            Scale factor for the diffusion model.
        diffusion_coefficient : float or Parameter, optional
            Diffusion coefficient D. If a number is provided,
            it is assumed to be in the unit given by diffusion_unit.
            Defaults to 1.0.
        diffusion_unit : str, optional
            Unit for the diffusion coefficient D. Default is m**2/s.
            Options are 'meV*Å**2' or 'm**2/s'
        """
        if not isinstance(scale, (Parameter, Numeric)):
            raise TypeError('scale must be a number.')

        if not isinstance(diffusion_coefficient, (Parameter, Numeric)):
            raise TypeError('diffusion_coefficient must be a number.')

        if not isinstance(diffusion_unit, str):
            raise TypeError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.")

        if diffusion_unit == 'meV*Å**2' or diffusion_unit == 'meV*angstrom**2':
            # In this case, hbar is absorbed in the unit of D
            self._hbar = DescriptorNumber('hbar', 1.0)
        elif diffusion_unit == 'm**2/s' or diffusion_unit == 'm^2/s':
            self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar)
        else:
            raise ValueError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.")

        scale = Parameter(name='scale', value=float(scale), fixed=False, min=0.0)

        diffusion_coefficient = Parameter(
            name='diffusion_coefficient',
            value=float(diffusion_coefficient),
            fixed=False,
            unit=diffusion_unit,
        )
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
            unit=unit,
        )
        self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
        self._scale = scale
        self._diffusion_coefficient = diffusion_coefficient

    @property
    def scale(self) -> Parameter:
        """Get the scale parameter of the diffusion model.

        Returns
        -------
        Parameter
            Scale parameter.
        """
        return self._scale

    @scale.setter
    def scale(self, scale: Numeric) -> None:
        """Set the scale parameter of the diffusion model."""
        if not isinstance(scale, (Numeric)):
            raise TypeError('scale must be a number.')
        self._scale.value = scale

    @property
    def diffusion_coefficient(self) -> Parameter:
        """Get the diffusion coefficient parameter D.

        Returns
        -------
        Parameter
            Diffusion coefficient D.
        """
        return self._diffusion_coefficient

    @diffusion_coefficient.setter
    def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None:
        """Set the diffusion coefficient parameter D."""
        if not isinstance(diffusion_coefficient, (Numeric)):
            raise TypeError('diffusion_coefficient must be a number.')
        self._diffusion_coefficient.value = diffusion_coefficient

    def calculate_width(self, Q: Q_type) -> np.ndarray:
        """Calculate the half-width at half-maximum (HWHM) for the
        diffusion model.

        Parameters
        ----------
        Q : np.ndarray | Numeric | list | ArrayLike
            Scattering vector in 1/angstrom

        Returns
        -------
        np.ndarray
            HWHM values in the unit of the model (e.g., meV).
        """

        Q = _validate_and_convert_Q(Q)

        unit_conversion_factor = self._hbar * self.diffusion_coefficient / (self._angstrom**2)
        unit_conversion_factor.convert_unit(self.unit)
        width = Q**2 * unit_conversion_factor.value

        return width

    def calculate_EISF(self, Q: Q_type) -> np.ndarray:
        """Calculate the Elastic Incoherent Structure Factor (EISF) for
        the Brownian translational diffusion model.

        Parameters
        ----------
        Q : np.ndarray | Numeric | list | ArrayLike
            Scattering vector in 1/angstrom

        Returns
        -------
        np.ndarray
            EISF values (dimensionless).
        """
        Q = _validate_and_convert_Q(Q)
        EISF = np.zeros_like(Q)
        return EISF

    def calculate_QISF(self, Q: Q_type) -> np.ndarray:
        """Calculate the Quasi-Elastic Incoherent Structure Factor
        (QISF).

        Parameters
        ----------
        Q : np.ndarray | Numeric | list | ArrayLike
            Scattering vector in 1/angstrom

        Returns
        -------
        np.ndarray
            QISF values (dimensionless).
        """

        Q = _validate_and_convert_Q(Q)
        QISF = np.ones_like(Q)
        return QISF

    def create_component_collections(
        self,
        Q: Q_type,
        component_display_name: str = 'Brownian translational diffusion',
    ) -> List[ComponentCollection]:
        """Create ComponentCollection components for the Brownian
        translational diffusion model at given Q values.

        Args:
        ----------
        Q : Number, list, or np.ndarray
            Scattering vector values.
        component_display_name : str
            Name of the Lorentzian component.
        Returns
        -------
        List[ComponentCollection]
            List of ComponentCollections with Lorentzian components.
        """
        Q = _validate_and_convert_Q(Q)

        if not isinstance(component_display_name, str):
            raise TypeError('component_name must be a string.')

        component_collection_list = [None] * len(Q)
        # In more complex models, this is used to scale the area of the
        # Lorentzians and the delta function.
        QISF = self.calculate_QISF(Q)

        # Create a Lorentzian component for each Q-value, with
        # width D*Q^2 and area equal to scale.
        # No delta function, as the EISF is 0.
        for i, Q_value in enumerate(Q):
            component_collection_list[i] = ComponentCollection(
                display_name=f'{self.display_name}_Q{Q_value:.2f}', unit=self.unit
            )

            lorentzian_component = Lorentzian(
                display_name=component_display_name,
                unit=self.unit,
            )

            # Make the width dependent on Q
            dependency_expression = self._write_width_dependency_expression(Q[i])
            dependency_map = self._write_width_dependency_map_expression()

            lorentzian_component.width.make_dependent_on(
                dependency_expression=dependency_expression,
                dependency_map=dependency_map,
            )

            # Make the area dependent on Q
            area_dependency_map = self._write_area_dependency_map_expression()
            lorentzian_component.area.make_dependent_on(
                dependency_expression=self._write_area_dependency_expression(QISF[i]),
                dependency_map=area_dependency_map,
            )

            # Resolving the dependency can do weird things to the units,
            # so we make sure it's correct.
            lorentzian_component.width.convert_unit(self.unit)
            component_collection_list[i].append_component(lorentzian_component)

        return component_collection_list

    def _write_width_dependency_expression(self, Q: float) -> str:
        """Write the dependency expression for the width as a function
        of Q to make dependent Parameters.

        Parameters
        ----------
        Q : float
            Scattering vector in 1/angstrom
        Returns
        -------
        str
            Dependency expression for the width.
        """
        if not isinstance(Q, (float)):
            raise TypeError('Q must be a float.')

        # Q is given as a float, so we need to add the units
        return f'hbar * D* {Q} **2*1/(angstrom**2)'

    def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]:
        """Write the dependency map expression to make dependent
        Parameters.
        """
        return {
            'D': self.diffusion_coefficient,
            'hbar': self._hbar,
            'angstrom': self._angstrom,
        }

    def _write_area_dependency_expression(self, QISF: float) -> str:
        """Write the dependency expression for the area to make
        dependent Parameters.

        Returns
        -------
        str
            Dependency expression for the area.
        """
        if not isinstance(QISF, (float)):
            raise TypeError('QISF must be a float.')

        return f'{QISF} * scale'

    def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]:
        """Write the dependency map expression to make dependent
        Parameters.
        """
        return {
            'scale': self.scale,
        }

    def __repr__(self):
        """String representation of the BrownianTranslationalDiffusion
        model.
        """
        return (
            f'BrownianTranslationalDiffusion(display_name={self.display_name},'
            f'diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})'
        )

__init__(display_name='BrownianTranslationalDiffusion', unique_name=None, unit='meV', scale=1.0, diffusion_coefficient=1.0, diffusion_unit='m**2/s')

Initialize a new BrownianTranslationalDiffusion model.

Parameters

display_name : str Display name of the diffusion model. unique_name : str or None Unique name of the diffusion model. If None, a unique name is automatically generated. unit : str or sc.Unit, optional Energy unit for the underlying Lorentzian components. Defaults to "meV". scale : float or Parameter, optional Scale factor for the diffusion model. diffusion_coefficient : float or Parameter, optional Diffusion coefficient D. If a number is provided, it is assumed to be in the unit given by diffusion_unit. Defaults to 1.0. diffusion_unit : str, optional Unit for the diffusion coefficient D. Default is m2/s. Options are 'meV*Å2' or 'm**2/s'

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
 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
def __init__(
    self,
    display_name: str | None = 'BrownianTranslationalDiffusion',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
    scale: Numeric = 1.0,
    diffusion_coefficient: Numeric = 1.0,
    diffusion_unit: str = 'm**2/s',
):
    """Initialize a new BrownianTranslationalDiffusion model.

    Parameters
    ----------
    display_name : str
        Display name of the diffusion model.
    unique_name : str or None
        Unique name of the diffusion model. If None, a unique name
        is automatically generated.
    unit : str or sc.Unit, optional
        Energy unit for the underlying Lorentzian components.
        Defaults to "meV".
    scale : float or Parameter, optional
        Scale factor for the diffusion model.
    diffusion_coefficient : float or Parameter, optional
        Diffusion coefficient D. If a number is provided,
        it is assumed to be in the unit given by diffusion_unit.
        Defaults to 1.0.
    diffusion_unit : str, optional
        Unit for the diffusion coefficient D. Default is m**2/s.
        Options are 'meV*Å**2' or 'm**2/s'
    """
    if not isinstance(scale, (Parameter, Numeric)):
        raise TypeError('scale must be a number.')

    if not isinstance(diffusion_coefficient, (Parameter, Numeric)):
        raise TypeError('diffusion_coefficient must be a number.')

    if not isinstance(diffusion_unit, str):
        raise TypeError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.")

    if diffusion_unit == 'meV*Å**2' or diffusion_unit == 'meV*angstrom**2':
        # In this case, hbar is absorbed in the unit of D
        self._hbar = DescriptorNumber('hbar', 1.0)
    elif diffusion_unit == 'm**2/s' or diffusion_unit == 'm^2/s':
        self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar)
    else:
        raise ValueError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.")

    scale = Parameter(name='scale', value=float(scale), fixed=False, min=0.0)

    diffusion_coefficient = Parameter(
        name='diffusion_coefficient',
        value=float(diffusion_coefficient),
        fixed=False,
        unit=diffusion_unit,
    )
    super().__init__(
        display_name=display_name,
        unique_name=unique_name,
        unit=unit,
    )
    self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
    self._scale = scale
    self._diffusion_coefficient = diffusion_coefficient

__repr__()

String representation of the BrownianTranslationalDiffusion model.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
319
320
321
322
323
324
325
326
def __repr__(self):
    """String representation of the BrownianTranslationalDiffusion
    model.
    """
    return (
        f'BrownianTranslationalDiffusion(display_name={self.display_name},'
        f'diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})'
    )

calculate_EISF(Q)

Calculate the Elastic Incoherent Structure Factor (EISF) for the Brownian translational diffusion model.

Parameters

Q : np.ndarray | Numeric | list | ArrayLike Scattering vector in 1/angstrom

Returns

np.ndarray EISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def calculate_EISF(self, Q: Q_type) -> np.ndarray:
    """Calculate the Elastic Incoherent Structure Factor (EISF) for
    the Brownian translational diffusion model.

    Parameters
    ----------
    Q : np.ndarray | Numeric | list | ArrayLike
        Scattering vector in 1/angstrom

    Returns
    -------
    np.ndarray
        EISF values (dimensionless).
    """
    Q = _validate_and_convert_Q(Q)
    EISF = np.zeros_like(Q)
    return EISF

calculate_QISF(Q)

Calculate the Quasi-Elastic Incoherent Structure Factor (QISF).

Parameters

Q : np.ndarray | Numeric | list | ArrayLike Scattering vector in 1/angstrom

Returns

np.ndarray QISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def calculate_QISF(self, Q: Q_type) -> np.ndarray:
    """Calculate the Quasi-Elastic Incoherent Structure Factor
    (QISF).

    Parameters
    ----------
    Q : np.ndarray | Numeric | list | ArrayLike
        Scattering vector in 1/angstrom

    Returns
    -------
    np.ndarray
        QISF values (dimensionless).
    """

    Q = _validate_and_convert_Q(Q)
    QISF = np.ones_like(Q)
    return QISF

calculate_width(Q)

Calculate the half-width at half-maximum (HWHM) for the diffusion model.

Parameters

Q : np.ndarray | Numeric | list | ArrayLike Scattering vector in 1/angstrom

Returns

np.ndarray HWHM values in the unit of the model (e.g., meV).

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def calculate_width(self, Q: Q_type) -> np.ndarray:
    """Calculate the half-width at half-maximum (HWHM) for the
    diffusion model.

    Parameters
    ----------
    Q : np.ndarray | Numeric | list | ArrayLike
        Scattering vector in 1/angstrom

    Returns
    -------
    np.ndarray
        HWHM values in the unit of the model (e.g., meV).
    """

    Q = _validate_and_convert_Q(Q)

    unit_conversion_factor = self._hbar * self.diffusion_coefficient / (self._angstrom**2)
    unit_conversion_factor.convert_unit(self.unit)
    width = Q**2 * unit_conversion_factor.value

    return width

create_component_collections(Q, component_display_name='Brownian translational diffusion')

Create ComponentCollection components for the Brownian translational diffusion model at given Q values.

Args:

Q : Number, list, or np.ndarray Scattering vector values. component_display_name : str Name of the Lorentzian component. Returns


List[ComponentCollection] List of ComponentCollections with Lorentzian components.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
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
def create_component_collections(
    self,
    Q: Q_type,
    component_display_name: str = 'Brownian translational diffusion',
) -> List[ComponentCollection]:
    """Create ComponentCollection components for the Brownian
    translational diffusion model at given Q values.

    Args:
    ----------
    Q : Number, list, or np.ndarray
        Scattering vector values.
    component_display_name : str
        Name of the Lorentzian component.
    Returns
    -------
    List[ComponentCollection]
        List of ComponentCollections with Lorentzian components.
    """
    Q = _validate_and_convert_Q(Q)

    if not isinstance(component_display_name, str):
        raise TypeError('component_name must be a string.')

    component_collection_list = [None] * len(Q)
    # In more complex models, this is used to scale the area of the
    # Lorentzians and the delta function.
    QISF = self.calculate_QISF(Q)

    # Create a Lorentzian component for each Q-value, with
    # width D*Q^2 and area equal to scale.
    # No delta function, as the EISF is 0.
    for i, Q_value in enumerate(Q):
        component_collection_list[i] = ComponentCollection(
            display_name=f'{self.display_name}_Q{Q_value:.2f}', unit=self.unit
        )

        lorentzian_component = Lorentzian(
            display_name=component_display_name,
            unit=self.unit,
        )

        # Make the width dependent on Q
        dependency_expression = self._write_width_dependency_expression(Q[i])
        dependency_map = self._write_width_dependency_map_expression()

        lorentzian_component.width.make_dependent_on(
            dependency_expression=dependency_expression,
            dependency_map=dependency_map,
        )

        # Make the area dependent on Q
        area_dependency_map = self._write_area_dependency_map_expression()
        lorentzian_component.area.make_dependent_on(
            dependency_expression=self._write_area_dependency_expression(QISF[i]),
            dependency_map=area_dependency_map,
        )

        # Resolving the dependency can do weird things to the units,
        # so we make sure it's correct.
        lorentzian_component.width.convert_unit(self.unit)
        component_collection_list[i].append_component(lorentzian_component)

    return component_collection_list

diffusion_coefficient property writable

Get the diffusion coefficient parameter D.

Returns

Parameter Diffusion coefficient D.

scale property writable

Get the scale parameter of the diffusion model.

Returns

Parameter Scale parameter.

DiffusionModelBase

Bases: ModelBase

Base class for constructing diffusion models.

Source code in src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py
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
class DiffusionModelBase(ModelBase):
    """Base class for constructing diffusion models."""

    def __init__(
        self,
        display_name='MyDiffusionModel',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
    ):
        """Initialize a new DiffusionModel.

        Parameters
        ----------
        display_name : str
            Display name of the diffusion model.
        unit : str or sc.Unit, optional
            Unit of the diffusion model. Defaults to "meV".
        """

        try:
            test = DescriptorNumber(name='test', value=1, unit=unit)
            test.convert_unit('meV')
        except Exception as e:
            raise UnitError(
                f'Invalid unit: {unit}. Unit must be a string or scipp Unit and convertible to meV.'  # noqa: E501
            ) from e

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

    @property
    def unit(self) -> str:
        """Get the unit of the DiffusionModel.

        Returns
        -------
        str or sc.Unit or None
        """
        return str(self._unit)

    @unit.setter
    def unit(self, unit_str: str) -> None:
        raise AttributeError(
            (
                f'Unit is read-only. Use convert_unit to change the unit between allowed types '
                f'or create a new {self.__class__.__name__} with the desired unit.'
            )
        )  # noqa: E501

    def __repr__(self):
        """String representation of the Diffusion model."""
        return f'{self.__class__.__name__}(display_name={self.display_name}, unit={self.unit})'

__init__(display_name='MyDiffusionModel', unique_name=None, unit='meV')

Initialize a new DiffusionModel.

Parameters

display_name : str Display name of the diffusion model. unit : str or sc.Unit, optional Unit of the diffusion model. Defaults to "meV".

Source code in src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py
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
def __init__(
    self,
    display_name='MyDiffusionModel',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
):
    """Initialize a new DiffusionModel.

    Parameters
    ----------
    display_name : str
        Display name of the diffusion model.
    unit : str or sc.Unit, optional
        Unit of the diffusion model. Defaults to "meV".
    """

    try:
        test = DescriptorNumber(name='test', value=1, unit=unit)
        test.convert_unit('meV')
    except Exception as e:
        raise UnitError(
            f'Invalid unit: {unit}. Unit must be a string or scipp Unit and convertible to meV.'  # noqa: E501
        ) from e

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

__repr__()

String representation of the Diffusion model.

Source code in src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py
65
66
67
def __repr__(self):
    """String representation of the Diffusion model."""
    return f'{self.__class__.__name__}(display_name={self.display_name}, unit={self.unit})'

unit property writable

Get the unit of the DiffusionModel.

Returns

str or sc.Unit or None

brownian_translational_diffusion

BrownianTranslationalDiffusion

Bases: DiffusionModelBase

Model of Brownian translational diffusion, consisting of a Lorentzian function for each Q-value, where the width is given by :math:DQ^2. Q is assumed to have units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given Q-values.

Example usage: Q=np.linspace(0.5,2,7) energy=np.linspace(-2, 2, 501) scale=1.0 diffusion_coefficient = 2.4e-9 # m^2/s diffusion_model=BrownianTranslationalDiffusion(display_name="DiffusionModel", scale=scale, diffusion_coefficient= diffusion_coefficient) component_collections=diffusion_model.create_component_collections(Q) See also the examples.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
 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
class BrownianTranslationalDiffusion(DiffusionModelBase):
    """Model of Brownian translational diffusion, consisting of a
    Lorentzian function for each Q-value, where the width is given by
    :math:`DQ^2`. Q is assumed to have units of 1/angstrom. Creates
    ComponentCollections with Lorentzian components for given Q-values.

    Example usage:
    Q=np.linspace(0.5,2,7)
    energy=np.linspace(-2, 2, 501)
    scale=1.0
    diffusion_coefficient = 2.4e-9  # m^2/s
    diffusion_model=BrownianTranslationalDiffusion(display_name="DiffusionModel",
    scale=scale, diffusion_coefficient= diffusion_coefficient)
    component_collections=diffusion_model.create_component_collections(Q)
    See also the examples.
    """

    def __init__(
        self,
        display_name: str | None = 'BrownianTranslationalDiffusion',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
        scale: Numeric = 1.0,
        diffusion_coefficient: Numeric = 1.0,
        diffusion_unit: str = 'm**2/s',
    ):
        """Initialize a new BrownianTranslationalDiffusion model.

        Parameters
        ----------
        display_name : str
            Display name of the diffusion model.
        unique_name : str or None
            Unique name of the diffusion model. If None, a unique name
            is automatically generated.
        unit : str or sc.Unit, optional
            Energy unit for the underlying Lorentzian components.
            Defaults to "meV".
        scale : float or Parameter, optional
            Scale factor for the diffusion model.
        diffusion_coefficient : float or Parameter, optional
            Diffusion coefficient D. If a number is provided,
            it is assumed to be in the unit given by diffusion_unit.
            Defaults to 1.0.
        diffusion_unit : str, optional
            Unit for the diffusion coefficient D. Default is m**2/s.
            Options are 'meV*Å**2' or 'm**2/s'
        """
        if not isinstance(scale, (Parameter, Numeric)):
            raise TypeError('scale must be a number.')

        if not isinstance(diffusion_coefficient, (Parameter, Numeric)):
            raise TypeError('diffusion_coefficient must be a number.')

        if not isinstance(diffusion_unit, str):
            raise TypeError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.")

        if diffusion_unit == 'meV*Å**2' or diffusion_unit == 'meV*angstrom**2':
            # In this case, hbar is absorbed in the unit of D
            self._hbar = DescriptorNumber('hbar', 1.0)
        elif diffusion_unit == 'm**2/s' or diffusion_unit == 'm^2/s':
            self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar)
        else:
            raise ValueError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.")

        scale = Parameter(name='scale', value=float(scale), fixed=False, min=0.0)

        diffusion_coefficient = Parameter(
            name='diffusion_coefficient',
            value=float(diffusion_coefficient),
            fixed=False,
            unit=diffusion_unit,
        )
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
            unit=unit,
        )
        self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
        self._scale = scale
        self._diffusion_coefficient = diffusion_coefficient

    @property
    def scale(self) -> Parameter:
        """Get the scale parameter of the diffusion model.

        Returns
        -------
        Parameter
            Scale parameter.
        """
        return self._scale

    @scale.setter
    def scale(self, scale: Numeric) -> None:
        """Set the scale parameter of the diffusion model."""
        if not isinstance(scale, (Numeric)):
            raise TypeError('scale must be a number.')
        self._scale.value = scale

    @property
    def diffusion_coefficient(self) -> Parameter:
        """Get the diffusion coefficient parameter D.

        Returns
        -------
        Parameter
            Diffusion coefficient D.
        """
        return self._diffusion_coefficient

    @diffusion_coefficient.setter
    def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None:
        """Set the diffusion coefficient parameter D."""
        if not isinstance(diffusion_coefficient, (Numeric)):
            raise TypeError('diffusion_coefficient must be a number.')
        self._diffusion_coefficient.value = diffusion_coefficient

    def calculate_width(self, Q: Q_type) -> np.ndarray:
        """Calculate the half-width at half-maximum (HWHM) for the
        diffusion model.

        Parameters
        ----------
        Q : np.ndarray | Numeric | list | ArrayLike
            Scattering vector in 1/angstrom

        Returns
        -------
        np.ndarray
            HWHM values in the unit of the model (e.g., meV).
        """

        Q = _validate_and_convert_Q(Q)

        unit_conversion_factor = self._hbar * self.diffusion_coefficient / (self._angstrom**2)
        unit_conversion_factor.convert_unit(self.unit)
        width = Q**2 * unit_conversion_factor.value

        return width

    def calculate_EISF(self, Q: Q_type) -> np.ndarray:
        """Calculate the Elastic Incoherent Structure Factor (EISF) for
        the Brownian translational diffusion model.

        Parameters
        ----------
        Q : np.ndarray | Numeric | list | ArrayLike
            Scattering vector in 1/angstrom

        Returns
        -------
        np.ndarray
            EISF values (dimensionless).
        """
        Q = _validate_and_convert_Q(Q)
        EISF = np.zeros_like(Q)
        return EISF

    def calculate_QISF(self, Q: Q_type) -> np.ndarray:
        """Calculate the Quasi-Elastic Incoherent Structure Factor
        (QISF).

        Parameters
        ----------
        Q : np.ndarray | Numeric | list | ArrayLike
            Scattering vector in 1/angstrom

        Returns
        -------
        np.ndarray
            QISF values (dimensionless).
        """

        Q = _validate_and_convert_Q(Q)
        QISF = np.ones_like(Q)
        return QISF

    def create_component_collections(
        self,
        Q: Q_type,
        component_display_name: str = 'Brownian translational diffusion',
    ) -> List[ComponentCollection]:
        """Create ComponentCollection components for the Brownian
        translational diffusion model at given Q values.

        Args:
        ----------
        Q : Number, list, or np.ndarray
            Scattering vector values.
        component_display_name : str
            Name of the Lorentzian component.
        Returns
        -------
        List[ComponentCollection]
            List of ComponentCollections with Lorentzian components.
        """
        Q = _validate_and_convert_Q(Q)

        if not isinstance(component_display_name, str):
            raise TypeError('component_name must be a string.')

        component_collection_list = [None] * len(Q)
        # In more complex models, this is used to scale the area of the
        # Lorentzians and the delta function.
        QISF = self.calculate_QISF(Q)

        # Create a Lorentzian component for each Q-value, with
        # width D*Q^2 and area equal to scale.
        # No delta function, as the EISF is 0.
        for i, Q_value in enumerate(Q):
            component_collection_list[i] = ComponentCollection(
                display_name=f'{self.display_name}_Q{Q_value:.2f}', unit=self.unit
            )

            lorentzian_component = Lorentzian(
                display_name=component_display_name,
                unit=self.unit,
            )

            # Make the width dependent on Q
            dependency_expression = self._write_width_dependency_expression(Q[i])
            dependency_map = self._write_width_dependency_map_expression()

            lorentzian_component.width.make_dependent_on(
                dependency_expression=dependency_expression,
                dependency_map=dependency_map,
            )

            # Make the area dependent on Q
            area_dependency_map = self._write_area_dependency_map_expression()
            lorentzian_component.area.make_dependent_on(
                dependency_expression=self._write_area_dependency_expression(QISF[i]),
                dependency_map=area_dependency_map,
            )

            # Resolving the dependency can do weird things to the units,
            # so we make sure it's correct.
            lorentzian_component.width.convert_unit(self.unit)
            component_collection_list[i].append_component(lorentzian_component)

        return component_collection_list

    def _write_width_dependency_expression(self, Q: float) -> str:
        """Write the dependency expression for the width as a function
        of Q to make dependent Parameters.

        Parameters
        ----------
        Q : float
            Scattering vector in 1/angstrom
        Returns
        -------
        str
            Dependency expression for the width.
        """
        if not isinstance(Q, (float)):
            raise TypeError('Q must be a float.')

        # Q is given as a float, so we need to add the units
        return f'hbar * D* {Q} **2*1/(angstrom**2)'

    def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]:
        """Write the dependency map expression to make dependent
        Parameters.
        """
        return {
            'D': self.diffusion_coefficient,
            'hbar': self._hbar,
            'angstrom': self._angstrom,
        }

    def _write_area_dependency_expression(self, QISF: float) -> str:
        """Write the dependency expression for the area to make
        dependent Parameters.

        Returns
        -------
        str
            Dependency expression for the area.
        """
        if not isinstance(QISF, (float)):
            raise TypeError('QISF must be a float.')

        return f'{QISF} * scale'

    def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]:
        """Write the dependency map expression to make dependent
        Parameters.
        """
        return {
            'scale': self.scale,
        }

    def __repr__(self):
        """String representation of the BrownianTranslationalDiffusion
        model.
        """
        return (
            f'BrownianTranslationalDiffusion(display_name={self.display_name},'
            f'diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})'
        )
__init__(display_name='BrownianTranslationalDiffusion', unique_name=None, unit='meV', scale=1.0, diffusion_coefficient=1.0, diffusion_unit='m**2/s')

Initialize a new BrownianTranslationalDiffusion model.

Parameters

display_name : str Display name of the diffusion model. unique_name : str or None Unique name of the diffusion model. If None, a unique name is automatically generated. unit : str or sc.Unit, optional Energy unit for the underlying Lorentzian components. Defaults to "meV". scale : float or Parameter, optional Scale factor for the diffusion model. diffusion_coefficient : float or Parameter, optional Diffusion coefficient D. If a number is provided, it is assumed to be in the unit given by diffusion_unit. Defaults to 1.0. diffusion_unit : str, optional Unit for the diffusion coefficient D. Default is m2/s. Options are 'meV*Å2' or 'm**2/s'

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
 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
def __init__(
    self,
    display_name: str | None = 'BrownianTranslationalDiffusion',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
    scale: Numeric = 1.0,
    diffusion_coefficient: Numeric = 1.0,
    diffusion_unit: str = 'm**2/s',
):
    """Initialize a new BrownianTranslationalDiffusion model.

    Parameters
    ----------
    display_name : str
        Display name of the diffusion model.
    unique_name : str or None
        Unique name of the diffusion model. If None, a unique name
        is automatically generated.
    unit : str or sc.Unit, optional
        Energy unit for the underlying Lorentzian components.
        Defaults to "meV".
    scale : float or Parameter, optional
        Scale factor for the diffusion model.
    diffusion_coefficient : float or Parameter, optional
        Diffusion coefficient D. If a number is provided,
        it is assumed to be in the unit given by diffusion_unit.
        Defaults to 1.0.
    diffusion_unit : str, optional
        Unit for the diffusion coefficient D. Default is m**2/s.
        Options are 'meV*Å**2' or 'm**2/s'
    """
    if not isinstance(scale, (Parameter, Numeric)):
        raise TypeError('scale must be a number.')

    if not isinstance(diffusion_coefficient, (Parameter, Numeric)):
        raise TypeError('diffusion_coefficient must be a number.')

    if not isinstance(diffusion_unit, str):
        raise TypeError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.")

    if diffusion_unit == 'meV*Å**2' or diffusion_unit == 'meV*angstrom**2':
        # In this case, hbar is absorbed in the unit of D
        self._hbar = DescriptorNumber('hbar', 1.0)
    elif diffusion_unit == 'm**2/s' or diffusion_unit == 'm^2/s':
        self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar)
    else:
        raise ValueError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.")

    scale = Parameter(name='scale', value=float(scale), fixed=False, min=0.0)

    diffusion_coefficient = Parameter(
        name='diffusion_coefficient',
        value=float(diffusion_coefficient),
        fixed=False,
        unit=diffusion_unit,
    )
    super().__init__(
        display_name=display_name,
        unique_name=unique_name,
        unit=unit,
    )
    self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
    self._scale = scale
    self._diffusion_coefficient = diffusion_coefficient
__repr__()

String representation of the BrownianTranslationalDiffusion model.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
319
320
321
322
323
324
325
326
def __repr__(self):
    """String representation of the BrownianTranslationalDiffusion
    model.
    """
    return (
        f'BrownianTranslationalDiffusion(display_name={self.display_name},'
        f'diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})'
    )
calculate_EISF(Q)

Calculate the Elastic Incoherent Structure Factor (EISF) for the Brownian translational diffusion model.

Parameters

Q : np.ndarray | Numeric | list | ArrayLike Scattering vector in 1/angstrom

Returns

np.ndarray EISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def calculate_EISF(self, Q: Q_type) -> np.ndarray:
    """Calculate the Elastic Incoherent Structure Factor (EISF) for
    the Brownian translational diffusion model.

    Parameters
    ----------
    Q : np.ndarray | Numeric | list | ArrayLike
        Scattering vector in 1/angstrom

    Returns
    -------
    np.ndarray
        EISF values (dimensionless).
    """
    Q = _validate_and_convert_Q(Q)
    EISF = np.zeros_like(Q)
    return EISF
calculate_QISF(Q)

Calculate the Quasi-Elastic Incoherent Structure Factor (QISF).

Parameters

Q : np.ndarray | Numeric | list | ArrayLike Scattering vector in 1/angstrom

Returns

np.ndarray QISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def calculate_QISF(self, Q: Q_type) -> np.ndarray:
    """Calculate the Quasi-Elastic Incoherent Structure Factor
    (QISF).

    Parameters
    ----------
    Q : np.ndarray | Numeric | list | ArrayLike
        Scattering vector in 1/angstrom

    Returns
    -------
    np.ndarray
        QISF values (dimensionless).
    """

    Q = _validate_and_convert_Q(Q)
    QISF = np.ones_like(Q)
    return QISF
calculate_width(Q)

Calculate the half-width at half-maximum (HWHM) for the diffusion model.

Parameters

Q : np.ndarray | Numeric | list | ArrayLike Scattering vector in 1/angstrom

Returns

np.ndarray HWHM values in the unit of the model (e.g., meV).

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def calculate_width(self, Q: Q_type) -> np.ndarray:
    """Calculate the half-width at half-maximum (HWHM) for the
    diffusion model.

    Parameters
    ----------
    Q : np.ndarray | Numeric | list | ArrayLike
        Scattering vector in 1/angstrom

    Returns
    -------
    np.ndarray
        HWHM values in the unit of the model (e.g., meV).
    """

    Q = _validate_and_convert_Q(Q)

    unit_conversion_factor = self._hbar * self.diffusion_coefficient / (self._angstrom**2)
    unit_conversion_factor.convert_unit(self.unit)
    width = Q**2 * unit_conversion_factor.value

    return width
create_component_collections(Q, component_display_name='Brownian translational diffusion')

Create ComponentCollection components for the Brownian translational diffusion model at given Q values.

Args:

Q : Number, list, or np.ndarray Scattering vector values. component_display_name : str Name of the Lorentzian component. Returns


List[ComponentCollection] List of ComponentCollections with Lorentzian components.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
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
def create_component_collections(
    self,
    Q: Q_type,
    component_display_name: str = 'Brownian translational diffusion',
) -> List[ComponentCollection]:
    """Create ComponentCollection components for the Brownian
    translational diffusion model at given Q values.

    Args:
    ----------
    Q : Number, list, or np.ndarray
        Scattering vector values.
    component_display_name : str
        Name of the Lorentzian component.
    Returns
    -------
    List[ComponentCollection]
        List of ComponentCollections with Lorentzian components.
    """
    Q = _validate_and_convert_Q(Q)

    if not isinstance(component_display_name, str):
        raise TypeError('component_name must be a string.')

    component_collection_list = [None] * len(Q)
    # In more complex models, this is used to scale the area of the
    # Lorentzians and the delta function.
    QISF = self.calculate_QISF(Q)

    # Create a Lorentzian component for each Q-value, with
    # width D*Q^2 and area equal to scale.
    # No delta function, as the EISF is 0.
    for i, Q_value in enumerate(Q):
        component_collection_list[i] = ComponentCollection(
            display_name=f'{self.display_name}_Q{Q_value:.2f}', unit=self.unit
        )

        lorentzian_component = Lorentzian(
            display_name=component_display_name,
            unit=self.unit,
        )

        # Make the width dependent on Q
        dependency_expression = self._write_width_dependency_expression(Q[i])
        dependency_map = self._write_width_dependency_map_expression()

        lorentzian_component.width.make_dependent_on(
            dependency_expression=dependency_expression,
            dependency_map=dependency_map,
        )

        # Make the area dependent on Q
        area_dependency_map = self._write_area_dependency_map_expression()
        lorentzian_component.area.make_dependent_on(
            dependency_expression=self._write_area_dependency_expression(QISF[i]),
            dependency_map=area_dependency_map,
        )

        # Resolving the dependency can do weird things to the units,
        # so we make sure it's correct.
        lorentzian_component.width.convert_unit(self.unit)
        component_collection_list[i].append_component(lorentzian_component)

    return component_collection_list
diffusion_coefficient property writable

Get the diffusion coefficient parameter D.

Returns

Parameter Diffusion coefficient D.

scale property writable

Get the scale parameter of the diffusion model.

Returns

Parameter Scale parameter.

diffusion_model_base

DiffusionModelBase

Bases: ModelBase

Base class for constructing diffusion models.

Source code in src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py
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
class DiffusionModelBase(ModelBase):
    """Base class for constructing diffusion models."""

    def __init__(
        self,
        display_name='MyDiffusionModel',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
    ):
        """Initialize a new DiffusionModel.

        Parameters
        ----------
        display_name : str
            Display name of the diffusion model.
        unit : str or sc.Unit, optional
            Unit of the diffusion model. Defaults to "meV".
        """

        try:
            test = DescriptorNumber(name='test', value=1, unit=unit)
            test.convert_unit('meV')
        except Exception as e:
            raise UnitError(
                f'Invalid unit: {unit}. Unit must be a string or scipp Unit and convertible to meV.'  # noqa: E501
            ) from e

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

    @property
    def unit(self) -> str:
        """Get the unit of the DiffusionModel.

        Returns
        -------
        str or sc.Unit or None
        """
        return str(self._unit)

    @unit.setter
    def unit(self, unit_str: str) -> None:
        raise AttributeError(
            (
                f'Unit is read-only. Use convert_unit to change the unit between allowed types '
                f'or create a new {self.__class__.__name__} with the desired unit.'
            )
        )  # noqa: E501

    def __repr__(self):
        """String representation of the Diffusion model."""
        return f'{self.__class__.__name__}(display_name={self.display_name}, unit={self.unit})'
__init__(display_name='MyDiffusionModel', unique_name=None, unit='meV')

Initialize a new DiffusionModel.

Parameters

display_name : str Display name of the diffusion model. unit : str or sc.Unit, optional Unit of the diffusion model. Defaults to "meV".

Source code in src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py
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
def __init__(
    self,
    display_name='MyDiffusionModel',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
):
    """Initialize a new DiffusionModel.

    Parameters
    ----------
    display_name : str
        Display name of the diffusion model.
    unit : str or sc.Unit, optional
        Unit of the diffusion model. Defaults to "meV".
    """

    try:
        test = DescriptorNumber(name='test', value=1, unit=unit)
        test.convert_unit('meV')
    except Exception as e:
        raise UnitError(
            f'Invalid unit: {unit}. Unit must be a string or scipp Unit and convertible to meV.'  # noqa: E501
        ) from e

    super().__init__(display_name=display_name, unique_name=unique_name)
    self._unit = unit
__repr__()

String representation of the Diffusion model.

Source code in src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py
65
66
67
def __repr__(self):
    """String representation of the Diffusion model."""
    return f'{self.__class__.__name__}(display_name={self.display_name}, unit={self.unit})'
unit property writable

Get the unit of the DiffusionModel.

Returns

str or sc.Unit or None

model_base

ModelBase

Bases: ModelBase

Base class for Sample Models.

Contains common functionality for models with components and Q dependence.

Parameters

display_name : str Display name of the model. unique_name : str | None Unique name of the model. If None, a unique name will be generated. unit : str | sc.Unit | None Unit of the model. If None, unitless. components : ModelComponent | ComponentCollection | None Template components of the model. If None, no components are added. These components are copied into ComponentCollections for each Q value. Q : Q_type | None Q values for the model. If None, Q is not set.

Source code in src/easydynamics/sample_model/model_base.py
 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
class ModelBase(EasyScienceModelBase):
    """Base class for Sample Models.

    Contains common functionality for models with components and
    Q dependence.

    Parameters
    ----------
    display_name : str
        Display name of the model.
    unique_name : str | None
        Unique name of the model. If None, a unique name will be
        generated.
    unit : str | sc.Unit | None
        Unit of the model. If None, unitless.
    components : ModelComponent | ComponentCollection | None
        Template components of the model. If None, no components
        are added.
        These components are copied into ComponentCollections for each
        Q value.
    Q : Q_type | None
        Q values for the model. If None, Q is not set.
    """

    def __init__(
        self,
        display_name: str = 'MyModelBase',
        unique_name: str | None = None,
        unit: str | sc.Unit | None = 'meV',
        components: ModelComponent | ComponentCollection | None = None,
        Q: Q_type | None = None,
    ):
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
        )
        self._unit = _validate_unit(unit)
        self._Q = _validate_and_convert_Q(Q)

        if components is not None and not isinstance(
            components, (ModelComponent, ComponentCollection)
        ):
            raise TypeError(
                f'Components must be a ModelComponent, a ComponentCollection or None, '
                f'got {type(components).__name__}'
            )

        self._components = ComponentCollection()
        if isinstance(components, (ModelComponent, ComponentCollection)):
            self.append_component(components)

        self._generate_component_collections()

    def evaluate(
        self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray
    ) -> list[np.ndarray]:
        """Evaluate the sample model at all Q for the given x values.

        Parameters
        ----------
        x : Number, list, np.ndarray, sc.Variable, or sc.DataArray
            Energy axis.

        Returns
        -------
        list[np.ndarray]
            Evaluated model values.
        """

        if not self._component_collections:
            raise ValueError(
                'No components in the model to evaluate. '
                'Run generate_component_collections() first'
            )
        y = [collection.evaluate(x) for collection in self._component_collections]

        return y

    # ------------------------------------------------------------------
    # Component management
    # ------------------------------------------------------------------
    def append_component(self, component: ModelComponent | ComponentCollection) -> None:
        """Append a ModelComponent or ComponentCollection to the
        SampleModel.

        Args:
            component (ModelComponent | ComponentCollection):
            The ModelComponent or ComponentCollection to append.
        """
        self._components.append_component(component)
        self._generate_component_collections()

    def remove_component(self, unique_name: str) -> None:
        """Remove a ModelComponent from the SampleModel by its unique
        name.

        Args:
            unique_name (str): The unique name of the ModelComponent
            to remove.
        """
        self._components.remove_component(unique_name)
        self._generate_component_collections()

    def clear_components(self) -> None:
        """Clear all ModelComponents from the SampleModel."""
        self._components.clear_components()
        self._generate_component_collections()

    # ------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------

    @property
    def unit(self) -> str | sc.Unit:
        """Get the unit of the ComponentCollection.

        Returns
        -------
        str or sc.Unit or None
        """
        return self._unit

    @unit.setter
    def unit(self, unit_str: str) -> None:
        raise AttributeError(
            (
                f'Unit is read-only. Use convert_unit to change the unit between allowed types '
                f'or create a new {self.__class__.__name__} with the desired unit.'
            )
        )  # noqa: E501

    def convert_unit(self, unit: str | sc.Unit) -> None:
        """Convert the unit of the ComponentCollection and all its
        components.
        """

        old_unit = self._unit

        try:
            for component in self.components:
                component.convert_unit(unit)
            self._unit = unit
        except Exception as e:
            # Attempt to rollback on failure
            try:
                for component in self.components:
                    component.convert_unit(old_unit)
            except Exception:  # noqa: S110
                pass  # Best effort rollback
            raise e
        self._generate_component_collections()

    @property
    def components(self) -> list[ModelComponent]:
        """Get the components of the SampleModel."""
        return self._components.components

    @components.setter
    def components(self, value: ModelComponent | ComponentCollection | None) -> None:
        """Set the components of the SampleModel."""
        if not isinstance(value, (ModelComponent, ComponentCollection, type(None))):
            raise TypeError('Components must be a ModelComponent or a ComponentCollection')

        self.clear_components()
        if value is not None:
            self.append_component(value)

    @property
    def Q(self) -> np.ndarray | None:
        """Get the Q values of the SampleModel."""
        return self._Q

    @Q.setter
    def Q(self, value: Q_type | None) -> None:
        """Set the Q values of the SampleModel."""
        self._Q = _validate_and_convert_Q(value)
        self._generate_component_collections()

    # ------------------------------------------------------------------
    # Private methods
    # ------------------------------------------------------------------

    def _generate_component_collections(self) -> None:
        """Generate ComponentCollections for each Q value."""
        # TODO regenerate automatically if Q or components have changed

        if self._Q is None:
            warnings.warn('Q is not set. No component collections generated', UserWarning)
            self._component_collections = []
            return

        self._component_collections = [ComponentCollection() for _ in self._Q]

        # Add copies of components from self._components to each
        # component collection
        for collection in self._component_collections:
            for component in self._components.components:
                collection.append_component(copy(component))

    def get_all_variables(self):
        """Get all Parameters and Descriptors from all
        ComponentCollections in the ModelBase.

        Ignores the Parameters and Descriptors in self._components as
        these are just templates.
        """

        all_vars = [
            var
            for collection in self._component_collections
            for var in collection.get_all_variables()
        ]
        return all_vars

    # ------------------------------------------------------------------
    # dunder methods
    # ------------------------------------------------------------------

    def __repr__(self):
        return (
            f'{self.__class__.__name__}(unique_name={self.unique_name}, '
            f'unit={self.unit}), Q = {self.Q}, components = {self.components}'
        )

Q property writable

Get the Q values of the SampleModel.

append_component(component)

Append a ModelComponent or ComponentCollection to the SampleModel.

Parameters:

Name Type Description Default
component ModelComponent | ComponentCollection
required
Source code in src/easydynamics/sample_model/model_base.py
100
101
102
103
104
105
106
107
108
109
def append_component(self, component: ModelComponent | ComponentCollection) -> None:
    """Append a ModelComponent or ComponentCollection to the
    SampleModel.

    Args:
        component (ModelComponent | ComponentCollection):
        The ModelComponent or ComponentCollection to append.
    """
    self._components.append_component(component)
    self._generate_component_collections()

clear_components()

Clear all ModelComponents from the SampleModel.

Source code in src/easydynamics/sample_model/model_base.py
122
123
124
125
def clear_components(self) -> None:
    """Clear all ModelComponents from the SampleModel."""
    self._components.clear_components()
    self._generate_component_collections()

components property writable

Get the components of the SampleModel.

convert_unit(unit)

Convert the unit of the ComponentCollection and all its components.

Source code in src/easydynamics/sample_model/model_base.py
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def convert_unit(self, unit: str | sc.Unit) -> None:
    """Convert the unit of the ComponentCollection and all its
    components.
    """

    old_unit = self._unit

    try:
        for component in self.components:
            component.convert_unit(unit)
        self._unit = unit
    except Exception as e:
        # Attempt to rollback on failure
        try:
            for component in self.components:
                component.convert_unit(old_unit)
        except Exception:  # noqa: S110
            pass  # Best effort rollback
        raise e
    self._generate_component_collections()

evaluate(x)

Evaluate the sample model at all Q for the given x values.

Parameters

x : Number, list, np.ndarray, sc.Variable, or sc.DataArray Energy axis.

Returns

list[np.ndarray] Evaluated model values.

Source code in src/easydynamics/sample_model/model_base.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def evaluate(
    self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray
) -> list[np.ndarray]:
    """Evaluate the sample model at all Q for the given x values.

    Parameters
    ----------
    x : Number, list, np.ndarray, sc.Variable, or sc.DataArray
        Energy axis.

    Returns
    -------
    list[np.ndarray]
        Evaluated model values.
    """

    if not self._component_collections:
        raise ValueError(
            'No components in the model to evaluate. '
            'Run generate_component_collections() first'
        )
    y = [collection.evaluate(x) for collection in self._component_collections]

    return y

get_all_variables()

Get all Parameters and Descriptors from all ComponentCollections in the ModelBase.

Ignores the Parameters and Descriptors in self._components as these are just templates.

Source code in src/easydynamics/sample_model/model_base.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
def get_all_variables(self):
    """Get all Parameters and Descriptors from all
    ComponentCollections in the ModelBase.

    Ignores the Parameters and Descriptors in self._components as
    these are just templates.
    """

    all_vars = [
        var
        for collection in self._component_collections
        for var in collection.get_all_variables()
    ]
    return all_vars

remove_component(unique_name)

Remove a ModelComponent from the SampleModel by its unique name.

Parameters:

Name Type Description Default
unique_name str

The unique name of the ModelComponent

required
Source code in src/easydynamics/sample_model/model_base.py
111
112
113
114
115
116
117
118
119
120
def remove_component(self, unique_name: str) -> None:
    """Remove a ModelComponent from the SampleModel by its unique
    name.

    Args:
        unique_name (str): The unique name of the ModelComponent
        to remove.
    """
    self._components.remove_component(unique_name)
    self._generate_component_collections()

unit property writable

Get the unit of the ComponentCollection.

Returns

str or sc.Unit or None

resolution_model

ResolutionModel

Bases: ModelBase

ResolutionModel represents a model of the instrment resolution in an experiment at various Q.

Parameters

display_name : str Display name of the model. unique_name : str | None Unique name of the model. If None, a unique name will be generated. unit : str | sc.Unit | None Unit of the model. If None, unitless. components : ModelComponent | ComponentCollection | None Template components of the model. If None, no components are added. These components are copied into ComponentCollections for each Q value. Q : Number, list, np.ndarray or sc.Variable | None Q values for the model. If None, Q is not set.

Source code in src/easydynamics/sample_model/resolution_model.py
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
class ResolutionModel(ModelBase):
    """ResolutionModel represents a model of the instrment resolution in
    an experiment at various Q.

    Parameters
    ----------
    display_name : str
        Display name of the model.
    unique_name : str | None
        Unique name of the model. If None, a unique name will be
        generated.
    unit : str | sc.Unit | None
        Unit of the model. If None, unitless.
    components : ModelComponent | ComponentCollection | None
        Template components of the model. If None, no components
        are added. These components are copied into ComponentCollections
        for each Q value.
    Q : Number, list, np.ndarray or sc.Variable | None
        Q values for the model. If None, Q is not set.
    """

    def __init__(
        self,
        display_name: str = 'MyResolutionModel',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
        components: ComponentCollection | ModelComponent | None = None,
        Q: Q_type | None = None,
    ):
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
            unit=unit,
            components=components,
            Q=Q,
        )

    def append_component(self, component: ModelComponent | ComponentCollection):
        """Append a component to the ResolutionModel.

        Does not allow DeltaFunction or Polynomial components, as these
        are not physical resolution components.
        Args:
            component (ModelComponent | ComponentCollection):
            Component(s) to append.
        Raises:
            TypeError: If the component is a DeltaFunction or Polynomial
        """
        if isinstance(component, ComponentCollection):
            components = component.components
        else:
            components = (component,)

        for comp in components:
            if isinstance(comp, (DeltaFunction, Polynomial)):
                raise TypeError(
                    f'Component in ResolutionModel cannot be a {comp.__class__.__name__}'
                )

        super().append_component(component)

append_component(component)

Append a component to the ResolutionModel.

Does not allow DeltaFunction or Polynomial components, as these are not physical resolution components. Args: component (ModelComponent | ComponentCollection): Component(s) to append. Raises: TypeError: If the component is a DeltaFunction or Polynomial

Source code in src/easydynamics/sample_model/resolution_model.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
def append_component(self, component: ModelComponent | ComponentCollection):
    """Append a component to the ResolutionModel.

    Does not allow DeltaFunction or Polynomial components, as these
    are not physical resolution components.
    Args:
        component (ModelComponent | ComponentCollection):
        Component(s) to append.
    Raises:
        TypeError: If the component is a DeltaFunction or Polynomial
    """
    if isinstance(component, ComponentCollection):
        components = component.components
    else:
        components = (component,)

    for comp in components:
        if isinstance(comp, (DeltaFunction, Polynomial)):
            raise TypeError(
                f'Component in ResolutionModel cannot be a {comp.__class__.__name__}'
            )

    super().append_component(component)

sample_model

SampleModel

Bases: ModelBase

SampleModel represents a model of a sample with components and diffusion models, parameterized by Q and optionally temperature. Generates ComponentCollections for each Q value, combining components from the base model and diffusion models.

Applies detailed balancing based on temperature if provided. Parameters


display_name : str Display name of the model. unique_name : str | None Unique name of the model. If None, a unique name will be generated. unit : str | sc.Unit | None Unit of the model. If None, unitless. components : ModelComponent | ComponentCollection | None Template components of the model. If None, no components are added. These components are copied into ComponentCollections for each Q value. Q : Number, list, np.ndarray or sc.array or None. Q values for the model. If None, Q is not set. diffusion_models : DiffusionModelBase | list[DiffusionModelBase] | None Diffusion models to include in the SampleModel. If None, no diffusion models are added temperature : float | None Temperature for detailed balancing. If None, no detailed balancing is applied. temperature_unit : str | sc.Unit Unit of the temperature. Defaults to "K". divide_by_temperature : bool Whether to divide the detailed balance factor by temperature. Defaults to True.

Source code in src/easydynamics/sample_model/sample_model.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
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
class SampleModel(ModelBase):
    """SampleModel represents a model of a sample with components and
    diffusion models, parameterized by Q and optionally temperature.
    Generates ComponentCollections for each Q value, combining
    components from the base model and diffusion models.

    Applies detailed balancing based on temperature if provided.
    Parameters
    ----------
    display_name : str
        Display name of the model.
    unique_name : str | None
        Unique name of the model. If None, a unique name will be
        generated.
    unit : str | sc.Unit | None
        Unit of the model. If None, unitless.
    components : ModelComponent | ComponentCollection | None
        Template components of the model. If None, no components are
        added. These components are copied into ComponentCollections
        for each Q value.
    Q : Number, list, np.ndarray or sc.array or None.
        Q values for the model. If None, Q is not set.
    diffusion_models : DiffusionModelBase | list[DiffusionModelBase]
                        | None
        Diffusion models to include in the SampleModel. If None,
        no diffusion models are added
    temperature : float | None
        Temperature for detailed balancing.
        If None, no detailed balancing is applied.
    temperature_unit : str | sc.Unit
        Unit of the temperature. Defaults to "K".
    divide_by_temperature : bool
        Whether to divide the detailed balance factor by temperature.
        Defaults to True.
    """

    def __init__(
        self,
        display_name: str = 'MySampleModel',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
        components: ComponentCollection | ModelComponent | None = None,
        Q: Q_type | None = None,
        diffusion_models: DiffusionModelBase | list[DiffusionModelBase] | None = None,
        temperature: float | None = None,
        temperature_unit: str | sc.Unit = 'K',
        divide_by_temperature: bool = True,
    ):
        if diffusion_models is None:
            self._diffusion_models = []
        elif isinstance(diffusion_models, DiffusionModelBase):
            self._diffusion_models = [diffusion_models]
        else:
            if not isinstance(diffusion_models, list) or not all(
                isinstance(dm, DiffusionModelBase) for dm in diffusion_models
            ):
                raise TypeError(
                    'diffusion_models must be a DiffusionModelBase, '
                    'a list of DiffusionModelBase or None'
                )
            self._diffusion_models = diffusion_models

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

        if temperature is None:
            self._temperature = None
        else:
            if not isinstance(temperature, Numeric):
                raise TypeError('temperature must be a number or None')

            if temperature < 0:
                raise ValueError('temperature must be non-negative')
            self._temperature = Parameter(
                name='Temperature',
                value=temperature,
                unit=temperature_unit,
                display_name='Temperature',
                fixed=True,
            )
        self._temperature_unit = temperature_unit

        if not isinstance(divide_by_temperature, bool):
            raise TypeError('divide_by_temperature must be True or False')
        self._divide_by_temperature = divide_by_temperature

    # ------------------------------------------------------------------
    # Component management
    # ------------------------------------------------------------------

    def append_diffusion_model(self, diffusion_model: DiffusionModelBase) -> None:
        """Append a DiffusionModel to the SampleModel.

        Args:
            diffusion_model (DiffusionModelBase): The DiffusionModel
            to append.
        """

        if not isinstance(diffusion_model, DiffusionModelBase):
            raise TypeError(
                f'diffusion_model must be a DiffusionModelBase, got {type(diffusion_model).__name__}'  # noqa: E501
            )

        self._diffusion_models.append(diffusion_model)
        self._generate_component_collections()

    def remove_diffusion_model(self, name: 'str') -> None:
        """Remove a DiffusionModel from the SampleModel by unique name.

        Args:
            name (str): The unique name of the DiffusionModel to remove.
        """
        for i, dm in enumerate(self._diffusion_models):
            if dm.unique_name == name:
                del self._diffusion_models[i]
                self._generate_component_collections()
                return
        raise ValueError(
            f'No DiffusionModel with unique name {name} found. \n'
            f'The available unique names are: {[dm.unique_name for dm in self._diffusion_models]}'
        )

    def clear_diffusion_models(self) -> None:
        """Clear all DiffusionModels from the SampleModel."""
        self._diffusion_models.clear()
        self._generate_component_collections()

    # ------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------

    @property
    def diffusion_models(self) -> list[DiffusionModelBase]:
        """Get the diffusion models of the SampleModel."""
        return self._diffusion_models

    @diffusion_models.setter
    def diffusion_models(
        self, value: DiffusionModelBase | list[DiffusionModelBase] | None
    ) -> None:
        """Set the diffusion models of the SampleModel."""
        if value is None:
            self._diffusion_models = []
            return
        if isinstance(value, DiffusionModelBase):
            self._diffusion_models = [value]
            return
        if not isinstance(value, list) or not all(
            isinstance(dm, DiffusionModelBase) for dm in value
        ):
            raise TypeError(
                'diffusion_models must be a DiffusionModelBase, a list of DiffusionModelBase, '
                'or None'
            )
        self._diffusion_models = value
        self._generate_component_collections()

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

    @temperature.setter
    def temperature(self, value: Numeric | None) -> None:
        """Set the temperature of the SampleModel."""
        if value is None:
            self._temperature = None
            return

        if not isinstance(value, Numeric):
            raise TypeError('temperature must be a number or None')

        if value < 0:
            raise ValueError('temperature must be non-negative')

        if self._temperature is None:
            self._temperature = Parameter(
                name='Temperature',
                value=value,
                unit=self._temperature_unit,
                display_name='Temperature',
                fixed=True,
            )
        else:
            self._temperature.value = value

    @property
    def temperature_unit(self) -> str | sc.Unit:
        """Get the temperature unit of the SampleModel."""
        return self._temperature_unit

    @temperature_unit.setter
    def temperature_unit(self, value: str | sc.Unit) -> None:
        raise AttributeError(
            f'Temperature_unit is read-only. Use convert_temperature_unit to change the unit between allowed types '  # noqa: E501
            f'or create a new {self.__class__.__name__} with the desired unit.'
        )  # noqa: E501

    def convert_temperature_unit(self, unit: str | sc.Unit) -> None:
        """Convert the unit of the temperature Parameter."""

        if self._temperature is None:
            raise ValueError('Temperature is not set, cannot convert unit.')

        old_unit = self._temperature.unit

        try:
            self._temperature.convert_unit(unit)
            self._temperature_unit = unit
        except Exception as e:
            # Attempt to rollback on failure
            try:
                self._temperature.convert_unit(old_unit)
            except Exception:  # noqa: S110
                pass  # Best effort rollback
            raise e

    @property
    def divide_by_temperature(self) -> bool:
        """Get whether to divide the detailed balance factor by
        temperature.
        """
        return self._divide_by_temperature

    @divide_by_temperature.setter
    def divide_by_temperature(self, value: bool) -> None:
        """Set whether to divide the detailed balance factor by
        temperature.
        """
        if not isinstance(value, bool):
            raise TypeError('divide_by_temperature must be True or False')
        self._divide_by_temperature = value

    # ------------------------------------------------------------------
    # Other methods
    # ------------------------------------------------------------------

    def evaluate(
        self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray
    ) -> list[np.ndarray]:
        """Evaluate the sample model at all Q for the given x values.

        Parameters
        ----------
        x : Number, list, np.ndarray, sc.Variable, or sc.DataArray
            Energy axis.

        Returns
        -------
        list[np.ndarray]
            List of evaluated model values for each Q.
        """

        y = super().evaluate(x)

        if self._temperature is not None:
            DBF = _detailed_balance_factor(
                energy=x,
                temperature=self._temperature,
                divide_by_temperature=self._divide_by_temperature,
                energy_unit=self._unit,
            )
            y = [yi * DBF for yi in y]

        return y

    def get_all_variables(self):
        """Get all Parameters and Descriptors from all
        ComponentCollections in the SampleModel.

        Also includes temperature if set and all variables from
        diffusion models. Ignores the Parameters and Descriptors in
        self._components as these are just templates.
        """
        all_vars = super().get_all_variables()
        if self._temperature is not None:
            all_vars.append(self._temperature)

        for diffusion_model in self._diffusion_models:
            all_vars.extend(diffusion_model.get_all_variables())

        return all_vars

    # ------------------------------------------------------------------
    # Private methods
    # ------------------------------------------------------------------

    def _generate_component_collections(self) -> None:
        """Generate ComponentCollections from the DiffusionModels for
        each Q and add the components from self._components.
        """
        # TODO regenerate automatically if Q, diffusion models
        # or components have changed
        super()._generate_component_collections()

        if self._Q is None:
            return
        # Generate components from diffusion models
        # and add to component collections
        for diffusion_model in self._diffusion_models:
            diffusion_collections = diffusion_model.create_component_collections(Q=self._Q)
            for target, source in zip(self._component_collections, diffusion_collections):
                for component in source.components:
                    target.append_component(component)

    # ------------------------------------------------------------------
    # dunder methods
    # ------------------------------------------------------------------

    def __repr__(self):
        return (
            f'{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit}), '
            f'Q = {self._Q}, '
            f'components = {self._components}, diffusion_models = {self._diffusion_models}, '
            f'temperature = {self._temperature}, '
            f'divide_by_temperature = {self._divide_by_temperature}'
        )

append_diffusion_model(diffusion_model)

Append a DiffusionModel to the SampleModel.

Parameters:

Name Type Description Default
diffusion_model DiffusionModelBase

The DiffusionModel

required
Source code in src/easydynamics/sample_model/sample_model.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def append_diffusion_model(self, diffusion_model: DiffusionModelBase) -> None:
    """Append a DiffusionModel to the SampleModel.

    Args:
        diffusion_model (DiffusionModelBase): The DiffusionModel
        to append.
    """

    if not isinstance(diffusion_model, DiffusionModelBase):
        raise TypeError(
            f'diffusion_model must be a DiffusionModelBase, got {type(diffusion_model).__name__}'  # noqa: E501
        )

    self._diffusion_models.append(diffusion_model)
    self._generate_component_collections()

clear_diffusion_models()

Clear all DiffusionModels from the SampleModel.

Source code in src/easydynamics/sample_model/sample_model.py
145
146
147
148
def clear_diffusion_models(self) -> None:
    """Clear all DiffusionModels from the SampleModel."""
    self._diffusion_models.clear()
    self._generate_component_collections()

convert_temperature_unit(unit)

Convert the unit of the temperature Parameter.

Source code in src/easydynamics/sample_model/sample_model.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
def convert_temperature_unit(self, unit: str | sc.Unit) -> None:
    """Convert the unit of the temperature Parameter."""

    if self._temperature is None:
        raise ValueError('Temperature is not set, cannot convert unit.')

    old_unit = self._temperature.unit

    try:
        self._temperature.convert_unit(unit)
        self._temperature_unit = unit
    except Exception as e:
        # Attempt to rollback on failure
        try:
            self._temperature.convert_unit(old_unit)
        except Exception:  # noqa: S110
            pass  # Best effort rollback
        raise e

diffusion_models property writable

Get the diffusion models of the SampleModel.

divide_by_temperature property writable

Get whether to divide the detailed balance factor by temperature.

evaluate(x)

Evaluate the sample model at all Q for the given x values.

Parameters

x : Number, list, np.ndarray, sc.Variable, or sc.DataArray Energy axis.

Returns

list[np.ndarray] List of evaluated model values for each Q.

Source code in src/easydynamics/sample_model/sample_model.py
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
def evaluate(
    self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray
) -> list[np.ndarray]:
    """Evaluate the sample model at all Q for the given x values.

    Parameters
    ----------
    x : Number, list, np.ndarray, sc.Variable, or sc.DataArray
        Energy axis.

    Returns
    -------
    list[np.ndarray]
        List of evaluated model values for each Q.
    """

    y = super().evaluate(x)

    if self._temperature is not None:
        DBF = _detailed_balance_factor(
            energy=x,
            temperature=self._temperature,
            divide_by_temperature=self._divide_by_temperature,
            energy_unit=self._unit,
        )
        y = [yi * DBF for yi in y]

    return y

get_all_variables()

Get all Parameters and Descriptors from all ComponentCollections in the SampleModel.

Also includes temperature if set and all variables from diffusion models. Ignores the Parameters and Descriptors in self._components as these are just templates.

Source code in src/easydynamics/sample_model/sample_model.py
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
def get_all_variables(self):
    """Get all Parameters and Descriptors from all
    ComponentCollections in the SampleModel.

    Also includes temperature if set and all variables from
    diffusion models. Ignores the Parameters and Descriptors in
    self._components as these are just templates.
    """
    all_vars = super().get_all_variables()
    if self._temperature is not None:
        all_vars.append(self._temperature)

    for diffusion_model in self._diffusion_models:
        all_vars.extend(diffusion_model.get_all_variables())

    return all_vars

remove_diffusion_model(name)

Remove a DiffusionModel from the SampleModel by unique name.

Parameters:

Name Type Description Default
name str

The unique name of the DiffusionModel to remove.

required
Source code in src/easydynamics/sample_model/sample_model.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def remove_diffusion_model(self, name: 'str') -> None:
    """Remove a DiffusionModel from the SampleModel by unique name.

    Args:
        name (str): The unique name of the DiffusionModel to remove.
    """
    for i, dm in enumerate(self._diffusion_models):
        if dm.unique_name == name:
            del self._diffusion_models[i]
            self._generate_component_collections()
            return
    raise ValueError(
        f'No DiffusionModel with unique name {name} found. \n'
        f'The available unique names are: {[dm.unique_name for dm in self._diffusion_models]}'
    )

temperature property writable

Get the temperature of the SampleModel.

temperature_unit property writable

Get the temperature unit of the SampleModel.