Skip to content

sample_model

BackgroundModel

Bases: ModelBase

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

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

    def __init__(
        self,
        display_name: str | None = 'MyBackgroundModel',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
        components: ModelComponent | ComponentCollection | None = None,
        Q: Q_type | None = None,
    ) -> None:
        """
        Initialize the BackgroundModel.

        Parameters
        ----------
        display_name : str | None, default='MyBackgroundModel'
            Display name of the model.
        unique_name : str | None, default=None
            Unique name of the model. If None, a unique name will be generated.
        unit : str | sc.Unit, default='meV'
            Unit of the model.
        components : ModelComponent | ComponentCollection | None, default=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, default=None
            Q values for the model. If None, Q is not set.
        """
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
            unit=unit,
            components=components,
            Q=Q,
        )

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

Initialize the BackgroundModel.

Parameters:

Name Type Description Default
display_name str | None

Display name of the model.

'MyBackgroundModel'
unique_name str | None

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

None
unit str | Unit

Unit of the model.

'meV'
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.

None
Q Q_type | None

Q values for the model. If None, Q is not set.

None
Source code in src/easydynamics/sample_model/background_model.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
def __init__(
    self,
    display_name: str | None = 'MyBackgroundModel',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
    components: ModelComponent | ComponentCollection | None = None,
    Q: Q_type | None = None,
) -> None:
    """
    Initialize the BackgroundModel.

    Parameters
    ----------
    display_name : str | None, default='MyBackgroundModel'
        Display name of the model.
    unique_name : str | None, default=None
        Unique name of the model. If None, a unique name will be generated.
    unit : str | sc.Unit, default='meV'
        Unit of the model.
    components : ModelComponent | ComponentCollection | None, default=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, default=None
        Q values for the model. If None, Q is not set.
    """
    super().__init__(
        display_name=display_name,
        unique_name=unique_name,
        unit=unit,
        components=components,
        Q=Q,
    )

BrownianTranslationalDiffusion

Bases: DiffusionModelBase

Model of Brownian translational diffusion, consisting of a Lorentzian function for each Q-value, where the width is given by \(D Q^2\), where \(D\) is the diffusion coefficient. The area of the Lorentzians is given by the scale parameter multiplied by the QISF, which is 1 for this model. The EISF is 0 for this model, so there is no delta function component. Q is assumed to have units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given Q-values.

Example: >>>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 tutorials.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
class BrownianTranslationalDiffusion(DiffusionModelBase):
    r"""
    Model of Brownian translational diffusion, consisting of a Lorentzian function for each
    Q-value, where the width is given by $D Q^2$, where $D$ is the diffusion coefficient. The area
    of the Lorentzians is given by the scale parameter multiplied by the QISF, which is 1 for this
    model. The EISF is 0 for this model, so there is no delta function component. Q is assumed to
    have units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given
    Q-values.

    Example: >>>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
    tutorials.
    """

    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,
    ) -> None:
        """
        Initialize a new BrownianTranslationalDiffusion model.

        Parameters
        ----------
        display_name : str | None, default='BrownianTranslationalDiffusion'
            Display name of the diffusion model.
        unique_name : str | None, default=None
            Unique name of the diffusion model. If None, a unique name will be generated. By
            default, None.
        unit : str | sc.Unit, default='meV'
            Unit of the diffusion model. Must be convertible to meV.
        scale : Numeric, default=1.0
            Scale factor for the diffusion model. Must be a non-negative number.
        diffusion_coefficient : Numeric, default=1.0
            Diffusion coefficient D in m^2/s.

        Raises
        ------
        TypeError
            If scale or diffusion_coefficient is not a number.
        """
        if not isinstance(scale, Numeric):
            raise TypeError('scale must be a number.')

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

        diffusion_coefficient = Parameter(
            name='diffusion_coefficient',
            value=float(diffusion_coefficient),
            fixed=False,
            unit='m**2/s',
            min=0.0,
        )
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
            unit=unit,
            scale=scale,
        )
        self._hbar = hbar
        self._angstrom = angstrom
        self._diffusion_coefficient = diffusion_coefficient

    # ------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------

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

        Returns
        -------
        Parameter
            Diffusion coefficient D in m^2/s.
        """
        return self._diffusion_coefficient

    @diffusion_coefficient.setter
    def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None:
        """
        Set the diffusion coefficient parameter D.

        Parameters
        ----------
        diffusion_coefficient : Numeric
            The new value for the diffusion coefficient D in m^2/s.

        Raises
        ------
        TypeError
            If diffusion_coefficient is not a number.
        ValueError
            If diffusion_coefficient is negative.
        """
        if not isinstance(diffusion_coefficient, Numeric):
            raise TypeError('diffusion_coefficient must be a number.')

        if float(diffusion_coefficient) < 0:
            raise ValueError('diffusion_coefficient must be non-negative.')
        self._diffusion_coefficient.value = float(diffusion_coefficient)

    # ------------------------------------------------------------------
    # Other methods
    # ------------------------------------------------------------------

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

        Parameters
        ----------
        Q : Q_type
            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)
        return Q**2 * unit_conversion_factor.value

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

        Parameters
        ----------
        Q : Q_type
            Scattering vector in 1/angstrom.

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

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

        Parameters
        ----------
        Q : Q_type
            Scattering vector in 1/angstrom.

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

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

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

        Parameters
        ----------
        Q : Q_type
            Scattering vector values.
        component_display_name : str, default='Brownian diffusion'
            Name of the Lorentzian component.

        Raises
        ------
        TypeError
            If component_display_name is not a string.

        Returns
        -------
        list[ComponentCollection]
            List of ComponentCollections with Lorentzian components for each Q value. Each
            Lorentzian has a width given by $D*Q^2$ and an area given by the scale parameter
            multiplied by the QISF (which is 1 for this model).
        """
        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,
                desired_unit=self.unit,
            )

            # 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,
            )

            component_collection_list[i].append_component(lorentzian_component)

        return component_collection_list

    # ------------------------------------------------------------------
    # Private methods
    # ------------------------------------------------------------------

    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.

        Raises
        ------
        TypeError
            If Q is not a float.

        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.

        Returns
        -------
        dict[str, DescriptorNumber]
            Dependency map for the width.
        """
        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.

        Parameters
        ----------
        QISF : float
            Quasielastic Incoherent Scattering Function.

        Raises
        ------
        TypeError
            If QISF is not a float.

        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.

        Returns
        -------
        dict[str, DescriptorNumber]
            Dependency map for the area.
        """
        return {
            'scale': self.scale,
        }

    # ------------------------------------------------------------------
    # dunder methods
    # ------------------------------------------------------------------

    def __repr__(self) -> str:
        """
        String representation of the BrownianTranslationalDiffusion model.

        Returns
        -------
        str
            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)

Initialize a new BrownianTranslationalDiffusion model.

Parameters:

Name Type Description Default
display_name str | None

Display name of the diffusion model.

'BrownianTranslationalDiffusion'
unique_name str | None

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

None
unit str | Unit

Unit of the diffusion model. Must be convertible to meV.

'meV'
scale Numeric

Scale factor for the diffusion model. Must be a non-negative number.

1.0
diffusion_coefficient Numeric

Diffusion coefficient D in m^2/s.

1.0

Raises:

Type Description
TypeError

If scale or diffusion_coefficient is not a number.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
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
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,
) -> None:
    """
    Initialize a new BrownianTranslationalDiffusion model.

    Parameters
    ----------
    display_name : str | None, default='BrownianTranslationalDiffusion'
        Display name of the diffusion model.
    unique_name : str | None, default=None
        Unique name of the diffusion model. If None, a unique name will be generated. By
        default, None.
    unit : str | sc.Unit, default='meV'
        Unit of the diffusion model. Must be convertible to meV.
    scale : Numeric, default=1.0
        Scale factor for the diffusion model. Must be a non-negative number.
    diffusion_coefficient : Numeric, default=1.0
        Diffusion coefficient D in m^2/s.

    Raises
    ------
    TypeError
        If scale or diffusion_coefficient is not a number.
    """
    if not isinstance(scale, Numeric):
        raise TypeError('scale must be a number.')

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

    diffusion_coefficient = Parameter(
        name='diffusion_coefficient',
        value=float(diffusion_coefficient),
        fixed=False,
        unit='m**2/s',
        min=0.0,
    )
    super().__init__(
        display_name=display_name,
        unique_name=unique_name,
        unit=unit,
        scale=scale,
    )
    self._hbar = hbar
    self._angstrom = angstrom
    self._diffusion_coefficient = diffusion_coefficient

__repr__()

String representation of the BrownianTranslationalDiffusion model.

Returns:

Type Description
str

String representation of the BrownianTranslationalDiffusion model.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
349
350
351
352
353
354
355
356
357
358
359
360
361
def __repr__(self) -> str:
    """
    String representation of the BrownianTranslationalDiffusion model.

    Returns
    -------
    str
        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:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom.

required

Returns:

Type Description
ndarray

EISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def calculate_EISF(self, Q: Q_type) -> np.ndarray:
    """
    Calculate the Elastic Incoherent Structure Factor (EISF) for the Brownian translational
    diffusion model.

    Parameters
    ----------
    Q : Q_type
        Scattering vector in 1/angstrom.

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

calculate_QISF(Q)

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

Parameters:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom.

required

Returns:

Type Description
ndarray

QISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def calculate_QISF(self, Q: Q_type) -> np.ndarray:
    """
    Calculate the Quasi-Elastic Incoherent Structure Factor (QISF).

    Parameters
    ----------
    Q : Q_type
        Scattering vector in 1/angstrom.

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

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

calculate_width(Q)

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

Parameters:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom.

required

Returns:

Type Description
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def calculate_width(self, Q: Q_type) -> np.ndarray:
    """
    Calculate the half-width at half-maximum (HWHM) for the diffusion model.

    Parameters
    ----------
    Q : Q_type
        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)
    return Q**2 * unit_conversion_factor.value

create_component_collections(Q, component_display_name='Brownian diffusion')

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

Parameters:

Name Type Description Default
Q Q_type

Scattering vector values.

required
component_display_name str

Name of the Lorentzian component.

'Brownian diffusion'

Raises:

Type Description
TypeError

If component_display_name is not a string.

Returns:

Type Description
list[ComponentCollection]

List of ComponentCollections with Lorentzian components for each Q value. Each Lorentzian has a width given by \(D*Q^2\) and an area given by the scale parameter multiplied by the QISF (which is 1 for this model).

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

    Parameters
    ----------
    Q : Q_type
        Scattering vector values.
    component_display_name : str, default='Brownian diffusion'
        Name of the Lorentzian component.

    Raises
    ------
    TypeError
        If component_display_name is not a string.

    Returns
    -------
    list[ComponentCollection]
        List of ComponentCollections with Lorentzian components for each Q value. Each
        Lorentzian has a width given by $D*Q^2$ and an area given by the scale parameter
        multiplied by the QISF (which is 1 for this model).
    """
    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,
            desired_unit=self.unit,
        )

        # 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,
        )

        component_collection_list[i].append_component(lorentzian_component)

    return component_collection_list

diffusion_coefficient property writable

Get the diffusion coefficient parameter D.

Returns:

Type Description
Parameter

Diffusion coefficient D in m^2/s.

ComponentCollection

Bases: ModelBase

Collection of model components representing a sample, background or resolution model.

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

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

        Parameters
        ----------
        unit : str | sc.Unit, default='meV'
            Unit of the collection.
        display_name : str | None, default='MyComponentCollection'
            Display name of the collection.
        unique_name : str | None, default=None
            Unique name of the collection.
        components : list[ModelComponent] | None, default=None
            Initial model components to add to the ComponentCollection.

        Raises
        ------
        TypeError
            If unit is not a string or sc.Unit, or if components is not a list of ModelComponent.
        """

        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)

    # ------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------

    @property
    def components(self) -> list[ModelComponent]:
        """
        Get the list of components in the collection.

        Returns
        -------
        list[ModelComponent]
            The components in the collection.
        """

        return list(self._components)

    @components.setter
    def components(self, components: list[ModelComponent]) -> None:
        """
        Set the list of components in the collection.

        Parameters
        ----------
        components : list[ModelComponent]
            The new list of components.

        Raises
        ------
        TypeError
            If components is not a list of ModelComponent.
        """

        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

    @property
    def is_empty(self) -> bool:
        """
        Check if the ComponentCollection has no components.

        Returns
        -------
        bool
            True if the collection has no components, False otherwise.
        """
        return not self._components

    @is_empty.setter
    def is_empty(self, _value: bool) -> None:
        """
        Is_empty is a read-only property that indicates whether the collection has components.

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

        Raises
        ------
        AttributeError
            Always raised since is_empty is read-only.
        """
        raise AttributeError(
            'is_empty is a read-only property that indicates '
            'whether the collection has components.'
        )

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

        Returns
        -------
        str | sc.Unit | None
            The unit of the ComponentCollection, which is the same as the unit of its components.
        """
        return self._unit

    @unit.setter
    def unit(self, _unit_str: str) -> None:
        """
        Unit is read-only and cannot be set directly.

        Parameters
        ----------
        _unit_str : str
            The unit to set (ignored).

        Raises
        ------
        AttributeError
            Always raised since unit is read-only.
        """

        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.'
        )

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

        Parameters
        ----------
        unit : str | sc.Unit
            The target unit to convert to.

        Raises
        ------
        TypeError
            If unit is not a string or sc.Unit.
        Exception
            If any component cannot be converted to the specified unit.
        """

        if not isinstance(unit, (str, sc.Unit)):
            raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}')

        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

    # ------------------------------------------------------------------
    # Component management
    # ------------------------------------------------------------------

    def append_component(self, component: ModelComponent | ComponentCollection) -> None:
        """
        Append a model component or the components from another ComponentCollection to this
        ComponentCollection.

        Parameters
        ----------
        component : ModelComponent | ComponentCollection
            The component to append. If a ComponentCollection is provided, all of its components
            will be appended.

        Raises
        ------
        TypeError
            If component is not a ModelComponent or ComponentCollection.
        ValueError
            If a component with the same unique name already exists in the collection.
        """
        if not isinstance(component, (ModelComponent, ComponentCollection)):
            raise TypeError(
                'Component must be an instance of ModelComponent or ComponentCollection. '
                f'Got {type(component).__name__} instead.'
            )
        if isinstance(component, ModelComponent):
            components = (component,)
        if isinstance(component, ComponentCollection):
            components = component.components

        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:
        """
        Remove a component from the collection by its unique name.

        Parameters
        ----------
        unique_name : str
            Unique name of the component to remove.

        Raises
        ------
        TypeError
            If unique_name is not a string.
        KeyError
            If no component with the given unique name exists in the collection.
        """

        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]:
        """
        Get the list of components in the collection.

        Returns
        -------
        list[ModelComponent]
            The components in the collection.
        """
        return list(self._components)

    @components.setter
    def components(self, components: list[ModelComponent]) -> None:
        """
        Set the components in the collection.

        Parameters
        ----------
        components : list[ModelComponent]
            The new components in the collection.

        Raises
        ------
        TypeError
            If components is not a list of ModelComponent.
        """
        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

    @property
    def is_empty(self) -> bool:
        """
        Returns True if the collection has no components, otherwise False.

        Returns
        -------
        bool
            True if the collection has no components, otherwise False.
        """
        return not self._components

    @is_empty.setter
    def is_empty(self, _value: bool) -> None:
        """
        Is_empty is read-only.

        Parameters
        ----------
        _value : bool
            Ignored.

        Raises
        ------
        AttributeError
            Always raised since is_empty is read-only.
        """
        raise AttributeError(
            'is_empty is a read-only property that indicates '
            'whether the collection has components.'
        )

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

        Returns
        -------
        list[str]
            List of unique names of the components in the collection.
        """

        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:
        """
        Normalize the areas of all components so they sum to 1.

        This is useful for convolutions.

        Raises
        ------
        ValueError
            If there are no components in the model or if the total area is zero or not finite,
            which would prevent normalization.
        """
        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,
                    stacklevel=2,
                )

        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

    # ------------------------------------------------------------------
    # Other methods
    # ------------------------------------------------------------------

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

        Returns
        -------
        list[DescriptorBase]
            List of parameters in the component.
        """

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

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

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            Energy axis.

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

        if not self.components:
            return np.zeros_like(x)
        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 : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            Energy axis.
        unique_name : str
            Component unique name.

        Raises
        ------
        ValueError
            If there are no components in the model.
        TypeError
            If unique_name is not a string.
        KeyError
            If no component with the given unique name exists in the collection.

        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]

        return component.evaluate(x)

    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

    # ------------------------------------------------------------------
    # Dunder methods
    # ------------------------------------------------------------------

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

        Parameters
        ----------
        item : str | 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)
        if isinstance(item, ModelComponent):
            # Check by component instance
            return any(comp is item for comp in self.components)
        return False

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

        Returns
        -------
        str
            String representation of the ComponentCollection.
        """
        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.

Parameters:

Name Type Description Default
item str | ModelComponent

The component name or instance to check for.

required

Returns:

Type Description
bool

True if the component exists, False otherwise.

Source code in src/easydynamics/sample_model/component_collection.py
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def __contains__(self, item: str | ModelComponent) -> bool:
    """
    Check if a component with the given name or instance exists in the ComponentCollection.

    Parameters
    ----------
    item : str | 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)
    if isinstance(item, ModelComponent):
        # Check by component instance
        return any(comp is item for comp in self.components)
    return False

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

Initialize a new ComponentCollection.

Parameters:

Name Type Description Default
unit str | Unit

Unit of the collection.

'meV'
display_name str | None

Display name of the collection.

'MyComponentCollection'
unique_name str | None

Unique name of the collection.

None
components list[ModelComponent] | None

Initial model components to add to the ComponentCollection.

None

Raises:

Type Description
TypeError

If unit is not a string or sc.Unit, or if components is not a list of ModelComponent.

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

    Parameters
    ----------
    unit : str | sc.Unit, default='meV'
        Unit of the collection.
    display_name : str | None, default='MyComponentCollection'
        Display name of the collection.
    unique_name : str | None, default=None
        Unique name of the collection.
    components : list[ModelComponent] | None, default=None
        Initial model components to add to the ComponentCollection.

    Raises
    ------
    TypeError
        If unit is not a string or sc.Unit, or if components is not a list of ModelComponent.
    """

    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:

Type Description
str

String representation of the ComponentCollection.

Source code in src/easydynamics/sample_model/component_collection.py
527
528
529
530
531
532
533
534
535
536
537
538
def __repr__(self) -> str:
    """
    Return a string representation of the ComponentCollection.

    Returns
    -------
    str
        String representation of the ComponentCollection.
    """
    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}>"

append_component(component)

Append a model component or the components from another ComponentCollection to this ComponentCollection.

Parameters:

Name Type Description Default
component ModelComponent | ComponentCollection

The component to append. If a ComponentCollection is provided, all of its components will be appended.

required

Raises:

Type Description
TypeError

If component is not a ModelComponent or ComponentCollection.

ValueError

If a component with the same unique name already exists in the collection.

Source code in src/easydynamics/sample_model/component_collection.py
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
def append_component(self, component: ModelComponent | ComponentCollection) -> None:
    """
    Append a model component or the components from another ComponentCollection to this
    ComponentCollection.

    Parameters
    ----------
    component : ModelComponent | ComponentCollection
        The component to append. If a ComponentCollection is provided, all of its components
        will be appended.

    Raises
    ------
    TypeError
        If component is not a ModelComponent or ComponentCollection.
    ValueError
        If a component with the same unique name already exists in the collection.
    """
    if not isinstance(component, (ModelComponent, ComponentCollection)):
        raise TypeError(
            'Component must be an instance of ModelComponent or ComponentCollection. '
            f'Got {type(component).__name__} instead.'
        )
    if isinstance(component, ModelComponent):
        components = (component,)
    if isinstance(component, ComponentCollection):
        components = component.components

    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)

clear_components()

Remove all components.

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

components property writable

Get the list of components in the collection.

Returns:

Type Description
list[ModelComponent]

The components in the collection.

convert_unit(unit)

Convert the unit of the ComponentCollection and all its components.

Parameters:

Name Type Description Default
unit str | Unit

The target unit to convert to.

required

Raises:

Type Description
TypeError

If unit is not a string or sc.Unit.

Exception

If any component cannot be converted to the specified unit.

Source code in src/easydynamics/sample_model/component_collection.py
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
def convert_unit(self, unit: str | sc.Unit) -> None:
    """
    Convert the unit of the ComponentCollection and all its components.

    Parameters
    ----------
    unit : str | sc.Unit
        The target unit to convert to.

    Raises
    ------
    TypeError
        If unit is not a string or sc.Unit.
    Exception
        If any component cannot be converted to the specified unit.
    """

    if not isinstance(unit, (str, sc.Unit)):
        raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}')

    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:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

Energy axis.

required

Returns:

Type Description
ndarray

Evaluated model values.

Source code in src/easydynamics/sample_model/component_collection.py
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """
    Evaluate the sum of all components.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        Energy axis.

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

    if not self.components:
        return np.zeros_like(x)
    return sum(component.evaluate(x) for component in self.components)

evaluate_component(x, unique_name)

Evaluate a single component by name.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

Energy axis.

required
unique_name str

Component unique name.

required

Raises:

Type Description
ValueError

If there are no components in the model.

TypeError

If unique_name is not a string.

KeyError

If no component with the given unique name exists in the collection.

Returns:

Type Description
ndarray

Evaluated values for the specified component.

Source code in src/easydynamics/sample_model/component_collection.py
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
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 : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        Energy axis.
    unique_name : str
        Component unique name.

    Raises
    ------
    ValueError
        If there are no components in the model.
    TypeError
        If unique_name is not a string.
    KeyError
        If no component with the given unique name exists in the collection.

    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]

    return component.evaluate(x)

fix_all_parameters()

Fix all free parameters in the model.

Source code in src/easydynamics/sample_model/component_collection.py
490
491
492
493
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
495
496
497
498
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:

Type Description
list[DescriptorBase]

List of parameters in the component.

Source code in src/easydynamics/sample_model/component_collection.py
414
415
416
417
418
419
420
421
422
423
424
def get_all_variables(self) -> list[DescriptorBase]:
    """
    Get all parameters from the model component.

    Returns
    -------
    list[DescriptorBase]
        List of parameters in the component.
    """

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

is_empty property writable

Returns True if the collection has no components, otherwise False.

Returns:

Type Description
bool

True if the collection has no components, otherwise False.

list_component_names()

List the names of all components in the model.

Returns:

Type Description
list[str]

List of unique names of the components in the collection.

Source code in src/easydynamics/sample_model/component_collection.py
355
356
357
358
359
360
361
362
363
364
365
def list_component_names(self) -> list[str]:
    """
    List the names of all components in the model.

    Returns
    -------
    list[str]
        List of unique names of the components in the collection.
    """

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

normalize_area()

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

This is useful for convolutions.

Raises:

Type Description
ValueError

If there are no components in the model or if the total area is zero or not finite, which would prevent normalization.

Source code in src/easydynamics/sample_model/component_collection.py
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
def normalize_area(self) -> None:
    """
    Normalize the areas of all components so they sum to 1.

    This is useful for convolutions.

    Raises
    ------
    ValueError
        If there are no components in the model or if the total area is zero or not finite,
        which would prevent normalization.
    """
    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,
                stacklevel=2,
            )

    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

remove_component(unique_name)

Remove a component from the collection by its unique name.

Parameters:

Name Type Description Default
unique_name str

Unique name of the component to remove.

required

Raises:

Type Description
TypeError

If unique_name is not a string.

KeyError

If no component with the given unique name exists in the collection.

Source code in src/easydynamics/sample_model/component_collection.py
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
def remove_component(self, unique_name: str) -> None:
    """
    Remove a component from the collection by its unique name.

    Parameters
    ----------
    unique_name : str
        Unique name of the component to remove.

    Raises
    ------
    TypeError
        If unique_name is not a string.
    KeyError
        If no component with the given unique name exists in the collection.
    """

    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()}'
    )

unit property writable

Get the unit of the ComponentCollection.

Returns:

Type Description
str | Unit | None

The unit of the ComponentCollection, which is the same as the unit of its components.

DampedHarmonicOscillator

Bases: CreateParametersMixin, ModelComponent

Model of a Damped Harmonic Oscillator (DHO).

The intensity is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2 \gamma x)^2 \right)}, $$ where \(A\) is the area, \(x_0\) is the center, and \(\gamma\) is the width.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.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
class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent):
    r"""
    Model of a Damped Harmonic Oscillator (DHO).

    The intensity is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2
    \gamma x)^2 \right)}, $$ where $A$ is the area, $x_0$ is the center, and $\gamma$ is the width.
    """

    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,
    ) -> None:
        """
        Initialize the Damped Harmonic Oscillator.

        Parameters
        ----------
        area : Numeric | Parameter, default=1.0
            Area under the curve.
        center : Numeric | Parameter, default=1.0
            Resonance frequency, approximately the peak position.
        width : Numeric | Parameter, default=1.0
            Damping constant, approximately the half width at half max (HWHM) of the peaks. By
            default, 1.0.
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='DampedHarmonicOscillator'
            Display name of the component.
        unique_name : str | None, default=None
            Unique name of the component. If None, a unique_name is automatically generated. By
            default, 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.

        Returns
        -------
        Parameter
            The area parameter.
        """
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """
        Set the value of the area parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the area parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """
        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.

        Returns
        -------
        Parameter
            The center parameter.
        """
        return self._center

    @center.setter
    def center(self, value: Numeric) -> None:
        """
        Set the value of the center parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the center parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')

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

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

        Returns
        -------
        Parameter
            The width parameter.
        """
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """
        Set the value of the width parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the width parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')

        if float(value) <= 0:
            raise ValueError('width must be positive')

        self._width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        r"""
        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 intensity
        is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2 \gamma x)^2
        \right)}, $$ where $A$ is the area, $x_0$ is the center, and $\gamma$ is the width.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the DHO.

        Returns
        -------
        np.ndarray
            The intensity of the DHO at the given x values.
        """

        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) -> str:
        """
        Return a string representation of the Damped Harmonic Oscillator.

        Returns
        -------
        str
            A string representation of the Damped Harmonic Oscillator.
        """
        return (
            f'DampedHarmonicOscillator(display_name = {self.display_name}, '
            f'unit = {self._unit},\n '
            f'area = {self.area},\n center = {self.center},\n width = {self.width})'
        )

__init__(area=1.0, center=1.0, width=1.0, unit='meV', display_name='DampedHarmonicOscillator', unique_name=None)

Initialize the Damped Harmonic Oscillator.

Parameters:

Name Type Description Default
area Numeric | Parameter

Area under the curve.

1.0
center Numeric | Parameter

Resonance frequency, approximately the peak position.

1.0
width Numeric | Parameter

Damping constant, approximately the half width at half max (HWHM) of the peaks. By default, 1.0.

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Display name of the component.

'DampedHarmonicOscillator'
unique_name str | None

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

None
Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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,
) -> None:
    """
    Initialize the Damped Harmonic Oscillator.

    Parameters
    ----------
    area : Numeric | Parameter, default=1.0
        Area under the curve.
    center : Numeric | Parameter, default=1.0
        Resonance frequency, approximately the peak position.
    width : Numeric | Parameter, default=1.0
        Damping constant, approximately the half width at half max (HWHM) of the peaks. By
        default, 1.0.
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='DampedHarmonicOscillator'
        Display name of the component.
    unique_name : str | None, default=None
        Unique name of the component. If None, a unique_name is automatically generated. By
        default, 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

__repr__()

Return a string representation of the Damped Harmonic Oscillator.

Returns:

Type Description
str

A string representation of the Damped Harmonic Oscillator.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def __repr__(self) -> str:
    """
    Return a string representation of the Damped Harmonic Oscillator.

    Returns
    -------
    str
        A string representation of the Damped Harmonic Oscillator.
    """
    return (
        f'DampedHarmonicOscillator(display_name = {self.display_name}, '
        f'unit = {self._unit},\n '
        f'area = {self.area},\n center = {self.center},\n width = {self.width})'
    )

area property writable

Get the area parameter.

Returns:

Type Description
Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

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 intensity is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2 \gamma x)^2 \right)}, $$ where \(A\) is the area, \(x_0\) is the center, and \(\gamma\) is the width.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the DHO.

required

Returns:

Type Description
ndarray

The intensity of the DHO at the given x values.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
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
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    r"""
    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 intensity
    is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2 \gamma x)^2
    \right)}, $$ where $A$ is the area, $x_0$ is the center, and $\gamma$ is the width.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the DHO.

    Returns
    -------
    np.ndarray
        The intensity of the DHO at the given x values.
    """

    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.

Returns:

Type Description
Parameter

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 by the Convolution method. If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Source code in src/easydynamics/sample_model/components/delta_function.py
 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
class DeltaFunction(CreateParametersMixin, ModelComponent):
    """
    Delta function.

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

    def __init__(
        self,
        center: Numeric | Parameter | None = None,
        area: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'DeltaFunction',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the Delta function.

        Parameters
        ----------
        center : Numeric | Parameter | None, default=None
            Center of the delta function. If None.
        area : Numeric | Parameter, default=1.0
            Total area under the curve.
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='DeltaFunction'
            Name of the component.
        unique_name : str | None, default=None
            Unique name of the component. If None, a unique_name is automatically generated. By
            default, 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.

        Returns
        -------
        Parameter
            The area parameter.
        """

        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """
        Set the value of the area parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the area parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """

        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.

        Returns
        -------
        Parameter
            The center parameter.
        """

        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """
        Set the center parameter value.

        Parameters
        ----------
        value : Numeric | None
            The new value for the center parameter. If None, defaults to 0 and is fixed.

        Raises
        ------
        TypeError
            If the value is not a number or None.
        """

        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.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Delta function.

        Returns
        -------
        np.ndarray
            The evaluated Delta function at the given x values.
        """

        # 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:  # noqa: SIM108
                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:  # noqa: SIM108
                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) -> str:
        """
        Return a string representation of the Delta function.

        Returns
        -------
        str
            A string representation of the Delta function.
        """

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

__init__(center=None, area=1.0, unit='meV', display_name='DeltaFunction', unique_name=None)

Initialize the Delta function.

Parameters:

Name Type Description Default
center Numeric | Parameter | None

Center of the delta function. If None.

None
area Numeric | Parameter

Total area under the curve.

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Name of the component.

'DeltaFunction'
unique_name str | None

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

None
Source code in src/easydynamics/sample_model/components/delta_function.py
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
def __init__(
    self,
    center: Numeric | Parameter | None = None,
    area: Numeric | Parameter = 1.0,
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'DeltaFunction',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the Delta function.

    Parameters
    ----------
    center : Numeric | Parameter | None, default=None
        Center of the delta function. If None.
    area : Numeric | Parameter, default=1.0
        Total area under the curve.
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='DeltaFunction'
        Name of the component.
    unique_name : str | None, default=None
        Unique name of the component. If None, a unique_name is automatically generated. By
        default, 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

__repr__()

Return a string representation of the Delta function.

Returns:

Type Description
str

A string representation of the Delta function.

Source code in src/easydynamics/sample_model/components/delta_function.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def __repr__(self) -> str:
    """
    Return a string representation of the Delta function.

    Returns
    -------
    str
        A string representation of the Delta function.
    """

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

area property writable

Get the area parameter.

Returns:

Type Description
Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

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.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Delta function.

required

Returns:

Type Description
ndarray

The evaluated Delta function at the given x values.

Source code in src/easydynamics/sample_model/components/delta_function.py
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
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.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Delta function.

    Returns
    -------
    np.ndarray
        The evaluated Delta function at the given x values.
    """

    # 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:  # noqa: SIM108
            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:  # noqa: SIM108
            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

Exponential

Bases: CreateParametersMixin, ModelComponent

Model of an exponential function.

The intensity is given by

\[ I(x) = A e^{B (x-x_0)}, \]

where \(A\) is the amplitude, \(x_0\) is the center, and \(B\) describes the rate of decay or growth.

Source code in src/easydynamics/sample_model/components/exponential.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
 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
class Exponential(CreateParametersMixin, ModelComponent):
    r"""
    Model of an exponential function.

    The intensity is given by

    $$ I(x) = A e^{B (x-x_0)}, $$

    where $A$ is the amplitude, $x_0$ is the center, and $B$ describes the rate of decay or growth.
    """

    def __init__(
        self,
        amplitude: Numeric | Parameter = 1.0,
        center: Numeric | Parameter | None = None,
        rate: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Exponential',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the Exponential component.

        Parameters
        ----------
        amplitude : Numeric | Parameter, default=1.0
            Amplitude of the Exponential.
        center : Numeric | Parameter | None, default=None
            Center of the Exponential. If None, the center is fixed at 0.
        rate : Numeric | Parameter, default=1.0
            Decay or growth constant of the Exponential.
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='Exponential'
            Name of the component.
        unique_name : str | None, default=None
            Unique name of the component. if None, a unique_name is automatically generated. By
            default, None.

        Raises
        ------
        TypeError
            If amplitude, center, or rate are not numbers or Parameters.
        ValueError
            If amplitude, center or rate are not finite numbers.
        """
        # Validate inputs and create Parameters if not given
        super().__init__(
            display_name=display_name,
            unit=unit,
            unique_name=unique_name,
        )

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

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

            amplitude = Parameter(
                name=display_name + ' amplitude', value=float(amplitude), unit=unit
            )

        center = self._create_center_parameter(
            center=center, name=display_name, fix_if_none=True, unit=self._unit
        )

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

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

            rate = Parameter(name=display_name + ' rate', value=float(rate), unit='1/' + str(unit))

        self._amplitude = amplitude
        self._center = center
        self._rate = rate

    @property
    def amplitude(self) -> Parameter:
        """
        Get the amplitude parameter.

        Returns
        -------
        Parameter
            The amplitude parameter.
        """

        return self._amplitude

    @amplitude.setter
    def amplitude(self, value: Numeric) -> None:
        """
        Set the value of the amplitude parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the amplitude parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """

        if not isinstance(value, Numeric):
            raise TypeError('amplitude must be a number')
        self._amplitude.value = value

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

        Returns
        -------
        Parameter
            The center parameter.
        """

        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """
        Set the center parameter value.

        Parameters
        ----------
        value : Numeric | None
            The new value for the center parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """

        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 rate(self) -> Parameter:
        """
        Get the rate parameter.

        Returns
        -------
        Parameter
            The rate parameter.
        """
        return self._rate

    @rate.setter
    def rate(self, value: Numeric) -> None:
        """
        Set the rate parameter value.

        Parameters
        ----------
        value : Numeric
            The new value for the rate parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """
        if not isinstance(value, Numeric):
            raise TypeError('rate must be a number')

        self._rate.value = value

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

        If x is a scipp Variable, the unit of the Exponential will be converted to match x. The
        intensity is given by $$ I(x) = A \exp\left( r (x - x_0) \right) $$

        where $A$ is the amplitude, $x_0$ is the center, and $r$ is the rate.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Exponential.

        Returns
        -------
        np.ndarray
            The intensity of the Exponential at the given x values.
        """

        x = self._prepare_x_for_evaluate(x)
        exponent = self.rate.value * (x - self.center.value)

        return self.amplitude.value * np.exp(exponent)

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

        Parameters
        ----------
        unit : str | sc.Unit
            The new unit to convert to.

        Raises
        ------
        TypeError
            If unit is not a string or sc.Unit.
        Exception
            If conversion fails for any parameter.
        """

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

        old_unit = self._unit
        pars = [self.amplitude, self.center]
        try:
            for p in pars:
                p.convert_unit(unit)
            self.rate.convert_unit('1/' + str(unit))
            self._unit = unit
        except Exception as e:
            # Attempt to rollback on failure
            try:
                for p in pars:
                    p.convert_unit(old_unit)
                self.rate.convert_unit('1/' + str(old_unit))
            except Exception:  # noqa: S110
                pass  # Best effort rollback
            raise e

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

        Returns
        -------
        str
            A string representation of the Exponential.
        """

        return f'Exponential(unique_name = {self.unique_name}, unit = {self._unit},\n \
            amplitude = {self.amplitude},\n center = {self.center},\n rate = {self.rate})'

__init__(amplitude=1.0, center=None, rate=1.0, unit='meV', display_name='Exponential', unique_name=None)

Initialize the Exponential component.

Parameters:

Name Type Description Default
amplitude Numeric | Parameter

Amplitude of the Exponential.

1.0
center Numeric | Parameter | None

Center of the Exponential. If None, the center is fixed at 0.

None
rate Numeric | Parameter

Decay or growth constant of the Exponential.

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Name of the component.

'Exponential'
unique_name str | None

Unique name of the component. if None, a unique_name is automatically generated. By default, None.

None

Raises:

Type Description
TypeError

If amplitude, center, or rate are not numbers or Parameters.

ValueError

If amplitude, center or rate are not finite numbers.

Source code in src/easydynamics/sample_model/components/exponential.py
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
def __init__(
    self,
    amplitude: Numeric | Parameter = 1.0,
    center: Numeric | Parameter | None = None,
    rate: Numeric | Parameter = 1.0,
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'Exponential',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the Exponential component.

    Parameters
    ----------
    amplitude : Numeric | Parameter, default=1.0
        Amplitude of the Exponential.
    center : Numeric | Parameter | None, default=None
        Center of the Exponential. If None, the center is fixed at 0.
    rate : Numeric | Parameter, default=1.0
        Decay or growth constant of the Exponential.
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='Exponential'
        Name of the component.
    unique_name : str | None, default=None
        Unique name of the component. if None, a unique_name is automatically generated. By
        default, None.

    Raises
    ------
    TypeError
        If amplitude, center, or rate are not numbers or Parameters.
    ValueError
        If amplitude, center or rate are not finite numbers.
    """
    # Validate inputs and create Parameters if not given
    super().__init__(
        display_name=display_name,
        unit=unit,
        unique_name=unique_name,
    )

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

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

        amplitude = Parameter(
            name=display_name + ' amplitude', value=float(amplitude), unit=unit
        )

    center = self._create_center_parameter(
        center=center, name=display_name, fix_if_none=True, unit=self._unit
    )

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

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

        rate = Parameter(name=display_name + ' rate', value=float(rate), unit='1/' + str(unit))

    self._amplitude = amplitude
    self._center = center
    self._rate = rate

__repr__()

Return a string representation of the Exponential.

Returns:

Type Description
str

A string representation of the Exponential.

Source code in src/easydynamics/sample_model/components/exponential.py
263
264
265
266
267
268
269
270
271
272
273
274
def __repr__(self) -> str:
    """
    Return a string representation of the Exponential.

    Returns
    -------
    str
        A string representation of the Exponential.
    """

    return f'Exponential(unique_name = {self.unique_name}, unit = {self._unit},\n \
        amplitude = {self.amplitude},\n center = {self.center},\n rate = {self.rate})'

amplitude property writable

Get the amplitude parameter.

Returns:

Type Description
Parameter

The amplitude parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

The center parameter.

convert_unit(unit)

Convert the unit of the Parameters in the component.

Parameters:

Name Type Description Default
unit str | Unit

The new unit to convert to.

required

Raises:

Type Description
TypeError

If unit is not a string or sc.Unit.

Exception

If conversion fails for any parameter.

Source code in src/easydynamics/sample_model/components/exponential.py
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
def convert_unit(self, unit: str | sc.Unit) -> None:
    """
    Convert the unit of the Parameters in the component.

    Parameters
    ----------
    unit : str | sc.Unit
        The new unit to convert to.

    Raises
    ------
    TypeError
        If unit is not a string or sc.Unit.
    Exception
        If conversion fails for any parameter.
    """

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

    old_unit = self._unit
    pars = [self.amplitude, self.center]
    try:
        for p in pars:
            p.convert_unit(unit)
        self.rate.convert_unit('1/' + str(unit))
        self._unit = unit
    except Exception as e:
        # Attempt to rollback on failure
        try:
            for p in pars:
                p.convert_unit(old_unit)
            self.rate.convert_unit('1/' + str(old_unit))
        except Exception:  # noqa: S110
            pass  # Best effort rollback
        raise e

evaluate(x)

Evaluate the Exponential at the given x values.

If x is a scipp Variable, the unit of the Exponential will be converted to match x. The intensity is given by $$ I(x) = A \exp\left( r (x - x_0) \right) $$

where \(A\) is the amplitude, \(x_0\) is the center, and \(r\) is the rate.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Exponential.

required

Returns:

Type Description
ndarray

The intensity of the Exponential at the given x values.

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

    If x is a scipp Variable, the unit of the Exponential will be converted to match x. The
    intensity is given by $$ I(x) = A \exp\left( r (x - x_0) \right) $$

    where $A$ is the amplitude, $x_0$ is the center, and $r$ is the rate.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Exponential.

    Returns
    -------
    np.ndarray
        The intensity of the Exponential at the given x values.
    """

    x = self._prepare_x_for_evaluate(x)
    exponent = self.rate.value * (x - self.center.value)

    return self.amplitude.value * np.exp(exponent)

rate property writable

Get the rate parameter.

Returns:

Type Description
Parameter

The rate parameter.

ExpressionComponent

Bases: ModelComponent

Model component defined by a symbolic expression.

Example: expr = ExpressionComponent( "A * exp(-(x - x0)2 / (2*sigma2))", parameters={"A": 10, "x0": 0, "sigma": 1}, )

expr.A = 5 y = expr.evaluate(x)
Source code in src/easydynamics/sample_model/components/expression_component.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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
class ExpressionComponent(ModelComponent):
    """
    Model component defined by a symbolic expression.

    Example: expr = ExpressionComponent( "A * exp(-(x - x0)**2 / (2*sigma**2))", parameters={"A":
    10, "x0": 0, "sigma": 1}, )

        expr.A = 5 y = expr.evaluate(x)
    """

    # -------------------------
    # Allowed symbolic functions
    # -------------------------
    _ALLOWED_FUNCS: ClassVar[dict[str, object]] = {
        # Exponentials & logs
        'exp': sp.exp,
        'log': sp.log,
        'ln': sp.log,
        'sqrt': sp.sqrt,
        # Trigonometric
        'sin': sp.sin,
        'cos': sp.cos,
        'tan': sp.tan,
        'sinc': sp.sinc,
        'cot': sp.cot,
        'sec': sp.sec,
        'csc': sp.csc,
        'asin': sp.asin,
        'acos': sp.acos,
        'atan': sp.atan,
        # Hyperbolic
        'sinh': sp.sinh,
        'cosh': sp.cosh,
        'tanh': sp.tanh,
        # Misc
        'abs': sp.Abs,
        'sign': sp.sign,
        'floor': sp.floor,
        'ceil': sp.ceiling,
        # Special functions
        'erf': sp.erf,
    }

    # -------------------------
    # Allowed constants
    # -------------------------
    _ALLOWED_CONSTANTS: ClassVar[dict[str, object]] = {
        'pi': sp.pi,
        'E': sp.E,
    }

    _RESERVED_NAMES: ClassVar[dict[str, object]] = {'x'}

    def __init__(
        self,
        expression: str,
        parameters: dict[str, Numeric] | None = None,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Expression',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the ExpressionComponent.

        Parameters
        ----------
        expression : str
            The symbolic expression as a string. Must contain 'x' as the independent variable.
        parameters : dict[str, Numeric] | None, default=None
            Dictionary of parameter names and their initial values.
        unit : str | sc.Unit, default='meV'
            Unit of the output.
        display_name : str | None, default='Expression'
            Display name for the component.
        unique_name : str | None, default=None
            Unique name for the component.

        Raises
        ------
        ValueError
            If the expression is invalid or does not contain 'x'.
        TypeError
            If any parameter value is not numeric.
        """
        super().__init__(unit=unit, display_name=display_name, unique_name=unique_name)

        if 'np.' in expression:
            raise ValueError(
                'NumPy syntax (np.*) is not supported. '
                "Use functions like 'exp', 'sin', etc. directly."
            )

        self._expression_str = expression

        locals_dict = {}
        locals_dict.update(self._ALLOWED_FUNCS)
        locals_dict.update(self._ALLOWED_CONSTANTS)

        try:
            self._expr = sp.sympify(expression, locals=locals_dict)
        except Exception as e:
            raise ValueError(f'Invalid expression: {expression}') from e

        # Extract symbols from the expression
        symbols = self._expr.free_symbols
        symbol_names = sorted(str(s) for s in symbols)

        if 'x' not in symbol_names:
            raise ValueError("Expression must contain 'x' as independent variable")

        # Reject unknown functions early so invalid expressions fail at init,
        # not later during numerical evaluation.
        allowed_function_names = set(self._ALLOWED_FUNCS) | {
            func.__name__ for func in self._ALLOWED_FUNCS.values()
        }

        # Walk all function-call nodes in the parsed expression (e.g. sin(x), foo(x)).
        # Keep only function names that are not in our allowlist.
        unknown_function_names: set[str] = set()
        function_atoms = self._expr.atoms(sp.Function)
        for function_atom in function_atoms:
            function_name = function_atom.func.__name__
            if function_name not in allowed_function_names:
                unknown_function_names.add(function_name)

        unknown_functions = sorted(unknown_function_names)

        if unknown_functions:
            raise ValueError(
                f'Unsupported function(s) in expression: {", ".join(unknown_functions)}'
            )

        # Create parameters
        if parameters is not None and not isinstance(parameters, dict):
            raise TypeError(
                f'Parameters must be None or a dictionary, got {type(parameters).__name__}'
            )

        if parameters is not None:
            for name, value in parameters.items():
                if not isinstance(value, Numeric):
                    raise TypeError(f"Parameter '{name}' must be numeric")
        parameters = parameters or {}
        self._parameters: dict[str, Parameter] = {}

        self._symbol_names = symbol_names
        for name in self._symbol_names:
            if name in self._RESERVED_NAMES:
                continue

            value = parameters.get(name, 1.0)

            self._parameters[name] = Parameter(
                name=name,
                value=value,
                unit=self._unit,
            )

        # Create numerical function
        ordered_symbols = [sp.Symbol(name) for name in self._symbol_names]

        self._func = sp.lambdify(
            ordered_symbols,
            self._expr,
            modules=['numpy'],
        )

        # -------------------------
        # Properties
        # -------------------------

    @property
    def expression(self) -> str:
        """Return the original expression string."""
        return self._expression_str

    @expression.setter
    def expression(self, _new_expr: str) -> None:
        """
        Prevent changing the expression after initialization.

        Parameters
        ----------
        _new_expr : str
            New expression string (ignored).

        Raises
        ------
        AttributeError
            Always raised to prevent changing the expression.
        """
        raise AttributeError('Expression cannot be changed after initialization')

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

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            Input values for the independent variable.

        Returns
        -------
        np.ndarray
            Evaluated results.
        """
        x = self._prepare_x_for_evaluate(x)

        args = []
        for name in self._symbol_names:
            if name == 'x':
                args.append(x)
            else:
                args.append(self._parameters[name].value)

        return self._func(*args)

    def get_all_variables(self) -> list[Parameter]:
        """
        Return all parameters.

        Returns
        -------
        list[Parameter]
            List of all parameters in the expression.
        """
        return list(self._parameters.values())

    def convert_unit(self, _new_unit: str | sc.Unit) -> None:
        """
        Convert the unit of the expression.

        Unit conversion is not implemented for ExpressionComponent.

        Parameters
        ----------
        _new_unit : str | sc.Unit
            The new unit to convert to (ignored).

        Raises
        ------
        NotImplementedError
            Always raised to indicate unit conversion is not supported.
        """

        raise NotImplementedError('Unit conversion is not implemented for ExpressionComponent')

    # -------------------------
    # dunder methods
    # -------------------------

    def __getattr__(self, name: str) -> Parameter:
        """
        Allow access to parameters as attributes.

        Parameters
        ----------
        name : str
            Name of the parameter to access.

        Raises
        ------
        AttributeError
            If the parameter does not exist.

        Returns
        -------
        Parameter
            The parameter with the given name.
        """
        if '_parameters' in self.__dict__ and name in self._parameters:
            return self._parameters[name]
        raise AttributeError(f"{self.__class__.__name__} has no attribute '{name}'")

    def __setattr__(self, name: str, value: Numeric) -> None:
        """
        Allow setting parameter values as attributes.

        Parameters
        ----------
        name : str
            Name of the parameter to set.
        value : Numeric
            New value for the parameter.

        Raises
        ------
        TypeError
            If the value is not numeric.
        """
        if '_parameters' in self.__dict__ and name in self._parameters:
            param = self._parameters[name]

            if not isinstance(value, Numeric):
                raise TypeError(f'{name} must be numeric')

            param.value = value
        else:
            # For other attributes, use default behavior
            super().__setattr__(name, value)

    def __dir__(self) -> list[str]:
        """
        Include parameter names in dir() output for better IDE support.

        Returns
        -------
        list[str]
            List of attribute names, including parameters.
        """
        return super().__dir__() + list(self._parameters.keys())

    def __repr__(self) -> str:
        """Repr function."""
        param_str = ', '.join(f'{k}={v.value}' for k, v in self._parameters.items())
        return (
            f'{self.__class__.__name__}(\n'
            f"  expr='{self._expression_str}',\n"
            f'  unit={self._unit},\n'
            f'  parameters={{ {param_str} }}\n'
            f')'
        )

__dir__()

Include parameter names in dir() output for better IDE support.

Returns:

Type Description
list[str]

List of attribute names, including parameters.

Source code in src/easydynamics/sample_model/components/expression_component.py
325
326
327
328
329
330
331
332
333
334
def __dir__(self) -> list[str]:
    """
    Include parameter names in dir() output for better IDE support.

    Returns
    -------
    list[str]
        List of attribute names, including parameters.
    """
    return super().__dir__() + list(self._parameters.keys())

__getattr__(name)

Allow access to parameters as attributes.

Parameters:

Name Type Description Default
name str

Name of the parameter to access.

required

Raises:

Type Description
AttributeError

If the parameter does not exist.

Returns:

Type Description
Parameter

The parameter with the given name.

Source code in src/easydynamics/sample_model/components/expression_component.py
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def __getattr__(self, name: str) -> Parameter:
    """
    Allow access to parameters as attributes.

    Parameters
    ----------
    name : str
        Name of the parameter to access.

    Raises
    ------
    AttributeError
        If the parameter does not exist.

    Returns
    -------
    Parameter
        The parameter with the given name.
    """
    if '_parameters' in self.__dict__ and name in self._parameters:
        return self._parameters[name]
    raise AttributeError(f"{self.__class__.__name__} has no attribute '{name}'")

__init__(expression, parameters=None, unit='meV', display_name='Expression', unique_name=None)

Initialize the ExpressionComponent.

Parameters:

Name Type Description Default
expression str

The symbolic expression as a string. Must contain 'x' as the independent variable.

required
parameters dict[str, Numeric] | None

Dictionary of parameter names and their initial values.

None
unit str | Unit

Unit of the output.

'meV'
display_name str | None

Display name for the component.

'Expression'
unique_name str | None

Unique name for the component.

None

Raises:

Type Description
ValueError

If the expression is invalid or does not contain 'x'.

TypeError

If any parameter value is not numeric.

Source code in src/easydynamics/sample_model/components/expression_component.py
 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
def __init__(
    self,
    expression: str,
    parameters: dict[str, Numeric] | None = None,
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'Expression',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the ExpressionComponent.

    Parameters
    ----------
    expression : str
        The symbolic expression as a string. Must contain 'x' as the independent variable.
    parameters : dict[str, Numeric] | None, default=None
        Dictionary of parameter names and their initial values.
    unit : str | sc.Unit, default='meV'
        Unit of the output.
    display_name : str | None, default='Expression'
        Display name for the component.
    unique_name : str | None, default=None
        Unique name for the component.

    Raises
    ------
    ValueError
        If the expression is invalid or does not contain 'x'.
    TypeError
        If any parameter value is not numeric.
    """
    super().__init__(unit=unit, display_name=display_name, unique_name=unique_name)

    if 'np.' in expression:
        raise ValueError(
            'NumPy syntax (np.*) is not supported. '
            "Use functions like 'exp', 'sin', etc. directly."
        )

    self._expression_str = expression

    locals_dict = {}
    locals_dict.update(self._ALLOWED_FUNCS)
    locals_dict.update(self._ALLOWED_CONSTANTS)

    try:
        self._expr = sp.sympify(expression, locals=locals_dict)
    except Exception as e:
        raise ValueError(f'Invalid expression: {expression}') from e

    # Extract symbols from the expression
    symbols = self._expr.free_symbols
    symbol_names = sorted(str(s) for s in symbols)

    if 'x' not in symbol_names:
        raise ValueError("Expression must contain 'x' as independent variable")

    # Reject unknown functions early so invalid expressions fail at init,
    # not later during numerical evaluation.
    allowed_function_names = set(self._ALLOWED_FUNCS) | {
        func.__name__ for func in self._ALLOWED_FUNCS.values()
    }

    # Walk all function-call nodes in the parsed expression (e.g. sin(x), foo(x)).
    # Keep only function names that are not in our allowlist.
    unknown_function_names: set[str] = set()
    function_atoms = self._expr.atoms(sp.Function)
    for function_atom in function_atoms:
        function_name = function_atom.func.__name__
        if function_name not in allowed_function_names:
            unknown_function_names.add(function_name)

    unknown_functions = sorted(unknown_function_names)

    if unknown_functions:
        raise ValueError(
            f'Unsupported function(s) in expression: {", ".join(unknown_functions)}'
        )

    # Create parameters
    if parameters is not None and not isinstance(parameters, dict):
        raise TypeError(
            f'Parameters must be None or a dictionary, got {type(parameters).__name__}'
        )

    if parameters is not None:
        for name, value in parameters.items():
            if not isinstance(value, Numeric):
                raise TypeError(f"Parameter '{name}' must be numeric")
    parameters = parameters or {}
    self._parameters: dict[str, Parameter] = {}

    self._symbol_names = symbol_names
    for name in self._symbol_names:
        if name in self._RESERVED_NAMES:
            continue

        value = parameters.get(name, 1.0)

        self._parameters[name] = Parameter(
            name=name,
            value=value,
            unit=self._unit,
        )

    # Create numerical function
    ordered_symbols = [sp.Symbol(name) for name in self._symbol_names]

    self._func = sp.lambdify(
        ordered_symbols,
        self._expr,
        modules=['numpy'],
    )

__repr__()

Repr function.

Source code in src/easydynamics/sample_model/components/expression_component.py
336
337
338
339
340
341
342
343
344
345
def __repr__(self) -> str:
    """Repr function."""
    param_str = ', '.join(f'{k}={v.value}' for k, v in self._parameters.items())
    return (
        f'{self.__class__.__name__}(\n'
        f"  expr='{self._expression_str}',\n"
        f'  unit={self._unit},\n'
        f'  parameters={{ {param_str} }}\n'
        f')'
    )

__setattr__(name, value)

Allow setting parameter values as attributes.

Parameters:

Name Type Description Default
name str

Name of the parameter to set.

required
value Numeric

New value for the parameter.

required

Raises:

Type Description
TypeError

If the value is not numeric.

Source code in src/easydynamics/sample_model/components/expression_component.py
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
def __setattr__(self, name: str, value: Numeric) -> None:
    """
    Allow setting parameter values as attributes.

    Parameters
    ----------
    name : str
        Name of the parameter to set.
    value : Numeric
        New value for the parameter.

    Raises
    ------
    TypeError
        If the value is not numeric.
    """
    if '_parameters' in self.__dict__ and name in self._parameters:
        param = self._parameters[name]

        if not isinstance(value, Numeric):
            raise TypeError(f'{name} must be numeric')

        param.value = value
    else:
        # For other attributes, use default behavior
        super().__setattr__(name, value)

convert_unit(_new_unit)

Convert the unit of the expression.

Unit conversion is not implemented for ExpressionComponent.

Parameters:

Name Type Description Default
_new_unit str | Unit

The new unit to convert to (ignored).

required

Raises:

Type Description
NotImplementedError

Always raised to indicate unit conversion is not supported.

Source code in src/easydynamics/sample_model/components/expression_component.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def convert_unit(self, _new_unit: str | sc.Unit) -> None:
    """
    Convert the unit of the expression.

    Unit conversion is not implemented for ExpressionComponent.

    Parameters
    ----------
    _new_unit : str | sc.Unit
        The new unit to convert to (ignored).

    Raises
    ------
    NotImplementedError
        Always raised to indicate unit conversion is not supported.
    """

    raise NotImplementedError('Unit conversion is not implemented for ExpressionComponent')

evaluate(x)

Evaluate the expression for given x values.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

Input values for the independent variable.

required

Returns:

Type Description
ndarray

Evaluated results.

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

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        Input values for the independent variable.

    Returns
    -------
    np.ndarray
        Evaluated results.
    """
    x = self._prepare_x_for_evaluate(x)

    args = []
    for name in self._symbol_names:
        if name == 'x':
            args.append(x)
        else:
            args.append(self._parameters[name].value)

    return self._func(*args)

expression property writable

Return the original expression string.

get_all_variables()

Return all parameters.

Returns:

Type Description
list[Parameter]

List of all parameters in the expression.

Source code in src/easydynamics/sample_model/components/expression_component.py
241
242
243
244
245
246
247
248
249
250
def get_all_variables(self) -> list[Parameter]:
    """
    Return all parameters.

    Returns
    -------
    list[Parameter]
        List of all parameters in the expression.
    """
    return list(self._parameters.values())

Gaussian

Bases: CreateParametersMixin, ModelComponent

Model of a Gaussian function.

The intensity is given by

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

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

If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Source code in src/easydynamics/sample_model/components/gaussian.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
class Gaussian(CreateParametersMixin, ModelComponent):
    r"""
    Model of a Gaussian function.

     The intensity is given by

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

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

    If the center is not provided, it will be centered at 0 and fixed, which is typically what you
    want in QENS.
    """

    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,
    ) -> None:
        """
        Initialize the Gaussian component.

        Parameters
        ----------
        area : Numeric | Parameter, default=1.0
            Area of the Gaussian.
        center : Numeric | Parameter | None, default=None
            Center of the Gaussian. If None.
        width : Numeric | Parameter, default=1.0
            Standard deviation.
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='Gaussian'
            Name of the component.
        unique_name : str | None, default=None
            Unique name of the component. if None, a unique_name is automatically generated. By
            default, 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.

        Returns
        -------
        Parameter
            The area parameter.
        """

        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """
        Set the value of the area parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the area parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """

        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.

        Returns
        -------
        Parameter
            The center parameter.
        """

        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """
        Set the center parameter value.

        Parameters
        ----------
        value : Numeric | None
            The new value for the center parameter. If None, defaults to 0 and is fixed.

        Raises
        ------
        TypeError
            If the value is not a number or None.
        """

        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 (standard deviation).

        Returns
        -------
        Parameter
            The width parameter.
        """
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """
        Set the width parameter value.

        Parameters
        ----------
        value : Numeric
            The new value for the width parameter.

        Raises
        ------
        TypeError
            If the value is not a number or None.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')

        if float(value) <= 0:
            raise ValueError('width must be positive')

        self._width.value = value

    def evaluate(
        self,
        x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray,
    ) -> np.ndarray:
        r"""
        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
        intensity is given by $$ I(x) = \frac{A}{\sigma \sqrt{2\pi}} \exp\left( -\frac{1}{2}
        \left(\frac{x - x_0}{\sigma}\right)^2 \right) $$

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

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Gaussian.

        Returns
        -------
        np.ndarray
            The intensity of the Gaussian at the given x values.
        """

        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) -> str:
        """
        Return a string representation of the Gaussian.

        Returns
        -------
        str
            A string representation of the Gaussian.
        """

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

__init__(area=1.0, center=None, width=1.0, unit='meV', display_name='Gaussian', unique_name=None)

Initialize the Gaussian component.

Parameters:

Name Type Description Default
area Numeric | Parameter

Area of the Gaussian.

1.0
center Numeric | Parameter | None

Center of the Gaussian. If None.

None
width Numeric | Parameter

Standard deviation.

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Name of the component.

'Gaussian'
unique_name str | None

Unique name of the component. if None, a unique_name is automatically generated. By default, None.

None
Source code in src/easydynamics/sample_model/components/gaussian.py
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
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,
) -> None:
    """
    Initialize the Gaussian component.

    Parameters
    ----------
    area : Numeric | Parameter, default=1.0
        Area of the Gaussian.
    center : Numeric | Parameter | None, default=None
        Center of the Gaussian. If None.
    width : Numeric | Parameter, default=1.0
        Standard deviation.
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='Gaussian'
        Name of the component.
    unique_name : str | None, default=None
        Unique name of the component. if None, a unique_name is automatically generated. By
        default, 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

__repr__()

Return a string representation of the Gaussian.

Returns:

Type Description
str

A string representation of the Gaussian.

Source code in src/easydynamics/sample_model/components/gaussian.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def __repr__(self) -> str:
    """
    Return a string representation of the Gaussian.

    Returns
    -------
    str
        A string representation of the Gaussian.
    """

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

area property writable

Get the area parameter.

Returns:

Type Description
Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

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 intensity is given by $$ I(x) = \frac{A}{\sigma \sqrt{2\pi}} \exp\left( -\frac{1}{2} \left(\frac{x - x_0}{\sigma}\right)^2 \right) $$

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

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Gaussian.

required

Returns:

Type Description
ndarray

The intensity of the Gaussian at the given x values.

Source code in src/easydynamics/sample_model/components/gaussian.py
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
def evaluate(
    self,
    x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray,
) -> np.ndarray:
    r"""
    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
    intensity is given by $$ I(x) = \frac{A}{\sigma \sqrt{2\pi}} \exp\left( -\frac{1}{2}
    \left(\frac{x - x_0}{\sigma}\right)^2 \right) $$

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

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Gaussian.

    Returns
    -------
    np.ndarray
        The intensity of the Gaussian at the given x values.
    """

    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 (standard deviation).

Returns:

Type Description
Parameter

The width parameter.

InstrumentModel

Bases: NewBase

InstrumentModel represents a model of the instrument in an experiment at various Q.

It can contain a model of the resolution function for convolutions, of the background and an offset in the energy axis.

Source code in src/easydynamics/sample_model/instrument_model.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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
class InstrumentModel(NewBase):
    """
    InstrumentModel represents a model of the instrument in an experiment at various Q.

    It can contain a model of the resolution function for convolutions, of the background and an
    offset in the energy axis.
    """

    def __init__(
        self,
        display_name: str = 'MyInstrumentModel',
        unique_name: str | None = None,
        Q: Q_type | None = None,
        resolution_model: ResolutionModel | None = None,
        background_model: BackgroundModel | None = None,
        energy_offset: Numeric | None = None,
        unit: str | sc.Unit = 'meV',
    ) -> None:
        """
        Initialize an InstrumentModel.

        Parameters
        ----------
        display_name : str, default='MyInstrumentModel'
            The display name of the InstrumentModel.
        unique_name : str | None, default=None
            The unique name of the InstrumentModel.
        Q : Q_type | None, default=None
            The Q values where the instrument is modelled.
        resolution_model : ResolutionModel | None, default=None
            The resolution model of the instrument. If None, an empty resolution model is created
            and no resolution convolution is carried out.
        background_model : BackgroundModel | None, default=None
            The background model of the instrument. If None, an empty background model is created,
            and the background evaluates to 0.
        energy_offset : Numeric | None, default=None
            Template energy offset of the instrument. Will be copied to each Q value. If None, the
            energy offset will be 0.
        unit : str | sc.Unit, default='meV'
            The unit of the energy axis.

        Raises
        ------
        TypeError
            If resolution_model is not a ResolutionModel or None, or if background_model is not a
            BackgroundModel or None, or if energy_offset is not a number or None.
        """
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
        )

        self._unit = _validate_unit(unit)

        if resolution_model is None:
            self._resolution_model = ResolutionModel()
        else:
            if not isinstance(resolution_model, ResolutionModel):
                raise TypeError(
                    f'resolution_model must be a ResolutionModel or None, '
                    f'got {type(resolution_model).__name__}'
                )
            self._resolution_model = resolution_model

        if background_model is None:
            self._background_model = BackgroundModel()
        else:
            if not isinstance(background_model, BackgroundModel):
                raise TypeError(
                    f'background_model must be a BackgroundModel or None, '
                    f'got {type(background_model).__name__}'
                )
            self._background_model = background_model

        if energy_offset is None:
            energy_offset = 0.0

        if not isinstance(energy_offset, Numeric):
            raise TypeError('energy_offset must be a number or None')

        self._energy_offset = Parameter(
            name='energy_offset',
            value=float(energy_offset),
            unit=self.unit,
            fixed=False,
        )
        self._Q = _validate_and_convert_Q(Q)
        self._on_Q_change()

    # -------------------------------------------------------------
    # Properties
    # -------------------------------------------------------------

    @property
    def resolution_model(self) -> ResolutionModel:
        """
        Get the resolution model of the instrument.

        Returns
        -------
        ResolutionModel
            The resolution model of the instrument.
        """
        return self._resolution_model

    @resolution_model.setter
    def resolution_model(self, value: ResolutionModel) -> None:
        """
        Set the resolution model of the instrument.

        Parameters
        ----------
        value : ResolutionModel
            The new resolution model of the instrument.

        Raises
        ------
        TypeError
            If value is not a ResolutionModel.
        """
        if not isinstance(value, ResolutionModel):
            raise TypeError(
                f'resolution_model must be a ResolutionModel, got {type(value).__name__}'
            )
        self._resolution_model = value
        self._on_resolution_model_change()

    @property
    def background_model(self) -> BackgroundModel:
        """
        Get the background model of the instrument.

        Returns
        -------
        BackgroundModel
            The background model of the instrument.
        """

        return self._background_model

    @background_model.setter
    def background_model(self, value: BackgroundModel) -> None:
        """
        Set the background model of the instrument.

        Parameters
        ----------
        value : BackgroundModel
            The new background model of the instrument.

        Raises
        ------
        TypeError
            If value is not a BackgroundModel.
        """

        if not isinstance(value, BackgroundModel):
            raise TypeError(
                f'background_model must be a BackgroundModel, got {type(value).__name__}'
            )
        self._background_model = value
        self._on_background_model_change()

    @property
    def Q(self) -> np.ndarray | None:
        """
        Get the Q values of the InstrumentModel.

        Returns
        -------
        np.ndarray | None
            The Q values of the InstrumentModel, or None if not set.
        """
        return self._Q

    @Q.setter
    def Q(self, value: Q_type | None) -> None:
        """
        Set the Q values of the InstrumentModel.

        If Q is already set, it raises an error if the new Q values are not similar to the old ones
        to prevent accidental changes to the background and resolution models. To change Q values,
        first run clear_Q().

        Parameters
        ----------
        value : Q_type | None
            The new Q values to set. If None, Q values are not changed.

        Raises
        ------
        ValueError
            If the new Q values are not similar to the old ones when Q is not None.
        """
        if value is None:
            return
        old_Q = self._Q
        new_Q = _validate_and_convert_Q(value)

        if old_Q is None:
            self._Q = new_Q
            self._on_Q_change()
            return

        if len(old_Q) != len(new_Q) or not np.allclose(old_Q, new_Q):
            raise ValueError(
                'New Q values are not similar to the old ones. '
                'To change Q values, first run clear_Q().'
            )

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

        Returns
        -------
        str | sc.Unit
            The unit of the InstrumentModel.
        """
        return self._unit

    @unit.setter
    def unit(self, _unit_str: str) -> None:
        """
        Set the unit of the InstrumentModel.

        The unit is read-only and cannot be set directly. Use convert_unit to change the unit
        between allowed types or create a new InstrumentModel with the desired unit.

        Parameters
        ----------
        _unit_str : str
            The new unit for the InstrumentModel (ignored).

        Raises
        ------
        AttributeError
            Always, as the unit is read-only.
        """
        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.'
        )

    @property
    def energy_offset(self) -> Parameter:
        """
        Get the energy offset template parameter of the instrument model.

        Returns
        -------
        Parameter
            The energy offset template parameter of the instrument model.
        """
        return self._energy_offset

    @energy_offset.setter
    def energy_offset(self, value: Numeric) -> None:
        """
        Set the offset parameter of the instrument model.

        Parameters
        ----------
        value : Numeric
            The new value for the energy offset parameter. Will be copied to all Q values.

        Raises
        ------
        TypeError
            If value is not a number.
        """
        if not isinstance(value, Numeric):
            raise TypeError(f'energy_offset must be a number, got {type(value).__name__}')
        self._energy_offset.value = value

        self._on_energy_offset_change()

    # --------------------------------------------------------------
    # Other methods
    # --------------------------------------------------------------

    def clear_Q(self, confirm: bool = False) -> None:
        """
        Clear the Q values of the InstrumentModel and any associated ResolutionModel and
        BackgroundModel, removing all component collections and their associated Parameters.

        Parameters
        ----------
        confirm : bool, default=False
            Confirmation to clear Q values.

        Raises
        ------
        ValueError
            If confirm is not True.
        """
        if not confirm:
            raise ValueError(
                'Clearing Q values requires confirmation. Set confirm=True to proceed.'
            )
        self._Q = None
        self.background_model.clear_Q(confirm=True)
        self.resolution_model.clear_Q(confirm=True)
        self._on_Q_change()

    def convert_unit(self, unit_str: str | sc.Unit) -> None:
        """
        Convert the unit of the InstrumentModel.

        Parameters
        ----------
        unit_str : str | sc.Unit
            The unit to convert to.

        Raises
        ------
        ValueError
            If unit_str is not a valid unit string or scipp Unit.
        """
        unit = _validate_unit(unit_str)
        if unit is None:
            raise ValueError('unit_str must be a valid unit string or scipp Unit')

        self._background_model.convert_unit(unit)
        self._resolution_model.convert_unit(unit)
        self._energy_offset.convert_unit(unit)
        for offset in self._energy_offsets:
            offset.convert_unit(unit)

        self._unit = unit

    def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]:
        """
        Get all variables in the InstrumentModel.

        Parameters
        ----------
        Q_index : int | None, default=None
            The index of the Q value to get variables for. If None, get variables for all Q values.


        Raises
        ------
        TypeError
            If Q_index is not an int or None.
        IndexError
            If Q_index is out of bounds for the Q values in the InstrumentModel.

        Returns
        -------
        list[Parameter]
            A list of all variables in the InstrumentModel. If Q_index is specified, only variables
            from the ComponentCollection at the given Q index are included. Otherwise, all
            variables in the InstrumentModel are included.
        """
        if self._Q is None:
            return []

        if Q_index is None:
            variables = [self._energy_offsets[i] for i in range(len(self._Q))]
        else:
            if not isinstance(Q_index, int):
                raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}')
            if Q_index < 0 or Q_index >= len(self._Q):
                raise IndexError(
                    f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}'
                )
            variables = [self._energy_offsets[Q_index]]

        variables.extend(self._background_model.get_all_variables(Q_index=Q_index))
        variables.extend(self._resolution_model.get_all_variables(Q_index=Q_index))

        return variables

    def fix_resolution_parameters(self) -> None:
        """Fix all parameters in the resolution model."""
        self.resolution_model.fix_all_parameters()

    def free_resolution_parameters(self) -> None:
        """Free all parameters in the resolution model."""
        self.resolution_model.free_all_parameters()

    def normalize_resolution(self) -> None:
        """Normalize the resolution model to have area 1."""
        self.resolution_model.normalize_area()

    def get_energy_offset(
        self,
        Q_index: int | None = None,
    ) -> Parameter | list[Parameter]:
        """
        Get the energy offset Parameter at a specific Q index.

        Parameters
        ----------
        Q_index : int | None, default=None
            The index of the Q value to get the energy offset for. If None, get the energy offset
            for all Q values.

        Raises
        ------
        ValueError
            If no Q values are set in the InstrumentModel.
        IndexError
            If Q_index is out of bounds.
        TypeError
            If Q_index is not an int or None.

        Returns
        -------
        Parameter | list[Parameter]
            The energy offset Parameter at the specified Q index, or a list of Parameters if
            Q_index is None.
        """
        if self._Q is None:
            raise ValueError('No Q values are set in the InstrumentModel.')

        if Q_index is None:
            return self._energy_offsets

        if not isinstance(Q_index, int):
            raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}')

        if Q_index < 0 or Q_index >= len(self._Q):
            raise IndexError(f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}')

        return self._energy_offsets[Q_index]

    def fix_energy_offset(self, Q_index: int | None = None) -> None:
        """
        Fix energy offset parameters.

        If Q_index is specified, only fix the energy offset for that Q value. If Q_index is None,
        fix energy offsets for all Q values.

        Parameters
        ----------
        Q_index : int | None, default=None
            The index of the Q value to fix the energy offset for. If None, fix energy offsets for
            all Q values.
        """
        self._fix_or_free_energy_offset(Q_index, fixed=True)

    def free_energy_offset(self, Q_index: int | None = None) -> None:
        """
        Free energy offset parameters.

        If Q_index is specified, only free the energy offset for that Q value. If Q_index is None,
        free energy offsets for all Q values.

        Parameters
        ----------
        Q_index : int | None, default=None
            The index of the Q value to free the energy offset for. If None, free energy offsets
            for all Q values.
        """
        self._fix_or_free_energy_offset(Q_index, fixed=False)

    # --------------------------------------------------------------
    # Private methods
    # --------------------------------------------------------------
    def _fix_or_free_energy_offset(self, Q_index: int | None = None, fixed: bool = True) -> None:
        """
        Fix or free energy offset parameters.

        If Q_index is specified, only fix or free the energy offset for that Q value. If Q_index is
        None, fix or free energy offsets for all Q values.

        Parameters
        ----------
        Q_index : int | None, default=None
            The index of the Q value to fix or free the energy offset for. If None, fix or free
            energy offsets for all Q values.
        fixed : bool, default=True
            Whether to fix (True) or free (False) the energy offset.

        Raises
        ------
        TypeError
            If Q_index is not an int or None.
        IndexError
            If Q_index is out of bounds for the Q values in the InstrumentModel.
        """

        if Q_index is None:
            for offset in self._energy_offsets:
                offset.fixed = fixed
        else:
            if not isinstance(Q_index, int):
                raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}')

            if Q_index < 0 or Q_index >= len(self._Q):
                raise IndexError(
                    f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}'
                )
            self._energy_offsets[Q_index].fixed = fixed

    def _generate_energy_offsets(self) -> None:
        """Generate energy offset Parameters for each Q value."""
        if self._Q is None:
            self._energy_offsets = []
            return

        self._energy_offsets = [copy(self._energy_offset) for _ in self._Q]

    def _on_Q_change(self) -> None:
        """Handle changes to the Q values."""
        self._generate_energy_offsets()
        self.resolution_model.Q = self.Q
        self.background_model.Q = self.Q

    def _on_energy_offset_change(self) -> None:
        """Handle changes to the energy offset."""
        for offset in self._energy_offsets:
            offset.value = self._energy_offset.value

    def _on_resolution_model_change(self) -> None:
        """Handle changes to the resolution model."""
        self.resolution_model.Q = self.Q

    def _on_background_model_change(self) -> None:
        """Handle changes to the background model."""
        self.background_model.Q = self.Q

    # -------------------------------------------------------------
    # Dunder methods
    # -------------------------------------------------------------

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

        Returns
        -------
        str
            A string representation of the InstrumentModel.
        """

        return (
            f'{self.__class__.__name__}('
            f'unique_name={self.unique_name!r}, '
            f'unit={self.unit}, '
            f'Q_len={None if self._Q is None else len(self._Q)}, '
            f'resolution_model={self._resolution_model!r}, '
            f'background_model={self._background_model!r}'
            f')'
        )

Q property writable

Get the Q values of the InstrumentModel.

Returns:

Type Description
ndarray | None

The Q values of the InstrumentModel, or None if not set.

__init__(display_name='MyInstrumentModel', unique_name=None, Q=None, resolution_model=None, background_model=None, energy_offset=None, unit='meV')

Initialize an InstrumentModel.

Parameters:

Name Type Description Default
display_name str

The display name of the InstrumentModel.

'MyInstrumentModel'
unique_name str | None

The unique name of the InstrumentModel.

None
Q Q_type | None

The Q values where the instrument is modelled.

None
resolution_model ResolutionModel | None

The resolution model of the instrument. If None, an empty resolution model is created and no resolution convolution is carried out.

None
background_model BackgroundModel | None

The background model of the instrument. If None, an empty background model is created, and the background evaluates to 0.

None
energy_offset Numeric | None

Template energy offset of the instrument. Will be copied to each Q value. If None, the energy offset will be 0.

None
unit str | Unit

The unit of the energy axis.

'meV'

Raises:

Type Description
TypeError

If resolution_model is not a ResolutionModel or None, or if background_model is not a BackgroundModel or None, or if energy_offset is not a number or None.

Source code in src/easydynamics/sample_model/instrument_model.py
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 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
def __init__(
    self,
    display_name: str = 'MyInstrumentModel',
    unique_name: str | None = None,
    Q: Q_type | None = None,
    resolution_model: ResolutionModel | None = None,
    background_model: BackgroundModel | None = None,
    energy_offset: Numeric | None = None,
    unit: str | sc.Unit = 'meV',
) -> None:
    """
    Initialize an InstrumentModel.

    Parameters
    ----------
    display_name : str, default='MyInstrumentModel'
        The display name of the InstrumentModel.
    unique_name : str | None, default=None
        The unique name of the InstrumentModel.
    Q : Q_type | None, default=None
        The Q values where the instrument is modelled.
    resolution_model : ResolutionModel | None, default=None
        The resolution model of the instrument. If None, an empty resolution model is created
        and no resolution convolution is carried out.
    background_model : BackgroundModel | None, default=None
        The background model of the instrument. If None, an empty background model is created,
        and the background evaluates to 0.
    energy_offset : Numeric | None, default=None
        Template energy offset of the instrument. Will be copied to each Q value. If None, the
        energy offset will be 0.
    unit : str | sc.Unit, default='meV'
        The unit of the energy axis.

    Raises
    ------
    TypeError
        If resolution_model is not a ResolutionModel or None, or if background_model is not a
        BackgroundModel or None, or if energy_offset is not a number or None.
    """
    super().__init__(
        display_name=display_name,
        unique_name=unique_name,
    )

    self._unit = _validate_unit(unit)

    if resolution_model is None:
        self._resolution_model = ResolutionModel()
    else:
        if not isinstance(resolution_model, ResolutionModel):
            raise TypeError(
                f'resolution_model must be a ResolutionModel or None, '
                f'got {type(resolution_model).__name__}'
            )
        self._resolution_model = resolution_model

    if background_model is None:
        self._background_model = BackgroundModel()
    else:
        if not isinstance(background_model, BackgroundModel):
            raise TypeError(
                f'background_model must be a BackgroundModel or None, '
                f'got {type(background_model).__name__}'
            )
        self._background_model = background_model

    if energy_offset is None:
        energy_offset = 0.0

    if not isinstance(energy_offset, Numeric):
        raise TypeError('energy_offset must be a number or None')

    self._energy_offset = Parameter(
        name='energy_offset',
        value=float(energy_offset),
        unit=self.unit,
        fixed=False,
    )
    self._Q = _validate_and_convert_Q(Q)
    self._on_Q_change()

__repr__()

Return a string representation of the InstrumentModel.

Returns:

Type Description
str

A string representation of the InstrumentModel.

Source code in src/easydynamics/sample_model/instrument_model.py
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
def __repr__(self) -> str:
    """
    Return a string representation of the InstrumentModel.

    Returns
    -------
    str
        A string representation of the InstrumentModel.
    """

    return (
        f'{self.__class__.__name__}('
        f'unique_name={self.unique_name!r}, '
        f'unit={self.unit}, '
        f'Q_len={None if self._Q is None else len(self._Q)}, '
        f'resolution_model={self._resolution_model!r}, '
        f'background_model={self._background_model!r}'
        f')'
    )

background_model property writable

Get the background model of the instrument.

Returns:

Type Description
BackgroundModel

The background model of the instrument.

clear_Q(confirm=False)

Clear the Q values of the InstrumentModel and any associated ResolutionModel and BackgroundModel, removing all component collections and their associated Parameters.

Parameters:

Name Type Description Default
confirm bool

Confirmation to clear Q values.

False

Raises:

Type Description
ValueError

If confirm is not True.

Source code in src/easydynamics/sample_model/instrument_model.py
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
def clear_Q(self, confirm: bool = False) -> None:
    """
    Clear the Q values of the InstrumentModel and any associated ResolutionModel and
    BackgroundModel, removing all component collections and their associated Parameters.

    Parameters
    ----------
    confirm : bool, default=False
        Confirmation to clear Q values.

    Raises
    ------
    ValueError
        If confirm is not True.
    """
    if not confirm:
        raise ValueError(
            'Clearing Q values requires confirmation. Set confirm=True to proceed.'
        )
    self._Q = None
    self.background_model.clear_Q(confirm=True)
    self.resolution_model.clear_Q(confirm=True)
    self._on_Q_change()

convert_unit(unit_str)

Convert the unit of the InstrumentModel.

Parameters:

Name Type Description Default
unit_str str | Unit

The unit to convert to.

required

Raises:

Type Description
ValueError

If unit_str is not a valid unit string or scipp Unit.

Source code in src/easydynamics/sample_model/instrument_model.py
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
def convert_unit(self, unit_str: str | sc.Unit) -> None:
    """
    Convert the unit of the InstrumentModel.

    Parameters
    ----------
    unit_str : str | sc.Unit
        The unit to convert to.

    Raises
    ------
    ValueError
        If unit_str is not a valid unit string or scipp Unit.
    """
    unit = _validate_unit(unit_str)
    if unit is None:
        raise ValueError('unit_str must be a valid unit string or scipp Unit')

    self._background_model.convert_unit(unit)
    self._resolution_model.convert_unit(unit)
    self._energy_offset.convert_unit(unit)
    for offset in self._energy_offsets:
        offset.convert_unit(unit)

    self._unit = unit

energy_offset property writable

Get the energy offset template parameter of the instrument model.

Returns:

Type Description
Parameter

The energy offset template parameter of the instrument model.

fix_energy_offset(Q_index=None)

Fix energy offset parameters.

If Q_index is specified, only fix the energy offset for that Q value. If Q_index is None, fix energy offsets for all Q values.

Parameters:

Name Type Description Default
Q_index int | None

The index of the Q value to fix the energy offset for. If None, fix energy offsets for all Q values.

None
Source code in src/easydynamics/sample_model/instrument_model.py
448
449
450
451
452
453
454
455
456
457
458
459
460
461
def fix_energy_offset(self, Q_index: int | None = None) -> None:
    """
    Fix energy offset parameters.

    If Q_index is specified, only fix the energy offset for that Q value. If Q_index is None,
    fix energy offsets for all Q values.

    Parameters
    ----------
    Q_index : int | None, default=None
        The index of the Q value to fix the energy offset for. If None, fix energy offsets for
        all Q values.
    """
    self._fix_or_free_energy_offset(Q_index, fixed=True)

fix_resolution_parameters()

Fix all parameters in the resolution model.

Source code in src/easydynamics/sample_model/instrument_model.py
394
395
396
def fix_resolution_parameters(self) -> None:
    """Fix all parameters in the resolution model."""
    self.resolution_model.fix_all_parameters()

free_energy_offset(Q_index=None)

Free energy offset parameters.

If Q_index is specified, only free the energy offset for that Q value. If Q_index is None, free energy offsets for all Q values.

Parameters:

Name Type Description Default
Q_index int | None

The index of the Q value to free the energy offset for. If None, free energy offsets for all Q values.

None
Source code in src/easydynamics/sample_model/instrument_model.py
463
464
465
466
467
468
469
470
471
472
473
474
475
476
def free_energy_offset(self, Q_index: int | None = None) -> None:
    """
    Free energy offset parameters.

    If Q_index is specified, only free the energy offset for that Q value. If Q_index is None,
    free energy offsets for all Q values.

    Parameters
    ----------
    Q_index : int | None, default=None
        The index of the Q value to free the energy offset for. If None, free energy offsets
        for all Q values.
    """
    self._fix_or_free_energy_offset(Q_index, fixed=False)

free_resolution_parameters()

Free all parameters in the resolution model.

Source code in src/easydynamics/sample_model/instrument_model.py
398
399
400
def free_resolution_parameters(self) -> None:
    """Free all parameters in the resolution model."""
    self.resolution_model.free_all_parameters()

get_all_variables(Q_index=None)

Get all variables in the InstrumentModel.

Parameters:

Name Type Description Default
Q_index int | None

The index of the Q value to get variables for. If None, get variables for all Q values.

None

Raises:

Type Description
TypeError

If Q_index is not an int or None.

IndexError

If Q_index is out of bounds for the Q values in the InstrumentModel.

Returns:

Type Description
list[Parameter]

A list of all variables in the InstrumentModel. If Q_index is specified, only variables from the ComponentCollection at the given Q index are included. Otherwise, all variables in the InstrumentModel are included.

Source code in src/easydynamics/sample_model/instrument_model.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]:
    """
    Get all variables in the InstrumentModel.

    Parameters
    ----------
    Q_index : int | None, default=None
        The index of the Q value to get variables for. If None, get variables for all Q values.


    Raises
    ------
    TypeError
        If Q_index is not an int or None.
    IndexError
        If Q_index is out of bounds for the Q values in the InstrumentModel.

    Returns
    -------
    list[Parameter]
        A list of all variables in the InstrumentModel. If Q_index is specified, only variables
        from the ComponentCollection at the given Q index are included. Otherwise, all
        variables in the InstrumentModel are included.
    """
    if self._Q is None:
        return []

    if Q_index is None:
        variables = [self._energy_offsets[i] for i in range(len(self._Q))]
    else:
        if not isinstance(Q_index, int):
            raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}')
        if Q_index < 0 or Q_index >= len(self._Q):
            raise IndexError(
                f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}'
            )
        variables = [self._energy_offsets[Q_index]]

    variables.extend(self._background_model.get_all_variables(Q_index=Q_index))
    variables.extend(self._resolution_model.get_all_variables(Q_index=Q_index))

    return variables

get_energy_offset(Q_index=None)

Get the energy offset Parameter at a specific Q index.

Parameters:

Name Type Description Default
Q_index int | None

The index of the Q value to get the energy offset for. If None, get the energy offset for all Q values.

None

Raises:

Type Description
ValueError

If no Q values are set in the InstrumentModel.

IndexError

If Q_index is out of bounds.

TypeError

If Q_index is not an int or None.

Returns:

Type Description
Parameter | list[Parameter]

The energy offset Parameter at the specified Q index, or a list of Parameters if Q_index is None.

Source code in src/easydynamics/sample_model/instrument_model.py
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
def get_energy_offset(
    self,
    Q_index: int | None = None,
) -> Parameter | list[Parameter]:
    """
    Get the energy offset Parameter at a specific Q index.

    Parameters
    ----------
    Q_index : int | None, default=None
        The index of the Q value to get the energy offset for. If None, get the energy offset
        for all Q values.

    Raises
    ------
    ValueError
        If no Q values are set in the InstrumentModel.
    IndexError
        If Q_index is out of bounds.
    TypeError
        If Q_index is not an int or None.

    Returns
    -------
    Parameter | list[Parameter]
        The energy offset Parameter at the specified Q index, or a list of Parameters if
        Q_index is None.
    """
    if self._Q is None:
        raise ValueError('No Q values are set in the InstrumentModel.')

    if Q_index is None:
        return self._energy_offsets

    if not isinstance(Q_index, int):
        raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}')

    if Q_index < 0 or Q_index >= len(self._Q):
        raise IndexError(f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}')

    return self._energy_offsets[Q_index]

normalize_resolution()

Normalize the resolution model to have area 1.

Source code in src/easydynamics/sample_model/instrument_model.py
402
403
404
def normalize_resolution(self) -> None:
    """Normalize the resolution model to have area 1."""
    self.resolution_model.normalize_area()

resolution_model property writable

Get the resolution model of the instrument.

Returns:

Type Description
ResolutionModel

The resolution model of the instrument.

unit property writable

Get the unit of the InstrumentModel.

Returns:

Type Description
str | Unit

The unit of the InstrumentModel.

Lorentzian

Bases: CreateParametersMixin, ModelComponent

Model of a Lorentzian function.

The intensity is given by $$ I(x) = \frac{A}{\pi} \frac{\Gamma}{(x - x_0)^2 + \Gamma^2}, $$ where \(A\) is the area, \(x_0\) is the center, and \(\Gamma\) is the half width at half maximum (HWHM).

If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Source code in src/easydynamics/sample_model/components/lorentzian.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
class Lorentzian(CreateParametersMixin, ModelComponent):
    r"""
    Model of a Lorentzian function.

    The intensity is given by $$ I(x) = \frac{A}{\pi} \frac{\Gamma}{(x - x_0)^2 + \Gamma^2}, $$
    where $A$ is the area, $x_0$ is the center, and $\Gamma$ is the half width at half maximum
    (HWHM).

    If the center is not provided, it will be centered at 0 and fixed, which is typically what you
    want in QENS.
    """

    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,
    ) -> None:
        """
        Initialize the Lorentzian component.

        Parameters
        ----------
        area : Numeric | Parameter, default=1.0
            Area of the Lorentzian.
        center : Numeric | Parameter | None, default=None
            Center of the Lorentzian. If None.
        width : Numeric | Parameter, default=1.0
            Half width at half maximum (HWHM).
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='Lorentzian'
            Name of the component.
        unique_name : str | None, default=None
            Unique name of the component. If None, a unique_name is automatically generated. By
            default, 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.

        Returns
        -------
        Parameter
            The area parameter.
        """
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """
        Set the value of the area parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the area parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """
        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.

        Returns
        -------
        Parameter
            The center parameter.
        """
        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """
        Set the value of the center parameter.

        Parameters
        ----------
        value : Numeric | None
            The new value for the center parameter. If None, defaults to 0 and is fixed.

        Raises
        ------
        TypeError
            If the value is not a number or None.
        """

        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 (HWHM).

        Returns
        -------
        Parameter
            The width parameter.
        """
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """
        Set the width parameter value (HWHM).

        Parameters
        ----------
        value : Numeric
            The new value for the width parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')

        if float(value) <= 0:
            raise ValueError('width must be positive')
        self._width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        r"""
        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
        intensity is given by

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

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

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Lorentzian.

        Returns
        -------
        np.ndarray
            The intensity of the Lorentzian at the given x values.
        """

        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) -> str:
        """
        Return a string representation of the Lorentzian.

        Returns
        -------
        str
            A string representation of the Lorentzian.
        """
        return (
            f'Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n'
            f'area = {self.area},\n center = {self.center},\n width = {self.width})'
        )

__init__(area=1.0, center=None, width=1.0, unit='meV', display_name='Lorentzian', unique_name=None)

Initialize the Lorentzian component.

Parameters:

Name Type Description Default
area Numeric | Parameter

Area of the Lorentzian.

1.0
center Numeric | Parameter | None

Center of the Lorentzian. If None.

None
width Numeric | Parameter

Half width at half maximum (HWHM).

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Name of the component.

'Lorentzian'
unique_name str | None

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

None
Source code in src/easydynamics/sample_model/components/lorentzian.py
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
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,
) -> None:
    """
    Initialize the Lorentzian component.

    Parameters
    ----------
    area : Numeric | Parameter, default=1.0
        Area of the Lorentzian.
    center : Numeric | Parameter | None, default=None
        Center of the Lorentzian. If None.
    width : Numeric | Parameter, default=1.0
        Half width at half maximum (HWHM).
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='Lorentzian'
        Name of the component.
    unique_name : str | None, default=None
        Unique name of the component. If None, a unique_name is automatically generated. By
        default, 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

__repr__()

Return a string representation of the Lorentzian.

Returns:

Type Description
str

A string representation of the Lorentzian.

Source code in src/easydynamics/sample_model/components/lorentzian.py
209
210
211
212
213
214
215
216
217
218
219
220
221
def __repr__(self) -> str:
    """
    Return a string representation of the Lorentzian.

    Returns
    -------
    str
        A string representation of the Lorentzian.
    """
    return (
        f'Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n'
        f'area = {self.area},\n center = {self.center},\n width = {self.width})'
    )

area property writable

Get the area parameter.

Returns:

Type Description
Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

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 intensity is given by

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

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

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Lorentzian.

required

Returns:

Type Description
ndarray

The intensity of the Lorentzian at the given x values.

Source code in src/easydynamics/sample_model/components/lorentzian.py
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
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    r"""
    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
    intensity is given by

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

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

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Lorentzian.

    Returns
    -------
    np.ndarray
        The intensity of the Lorentzian at the given x values.
    """

    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 (HWHM).

Returns:

Type Description
Parameter

The width parameter.

Polynomial

Bases: ModelComponent

Polynomial function component.

The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N, $$ where \(C_i\) are the coefficients.

Source code in src/easydynamics/sample_model/components/polynomial.py
 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
class Polynomial(ModelComponent):
    r"""
    Polynomial function component.

    The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N, $$ where $C_i$ are
    the coefficients.
    """

    def __init__(
        self,
        coefficients: Sequence[Numeric | Parameter] = (0.0,),
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Polynomial',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the Polynomial component.

        Parameters
        ----------
        coefficients : Sequence[Numeric | Parameter], default=(0.0,)
            Coefficients c0, c1, ..., cN.
        unit : str | sc.Unit, default='meV'
            Unit of the Polynomial component.
        display_name : str | None, default='Polynomial'
            Display name of the Polynomial component.
        unique_name : str | None, default=None
            Unique name of the component. If None, a unique_name is automatically generated. By
            default, None.

        Raises
        ------
        TypeError
            If coefficients is not a sequence of numbers or Parameters or if any item in
            coefficients is not a number or Parameter.
        ValueError
            If coefficients is an empty sequence.
        """

        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.

        Returns
        -------
        list[Parameter]
            The coefficients of the polynomial.
        """
        return list(self._coefficients)

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

        Length must match current number of coefficients.

        Parameters
        ----------
        coeffs : Sequence[Numeric | Parameter]
            New coefficients as a sequence of numbers or Parameters.

        Raises
        ------
        TypeError
            If coeffs is not a sequence of numbers or Parameters or if any item in coeffs is not a
            number or Parameter.
        ValueError
            If the length of coeffs does not match the existing 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.

        Returns
        -------
        list[float]
            The coefficient values of the polynomial.
        """
        return [param.value for param in self._coefficients]

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

        The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ...
        + c_N x^N, $$ where $C_i$ are the coefficients.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Polynomial.

        Returns
        -------
        np.ndarray
            The evaluated Polynomial at the given x values.
        """

        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,
                stacklevel=2,
            )
        return result

    @property
    def degree(self) -> int:
        """
        Get the degree of the polynomial.

        Returns
        -------
        int
            The degree of the polynomial.
        """
        return len(self._coefficients) - 1

    @degree.setter
    def degree(self, _value: int) -> None:
        """
        The degree is determined by the number of coefficients and cannot be set directly.

        Parameters
        ----------
        _value : int
            The new degree of the polynomial.

        Raises
        ------
        AttributeError
            Always raised since degree cannot be set directly.
        """
        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 variables from the model component.

        Returns
        -------
        list[DescriptorBase]
            List of variables in the component.
        """
        return list(self._coefficients)

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

        Parameters
        ----------
        unit : str | sc.Unit
            The target unit to convert to.

        Raises
        ------
        UnitError
            If the provided unit is not a string or sc.Unit.
        """

        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:
        """
        Return a string representation of the Polynomial.

        Returns
        -------
        str
            A string representation of the Polynomial.
        """

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

__init__(coefficients=(0.0,), unit='meV', display_name='Polynomial', unique_name=None)

Initialize the Polynomial component.

Parameters:

Name Type Description Default
coefficients Sequence[Numeric | Parameter]

Coefficients c0, c1, ..., cN.

(0.0,)
unit str | Unit

Unit of the Polynomial component.

'meV'
display_name str | None

Display name of the Polynomial component.

'Polynomial'
unique_name str | None

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

None

Raises:

Type Description
TypeError

If coefficients is not a sequence of numbers or Parameters or if any item in coefficients is not a number or Parameter.

ValueError

If coefficients is an empty sequence.

Source code in src/easydynamics/sample_model/components/polynomial.py
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
def __init__(
    self,
    coefficients: Sequence[Numeric | Parameter] = (0.0,),
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'Polynomial',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the Polynomial component.

    Parameters
    ----------
    coefficients : Sequence[Numeric | Parameter], default=(0.0,)
        Coefficients c0, c1, ..., cN.
    unit : str | sc.Unit, default='meV'
        Unit of the Polynomial component.
    display_name : str | None, default='Polynomial'
        Display name of the Polynomial component.
    unique_name : str | None, default=None
        Unique name of the component. If None, a unique_name is automatically generated. By
        default, None.

    Raises
    ------
    TypeError
        If coefficients is not a sequence of numbers or Parameters or if any item in
        coefficients is not a number or Parameter.
    ValueError
        If coefficients is an empty sequence.
    """

    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)

__repr__()

Return a string representation of the Polynomial.

Returns:

Type Description
str

A string representation of the Polynomial.

Source code in src/easydynamics/sample_model/components/polynomial.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def __repr__(self) -> str:
    """
    Return a string representation of the Polynomial.

    Returns
    -------
    str
        A string representation of the Polynomial.
    """

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

coefficient_values()

Get the coefficients of the polynomial as a list.

Returns:

Type Description
list[float]

The coefficient values of the polynomial.

Source code in src/easydynamics/sample_model/components/polynomial.py
138
139
140
141
142
143
144
145
146
147
def coefficient_values(self) -> list[float]:
    """
    Get the coefficients of the polynomial as a list.

    Returns
    -------
    list[float]
        The coefficient values of the polynomial.
    """
    return [param.value for param in self._coefficients]

coefficients property writable

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

Returns:

Type Description
list[Parameter]

The coefficients of the polynomial.

convert_unit(unit)

Convert the unit of the polynomial.

Parameters:

Name Type Description Default
unit str | Unit

The target unit to convert to.

required

Raises:

Type Description
UnitError

If the provided unit is not a string or sc.Unit.

Source code in src/easydynamics/sample_model/components/polynomial.py
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
def convert_unit(self, unit: str | sc.Unit) -> None:
    """
    Convert the unit of the polynomial.

    Parameters
    ----------
    unit : str | sc.Unit
        The target unit to convert to.

    Raises
    ------
    UnitError
        If the provided unit is not a string or sc.Unit.
    """

    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

Get the degree of the polynomial.

Returns:

Type Description
int

The degree of the polynomial.

evaluate(x)

Evaluate the Polynomial at the given x values.

The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N, $$ where \(C_i\) are the coefficients.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Polynomial.

required

Returns:

Type Description
ndarray

The evaluated Polynomial at the given x values.

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

    The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ...
    + c_N x^N, $$ where $C_i$ are the coefficients.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Polynomial.

    Returns
    -------
    np.ndarray
        The evaluated Polynomial at the given x values.
    """

    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,
            stacklevel=2,
        )
    return result

get_all_variables()

Get all variables from the model component.

Returns:

Type Description
list[DescriptorBase]

List of variables in the component.

Source code in src/easydynamics/sample_model/components/polynomial.py
214
215
216
217
218
219
220
221
222
223
def get_all_variables(self) -> list[DescriptorBase]:
    """
    Get all variables from the model component.

    Returns
    -------
    list[DescriptorBase]
        List of variables in the component.
    """
    return list(self._coefficients)

ResolutionModel

Bases: ModelBase

ResolutionModel represents a model of the instrment resolution in an experiment at various Q.

Source code in src/easydynamics/sample_model/resolution_model.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class ResolutionModel(ModelBase):
    """
    ResolutionModel represents a model of the instrment resolution in an experiment at various Q.
    """

    def __init__(
        self,
        display_name: str = 'MyResolutionModel',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
        components: ModelComponent | ComponentCollection | None = None,
        Q: Q_type | None = None,
    ) -> None:
        """
        Initialize a ResolutionModel.

        Parameters
        ----------
        display_name : str, default='MyResolutionModel'
            Display name of the model.
        unique_name : str | None, default=None
            Unique name of the model. If None, a unique name will be generated.
        unit : str | sc.Unit, default='meV'
            Unit of the model.
        components : ModelComponent | ComponentCollection | None, default=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, default=None
            Q values for the model. If None, Q is not set.
        """

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

    def append_component(self, component: ModelComponent | ComponentCollection) -> None:
        """
        Append a component to the ResolutionModel.

        Does not allow DeltaFunction or Polynomial components, as these are not physical resolution
        components.

        Parameters
        ----------
        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)

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

Initialize a ResolutionModel.

Parameters:

Name Type Description Default
display_name str

Display name of the model.

'MyResolutionModel'
unique_name str | None

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

None
unit str | Unit

Unit of the model.

'meV'
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.

None
Q Q_type | None

Q values for the model. If None, Q is not set.

None
Source code in src/easydynamics/sample_model/resolution_model.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
def __init__(
    self,
    display_name: str = 'MyResolutionModel',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
    components: ModelComponent | ComponentCollection | None = None,
    Q: Q_type | None = None,
) -> None:
    """
    Initialize a ResolutionModel.

    Parameters
    ----------
    display_name : str, default='MyResolutionModel'
        Display name of the model.
    unique_name : str | None, default=None
        Unique name of the model. If None, a unique name will be generated.
    unit : str | sc.Unit, default='meV'
        Unit of the model.
    components : ModelComponent | ComponentCollection | None, default=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, default=None
        Q values for the model. If None, Q is not set.
    """

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

append_component(component)

Append a component to the ResolutionModel.

Does not allow DeltaFunction or Polynomial components, as these are not physical resolution components.

Parameters:

Name Type Description Default
component ModelComponent | ComponentCollection

Component(s) to append.

required

Raises:

Type Description
TypeError

If the component is a DeltaFunction or Polynomial.

Source code in src/easydynamics/sample_model/resolution_model.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def append_component(self, component: ModelComponent | ComponentCollection) -> None:
    """
    Append a component to the ResolutionModel.

    Does not allow DeltaFunction or Polynomial components, as these are not physical resolution
    components.

    Parameters
    ----------
    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)

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.

Source code in src/easydynamics/sample_model/sample_model.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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
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.
    """

    def __init__(
        self,
        display_name: str = 'MySampleModel',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
        components: ModelComponent | ComponentCollection | None = None,
        Q: Q_type | None = None,
        diffusion_models: DiffusionModelBase | list[DiffusionModelBase] | None = None,
        temperature: float | None = None,
        temperature_unit: str | sc.Unit = 'K',
        detailed_balance_settings: DetailedBalanceSettings | None = None,
    ) -> None:
        """
        Initialize the SampleModel.

        Parameters
        ----------
        display_name : str, default='MySampleModel'
            Display name of the model.
        unique_name : str | None, default=None
            Unique name of the model. If None, a unique name will be generated.
        unit : str | sc.Unit, default='meV'
            Unit of the model. If None,.
        components : ModelComponent | ComponentCollection | None, default=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, default=None
            Q values for the model. If None, Q is not set.
        diffusion_models : DiffusionModelBase | list[DiffusionModelBase] | None, default=None
            Diffusion models to include in the SampleModel. If None, no diffusion models are added.
        temperature : float | None, default=None
            Temperature for detailed balancing. If None, no detailed balancing is applied. By
            default, None.
        temperature_unit : str | sc.Unit, default='K'
            Unit of the temperature.
        detailed_balance_settings : DetailedBalanceSettings | None, default=None
            Settings for detailed balancing.

        Raises
        ------
        TypeError
            If diffusion_models is not a DiffusionModelBase, a list of DiffusionModelBase, or None,
            or if temperature is not a number or None, or if detailed_balance_settings is not a
            DetailedBalanceSettings instance.
        ValueError
            If temperature is negative.
        """
        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 detailed_balance_settings is None:
            self._detailed_balance_settings = DetailedBalanceSettings()
        elif isinstance(detailed_balance_settings, DetailedBalanceSettings):
            self._detailed_balance_settings = detailed_balance_settings
        else:
            raise TypeError('detailed_balance_settings must be a DetailedBalanceSettings or None')

    # ------------------------------------------------------------------
    # Component management
    # ------------------------------------------------------------------

    def append_diffusion_model(self, diffusion_model: DiffusionModelBase) -> None:
        """
        Append a DiffusionModel to the SampleModel.

        Parameters
        ----------
        diffusion_model : DiffusionModelBase
            The DiffusionModel to append.

        Raises
        ------
        TypeError
            If the diffusion_model is not a DiffusionModelBase.
        """

        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.

        Parameters
        ----------
        name : 'str'
            The unique name of the DiffusionModel to remove.

        Raises
        ------
        ValueError
            If no DiffusionModel with the given unique name is found.
        """
        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.

        Returns
        -------
        list[DiffusionModelBase]
            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.

        Parameters
        ----------
        value : DiffusionModelBase | list[DiffusionModelBase] | None
            The diffusion model(s) to set. Can be a single DiffusionModelBase, a list of
            DiffusionModelBase, or None to clear all diffusion models.

        Raises
        ------
        TypeError
            If value is not a DiffusionModelBase, a list of DiffusionModelBase, or None.
        """

        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._on_diffusion_models_change()

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

        Returns
        -------
        Parameter | None
            The temperature Parameter of the SampleModel, or None if not set.
        """
        return self._temperature

    @temperature.setter
    def temperature(self, value: Numeric | None) -> None:
        """
        Set the temperature of the SampleModel.

        Parameters
        ----------
        value : Numeric | None
            The temperature value to set. Can be a number or None to unset the temperature.

        Raises
        ------
        TypeError
            If value is not a number or None.
        ValueError
            If value is negative.
        """
        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.

        Returns
        -------
        str | sc.Unit
            The unit of the temperature Parameter.
        """
        return self._temperature_unit

    @temperature_unit.setter
    def temperature_unit(self, _value: str | sc.Unit) -> None:
        """
        The temperature unit of the SampleModel is read-only.

        Parameters
        ----------
        _value : str | sc.Unit
            The unit to set for the temperature Parameter.

        Raises
        ------
        AttributeError
            Always, as temperature_unit is read-only.
        """

        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.'
        )

    def convert_temperature_unit(self, unit: str | sc.Unit) -> None:
        """
        Convert the unit of the temperature Parameter.

        Parameters
        ----------
        unit : str | sc.Unit
            The unit to convert the temperature Parameter to.

        Raises
        ------
        ValueError
            If temperature is not set or conversion fails.
        Exception
            If the provided unit is invalid or cannot be converted.
        """

        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:
            # Attempt to rollback on failure
            with suppress(Exception):
                self.temperature.convert_unit(old_unit)
            raise

    @property
    def normalize_detailed_balance(self) -> bool:
        """
        Get whether to divide the detailed balance factor by temperature.

        Returns
        -------
        bool
            True if the detailed balance factor is divided by temperature, False otherwise.
        """
        return self.detailed_balance_settings.normalize_detailed_balance

    @normalize_detailed_balance.setter
    def normalize_detailed_balance(self, value: bool) -> None:
        """
        Set whether to divide the detailed balance factor by temperature.

        Parameters
        ----------
        value : bool
            True to divide the detailed balance factor by temperature, False otherwise.

        Raises
        ------
        TypeError
            If value is not a bool.
        """
        if not isinstance(value, bool):
            raise TypeError('normalize_detailed_balance must be True or False')
        self.detailed_balance_settings.normalize_detailed_balance = value

    @property
    def use_detailed_balance(self) -> bool:
        """
        Get whether to apply detailed balance to the model.

        Returns
        -------
        bool
            True if detailed balance is applied, False otherwise.
        """
        return self.detailed_balance_settings.use_detailed_balance

    @use_detailed_balance.setter
    def use_detailed_balance(self, value: bool) -> None:
        """
        Set whether to apply detailed balance to the model.

        Parameters
        ----------
        value : bool
            True to apply detailed balance, False otherwise.

        Raises
        ------
        TypeError
            If value is not a bool.
        """
        if not isinstance(value, bool):
            raise TypeError('use_detailed_balance must be True or False')
        self.detailed_balance_settings.use_detailed_balance = value

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

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

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

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

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

    # ------------------------------------------------------------------
    # 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 : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values to evaluate the model at. Can be a number, list, numpy array, scipp
            Variable, or scipp DataArray.

        Returns
        -------
        list[np.ndarray]
            List of evaluated model values for each Q.
        """

        y = super().evaluate(x)

        if self.temperature is not None and self.detailed_balance_settings.use_detailed_balance:
            DBF = detailed_balance_factor(
                energy=x,
                temperature=self.temperature,
                divide_by_temperature=self.detailed_balance_settings.normalize_detailed_balance,
                energy_unit=self.unit,
            )
            y = [yi * DBF for yi in y]

        return y

    def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]:
        """
        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.

        Parameters
        ----------
        Q_index : int | None, default=None
            If specified, only get variables from the ComponentCollection at the given Q index. If
            None, get variables from all ComponentCollections.

        Returns
        -------
        list[Parameter]
            List of all Parameters and Descriptors, including temperature if set and all variables
            from diffusion models.
        """

        all_vars = super().get_all_variables(Q_index=Q_index)
        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.
        """
        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, strict=True
            ):
                for component in source.components:
                    target.append_component(component)

    def _on_diffusion_models_change(self) -> None:
        """Handle changes to the diffusion models."""
        self._generate_component_collections()

    # ------------------------------------------------------------------
    # dunder methods
    # ------------------------------------------------------------------

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

        Returns
        -------
        str
            A string representation of the SampleModel.
        """

        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'detailed_balance_settings = {self.detailed_balance_settings}'
        )

__init__(display_name='MySampleModel', unique_name=None, unit='meV', components=None, Q=None, diffusion_models=None, temperature=None, temperature_unit='K', detailed_balance_settings=None)

Initialize the SampleModel.

Parameters:

Name Type Description Default
display_name str

Display name of the model.

'MySampleModel'
unique_name str | None

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

None
unit str | Unit

Unit of the model. If None,.

'meV'
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.

None
Q Q_type | None

Q values for the model. If None, Q is not set.

None
diffusion_models DiffusionModelBase | list[DiffusionModelBase] | None

Diffusion models to include in the SampleModel. If None, no diffusion models are added.

None
temperature float | None

Temperature for detailed balancing. If None, no detailed balancing is applied. By default, None.

None
temperature_unit str | Unit

Unit of the temperature.

'K'
detailed_balance_settings DetailedBalanceSettings | None

Settings for detailed balancing.

None

Raises:

Type Description
TypeError

If diffusion_models is not a DiffusionModelBase, a list of DiffusionModelBase, or None, or if temperature is not a number or None, or if detailed_balance_settings is not a DetailedBalanceSettings instance.

ValueError

If temperature is negative.

Source code in src/easydynamics/sample_model/sample_model.py
 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
def __init__(
    self,
    display_name: str = 'MySampleModel',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
    components: ModelComponent | ComponentCollection | None = None,
    Q: Q_type | None = None,
    diffusion_models: DiffusionModelBase | list[DiffusionModelBase] | None = None,
    temperature: float | None = None,
    temperature_unit: str | sc.Unit = 'K',
    detailed_balance_settings: DetailedBalanceSettings | None = None,
) -> None:
    """
    Initialize the SampleModel.

    Parameters
    ----------
    display_name : str, default='MySampleModel'
        Display name of the model.
    unique_name : str | None, default=None
        Unique name of the model. If None, a unique name will be generated.
    unit : str | sc.Unit, default='meV'
        Unit of the model. If None,.
    components : ModelComponent | ComponentCollection | None, default=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, default=None
        Q values for the model. If None, Q is not set.
    diffusion_models : DiffusionModelBase | list[DiffusionModelBase] | None, default=None
        Diffusion models to include in the SampleModel. If None, no diffusion models are added.
    temperature : float | None, default=None
        Temperature for detailed balancing. If None, no detailed balancing is applied. By
        default, None.
    temperature_unit : str | sc.Unit, default='K'
        Unit of the temperature.
    detailed_balance_settings : DetailedBalanceSettings | None, default=None
        Settings for detailed balancing.

    Raises
    ------
    TypeError
        If diffusion_models is not a DiffusionModelBase, a list of DiffusionModelBase, or None,
        or if temperature is not a number or None, or if detailed_balance_settings is not a
        DetailedBalanceSettings instance.
    ValueError
        If temperature is negative.
    """
    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 detailed_balance_settings is None:
        self._detailed_balance_settings = DetailedBalanceSettings()
    elif isinstance(detailed_balance_settings, DetailedBalanceSettings):
        self._detailed_balance_settings = detailed_balance_settings
    else:
        raise TypeError('detailed_balance_settings must be a DetailedBalanceSettings or None')

__repr__()

Return a string representation of the SampleModel.

Returns:

Type Description
str

A string representation of the SampleModel.

Source code in src/easydynamics/sample_model/sample_model.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
def __repr__(self) -> str:
    """
    Return a string representation of the SampleModel.

    Returns
    -------
    str
        A string representation of the SampleModel.
    """

    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'detailed_balance_settings = {self.detailed_balance_settings}'
    )

append_diffusion_model(diffusion_model)

Append a DiffusionModel to the SampleModel.

Parameters:

Name Type Description Default
diffusion_model DiffusionModelBase

The DiffusionModel to append.

required

Raises:

Type Description
TypeError

If the diffusion_model is not a DiffusionModelBase.

Source code in src/easydynamics/sample_model/sample_model.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def append_diffusion_model(self, diffusion_model: DiffusionModelBase) -> None:
    """
    Append a DiffusionModel to the SampleModel.

    Parameters
    ----------
    diffusion_model : DiffusionModelBase
        The DiffusionModel to append.

    Raises
    ------
    TypeError
        If the diffusion_model is not a DiffusionModelBase.
    """

    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
173
174
175
176
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.

Parameters:

Name Type Description Default
unit str | Unit

The unit to convert the temperature Parameter to.

required

Raises:

Type Description
ValueError

If temperature is not set or conversion fails.

Exception

If the provided unit is invalid or cannot be converted.

Source code in src/easydynamics/sample_model/sample_model.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def convert_temperature_unit(self, unit: str | sc.Unit) -> None:
    """
    Convert the unit of the temperature Parameter.

    Parameters
    ----------
    unit : str | sc.Unit
        The unit to convert the temperature Parameter to.

    Raises
    ------
    ValueError
        If temperature is not set or conversion fails.
    Exception
        If the provided unit is invalid or cannot be converted.
    """

    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:
        # Attempt to rollback on failure
        with suppress(Exception):
            self.temperature.convert_unit(old_unit)
        raise

detailed_balance_settings property writable

Get the DetailedBalanceSettings of the SampleModel.

Returns:

Type Description
DetailedBalanceSettings

The DetailedBalanceSettings of the SampleModel.

diffusion_models property writable

Get the diffusion models of the SampleModel.

Returns:

Type Description
list[DiffusionModelBase]

The diffusion models of the SampleModel.

evaluate(x)

Evaluate the sample model at all Q for the given x values.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values to evaluate the model at. Can be a number, list, numpy array, scipp Variable, or scipp DataArray.

required

Returns:

Type Description
list[ndarray]

List of evaluated model values for each Q.

Source code in src/easydynamics/sample_model/sample_model.py
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
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 : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values to evaluate the model at. Can be a number, list, numpy array, scipp
        Variable, or scipp DataArray.

    Returns
    -------
    list[np.ndarray]
        List of evaluated model values for each Q.
    """

    y = super().evaluate(x)

    if self.temperature is not None and self.detailed_balance_settings.use_detailed_balance:
        DBF = detailed_balance_factor(
            energy=x,
            temperature=self.temperature,
            divide_by_temperature=self.detailed_balance_settings.normalize_detailed_balance,
            energy_unit=self.unit,
        )
        y = [yi * DBF for yi in y]

    return y

get_all_variables(Q_index=None)

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.

Parameters:

Name Type Description Default
Q_index int | None

If specified, only get variables from the ComponentCollection at the given Q index. If None, get variables from all ComponentCollections.

None

Returns:

Type Description
list[Parameter]

List of all Parameters and Descriptors, including temperature if set and all variables from diffusion models.

Source code in src/easydynamics/sample_model/sample_model.py
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]:
    """
    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.

    Parameters
    ----------
    Q_index : int | None, default=None
        If specified, only get variables from the ComponentCollection at the given Q index. If
        None, get variables from all ComponentCollections.

    Returns
    -------
    list[Parameter]
        List of all Parameters and Descriptors, including temperature if set and all variables
        from diffusion models.
    """

    all_vars = super().get_all_variables(Q_index=Q_index)
    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

normalize_detailed_balance property writable

Get whether to divide the detailed balance factor by temperature.

Returns:

Type Description
bool

True if the detailed balance factor is divided by temperature, False otherwise.

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

Raises:

Type Description
ValueError

If no DiffusionModel with the given unique name is found.

Source code in src/easydynamics/sample_model/sample_model.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def remove_diffusion_model(self, name: 'str') -> None:
    """
    Remove a DiffusionModel from the SampleModel by unique name.

    Parameters
    ----------
    name : 'str'
        The unique name of the DiffusionModel to remove.

    Raises
    ------
    ValueError
        If no DiffusionModel with the given unique name is found.
    """
    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.

Returns:

Type Description
Parameter | None

The temperature Parameter of the SampleModel, or None if not set.

temperature_unit property writable

Get the temperature unit of the SampleModel.

Returns:

Type Description
str | Unit

The unit of the temperature Parameter.

use_detailed_balance property writable

Get whether to apply detailed balance to the model.

Returns:

Type Description
bool

True if detailed balance is applied, False otherwise.

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.

Use scipy.special.voigt_profile to evaluate the Voigt profile.

Source code in src/easydynamics/sample_model/components/voigt.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
class Voigt(CreateParametersMixin, ModelComponent):
    r"""
    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.

    Use scipy.special.voigt_profile to evaluate the Voigt profile.
    """

    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,
    ) -> None:
        """
        Initialize a Voigt component.

        Parameters
        ----------
        area : Numeric | Parameter, default=1.0
            Total area under the curve.
        center : Numeric | Parameter | None, default=None
            Center of the Voigt profile.
        gaussian_width : Numeric | Parameter, default=1.0
            Standard deviation of the Gaussian part.
        lorentzian_width : Numeric | Parameter, default=1.0
            Half width at half max (HWHM) of the Lorentzian part.
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='Voigt'
            Display name of the component.
        unique_name : str | None, default=None
            Unique name of the component. If None, a unique_name is automatically generated. By
            default, 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.

        Returns
        -------
        Parameter
            The area parameter.
        """
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """
        Set the value of the area parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the area parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """
        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.

        Returns
        -------
        Parameter
            The center parameter.
        """
        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """
        Set the value of the center parameter.

        Parameters
        ----------
        value : Numeric | None
            The new value for the center parameter. If None, defaults to 0 and is fixed.

        Raises
        ------
        TypeError
            If the value is not a number.
        """
        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 Gaussian width parameter.

        Returns
        -------
        Parameter
            The Gaussian width parameter.
        """
        return self._gaussian_width

    @gaussian_width.setter
    def gaussian_width(self, value: Numeric) -> None:
        """
        Set the width parameter value.

        Parameters
        ----------
        value : Numeric
            The new value for the width parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('gaussian_width must be a number')
        if float(value) <= 0:
            raise ValueError('gaussian_width must be positive')
        self._gaussian_width.value = value

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

        Returns
        -------
        Parameter
            The Lorentzian width parameter.
        """
        return self._lorentzian_width

    @lorentzian_width.setter
    def lorentzian_width(self, value: Numeric) -> None:
        """
        Set the value of the Lorentzian width parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the Lorentzian width parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('lorentzian_width must be a number')
        if float(value) <= 0:
            raise ValueError('lorentzian_width must be positive')
        self._lorentzian_width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        r"""
        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.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Voigt.

        Returns
        -------
        np.ndarray
            The intensity of the Voigt at the given x values.
        """

        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) -> str:
        """
        Return a string representation of the Voigt.

        Returns
        -------
        str
            A string representation of the Voigt.
        """

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

__init__(area=1.0, center=None, gaussian_width=1.0, lorentzian_width=1.0, unit='meV', display_name='Voigt', unique_name=None)

Initialize a Voigt component.

Parameters:

Name Type Description Default
area Numeric | Parameter

Total area under the curve.

1.0
center Numeric | Parameter | None

Center of the Voigt profile.

None
gaussian_width Numeric | Parameter

Standard deviation of the Gaussian part.

1.0
lorentzian_width Numeric | Parameter

Half width at half max (HWHM) of the Lorentzian part.

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Display name of the component.

'Voigt'
unique_name str | None

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

None
Source code in src/easydynamics/sample_model/components/voigt.py
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
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,
) -> None:
    """
    Initialize a Voigt component.

    Parameters
    ----------
    area : Numeric | Parameter, default=1.0
        Total area under the curve.
    center : Numeric | Parameter | None, default=None
        Center of the Voigt profile.
    gaussian_width : Numeric | Parameter, default=1.0
        Standard deviation of the Gaussian part.
    lorentzian_width : Numeric | Parameter, default=1.0
        Half width at half max (HWHM) of the Lorentzian part.
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='Voigt'
        Display name of the component.
    unique_name : str | None, default=None
        Unique name of the component. If None, a unique_name is automatically generated. By
        default, 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

__repr__()

Return a string representation of the Voigt.

Returns:

Type Description
str

A string representation of the Voigt.

Source code in src/easydynamics/sample_model/components/voigt.py
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def __repr__(self) -> str:
    """
    Return a string representation of the Voigt.

    Returns
    -------
    str
        A string representation of the Voigt.
    """

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

area property writable

Get the area parameter.

Returns:

Type Description
Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

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.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Voigt.

required

Returns:

Type Description
ndarray

The intensity of the Voigt at the given x values.

Source code in src/easydynamics/sample_model/components/voigt.py
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
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    r"""
    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.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Voigt.

    Returns
    -------
    np.ndarray
        The intensity of the Voigt at the given x values.
    """

    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 Gaussian width parameter.

Returns:

Type Description
Parameter

The Gaussian width parameter.

lorentzian_width property writable

Get the Lorentzian width parameter (HWHM).

Returns:

Type Description
Parameter

The Lorentzian width parameter.

background_model

BackgroundModel

Bases: ModelBase

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

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

    def __init__(
        self,
        display_name: str | None = 'MyBackgroundModel',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
        components: ModelComponent | ComponentCollection | None = None,
        Q: Q_type | None = None,
    ) -> None:
        """
        Initialize the BackgroundModel.

        Parameters
        ----------
        display_name : str | None, default='MyBackgroundModel'
            Display name of the model.
        unique_name : str | None, default=None
            Unique name of the model. If None, a unique name will be generated.
        unit : str | sc.Unit, default='meV'
            Unit of the model.
        components : ModelComponent | ComponentCollection | None, default=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, default=None
            Q values for the model. If None, Q is not set.
        """
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
            unit=unit,
            components=components,
            Q=Q,
        )

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

Initialize the BackgroundModel.

Parameters:

Name Type Description Default
display_name str | None

Display name of the model.

'MyBackgroundModel'
unique_name str | None

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

None
unit str | Unit

Unit of the model.

'meV'
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.

None
Q Q_type | None

Q values for the model. If None, Q is not set.

None
Source code in src/easydynamics/sample_model/background_model.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
def __init__(
    self,
    display_name: str | None = 'MyBackgroundModel',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
    components: ModelComponent | ComponentCollection | None = None,
    Q: Q_type | None = None,
) -> None:
    """
    Initialize the BackgroundModel.

    Parameters
    ----------
    display_name : str | None, default='MyBackgroundModel'
        Display name of the model.
    unique_name : str | None, default=None
        Unique name of the model. If None, a unique name will be generated.
    unit : str | sc.Unit, default='meV'
        Unit of the model.
    components : ModelComponent | ComponentCollection | None, default=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, default=None
        Q values for the model. If None, Q is not set.
    """
    super().__init__(
        display_name=display_name,
        unique_name=unique_name,
        unit=unit,
        components=components,
        Q=Q,
    )

component_collection

ComponentCollection

Bases: ModelBase

Collection of model components representing a sample, background or resolution model.

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

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

        Parameters
        ----------
        unit : str | sc.Unit, default='meV'
            Unit of the collection.
        display_name : str | None, default='MyComponentCollection'
            Display name of the collection.
        unique_name : str | None, default=None
            Unique name of the collection.
        components : list[ModelComponent] | None, default=None
            Initial model components to add to the ComponentCollection.

        Raises
        ------
        TypeError
            If unit is not a string or sc.Unit, or if components is not a list of ModelComponent.
        """

        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)

    # ------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------

    @property
    def components(self) -> list[ModelComponent]:
        """
        Get the list of components in the collection.

        Returns
        -------
        list[ModelComponent]
            The components in the collection.
        """

        return list(self._components)

    @components.setter
    def components(self, components: list[ModelComponent]) -> None:
        """
        Set the list of components in the collection.

        Parameters
        ----------
        components : list[ModelComponent]
            The new list of components.

        Raises
        ------
        TypeError
            If components is not a list of ModelComponent.
        """

        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

    @property
    def is_empty(self) -> bool:
        """
        Check if the ComponentCollection has no components.

        Returns
        -------
        bool
            True if the collection has no components, False otherwise.
        """
        return not self._components

    @is_empty.setter
    def is_empty(self, _value: bool) -> None:
        """
        Is_empty is a read-only property that indicates whether the collection has components.

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

        Raises
        ------
        AttributeError
            Always raised since is_empty is read-only.
        """
        raise AttributeError(
            'is_empty is a read-only property that indicates '
            'whether the collection has components.'
        )

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

        Returns
        -------
        str | sc.Unit | None
            The unit of the ComponentCollection, which is the same as the unit of its components.
        """
        return self._unit

    @unit.setter
    def unit(self, _unit_str: str) -> None:
        """
        Unit is read-only and cannot be set directly.

        Parameters
        ----------
        _unit_str : str
            The unit to set (ignored).

        Raises
        ------
        AttributeError
            Always raised since unit is read-only.
        """

        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.'
        )

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

        Parameters
        ----------
        unit : str | sc.Unit
            The target unit to convert to.

        Raises
        ------
        TypeError
            If unit is not a string or sc.Unit.
        Exception
            If any component cannot be converted to the specified unit.
        """

        if not isinstance(unit, (str, sc.Unit)):
            raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}')

        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

    # ------------------------------------------------------------------
    # Component management
    # ------------------------------------------------------------------

    def append_component(self, component: ModelComponent | ComponentCollection) -> None:
        """
        Append a model component or the components from another ComponentCollection to this
        ComponentCollection.

        Parameters
        ----------
        component : ModelComponent | ComponentCollection
            The component to append. If a ComponentCollection is provided, all of its components
            will be appended.

        Raises
        ------
        TypeError
            If component is not a ModelComponent or ComponentCollection.
        ValueError
            If a component with the same unique name already exists in the collection.
        """
        if not isinstance(component, (ModelComponent, ComponentCollection)):
            raise TypeError(
                'Component must be an instance of ModelComponent or ComponentCollection. '
                f'Got {type(component).__name__} instead.'
            )
        if isinstance(component, ModelComponent):
            components = (component,)
        if isinstance(component, ComponentCollection):
            components = component.components

        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:
        """
        Remove a component from the collection by its unique name.

        Parameters
        ----------
        unique_name : str
            Unique name of the component to remove.

        Raises
        ------
        TypeError
            If unique_name is not a string.
        KeyError
            If no component with the given unique name exists in the collection.
        """

        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]:
        """
        Get the list of components in the collection.

        Returns
        -------
        list[ModelComponent]
            The components in the collection.
        """
        return list(self._components)

    @components.setter
    def components(self, components: list[ModelComponent]) -> None:
        """
        Set the components in the collection.

        Parameters
        ----------
        components : list[ModelComponent]
            The new components in the collection.

        Raises
        ------
        TypeError
            If components is not a list of ModelComponent.
        """
        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

    @property
    def is_empty(self) -> bool:
        """
        Returns True if the collection has no components, otherwise False.

        Returns
        -------
        bool
            True if the collection has no components, otherwise False.
        """
        return not self._components

    @is_empty.setter
    def is_empty(self, _value: bool) -> None:
        """
        Is_empty is read-only.

        Parameters
        ----------
        _value : bool
            Ignored.

        Raises
        ------
        AttributeError
            Always raised since is_empty is read-only.
        """
        raise AttributeError(
            'is_empty is a read-only property that indicates '
            'whether the collection has components.'
        )

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

        Returns
        -------
        list[str]
            List of unique names of the components in the collection.
        """

        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:
        """
        Normalize the areas of all components so they sum to 1.

        This is useful for convolutions.

        Raises
        ------
        ValueError
            If there are no components in the model or if the total area is zero or not finite,
            which would prevent normalization.
        """
        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,
                    stacklevel=2,
                )

        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

    # ------------------------------------------------------------------
    # Other methods
    # ------------------------------------------------------------------

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

        Returns
        -------
        list[DescriptorBase]
            List of parameters in the component.
        """

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

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

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            Energy axis.

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

        if not self.components:
            return np.zeros_like(x)
        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 : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            Energy axis.
        unique_name : str
            Component unique name.

        Raises
        ------
        ValueError
            If there are no components in the model.
        TypeError
            If unique_name is not a string.
        KeyError
            If no component with the given unique name exists in the collection.

        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]

        return component.evaluate(x)

    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

    # ------------------------------------------------------------------
    # Dunder methods
    # ------------------------------------------------------------------

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

        Parameters
        ----------
        item : str | 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)
        if isinstance(item, ModelComponent):
            # Check by component instance
            return any(comp is item for comp in self.components)
        return False

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

        Returns
        -------
        str
            String representation of the ComponentCollection.
        """
        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.

Parameters:

Name Type Description Default
item str | ModelComponent

The component name or instance to check for.

required

Returns:

Type Description
bool

True if the component exists, False otherwise.

Source code in src/easydynamics/sample_model/component_collection.py
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def __contains__(self, item: str | ModelComponent) -> bool:
    """
    Check if a component with the given name or instance exists in the ComponentCollection.

    Parameters
    ----------
    item : str | 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)
    if isinstance(item, ModelComponent):
        # Check by component instance
        return any(comp is item for comp in self.components)
    return False

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

Initialize a new ComponentCollection.

Parameters:

Name Type Description Default
unit str | Unit

Unit of the collection.

'meV'
display_name str | None

Display name of the collection.

'MyComponentCollection'
unique_name str | None

Unique name of the collection.

None
components list[ModelComponent] | None

Initial model components to add to the ComponentCollection.

None

Raises:

Type Description
TypeError

If unit is not a string or sc.Unit, or if components is not a list of ModelComponent.

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

    Parameters
    ----------
    unit : str | sc.Unit, default='meV'
        Unit of the collection.
    display_name : str | None, default='MyComponentCollection'
        Display name of the collection.
    unique_name : str | None, default=None
        Unique name of the collection.
    components : list[ModelComponent] | None, default=None
        Initial model components to add to the ComponentCollection.

    Raises
    ------
    TypeError
        If unit is not a string or sc.Unit, or if components is not a list of ModelComponent.
    """

    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:

Type Description
str

String representation of the ComponentCollection.

Source code in src/easydynamics/sample_model/component_collection.py
527
528
529
530
531
532
533
534
535
536
537
538
def __repr__(self) -> str:
    """
    Return a string representation of the ComponentCollection.

    Returns
    -------
    str
        String representation of the ComponentCollection.
    """
    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}>"

append_component(component)

Append a model component or the components from another ComponentCollection to this ComponentCollection.

Parameters:

Name Type Description Default
component ModelComponent | ComponentCollection

The component to append. If a ComponentCollection is provided, all of its components will be appended.

required

Raises:

Type Description
TypeError

If component is not a ModelComponent or ComponentCollection.

ValueError

If a component with the same unique name already exists in the collection.

Source code in src/easydynamics/sample_model/component_collection.py
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
def append_component(self, component: ModelComponent | ComponentCollection) -> None:
    """
    Append a model component or the components from another ComponentCollection to this
    ComponentCollection.

    Parameters
    ----------
    component : ModelComponent | ComponentCollection
        The component to append. If a ComponentCollection is provided, all of its components
        will be appended.

    Raises
    ------
    TypeError
        If component is not a ModelComponent or ComponentCollection.
    ValueError
        If a component with the same unique name already exists in the collection.
    """
    if not isinstance(component, (ModelComponent, ComponentCollection)):
        raise TypeError(
            'Component must be an instance of ModelComponent or ComponentCollection. '
            f'Got {type(component).__name__} instead.'
        )
    if isinstance(component, ModelComponent):
        components = (component,)
    if isinstance(component, ComponentCollection):
        components = component.components

    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)

clear_components()

Remove all components.

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

components property writable

Get the list of components in the collection.

Returns:

Type Description
list[ModelComponent]

The components in the collection.

convert_unit(unit)

Convert the unit of the ComponentCollection and all its components.

Parameters:

Name Type Description Default
unit str | Unit

The target unit to convert to.

required

Raises:

Type Description
TypeError

If unit is not a string or sc.Unit.

Exception

If any component cannot be converted to the specified unit.

Source code in src/easydynamics/sample_model/component_collection.py
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
def convert_unit(self, unit: str | sc.Unit) -> None:
    """
    Convert the unit of the ComponentCollection and all its components.

    Parameters
    ----------
    unit : str | sc.Unit
        The target unit to convert to.

    Raises
    ------
    TypeError
        If unit is not a string or sc.Unit.
    Exception
        If any component cannot be converted to the specified unit.
    """

    if not isinstance(unit, (str, sc.Unit)):
        raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}')

    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:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

Energy axis.

required

Returns:

Type Description
ndarray

Evaluated model values.

Source code in src/easydynamics/sample_model/component_collection.py
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """
    Evaluate the sum of all components.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        Energy axis.

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

    if not self.components:
        return np.zeros_like(x)
    return sum(component.evaluate(x) for component in self.components)

evaluate_component(x, unique_name)

Evaluate a single component by name.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

Energy axis.

required
unique_name str

Component unique name.

required

Raises:

Type Description
ValueError

If there are no components in the model.

TypeError

If unique_name is not a string.

KeyError

If no component with the given unique name exists in the collection.

Returns:

Type Description
ndarray

Evaluated values for the specified component.

Source code in src/easydynamics/sample_model/component_collection.py
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
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 : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        Energy axis.
    unique_name : str
        Component unique name.

    Raises
    ------
    ValueError
        If there are no components in the model.
    TypeError
        If unique_name is not a string.
    KeyError
        If no component with the given unique name exists in the collection.

    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]

    return component.evaluate(x)

fix_all_parameters()

Fix all free parameters in the model.

Source code in src/easydynamics/sample_model/component_collection.py
490
491
492
493
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
495
496
497
498
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:

Type Description
list[DescriptorBase]

List of parameters in the component.

Source code in src/easydynamics/sample_model/component_collection.py
414
415
416
417
418
419
420
421
422
423
424
def get_all_variables(self) -> list[DescriptorBase]:
    """
    Get all parameters from the model component.

    Returns
    -------
    list[DescriptorBase]
        List of parameters in the component.
    """

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

is_empty property writable

Returns True if the collection has no components, otherwise False.

Returns:

Type Description
bool

True if the collection has no components, otherwise False.

list_component_names()

List the names of all components in the model.

Returns:

Type Description
list[str]

List of unique names of the components in the collection.

Source code in src/easydynamics/sample_model/component_collection.py
355
356
357
358
359
360
361
362
363
364
365
def list_component_names(self) -> list[str]:
    """
    List the names of all components in the model.

    Returns
    -------
    list[str]
        List of unique names of the components in the collection.
    """

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

normalize_area()

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

This is useful for convolutions.

Raises:

Type Description
ValueError

If there are no components in the model or if the total area is zero or not finite, which would prevent normalization.

Source code in src/easydynamics/sample_model/component_collection.py
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
def normalize_area(self) -> None:
    """
    Normalize the areas of all components so they sum to 1.

    This is useful for convolutions.

    Raises
    ------
    ValueError
        If there are no components in the model or if the total area is zero or not finite,
        which would prevent normalization.
    """
    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,
                stacklevel=2,
            )

    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

remove_component(unique_name)

Remove a component from the collection by its unique name.

Parameters:

Name Type Description Default
unique_name str

Unique name of the component to remove.

required

Raises:

Type Description
TypeError

If unique_name is not a string.

KeyError

If no component with the given unique name exists in the collection.

Source code in src/easydynamics/sample_model/component_collection.py
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
def remove_component(self, unique_name: str) -> None:
    """
    Remove a component from the collection by its unique name.

    Parameters
    ----------
    unique_name : str
        Unique name of the component to remove.

    Raises
    ------
    TypeError
        If unique_name is not a string.
    KeyError
        If no component with the given unique name exists in the collection.
    """

    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()}'
    )

unit property writable

Get the unit of the ComponentCollection.

Returns:

Type Description
str | Unit | None

The unit of the ComponentCollection, which is the same as the unit of its components.

components

DampedHarmonicOscillator

Bases: CreateParametersMixin, ModelComponent

Model of a Damped Harmonic Oscillator (DHO).

The intensity is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2 \gamma x)^2 \right)}, $$ where \(A\) is the area, \(x_0\) is the center, and \(\gamma\) is the width.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.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
class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent):
    r"""
    Model of a Damped Harmonic Oscillator (DHO).

    The intensity is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2
    \gamma x)^2 \right)}, $$ where $A$ is the area, $x_0$ is the center, and $\gamma$ is the width.
    """

    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,
    ) -> None:
        """
        Initialize the Damped Harmonic Oscillator.

        Parameters
        ----------
        area : Numeric | Parameter, default=1.0
            Area under the curve.
        center : Numeric | Parameter, default=1.0
            Resonance frequency, approximately the peak position.
        width : Numeric | Parameter, default=1.0
            Damping constant, approximately the half width at half max (HWHM) of the peaks. By
            default, 1.0.
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='DampedHarmonicOscillator'
            Display name of the component.
        unique_name : str | None, default=None
            Unique name of the component. If None, a unique_name is automatically generated. By
            default, 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.

        Returns
        -------
        Parameter
            The area parameter.
        """
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """
        Set the value of the area parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the area parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """
        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.

        Returns
        -------
        Parameter
            The center parameter.
        """
        return self._center

    @center.setter
    def center(self, value: Numeric) -> None:
        """
        Set the value of the center parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the center parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')

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

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

        Returns
        -------
        Parameter
            The width parameter.
        """
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """
        Set the value of the width parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the width parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')

        if float(value) <= 0:
            raise ValueError('width must be positive')

        self._width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        r"""
        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 intensity
        is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2 \gamma x)^2
        \right)}, $$ where $A$ is the area, $x_0$ is the center, and $\gamma$ is the width.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the DHO.

        Returns
        -------
        np.ndarray
            The intensity of the DHO at the given x values.
        """

        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) -> str:
        """
        Return a string representation of the Damped Harmonic Oscillator.

        Returns
        -------
        str
            A string representation of the Damped Harmonic Oscillator.
        """
        return (
            f'DampedHarmonicOscillator(display_name = {self.display_name}, '
            f'unit = {self._unit},\n '
            f'area = {self.area},\n center = {self.center},\n width = {self.width})'
        )

__init__(area=1.0, center=1.0, width=1.0, unit='meV', display_name='DampedHarmonicOscillator', unique_name=None)

Initialize the Damped Harmonic Oscillator.

Parameters:

Name Type Description Default
area Numeric | Parameter

Area under the curve.

1.0
center Numeric | Parameter

Resonance frequency, approximately the peak position.

1.0
width Numeric | Parameter

Damping constant, approximately the half width at half max (HWHM) of the peaks. By default, 1.0.

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Display name of the component.

'DampedHarmonicOscillator'
unique_name str | None

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

None
Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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,
) -> None:
    """
    Initialize the Damped Harmonic Oscillator.

    Parameters
    ----------
    area : Numeric | Parameter, default=1.0
        Area under the curve.
    center : Numeric | Parameter, default=1.0
        Resonance frequency, approximately the peak position.
    width : Numeric | Parameter, default=1.0
        Damping constant, approximately the half width at half max (HWHM) of the peaks. By
        default, 1.0.
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='DampedHarmonicOscillator'
        Display name of the component.
    unique_name : str | None, default=None
        Unique name of the component. If None, a unique_name is automatically generated. By
        default, 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

__repr__()

Return a string representation of the Damped Harmonic Oscillator.

Returns:

Type Description
str

A string representation of the Damped Harmonic Oscillator.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def __repr__(self) -> str:
    """
    Return a string representation of the Damped Harmonic Oscillator.

    Returns
    -------
    str
        A string representation of the Damped Harmonic Oscillator.
    """
    return (
        f'DampedHarmonicOscillator(display_name = {self.display_name}, '
        f'unit = {self._unit},\n '
        f'area = {self.area},\n center = {self.center},\n width = {self.width})'
    )

area property writable

Get the area parameter.

Returns:

Type Description
Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

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 intensity is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2 \gamma x)^2 \right)}, $$ where \(A\) is the area, \(x_0\) is the center, and \(\gamma\) is the width.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the DHO.

required

Returns:

Type Description
ndarray

The intensity of the DHO at the given x values.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
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
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    r"""
    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 intensity
    is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2 \gamma x)^2
    \right)}, $$ where $A$ is the area, $x_0$ is the center, and $\gamma$ is the width.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the DHO.

    Returns
    -------
    np.ndarray
        The intensity of the DHO at the given x values.
    """

    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.

Returns:

Type Description
Parameter

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 by the Convolution method. If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Source code in src/easydynamics/sample_model/components/delta_function.py
 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
class DeltaFunction(CreateParametersMixin, ModelComponent):
    """
    Delta function.

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

    def __init__(
        self,
        center: Numeric | Parameter | None = None,
        area: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'DeltaFunction',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the Delta function.

        Parameters
        ----------
        center : Numeric | Parameter | None, default=None
            Center of the delta function. If None.
        area : Numeric | Parameter, default=1.0
            Total area under the curve.
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='DeltaFunction'
            Name of the component.
        unique_name : str | None, default=None
            Unique name of the component. If None, a unique_name is automatically generated. By
            default, 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.

        Returns
        -------
        Parameter
            The area parameter.
        """

        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """
        Set the value of the area parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the area parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """

        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.

        Returns
        -------
        Parameter
            The center parameter.
        """

        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """
        Set the center parameter value.

        Parameters
        ----------
        value : Numeric | None
            The new value for the center parameter. If None, defaults to 0 and is fixed.

        Raises
        ------
        TypeError
            If the value is not a number or None.
        """

        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.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Delta function.

        Returns
        -------
        np.ndarray
            The evaluated Delta function at the given x values.
        """

        # 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:  # noqa: SIM108
                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:  # noqa: SIM108
                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) -> str:
        """
        Return a string representation of the Delta function.

        Returns
        -------
        str
            A string representation of the Delta function.
        """

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

__init__(center=None, area=1.0, unit='meV', display_name='DeltaFunction', unique_name=None)

Initialize the Delta function.

Parameters:

Name Type Description Default
center Numeric | Parameter | None

Center of the delta function. If None.

None
area Numeric | Parameter

Total area under the curve.

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Name of the component.

'DeltaFunction'
unique_name str | None

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

None
Source code in src/easydynamics/sample_model/components/delta_function.py
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
def __init__(
    self,
    center: Numeric | Parameter | None = None,
    area: Numeric | Parameter = 1.0,
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'DeltaFunction',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the Delta function.

    Parameters
    ----------
    center : Numeric | Parameter | None, default=None
        Center of the delta function. If None.
    area : Numeric | Parameter, default=1.0
        Total area under the curve.
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='DeltaFunction'
        Name of the component.
    unique_name : str | None, default=None
        Unique name of the component. If None, a unique_name is automatically generated. By
        default, 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

__repr__()

Return a string representation of the Delta function.

Returns:

Type Description
str

A string representation of the Delta function.

Source code in src/easydynamics/sample_model/components/delta_function.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def __repr__(self) -> str:
    """
    Return a string representation of the Delta function.

    Returns
    -------
    str
        A string representation of the Delta function.
    """

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

area property writable

Get the area parameter.

Returns:

Type Description
Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

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.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Delta function.

required

Returns:

Type Description
ndarray

The evaluated Delta function at the given x values.

Source code in src/easydynamics/sample_model/components/delta_function.py
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
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.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Delta function.

    Returns
    -------
    np.ndarray
        The evaluated Delta function at the given x values.
    """

    # 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:  # noqa: SIM108
            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:  # noqa: SIM108
            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

Exponential

Bases: CreateParametersMixin, ModelComponent

Model of an exponential function.

The intensity is given by

\[ I(x) = A e^{B (x-x_0)}, \]

where \(A\) is the amplitude, \(x_0\) is the center, and \(B\) describes the rate of decay or growth.

Source code in src/easydynamics/sample_model/components/exponential.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
 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
class Exponential(CreateParametersMixin, ModelComponent):
    r"""
    Model of an exponential function.

    The intensity is given by

    $$ I(x) = A e^{B (x-x_0)}, $$

    where $A$ is the amplitude, $x_0$ is the center, and $B$ describes the rate of decay or growth.
    """

    def __init__(
        self,
        amplitude: Numeric | Parameter = 1.0,
        center: Numeric | Parameter | None = None,
        rate: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Exponential',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the Exponential component.

        Parameters
        ----------
        amplitude : Numeric | Parameter, default=1.0
            Amplitude of the Exponential.
        center : Numeric | Parameter | None, default=None
            Center of the Exponential. If None, the center is fixed at 0.
        rate : Numeric | Parameter, default=1.0
            Decay or growth constant of the Exponential.
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='Exponential'
            Name of the component.
        unique_name : str | None, default=None
            Unique name of the component. if None, a unique_name is automatically generated. By
            default, None.

        Raises
        ------
        TypeError
            If amplitude, center, or rate are not numbers or Parameters.
        ValueError
            If amplitude, center or rate are not finite numbers.
        """
        # Validate inputs and create Parameters if not given
        super().__init__(
            display_name=display_name,
            unit=unit,
            unique_name=unique_name,
        )

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

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

            amplitude = Parameter(
                name=display_name + ' amplitude', value=float(amplitude), unit=unit
            )

        center = self._create_center_parameter(
            center=center, name=display_name, fix_if_none=True, unit=self._unit
        )

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

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

            rate = Parameter(name=display_name + ' rate', value=float(rate), unit='1/' + str(unit))

        self._amplitude = amplitude
        self._center = center
        self._rate = rate

    @property
    def amplitude(self) -> Parameter:
        """
        Get the amplitude parameter.

        Returns
        -------
        Parameter
            The amplitude parameter.
        """

        return self._amplitude

    @amplitude.setter
    def amplitude(self, value: Numeric) -> None:
        """
        Set the value of the amplitude parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the amplitude parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """

        if not isinstance(value, Numeric):
            raise TypeError('amplitude must be a number')
        self._amplitude.value = value

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

        Returns
        -------
        Parameter
            The center parameter.
        """

        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """
        Set the center parameter value.

        Parameters
        ----------
        value : Numeric | None
            The new value for the center parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """

        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 rate(self) -> Parameter:
        """
        Get the rate parameter.

        Returns
        -------
        Parameter
            The rate parameter.
        """
        return self._rate

    @rate.setter
    def rate(self, value: Numeric) -> None:
        """
        Set the rate parameter value.

        Parameters
        ----------
        value : Numeric
            The new value for the rate parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """
        if not isinstance(value, Numeric):
            raise TypeError('rate must be a number')

        self._rate.value = value

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

        If x is a scipp Variable, the unit of the Exponential will be converted to match x. The
        intensity is given by $$ I(x) = A \exp\left( r (x - x_0) \right) $$

        where $A$ is the amplitude, $x_0$ is the center, and $r$ is the rate.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Exponential.

        Returns
        -------
        np.ndarray
            The intensity of the Exponential at the given x values.
        """

        x = self._prepare_x_for_evaluate(x)
        exponent = self.rate.value * (x - self.center.value)

        return self.amplitude.value * np.exp(exponent)

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

        Parameters
        ----------
        unit : str | sc.Unit
            The new unit to convert to.

        Raises
        ------
        TypeError
            If unit is not a string or sc.Unit.
        Exception
            If conversion fails for any parameter.
        """

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

        old_unit = self._unit
        pars = [self.amplitude, self.center]
        try:
            for p in pars:
                p.convert_unit(unit)
            self.rate.convert_unit('1/' + str(unit))
            self._unit = unit
        except Exception as e:
            # Attempt to rollback on failure
            try:
                for p in pars:
                    p.convert_unit(old_unit)
                self.rate.convert_unit('1/' + str(old_unit))
            except Exception:  # noqa: S110
                pass  # Best effort rollback
            raise e

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

        Returns
        -------
        str
            A string representation of the Exponential.
        """

        return f'Exponential(unique_name = {self.unique_name}, unit = {self._unit},\n \
            amplitude = {self.amplitude},\n center = {self.center},\n rate = {self.rate})'

__init__(amplitude=1.0, center=None, rate=1.0, unit='meV', display_name='Exponential', unique_name=None)

Initialize the Exponential component.

Parameters:

Name Type Description Default
amplitude Numeric | Parameter

Amplitude of the Exponential.

1.0
center Numeric | Parameter | None

Center of the Exponential. If None, the center is fixed at 0.

None
rate Numeric | Parameter

Decay or growth constant of the Exponential.

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Name of the component.

'Exponential'
unique_name str | None

Unique name of the component. if None, a unique_name is automatically generated. By default, None.

None

Raises:

Type Description
TypeError

If amplitude, center, or rate are not numbers or Parameters.

ValueError

If amplitude, center or rate are not finite numbers.

Source code in src/easydynamics/sample_model/components/exponential.py
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
def __init__(
    self,
    amplitude: Numeric | Parameter = 1.0,
    center: Numeric | Parameter | None = None,
    rate: Numeric | Parameter = 1.0,
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'Exponential',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the Exponential component.

    Parameters
    ----------
    amplitude : Numeric | Parameter, default=1.0
        Amplitude of the Exponential.
    center : Numeric | Parameter | None, default=None
        Center of the Exponential. If None, the center is fixed at 0.
    rate : Numeric | Parameter, default=1.0
        Decay or growth constant of the Exponential.
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='Exponential'
        Name of the component.
    unique_name : str | None, default=None
        Unique name of the component. if None, a unique_name is automatically generated. By
        default, None.

    Raises
    ------
    TypeError
        If amplitude, center, or rate are not numbers or Parameters.
    ValueError
        If amplitude, center or rate are not finite numbers.
    """
    # Validate inputs and create Parameters if not given
    super().__init__(
        display_name=display_name,
        unit=unit,
        unique_name=unique_name,
    )

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

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

        amplitude = Parameter(
            name=display_name + ' amplitude', value=float(amplitude), unit=unit
        )

    center = self._create_center_parameter(
        center=center, name=display_name, fix_if_none=True, unit=self._unit
    )

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

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

        rate = Parameter(name=display_name + ' rate', value=float(rate), unit='1/' + str(unit))

    self._amplitude = amplitude
    self._center = center
    self._rate = rate

__repr__()

Return a string representation of the Exponential.

Returns:

Type Description
str

A string representation of the Exponential.

Source code in src/easydynamics/sample_model/components/exponential.py
263
264
265
266
267
268
269
270
271
272
273
274
def __repr__(self) -> str:
    """
    Return a string representation of the Exponential.

    Returns
    -------
    str
        A string representation of the Exponential.
    """

    return f'Exponential(unique_name = {self.unique_name}, unit = {self._unit},\n \
        amplitude = {self.amplitude},\n center = {self.center},\n rate = {self.rate})'

amplitude property writable

Get the amplitude parameter.

Returns:

Type Description
Parameter

The amplitude parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

The center parameter.

convert_unit(unit)

Convert the unit of the Parameters in the component.

Parameters:

Name Type Description Default
unit str | Unit

The new unit to convert to.

required

Raises:

Type Description
TypeError

If unit is not a string or sc.Unit.

Exception

If conversion fails for any parameter.

Source code in src/easydynamics/sample_model/components/exponential.py
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
def convert_unit(self, unit: str | sc.Unit) -> None:
    """
    Convert the unit of the Parameters in the component.

    Parameters
    ----------
    unit : str | sc.Unit
        The new unit to convert to.

    Raises
    ------
    TypeError
        If unit is not a string or sc.Unit.
    Exception
        If conversion fails for any parameter.
    """

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

    old_unit = self._unit
    pars = [self.amplitude, self.center]
    try:
        for p in pars:
            p.convert_unit(unit)
        self.rate.convert_unit('1/' + str(unit))
        self._unit = unit
    except Exception as e:
        # Attempt to rollback on failure
        try:
            for p in pars:
                p.convert_unit(old_unit)
            self.rate.convert_unit('1/' + str(old_unit))
        except Exception:  # noqa: S110
            pass  # Best effort rollback
        raise e

evaluate(x)

Evaluate the Exponential at the given x values.

If x is a scipp Variable, the unit of the Exponential will be converted to match x. The intensity is given by $$ I(x) = A \exp\left( r (x - x_0) \right) $$

where \(A\) is the amplitude, \(x_0\) is the center, and \(r\) is the rate.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Exponential.

required

Returns:

Type Description
ndarray

The intensity of the Exponential at the given x values.

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

    If x is a scipp Variable, the unit of the Exponential will be converted to match x. The
    intensity is given by $$ I(x) = A \exp\left( r (x - x_0) \right) $$

    where $A$ is the amplitude, $x_0$ is the center, and $r$ is the rate.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Exponential.

    Returns
    -------
    np.ndarray
        The intensity of the Exponential at the given x values.
    """

    x = self._prepare_x_for_evaluate(x)
    exponent = self.rate.value * (x - self.center.value)

    return self.amplitude.value * np.exp(exponent)

rate property writable

Get the rate parameter.

Returns:

Type Description
Parameter

The rate parameter.

ExpressionComponent

Bases: ModelComponent

Model component defined by a symbolic expression.

Example: expr = ExpressionComponent( "A * exp(-(x - x0)2 / (2*sigma2))", parameters={"A": 10, "x0": 0, "sigma": 1}, )

expr.A = 5 y = expr.evaluate(x)
Source code in src/easydynamics/sample_model/components/expression_component.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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
class ExpressionComponent(ModelComponent):
    """
    Model component defined by a symbolic expression.

    Example: expr = ExpressionComponent( "A * exp(-(x - x0)**2 / (2*sigma**2))", parameters={"A":
    10, "x0": 0, "sigma": 1}, )

        expr.A = 5 y = expr.evaluate(x)
    """

    # -------------------------
    # Allowed symbolic functions
    # -------------------------
    _ALLOWED_FUNCS: ClassVar[dict[str, object]] = {
        # Exponentials & logs
        'exp': sp.exp,
        'log': sp.log,
        'ln': sp.log,
        'sqrt': sp.sqrt,
        # Trigonometric
        'sin': sp.sin,
        'cos': sp.cos,
        'tan': sp.tan,
        'sinc': sp.sinc,
        'cot': sp.cot,
        'sec': sp.sec,
        'csc': sp.csc,
        'asin': sp.asin,
        'acos': sp.acos,
        'atan': sp.atan,
        # Hyperbolic
        'sinh': sp.sinh,
        'cosh': sp.cosh,
        'tanh': sp.tanh,
        # Misc
        'abs': sp.Abs,
        'sign': sp.sign,
        'floor': sp.floor,
        'ceil': sp.ceiling,
        # Special functions
        'erf': sp.erf,
    }

    # -------------------------
    # Allowed constants
    # -------------------------
    _ALLOWED_CONSTANTS: ClassVar[dict[str, object]] = {
        'pi': sp.pi,
        'E': sp.E,
    }

    _RESERVED_NAMES: ClassVar[dict[str, object]] = {'x'}

    def __init__(
        self,
        expression: str,
        parameters: dict[str, Numeric] | None = None,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Expression',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the ExpressionComponent.

        Parameters
        ----------
        expression : str
            The symbolic expression as a string. Must contain 'x' as the independent variable.
        parameters : dict[str, Numeric] | None, default=None
            Dictionary of parameter names and their initial values.
        unit : str | sc.Unit, default='meV'
            Unit of the output.
        display_name : str | None, default='Expression'
            Display name for the component.
        unique_name : str | None, default=None
            Unique name for the component.

        Raises
        ------
        ValueError
            If the expression is invalid or does not contain 'x'.
        TypeError
            If any parameter value is not numeric.
        """
        super().__init__(unit=unit, display_name=display_name, unique_name=unique_name)

        if 'np.' in expression:
            raise ValueError(
                'NumPy syntax (np.*) is not supported. '
                "Use functions like 'exp', 'sin', etc. directly."
            )

        self._expression_str = expression

        locals_dict = {}
        locals_dict.update(self._ALLOWED_FUNCS)
        locals_dict.update(self._ALLOWED_CONSTANTS)

        try:
            self._expr = sp.sympify(expression, locals=locals_dict)
        except Exception as e:
            raise ValueError(f'Invalid expression: {expression}') from e

        # Extract symbols from the expression
        symbols = self._expr.free_symbols
        symbol_names = sorted(str(s) for s in symbols)

        if 'x' not in symbol_names:
            raise ValueError("Expression must contain 'x' as independent variable")

        # Reject unknown functions early so invalid expressions fail at init,
        # not later during numerical evaluation.
        allowed_function_names = set(self._ALLOWED_FUNCS) | {
            func.__name__ for func in self._ALLOWED_FUNCS.values()
        }

        # Walk all function-call nodes in the parsed expression (e.g. sin(x), foo(x)).
        # Keep only function names that are not in our allowlist.
        unknown_function_names: set[str] = set()
        function_atoms = self._expr.atoms(sp.Function)
        for function_atom in function_atoms:
            function_name = function_atom.func.__name__
            if function_name not in allowed_function_names:
                unknown_function_names.add(function_name)

        unknown_functions = sorted(unknown_function_names)

        if unknown_functions:
            raise ValueError(
                f'Unsupported function(s) in expression: {", ".join(unknown_functions)}'
            )

        # Create parameters
        if parameters is not None and not isinstance(parameters, dict):
            raise TypeError(
                f'Parameters must be None or a dictionary, got {type(parameters).__name__}'
            )

        if parameters is not None:
            for name, value in parameters.items():
                if not isinstance(value, Numeric):
                    raise TypeError(f"Parameter '{name}' must be numeric")
        parameters = parameters or {}
        self._parameters: dict[str, Parameter] = {}

        self._symbol_names = symbol_names
        for name in self._symbol_names:
            if name in self._RESERVED_NAMES:
                continue

            value = parameters.get(name, 1.0)

            self._parameters[name] = Parameter(
                name=name,
                value=value,
                unit=self._unit,
            )

        # Create numerical function
        ordered_symbols = [sp.Symbol(name) for name in self._symbol_names]

        self._func = sp.lambdify(
            ordered_symbols,
            self._expr,
            modules=['numpy'],
        )

        # -------------------------
        # Properties
        # -------------------------

    @property
    def expression(self) -> str:
        """Return the original expression string."""
        return self._expression_str

    @expression.setter
    def expression(self, _new_expr: str) -> None:
        """
        Prevent changing the expression after initialization.

        Parameters
        ----------
        _new_expr : str
            New expression string (ignored).

        Raises
        ------
        AttributeError
            Always raised to prevent changing the expression.
        """
        raise AttributeError('Expression cannot be changed after initialization')

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

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            Input values for the independent variable.

        Returns
        -------
        np.ndarray
            Evaluated results.
        """
        x = self._prepare_x_for_evaluate(x)

        args = []
        for name in self._symbol_names:
            if name == 'x':
                args.append(x)
            else:
                args.append(self._parameters[name].value)

        return self._func(*args)

    def get_all_variables(self) -> list[Parameter]:
        """
        Return all parameters.

        Returns
        -------
        list[Parameter]
            List of all parameters in the expression.
        """
        return list(self._parameters.values())

    def convert_unit(self, _new_unit: str | sc.Unit) -> None:
        """
        Convert the unit of the expression.

        Unit conversion is not implemented for ExpressionComponent.

        Parameters
        ----------
        _new_unit : str | sc.Unit
            The new unit to convert to (ignored).

        Raises
        ------
        NotImplementedError
            Always raised to indicate unit conversion is not supported.
        """

        raise NotImplementedError('Unit conversion is not implemented for ExpressionComponent')

    # -------------------------
    # dunder methods
    # -------------------------

    def __getattr__(self, name: str) -> Parameter:
        """
        Allow access to parameters as attributes.

        Parameters
        ----------
        name : str
            Name of the parameter to access.

        Raises
        ------
        AttributeError
            If the parameter does not exist.

        Returns
        -------
        Parameter
            The parameter with the given name.
        """
        if '_parameters' in self.__dict__ and name in self._parameters:
            return self._parameters[name]
        raise AttributeError(f"{self.__class__.__name__} has no attribute '{name}'")

    def __setattr__(self, name: str, value: Numeric) -> None:
        """
        Allow setting parameter values as attributes.

        Parameters
        ----------
        name : str
            Name of the parameter to set.
        value : Numeric
            New value for the parameter.

        Raises
        ------
        TypeError
            If the value is not numeric.
        """
        if '_parameters' in self.__dict__ and name in self._parameters:
            param = self._parameters[name]

            if not isinstance(value, Numeric):
                raise TypeError(f'{name} must be numeric')

            param.value = value
        else:
            # For other attributes, use default behavior
            super().__setattr__(name, value)

    def __dir__(self) -> list[str]:
        """
        Include parameter names in dir() output for better IDE support.

        Returns
        -------
        list[str]
            List of attribute names, including parameters.
        """
        return super().__dir__() + list(self._parameters.keys())

    def __repr__(self) -> str:
        """Repr function."""
        param_str = ', '.join(f'{k}={v.value}' for k, v in self._parameters.items())
        return (
            f'{self.__class__.__name__}(\n'
            f"  expr='{self._expression_str}',\n"
            f'  unit={self._unit},\n'
            f'  parameters={{ {param_str} }}\n'
            f')'
        )

__dir__()

Include parameter names in dir() output for better IDE support.

Returns:

Type Description
list[str]

List of attribute names, including parameters.

Source code in src/easydynamics/sample_model/components/expression_component.py
325
326
327
328
329
330
331
332
333
334
def __dir__(self) -> list[str]:
    """
    Include parameter names in dir() output for better IDE support.

    Returns
    -------
    list[str]
        List of attribute names, including parameters.
    """
    return super().__dir__() + list(self._parameters.keys())

__getattr__(name)

Allow access to parameters as attributes.

Parameters:

Name Type Description Default
name str

Name of the parameter to access.

required

Raises:

Type Description
AttributeError

If the parameter does not exist.

Returns:

Type Description
Parameter

The parameter with the given name.

Source code in src/easydynamics/sample_model/components/expression_component.py
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def __getattr__(self, name: str) -> Parameter:
    """
    Allow access to parameters as attributes.

    Parameters
    ----------
    name : str
        Name of the parameter to access.

    Raises
    ------
    AttributeError
        If the parameter does not exist.

    Returns
    -------
    Parameter
        The parameter with the given name.
    """
    if '_parameters' in self.__dict__ and name in self._parameters:
        return self._parameters[name]
    raise AttributeError(f"{self.__class__.__name__} has no attribute '{name}'")

__init__(expression, parameters=None, unit='meV', display_name='Expression', unique_name=None)

Initialize the ExpressionComponent.

Parameters:

Name Type Description Default
expression str

The symbolic expression as a string. Must contain 'x' as the independent variable.

required
parameters dict[str, Numeric] | None

Dictionary of parameter names and their initial values.

None
unit str | Unit

Unit of the output.

'meV'
display_name str | None

Display name for the component.

'Expression'
unique_name str | None

Unique name for the component.

None

Raises:

Type Description
ValueError

If the expression is invalid or does not contain 'x'.

TypeError

If any parameter value is not numeric.

Source code in src/easydynamics/sample_model/components/expression_component.py
 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
def __init__(
    self,
    expression: str,
    parameters: dict[str, Numeric] | None = None,
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'Expression',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the ExpressionComponent.

    Parameters
    ----------
    expression : str
        The symbolic expression as a string. Must contain 'x' as the independent variable.
    parameters : dict[str, Numeric] | None, default=None
        Dictionary of parameter names and their initial values.
    unit : str | sc.Unit, default='meV'
        Unit of the output.
    display_name : str | None, default='Expression'
        Display name for the component.
    unique_name : str | None, default=None
        Unique name for the component.

    Raises
    ------
    ValueError
        If the expression is invalid or does not contain 'x'.
    TypeError
        If any parameter value is not numeric.
    """
    super().__init__(unit=unit, display_name=display_name, unique_name=unique_name)

    if 'np.' in expression:
        raise ValueError(
            'NumPy syntax (np.*) is not supported. '
            "Use functions like 'exp', 'sin', etc. directly."
        )

    self._expression_str = expression

    locals_dict = {}
    locals_dict.update(self._ALLOWED_FUNCS)
    locals_dict.update(self._ALLOWED_CONSTANTS)

    try:
        self._expr = sp.sympify(expression, locals=locals_dict)
    except Exception as e:
        raise ValueError(f'Invalid expression: {expression}') from e

    # Extract symbols from the expression
    symbols = self._expr.free_symbols
    symbol_names = sorted(str(s) for s in symbols)

    if 'x' not in symbol_names:
        raise ValueError("Expression must contain 'x' as independent variable")

    # Reject unknown functions early so invalid expressions fail at init,
    # not later during numerical evaluation.
    allowed_function_names = set(self._ALLOWED_FUNCS) | {
        func.__name__ for func in self._ALLOWED_FUNCS.values()
    }

    # Walk all function-call nodes in the parsed expression (e.g. sin(x), foo(x)).
    # Keep only function names that are not in our allowlist.
    unknown_function_names: set[str] = set()
    function_atoms = self._expr.atoms(sp.Function)
    for function_atom in function_atoms:
        function_name = function_atom.func.__name__
        if function_name not in allowed_function_names:
            unknown_function_names.add(function_name)

    unknown_functions = sorted(unknown_function_names)

    if unknown_functions:
        raise ValueError(
            f'Unsupported function(s) in expression: {", ".join(unknown_functions)}'
        )

    # Create parameters
    if parameters is not None and not isinstance(parameters, dict):
        raise TypeError(
            f'Parameters must be None or a dictionary, got {type(parameters).__name__}'
        )

    if parameters is not None:
        for name, value in parameters.items():
            if not isinstance(value, Numeric):
                raise TypeError(f"Parameter '{name}' must be numeric")
    parameters = parameters or {}
    self._parameters: dict[str, Parameter] = {}

    self._symbol_names = symbol_names
    for name in self._symbol_names:
        if name in self._RESERVED_NAMES:
            continue

        value = parameters.get(name, 1.0)

        self._parameters[name] = Parameter(
            name=name,
            value=value,
            unit=self._unit,
        )

    # Create numerical function
    ordered_symbols = [sp.Symbol(name) for name in self._symbol_names]

    self._func = sp.lambdify(
        ordered_symbols,
        self._expr,
        modules=['numpy'],
    )

__repr__()

Repr function.

Source code in src/easydynamics/sample_model/components/expression_component.py
336
337
338
339
340
341
342
343
344
345
def __repr__(self) -> str:
    """Repr function."""
    param_str = ', '.join(f'{k}={v.value}' for k, v in self._parameters.items())
    return (
        f'{self.__class__.__name__}(\n'
        f"  expr='{self._expression_str}',\n"
        f'  unit={self._unit},\n'
        f'  parameters={{ {param_str} }}\n'
        f')'
    )

__setattr__(name, value)

Allow setting parameter values as attributes.

Parameters:

Name Type Description Default
name str

Name of the parameter to set.

required
value Numeric

New value for the parameter.

required

Raises:

Type Description
TypeError

If the value is not numeric.

Source code in src/easydynamics/sample_model/components/expression_component.py
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
def __setattr__(self, name: str, value: Numeric) -> None:
    """
    Allow setting parameter values as attributes.

    Parameters
    ----------
    name : str
        Name of the parameter to set.
    value : Numeric
        New value for the parameter.

    Raises
    ------
    TypeError
        If the value is not numeric.
    """
    if '_parameters' in self.__dict__ and name in self._parameters:
        param = self._parameters[name]

        if not isinstance(value, Numeric):
            raise TypeError(f'{name} must be numeric')

        param.value = value
    else:
        # For other attributes, use default behavior
        super().__setattr__(name, value)

convert_unit(_new_unit)

Convert the unit of the expression.

Unit conversion is not implemented for ExpressionComponent.

Parameters:

Name Type Description Default
_new_unit str | Unit

The new unit to convert to (ignored).

required

Raises:

Type Description
NotImplementedError

Always raised to indicate unit conversion is not supported.

Source code in src/easydynamics/sample_model/components/expression_component.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def convert_unit(self, _new_unit: str | sc.Unit) -> None:
    """
    Convert the unit of the expression.

    Unit conversion is not implemented for ExpressionComponent.

    Parameters
    ----------
    _new_unit : str | sc.Unit
        The new unit to convert to (ignored).

    Raises
    ------
    NotImplementedError
        Always raised to indicate unit conversion is not supported.
    """

    raise NotImplementedError('Unit conversion is not implemented for ExpressionComponent')

evaluate(x)

Evaluate the expression for given x values.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

Input values for the independent variable.

required

Returns:

Type Description
ndarray

Evaluated results.

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

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        Input values for the independent variable.

    Returns
    -------
    np.ndarray
        Evaluated results.
    """
    x = self._prepare_x_for_evaluate(x)

    args = []
    for name in self._symbol_names:
        if name == 'x':
            args.append(x)
        else:
            args.append(self._parameters[name].value)

    return self._func(*args)

expression property writable

Return the original expression string.

get_all_variables()

Return all parameters.

Returns:

Type Description
list[Parameter]

List of all parameters in the expression.

Source code in src/easydynamics/sample_model/components/expression_component.py
241
242
243
244
245
246
247
248
249
250
def get_all_variables(self) -> list[Parameter]:
    """
    Return all parameters.

    Returns
    -------
    list[Parameter]
        List of all parameters in the expression.
    """
    return list(self._parameters.values())

Gaussian

Bases: CreateParametersMixin, ModelComponent

Model of a Gaussian function.

The intensity is given by

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

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

If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Source code in src/easydynamics/sample_model/components/gaussian.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
class Gaussian(CreateParametersMixin, ModelComponent):
    r"""
    Model of a Gaussian function.

     The intensity is given by

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

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

    If the center is not provided, it will be centered at 0 and fixed, which is typically what you
    want in QENS.
    """

    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,
    ) -> None:
        """
        Initialize the Gaussian component.

        Parameters
        ----------
        area : Numeric | Parameter, default=1.0
            Area of the Gaussian.
        center : Numeric | Parameter | None, default=None
            Center of the Gaussian. If None.
        width : Numeric | Parameter, default=1.0
            Standard deviation.
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='Gaussian'
            Name of the component.
        unique_name : str | None, default=None
            Unique name of the component. if None, a unique_name is automatically generated. By
            default, 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.

        Returns
        -------
        Parameter
            The area parameter.
        """

        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """
        Set the value of the area parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the area parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """

        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.

        Returns
        -------
        Parameter
            The center parameter.
        """

        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """
        Set the center parameter value.

        Parameters
        ----------
        value : Numeric | None
            The new value for the center parameter. If None, defaults to 0 and is fixed.

        Raises
        ------
        TypeError
            If the value is not a number or None.
        """

        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 (standard deviation).

        Returns
        -------
        Parameter
            The width parameter.
        """
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """
        Set the width parameter value.

        Parameters
        ----------
        value : Numeric
            The new value for the width parameter.

        Raises
        ------
        TypeError
            If the value is not a number or None.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')

        if float(value) <= 0:
            raise ValueError('width must be positive')

        self._width.value = value

    def evaluate(
        self,
        x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray,
    ) -> np.ndarray:
        r"""
        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
        intensity is given by $$ I(x) = \frac{A}{\sigma \sqrt{2\pi}} \exp\left( -\frac{1}{2}
        \left(\frac{x - x_0}{\sigma}\right)^2 \right) $$

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

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Gaussian.

        Returns
        -------
        np.ndarray
            The intensity of the Gaussian at the given x values.
        """

        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) -> str:
        """
        Return a string representation of the Gaussian.

        Returns
        -------
        str
            A string representation of the Gaussian.
        """

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

__init__(area=1.0, center=None, width=1.0, unit='meV', display_name='Gaussian', unique_name=None)

Initialize the Gaussian component.

Parameters:

Name Type Description Default
area Numeric | Parameter

Area of the Gaussian.

1.0
center Numeric | Parameter | None

Center of the Gaussian. If None.

None
width Numeric | Parameter

Standard deviation.

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Name of the component.

'Gaussian'
unique_name str | None

Unique name of the component. if None, a unique_name is automatically generated. By default, None.

None
Source code in src/easydynamics/sample_model/components/gaussian.py
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
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,
) -> None:
    """
    Initialize the Gaussian component.

    Parameters
    ----------
    area : Numeric | Parameter, default=1.0
        Area of the Gaussian.
    center : Numeric | Parameter | None, default=None
        Center of the Gaussian. If None.
    width : Numeric | Parameter, default=1.0
        Standard deviation.
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='Gaussian'
        Name of the component.
    unique_name : str | None, default=None
        Unique name of the component. if None, a unique_name is automatically generated. By
        default, 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

__repr__()

Return a string representation of the Gaussian.

Returns:

Type Description
str

A string representation of the Gaussian.

Source code in src/easydynamics/sample_model/components/gaussian.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def __repr__(self) -> str:
    """
    Return a string representation of the Gaussian.

    Returns
    -------
    str
        A string representation of the Gaussian.
    """

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

area property writable

Get the area parameter.

Returns:

Type Description
Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

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 intensity is given by $$ I(x) = \frac{A}{\sigma \sqrt{2\pi}} \exp\left( -\frac{1}{2} \left(\frac{x - x_0}{\sigma}\right)^2 \right) $$

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

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Gaussian.

required

Returns:

Type Description
ndarray

The intensity of the Gaussian at the given x values.

Source code in src/easydynamics/sample_model/components/gaussian.py
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
def evaluate(
    self,
    x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray,
) -> np.ndarray:
    r"""
    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
    intensity is given by $$ I(x) = \frac{A}{\sigma \sqrt{2\pi}} \exp\left( -\frac{1}{2}
    \left(\frac{x - x_0}{\sigma}\right)^2 \right) $$

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

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Gaussian.

    Returns
    -------
    np.ndarray
        The intensity of the Gaussian at the given x values.
    """

    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 (standard deviation).

Returns:

Type Description
Parameter

The width parameter.

Lorentzian

Bases: CreateParametersMixin, ModelComponent

Model of a Lorentzian function.

The intensity is given by $$ I(x) = \frac{A}{\pi} \frac{\Gamma}{(x - x_0)^2 + \Gamma^2}, $$ where \(A\) is the area, \(x_0\) is the center, and \(\Gamma\) is the half width at half maximum (HWHM).

If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Source code in src/easydynamics/sample_model/components/lorentzian.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
class Lorentzian(CreateParametersMixin, ModelComponent):
    r"""
    Model of a Lorentzian function.

    The intensity is given by $$ I(x) = \frac{A}{\pi} \frac{\Gamma}{(x - x_0)^2 + \Gamma^2}, $$
    where $A$ is the area, $x_0$ is the center, and $\Gamma$ is the half width at half maximum
    (HWHM).

    If the center is not provided, it will be centered at 0 and fixed, which is typically what you
    want in QENS.
    """

    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,
    ) -> None:
        """
        Initialize the Lorentzian component.

        Parameters
        ----------
        area : Numeric | Parameter, default=1.0
            Area of the Lorentzian.
        center : Numeric | Parameter | None, default=None
            Center of the Lorentzian. If None.
        width : Numeric | Parameter, default=1.0
            Half width at half maximum (HWHM).
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='Lorentzian'
            Name of the component.
        unique_name : str | None, default=None
            Unique name of the component. If None, a unique_name is automatically generated. By
            default, 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.

        Returns
        -------
        Parameter
            The area parameter.
        """
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """
        Set the value of the area parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the area parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """
        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.

        Returns
        -------
        Parameter
            The center parameter.
        """
        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """
        Set the value of the center parameter.

        Parameters
        ----------
        value : Numeric | None
            The new value for the center parameter. If None, defaults to 0 and is fixed.

        Raises
        ------
        TypeError
            If the value is not a number or None.
        """

        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 (HWHM).

        Returns
        -------
        Parameter
            The width parameter.
        """
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """
        Set the width parameter value (HWHM).

        Parameters
        ----------
        value : Numeric
            The new value for the width parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')

        if float(value) <= 0:
            raise ValueError('width must be positive')
        self._width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        r"""
        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
        intensity is given by

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

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

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Lorentzian.

        Returns
        -------
        np.ndarray
            The intensity of the Lorentzian at the given x values.
        """

        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) -> str:
        """
        Return a string representation of the Lorentzian.

        Returns
        -------
        str
            A string representation of the Lorentzian.
        """
        return (
            f'Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n'
            f'area = {self.area},\n center = {self.center},\n width = {self.width})'
        )

__init__(area=1.0, center=None, width=1.0, unit='meV', display_name='Lorentzian', unique_name=None)

Initialize the Lorentzian component.

Parameters:

Name Type Description Default
area Numeric | Parameter

Area of the Lorentzian.

1.0
center Numeric | Parameter | None

Center of the Lorentzian. If None.

None
width Numeric | Parameter

Half width at half maximum (HWHM).

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Name of the component.

'Lorentzian'
unique_name str | None

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

None
Source code in src/easydynamics/sample_model/components/lorentzian.py
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
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,
) -> None:
    """
    Initialize the Lorentzian component.

    Parameters
    ----------
    area : Numeric | Parameter, default=1.0
        Area of the Lorentzian.
    center : Numeric | Parameter | None, default=None
        Center of the Lorentzian. If None.
    width : Numeric | Parameter, default=1.0
        Half width at half maximum (HWHM).
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='Lorentzian'
        Name of the component.
    unique_name : str | None, default=None
        Unique name of the component. If None, a unique_name is automatically generated. By
        default, 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

__repr__()

Return a string representation of the Lorentzian.

Returns:

Type Description
str

A string representation of the Lorentzian.

Source code in src/easydynamics/sample_model/components/lorentzian.py
209
210
211
212
213
214
215
216
217
218
219
220
221
def __repr__(self) -> str:
    """
    Return a string representation of the Lorentzian.

    Returns
    -------
    str
        A string representation of the Lorentzian.
    """
    return (
        f'Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n'
        f'area = {self.area},\n center = {self.center},\n width = {self.width})'
    )

area property writable

Get the area parameter.

Returns:

Type Description
Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

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 intensity is given by

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

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

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Lorentzian.

required

Returns:

Type Description
ndarray

The intensity of the Lorentzian at the given x values.

Source code in src/easydynamics/sample_model/components/lorentzian.py
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
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    r"""
    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
    intensity is given by

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

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

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Lorentzian.

    Returns
    -------
    np.ndarray
        The intensity of the Lorentzian at the given x values.
    """

    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 (HWHM).

Returns:

Type Description
Parameter

The width parameter.

Polynomial

Bases: ModelComponent

Polynomial function component.

The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N, $$ where \(C_i\) are the coefficients.

Source code in src/easydynamics/sample_model/components/polynomial.py
 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
class Polynomial(ModelComponent):
    r"""
    Polynomial function component.

    The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N, $$ where $C_i$ are
    the coefficients.
    """

    def __init__(
        self,
        coefficients: Sequence[Numeric | Parameter] = (0.0,),
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Polynomial',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the Polynomial component.

        Parameters
        ----------
        coefficients : Sequence[Numeric | Parameter], default=(0.0,)
            Coefficients c0, c1, ..., cN.
        unit : str | sc.Unit, default='meV'
            Unit of the Polynomial component.
        display_name : str | None, default='Polynomial'
            Display name of the Polynomial component.
        unique_name : str | None, default=None
            Unique name of the component. If None, a unique_name is automatically generated. By
            default, None.

        Raises
        ------
        TypeError
            If coefficients is not a sequence of numbers or Parameters or if any item in
            coefficients is not a number or Parameter.
        ValueError
            If coefficients is an empty sequence.
        """

        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.

        Returns
        -------
        list[Parameter]
            The coefficients of the polynomial.
        """
        return list(self._coefficients)

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

        Length must match current number of coefficients.

        Parameters
        ----------
        coeffs : Sequence[Numeric | Parameter]
            New coefficients as a sequence of numbers or Parameters.

        Raises
        ------
        TypeError
            If coeffs is not a sequence of numbers or Parameters or if any item in coeffs is not a
            number or Parameter.
        ValueError
            If the length of coeffs does not match the existing 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.

        Returns
        -------
        list[float]
            The coefficient values of the polynomial.
        """
        return [param.value for param in self._coefficients]

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

        The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ...
        + c_N x^N, $$ where $C_i$ are the coefficients.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Polynomial.

        Returns
        -------
        np.ndarray
            The evaluated Polynomial at the given x values.
        """

        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,
                stacklevel=2,
            )
        return result

    @property
    def degree(self) -> int:
        """
        Get the degree of the polynomial.

        Returns
        -------
        int
            The degree of the polynomial.
        """
        return len(self._coefficients) - 1

    @degree.setter
    def degree(self, _value: int) -> None:
        """
        The degree is determined by the number of coefficients and cannot be set directly.

        Parameters
        ----------
        _value : int
            The new degree of the polynomial.

        Raises
        ------
        AttributeError
            Always raised since degree cannot be set directly.
        """
        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 variables from the model component.

        Returns
        -------
        list[DescriptorBase]
            List of variables in the component.
        """
        return list(self._coefficients)

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

        Parameters
        ----------
        unit : str | sc.Unit
            The target unit to convert to.

        Raises
        ------
        UnitError
            If the provided unit is not a string or sc.Unit.
        """

        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:
        """
        Return a string representation of the Polynomial.

        Returns
        -------
        str
            A string representation of the Polynomial.
        """

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

__init__(coefficients=(0.0,), unit='meV', display_name='Polynomial', unique_name=None)

Initialize the Polynomial component.

Parameters:

Name Type Description Default
coefficients Sequence[Numeric | Parameter]

Coefficients c0, c1, ..., cN.

(0.0,)
unit str | Unit

Unit of the Polynomial component.

'meV'
display_name str | None

Display name of the Polynomial component.

'Polynomial'
unique_name str | None

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

None

Raises:

Type Description
TypeError

If coefficients is not a sequence of numbers or Parameters or if any item in coefficients is not a number or Parameter.

ValueError

If coefficients is an empty sequence.

Source code in src/easydynamics/sample_model/components/polynomial.py
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
def __init__(
    self,
    coefficients: Sequence[Numeric | Parameter] = (0.0,),
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'Polynomial',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the Polynomial component.

    Parameters
    ----------
    coefficients : Sequence[Numeric | Parameter], default=(0.0,)
        Coefficients c0, c1, ..., cN.
    unit : str | sc.Unit, default='meV'
        Unit of the Polynomial component.
    display_name : str | None, default='Polynomial'
        Display name of the Polynomial component.
    unique_name : str | None, default=None
        Unique name of the component. If None, a unique_name is automatically generated. By
        default, None.

    Raises
    ------
    TypeError
        If coefficients is not a sequence of numbers or Parameters or if any item in
        coefficients is not a number or Parameter.
    ValueError
        If coefficients is an empty sequence.
    """

    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)

__repr__()

Return a string representation of the Polynomial.

Returns:

Type Description
str

A string representation of the Polynomial.

Source code in src/easydynamics/sample_model/components/polynomial.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def __repr__(self) -> str:
    """
    Return a string representation of the Polynomial.

    Returns
    -------
    str
        A string representation of the Polynomial.
    """

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

coefficient_values()

Get the coefficients of the polynomial as a list.

Returns:

Type Description
list[float]

The coefficient values of the polynomial.

Source code in src/easydynamics/sample_model/components/polynomial.py
138
139
140
141
142
143
144
145
146
147
def coefficient_values(self) -> list[float]:
    """
    Get the coefficients of the polynomial as a list.

    Returns
    -------
    list[float]
        The coefficient values of the polynomial.
    """
    return [param.value for param in self._coefficients]

coefficients property writable

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

Returns:

Type Description
list[Parameter]

The coefficients of the polynomial.

convert_unit(unit)

Convert the unit of the polynomial.

Parameters:

Name Type Description Default
unit str | Unit

The target unit to convert to.

required

Raises:

Type Description
UnitError

If the provided unit is not a string or sc.Unit.

Source code in src/easydynamics/sample_model/components/polynomial.py
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
def convert_unit(self, unit: str | sc.Unit) -> None:
    """
    Convert the unit of the polynomial.

    Parameters
    ----------
    unit : str | sc.Unit
        The target unit to convert to.

    Raises
    ------
    UnitError
        If the provided unit is not a string or sc.Unit.
    """

    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

Get the degree of the polynomial.

Returns:

Type Description
int

The degree of the polynomial.

evaluate(x)

Evaluate the Polynomial at the given x values.

The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N, $$ where \(C_i\) are the coefficients.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Polynomial.

required

Returns:

Type Description
ndarray

The evaluated Polynomial at the given x values.

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

    The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ...
    + c_N x^N, $$ where $C_i$ are the coefficients.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Polynomial.

    Returns
    -------
    np.ndarray
        The evaluated Polynomial at the given x values.
    """

    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,
            stacklevel=2,
        )
    return result

get_all_variables()

Get all variables from the model component.

Returns:

Type Description
list[DescriptorBase]

List of variables in the component.

Source code in src/easydynamics/sample_model/components/polynomial.py
214
215
216
217
218
219
220
221
222
223
def get_all_variables(self) -> list[DescriptorBase]:
    """
    Get all variables from the model component.

    Returns
    -------
    list[DescriptorBase]
        List of variables 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.

Use scipy.special.voigt_profile to evaluate the Voigt profile.

Source code in src/easydynamics/sample_model/components/voigt.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
class Voigt(CreateParametersMixin, ModelComponent):
    r"""
    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.

    Use scipy.special.voigt_profile to evaluate the Voigt profile.
    """

    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,
    ) -> None:
        """
        Initialize a Voigt component.

        Parameters
        ----------
        area : Numeric | Parameter, default=1.0
            Total area under the curve.
        center : Numeric | Parameter | None, default=None
            Center of the Voigt profile.
        gaussian_width : Numeric | Parameter, default=1.0
            Standard deviation of the Gaussian part.
        lorentzian_width : Numeric | Parameter, default=1.0
            Half width at half max (HWHM) of the Lorentzian part.
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='Voigt'
            Display name of the component.
        unique_name : str | None, default=None
            Unique name of the component. If None, a unique_name is automatically generated. By
            default, 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.

        Returns
        -------
        Parameter
            The area parameter.
        """
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """
        Set the value of the area parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the area parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """
        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.

        Returns
        -------
        Parameter
            The center parameter.
        """
        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """
        Set the value of the center parameter.

        Parameters
        ----------
        value : Numeric | None
            The new value for the center parameter. If None, defaults to 0 and is fixed.

        Raises
        ------
        TypeError
            If the value is not a number.
        """
        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 Gaussian width parameter.

        Returns
        -------
        Parameter
            The Gaussian width parameter.
        """
        return self._gaussian_width

    @gaussian_width.setter
    def gaussian_width(self, value: Numeric) -> None:
        """
        Set the width parameter value.

        Parameters
        ----------
        value : Numeric
            The new value for the width parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('gaussian_width must be a number')
        if float(value) <= 0:
            raise ValueError('gaussian_width must be positive')
        self._gaussian_width.value = value

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

        Returns
        -------
        Parameter
            The Lorentzian width parameter.
        """
        return self._lorentzian_width

    @lorentzian_width.setter
    def lorentzian_width(self, value: Numeric) -> None:
        """
        Set the value of the Lorentzian width parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the Lorentzian width parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('lorentzian_width must be a number')
        if float(value) <= 0:
            raise ValueError('lorentzian_width must be positive')
        self._lorentzian_width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        r"""
        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.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Voigt.

        Returns
        -------
        np.ndarray
            The intensity of the Voigt at the given x values.
        """

        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) -> str:
        """
        Return a string representation of the Voigt.

        Returns
        -------
        str
            A string representation of the Voigt.
        """

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

__init__(area=1.0, center=None, gaussian_width=1.0, lorentzian_width=1.0, unit='meV', display_name='Voigt', unique_name=None)

Initialize a Voigt component.

Parameters:

Name Type Description Default
area Numeric | Parameter

Total area under the curve.

1.0
center Numeric | Parameter | None

Center of the Voigt profile.

None
gaussian_width Numeric | Parameter

Standard deviation of the Gaussian part.

1.0
lorentzian_width Numeric | Parameter

Half width at half max (HWHM) of the Lorentzian part.

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Display name of the component.

'Voigt'
unique_name str | None

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

None
Source code in src/easydynamics/sample_model/components/voigt.py
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
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,
) -> None:
    """
    Initialize a Voigt component.

    Parameters
    ----------
    area : Numeric | Parameter, default=1.0
        Total area under the curve.
    center : Numeric | Parameter | None, default=None
        Center of the Voigt profile.
    gaussian_width : Numeric | Parameter, default=1.0
        Standard deviation of the Gaussian part.
    lorentzian_width : Numeric | Parameter, default=1.0
        Half width at half max (HWHM) of the Lorentzian part.
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='Voigt'
        Display name of the component.
    unique_name : str | None, default=None
        Unique name of the component. If None, a unique_name is automatically generated. By
        default, 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

__repr__()

Return a string representation of the Voigt.

Returns:

Type Description
str

A string representation of the Voigt.

Source code in src/easydynamics/sample_model/components/voigt.py
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def __repr__(self) -> str:
    """
    Return a string representation of the Voigt.

    Returns
    -------
    str
        A string representation of the Voigt.
    """

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

area property writable

Get the area parameter.

Returns:

Type Description
Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

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.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Voigt.

required

Returns:

Type Description
ndarray

The intensity of the Voigt at the given x values.

Source code in src/easydynamics/sample_model/components/voigt.py
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
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    r"""
    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.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Voigt.

    Returns
    -------
    np.ndarray
        The intensity of the Voigt at the given x values.
    """

    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 Gaussian width parameter.

Returns:

Type Description
Parameter

The Gaussian width parameter.

lorentzian_width property writable

Get the Lorentzian width parameter (HWHM).

Returns:

Type Description
Parameter

The Lorentzian width parameter.

damped_harmonic_oscillator

DampedHarmonicOscillator

Bases: CreateParametersMixin, ModelComponent

Model of a Damped Harmonic Oscillator (DHO).

The intensity is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2 \gamma x)^2 \right)}, $$ where \(A\) is the area, \(x_0\) is the center, and \(\gamma\) is the width.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.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
class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent):
    r"""
    Model of a Damped Harmonic Oscillator (DHO).

    The intensity is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2
    \gamma x)^2 \right)}, $$ where $A$ is the area, $x_0$ is the center, and $\gamma$ is the width.
    """

    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,
    ) -> None:
        """
        Initialize the Damped Harmonic Oscillator.

        Parameters
        ----------
        area : Numeric | Parameter, default=1.0
            Area under the curve.
        center : Numeric | Parameter, default=1.0
            Resonance frequency, approximately the peak position.
        width : Numeric | Parameter, default=1.0
            Damping constant, approximately the half width at half max (HWHM) of the peaks. By
            default, 1.0.
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='DampedHarmonicOscillator'
            Display name of the component.
        unique_name : str | None, default=None
            Unique name of the component. If None, a unique_name is automatically generated. By
            default, 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.

        Returns
        -------
        Parameter
            The area parameter.
        """
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """
        Set the value of the area parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the area parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """
        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.

        Returns
        -------
        Parameter
            The center parameter.
        """
        return self._center

    @center.setter
    def center(self, value: Numeric) -> None:
        """
        Set the value of the center parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the center parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('center must be a number')

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

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

        Returns
        -------
        Parameter
            The width parameter.
        """
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """
        Set the value of the width parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the width parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')

        if float(value) <= 0:
            raise ValueError('width must be positive')

        self._width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        r"""
        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 intensity
        is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2 \gamma x)^2
        \right)}, $$ where $A$ is the area, $x_0$ is the center, and $\gamma$ is the width.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the DHO.

        Returns
        -------
        np.ndarray
            The intensity of the DHO at the given x values.
        """

        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) -> str:
        """
        Return a string representation of the Damped Harmonic Oscillator.

        Returns
        -------
        str
            A string representation of the Damped Harmonic Oscillator.
        """
        return (
            f'DampedHarmonicOscillator(display_name = {self.display_name}, '
            f'unit = {self._unit},\n '
            f'area = {self.area},\n center = {self.center},\n width = {self.width})'
        )
__init__(area=1.0, center=1.0, width=1.0, unit='meV', display_name='DampedHarmonicOscillator', unique_name=None)

Initialize the Damped Harmonic Oscillator.

Parameters:

Name Type Description Default
area Numeric | Parameter

Area under the curve.

1.0
center Numeric | Parameter

Resonance frequency, approximately the peak position.

1.0
width Numeric | Parameter

Damping constant, approximately the half width at half max (HWHM) of the peaks. By default, 1.0.

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Display name of the component.

'DampedHarmonicOscillator'
unique_name str | None

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

None
Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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,
) -> None:
    """
    Initialize the Damped Harmonic Oscillator.

    Parameters
    ----------
    area : Numeric | Parameter, default=1.0
        Area under the curve.
    center : Numeric | Parameter, default=1.0
        Resonance frequency, approximately the peak position.
    width : Numeric | Parameter, default=1.0
        Damping constant, approximately the half width at half max (HWHM) of the peaks. By
        default, 1.0.
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='DampedHarmonicOscillator'
        Display name of the component.
    unique_name : str | None, default=None
        Unique name of the component. If None, a unique_name is automatically generated. By
        default, 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
__repr__()

Return a string representation of the Damped Harmonic Oscillator.

Returns:

Type Description
str

A string representation of the Damped Harmonic Oscillator.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def __repr__(self) -> str:
    """
    Return a string representation of the Damped Harmonic Oscillator.

    Returns
    -------
    str
        A string representation of the Damped Harmonic Oscillator.
    """
    return (
        f'DampedHarmonicOscillator(display_name = {self.display_name}, '
        f'unit = {self._unit},\n '
        f'area = {self.area},\n center = {self.center},\n width = {self.width})'
    )
area property writable

Get the area parameter.

Returns:

Type Description
Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

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 intensity is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2 \gamma x)^2 \right)}, $$ where \(A\) is the area, \(x_0\) is the center, and \(\gamma\) is the width.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the DHO.

required

Returns:

Type Description
ndarray

The intensity of the DHO at the given x values.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
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
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    r"""
    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 intensity
    is given by $$ I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + (2 \gamma x)^2
    \right)}, $$ where $A$ is the area, $x_0$ is the center, and $\gamma$ is the width.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the DHO.

    Returns
    -------
    np.ndarray
        The intensity of the DHO at the given x values.
    """

    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.

Returns:

Type Description
Parameter

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 by the Convolution method. If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Source code in src/easydynamics/sample_model/components/delta_function.py
 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
class DeltaFunction(CreateParametersMixin, ModelComponent):
    """
    Delta function.

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

    def __init__(
        self,
        center: Numeric | Parameter | None = None,
        area: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'DeltaFunction',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the Delta function.

        Parameters
        ----------
        center : Numeric | Parameter | None, default=None
            Center of the delta function. If None.
        area : Numeric | Parameter, default=1.0
            Total area under the curve.
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='DeltaFunction'
            Name of the component.
        unique_name : str | None, default=None
            Unique name of the component. If None, a unique_name is automatically generated. By
            default, 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.

        Returns
        -------
        Parameter
            The area parameter.
        """

        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """
        Set the value of the area parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the area parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """

        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.

        Returns
        -------
        Parameter
            The center parameter.
        """

        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """
        Set the center parameter value.

        Parameters
        ----------
        value : Numeric | None
            The new value for the center parameter. If None, defaults to 0 and is fixed.

        Raises
        ------
        TypeError
            If the value is not a number or None.
        """

        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.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Delta function.

        Returns
        -------
        np.ndarray
            The evaluated Delta function at the given x values.
        """

        # 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:  # noqa: SIM108
                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:  # noqa: SIM108
                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) -> str:
        """
        Return a string representation of the Delta function.

        Returns
        -------
        str
            A string representation of the Delta function.
        """

        return (
            f'DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n'
            f'area = {self.area},\n center = {self.center})'
        )
__init__(center=None, area=1.0, unit='meV', display_name='DeltaFunction', unique_name=None)

Initialize the Delta function.

Parameters:

Name Type Description Default
center Numeric | Parameter | None

Center of the delta function. If None.

None
area Numeric | Parameter

Total area under the curve.

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Name of the component.

'DeltaFunction'
unique_name str | None

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

None
Source code in src/easydynamics/sample_model/components/delta_function.py
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
def __init__(
    self,
    center: Numeric | Parameter | None = None,
    area: Numeric | Parameter = 1.0,
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'DeltaFunction',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the Delta function.

    Parameters
    ----------
    center : Numeric | Parameter | None, default=None
        Center of the delta function. If None.
    area : Numeric | Parameter, default=1.0
        Total area under the curve.
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='DeltaFunction'
        Name of the component.
    unique_name : str | None, default=None
        Unique name of the component. If None, a unique_name is automatically generated. By
        default, 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
__repr__()

Return a string representation of the Delta function.

Returns:

Type Description
str

A string representation of the Delta function.

Source code in src/easydynamics/sample_model/components/delta_function.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def __repr__(self) -> str:
    """
    Return a string representation of the Delta function.

    Returns
    -------
    str
        A string representation of the Delta function.
    """

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

Get the area parameter.

Returns:

Type Description
Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

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.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Delta function.

required

Returns:

Type Description
ndarray

The evaluated Delta function at the given x values.

Source code in src/easydynamics/sample_model/components/delta_function.py
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
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.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Delta function.

    Returns
    -------
    np.ndarray
        The evaluated Delta function at the given x values.
    """

    # 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:  # noqa: SIM108
            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:  # noqa: SIM108
            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

exponential

Exponential

Bases: CreateParametersMixin, ModelComponent

Model of an exponential function.

The intensity is given by

\[ I(x) = A e^{B (x-x_0)}, \]

where \(A\) is the amplitude, \(x_0\) is the center, and \(B\) describes the rate of decay or growth.

Source code in src/easydynamics/sample_model/components/exponential.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
 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
class Exponential(CreateParametersMixin, ModelComponent):
    r"""
    Model of an exponential function.

    The intensity is given by

    $$ I(x) = A e^{B (x-x_0)}, $$

    where $A$ is the amplitude, $x_0$ is the center, and $B$ describes the rate of decay or growth.
    """

    def __init__(
        self,
        amplitude: Numeric | Parameter = 1.0,
        center: Numeric | Parameter | None = None,
        rate: Numeric | Parameter = 1.0,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Exponential',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the Exponential component.

        Parameters
        ----------
        amplitude : Numeric | Parameter, default=1.0
            Amplitude of the Exponential.
        center : Numeric | Parameter | None, default=None
            Center of the Exponential. If None, the center is fixed at 0.
        rate : Numeric | Parameter, default=1.0
            Decay or growth constant of the Exponential.
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='Exponential'
            Name of the component.
        unique_name : str | None, default=None
            Unique name of the component. if None, a unique_name is automatically generated. By
            default, None.

        Raises
        ------
        TypeError
            If amplitude, center, or rate are not numbers or Parameters.
        ValueError
            If amplitude, center or rate are not finite numbers.
        """
        # Validate inputs and create Parameters if not given
        super().__init__(
            display_name=display_name,
            unit=unit,
            unique_name=unique_name,
        )

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

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

            amplitude = Parameter(
                name=display_name + ' amplitude', value=float(amplitude), unit=unit
            )

        center = self._create_center_parameter(
            center=center, name=display_name, fix_if_none=True, unit=self._unit
        )

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

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

            rate = Parameter(name=display_name + ' rate', value=float(rate), unit='1/' + str(unit))

        self._amplitude = amplitude
        self._center = center
        self._rate = rate

    @property
    def amplitude(self) -> Parameter:
        """
        Get the amplitude parameter.

        Returns
        -------
        Parameter
            The amplitude parameter.
        """

        return self._amplitude

    @amplitude.setter
    def amplitude(self, value: Numeric) -> None:
        """
        Set the value of the amplitude parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the amplitude parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """

        if not isinstance(value, Numeric):
            raise TypeError('amplitude must be a number')
        self._amplitude.value = value

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

        Returns
        -------
        Parameter
            The center parameter.
        """

        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """
        Set the center parameter value.

        Parameters
        ----------
        value : Numeric | None
            The new value for the center parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """

        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 rate(self) -> Parameter:
        """
        Get the rate parameter.

        Returns
        -------
        Parameter
            The rate parameter.
        """
        return self._rate

    @rate.setter
    def rate(self, value: Numeric) -> None:
        """
        Set the rate parameter value.

        Parameters
        ----------
        value : Numeric
            The new value for the rate parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """
        if not isinstance(value, Numeric):
            raise TypeError('rate must be a number')

        self._rate.value = value

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

        If x is a scipp Variable, the unit of the Exponential will be converted to match x. The
        intensity is given by $$ I(x) = A \exp\left( r (x - x_0) \right) $$

        where $A$ is the amplitude, $x_0$ is the center, and $r$ is the rate.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Exponential.

        Returns
        -------
        np.ndarray
            The intensity of the Exponential at the given x values.
        """

        x = self._prepare_x_for_evaluate(x)
        exponent = self.rate.value * (x - self.center.value)

        return self.amplitude.value * np.exp(exponent)

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

        Parameters
        ----------
        unit : str | sc.Unit
            The new unit to convert to.

        Raises
        ------
        TypeError
            If unit is not a string or sc.Unit.
        Exception
            If conversion fails for any parameter.
        """

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

        old_unit = self._unit
        pars = [self.amplitude, self.center]
        try:
            for p in pars:
                p.convert_unit(unit)
            self.rate.convert_unit('1/' + str(unit))
            self._unit = unit
        except Exception as e:
            # Attempt to rollback on failure
            try:
                for p in pars:
                    p.convert_unit(old_unit)
                self.rate.convert_unit('1/' + str(old_unit))
            except Exception:  # noqa: S110
                pass  # Best effort rollback
            raise e

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

        Returns
        -------
        str
            A string representation of the Exponential.
        """

        return f'Exponential(unique_name = {self.unique_name}, unit = {self._unit},\n \
            amplitude = {self.amplitude},\n center = {self.center},\n rate = {self.rate})'
__init__(amplitude=1.0, center=None, rate=1.0, unit='meV', display_name='Exponential', unique_name=None)

Initialize the Exponential component.

Parameters:

Name Type Description Default
amplitude Numeric | Parameter

Amplitude of the Exponential.

1.0
center Numeric | Parameter | None

Center of the Exponential. If None, the center is fixed at 0.

None
rate Numeric | Parameter

Decay or growth constant of the Exponential.

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Name of the component.

'Exponential'
unique_name str | None

Unique name of the component. if None, a unique_name is automatically generated. By default, None.

None

Raises:

Type Description
TypeError

If amplitude, center, or rate are not numbers or Parameters.

ValueError

If amplitude, center or rate are not finite numbers.

Source code in src/easydynamics/sample_model/components/exponential.py
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
def __init__(
    self,
    amplitude: Numeric | Parameter = 1.0,
    center: Numeric | Parameter | None = None,
    rate: Numeric | Parameter = 1.0,
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'Exponential',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the Exponential component.

    Parameters
    ----------
    amplitude : Numeric | Parameter, default=1.0
        Amplitude of the Exponential.
    center : Numeric | Parameter | None, default=None
        Center of the Exponential. If None, the center is fixed at 0.
    rate : Numeric | Parameter, default=1.0
        Decay or growth constant of the Exponential.
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='Exponential'
        Name of the component.
    unique_name : str | None, default=None
        Unique name of the component. if None, a unique_name is automatically generated. By
        default, None.

    Raises
    ------
    TypeError
        If amplitude, center, or rate are not numbers or Parameters.
    ValueError
        If amplitude, center or rate are not finite numbers.
    """
    # Validate inputs and create Parameters if not given
    super().__init__(
        display_name=display_name,
        unit=unit,
        unique_name=unique_name,
    )

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

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

        amplitude = Parameter(
            name=display_name + ' amplitude', value=float(amplitude), unit=unit
        )

    center = self._create_center_parameter(
        center=center, name=display_name, fix_if_none=True, unit=self._unit
    )

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

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

        rate = Parameter(name=display_name + ' rate', value=float(rate), unit='1/' + str(unit))

    self._amplitude = amplitude
    self._center = center
    self._rate = rate
__repr__()

Return a string representation of the Exponential.

Returns:

Type Description
str

A string representation of the Exponential.

Source code in src/easydynamics/sample_model/components/exponential.py
263
264
265
266
267
268
269
270
271
272
273
274
def __repr__(self) -> str:
    """
    Return a string representation of the Exponential.

    Returns
    -------
    str
        A string representation of the Exponential.
    """

    return f'Exponential(unique_name = {self.unique_name}, unit = {self._unit},\n \
        amplitude = {self.amplitude},\n center = {self.center},\n rate = {self.rate})'
amplitude property writable

Get the amplitude parameter.

Returns:

Type Description
Parameter

The amplitude parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

The center parameter.

convert_unit(unit)

Convert the unit of the Parameters in the component.

Parameters:

Name Type Description Default
unit str | Unit

The new unit to convert to.

required

Raises:

Type Description
TypeError

If unit is not a string or sc.Unit.

Exception

If conversion fails for any parameter.

Source code in src/easydynamics/sample_model/components/exponential.py
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
def convert_unit(self, unit: str | sc.Unit) -> None:
    """
    Convert the unit of the Parameters in the component.

    Parameters
    ----------
    unit : str | sc.Unit
        The new unit to convert to.

    Raises
    ------
    TypeError
        If unit is not a string or sc.Unit.
    Exception
        If conversion fails for any parameter.
    """

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

    old_unit = self._unit
    pars = [self.amplitude, self.center]
    try:
        for p in pars:
            p.convert_unit(unit)
        self.rate.convert_unit('1/' + str(unit))
        self._unit = unit
    except Exception as e:
        # Attempt to rollback on failure
        try:
            for p in pars:
                p.convert_unit(old_unit)
            self.rate.convert_unit('1/' + str(old_unit))
        except Exception:  # noqa: S110
            pass  # Best effort rollback
        raise e
evaluate(x)

Evaluate the Exponential at the given x values.

If x is a scipp Variable, the unit of the Exponential will be converted to match x. The intensity is given by $$ I(x) = A \exp\left( r (x - x_0) \right) $$

where \(A\) is the amplitude, \(x_0\) is the center, and \(r\) is the rate.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Exponential.

required

Returns:

Type Description
ndarray

The intensity of the Exponential at the given x values.

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

    If x is a scipp Variable, the unit of the Exponential will be converted to match x. The
    intensity is given by $$ I(x) = A \exp\left( r (x - x_0) \right) $$

    where $A$ is the amplitude, $x_0$ is the center, and $r$ is the rate.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Exponential.

    Returns
    -------
    np.ndarray
        The intensity of the Exponential at the given x values.
    """

    x = self._prepare_x_for_evaluate(x)
    exponent = self.rate.value * (x - self.center.value)

    return self.amplitude.value * np.exp(exponent)
rate property writable

Get the rate parameter.

Returns:

Type Description
Parameter

The rate parameter.

expression_component

ExpressionComponent

Bases: ModelComponent

Model component defined by a symbolic expression.

Example: expr = ExpressionComponent( "A * exp(-(x - x0)2 / (2*sigma2))", parameters={"A": 10, "x0": 0, "sigma": 1}, )

expr.A = 5 y = expr.evaluate(x)
Source code in src/easydynamics/sample_model/components/expression_component.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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
class ExpressionComponent(ModelComponent):
    """
    Model component defined by a symbolic expression.

    Example: expr = ExpressionComponent( "A * exp(-(x - x0)**2 / (2*sigma**2))", parameters={"A":
    10, "x0": 0, "sigma": 1}, )

        expr.A = 5 y = expr.evaluate(x)
    """

    # -------------------------
    # Allowed symbolic functions
    # -------------------------
    _ALLOWED_FUNCS: ClassVar[dict[str, object]] = {
        # Exponentials & logs
        'exp': sp.exp,
        'log': sp.log,
        'ln': sp.log,
        'sqrt': sp.sqrt,
        # Trigonometric
        'sin': sp.sin,
        'cos': sp.cos,
        'tan': sp.tan,
        'sinc': sp.sinc,
        'cot': sp.cot,
        'sec': sp.sec,
        'csc': sp.csc,
        'asin': sp.asin,
        'acos': sp.acos,
        'atan': sp.atan,
        # Hyperbolic
        'sinh': sp.sinh,
        'cosh': sp.cosh,
        'tanh': sp.tanh,
        # Misc
        'abs': sp.Abs,
        'sign': sp.sign,
        'floor': sp.floor,
        'ceil': sp.ceiling,
        # Special functions
        'erf': sp.erf,
    }

    # -------------------------
    # Allowed constants
    # -------------------------
    _ALLOWED_CONSTANTS: ClassVar[dict[str, object]] = {
        'pi': sp.pi,
        'E': sp.E,
    }

    _RESERVED_NAMES: ClassVar[dict[str, object]] = {'x'}

    def __init__(
        self,
        expression: str,
        parameters: dict[str, Numeric] | None = None,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Expression',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the ExpressionComponent.

        Parameters
        ----------
        expression : str
            The symbolic expression as a string. Must contain 'x' as the independent variable.
        parameters : dict[str, Numeric] | None, default=None
            Dictionary of parameter names and their initial values.
        unit : str | sc.Unit, default='meV'
            Unit of the output.
        display_name : str | None, default='Expression'
            Display name for the component.
        unique_name : str | None, default=None
            Unique name for the component.

        Raises
        ------
        ValueError
            If the expression is invalid or does not contain 'x'.
        TypeError
            If any parameter value is not numeric.
        """
        super().__init__(unit=unit, display_name=display_name, unique_name=unique_name)

        if 'np.' in expression:
            raise ValueError(
                'NumPy syntax (np.*) is not supported. '
                "Use functions like 'exp', 'sin', etc. directly."
            )

        self._expression_str = expression

        locals_dict = {}
        locals_dict.update(self._ALLOWED_FUNCS)
        locals_dict.update(self._ALLOWED_CONSTANTS)

        try:
            self._expr = sp.sympify(expression, locals=locals_dict)
        except Exception as e:
            raise ValueError(f'Invalid expression: {expression}') from e

        # Extract symbols from the expression
        symbols = self._expr.free_symbols
        symbol_names = sorted(str(s) for s in symbols)

        if 'x' not in symbol_names:
            raise ValueError("Expression must contain 'x' as independent variable")

        # Reject unknown functions early so invalid expressions fail at init,
        # not later during numerical evaluation.
        allowed_function_names = set(self._ALLOWED_FUNCS) | {
            func.__name__ for func in self._ALLOWED_FUNCS.values()
        }

        # Walk all function-call nodes in the parsed expression (e.g. sin(x), foo(x)).
        # Keep only function names that are not in our allowlist.
        unknown_function_names: set[str] = set()
        function_atoms = self._expr.atoms(sp.Function)
        for function_atom in function_atoms:
            function_name = function_atom.func.__name__
            if function_name not in allowed_function_names:
                unknown_function_names.add(function_name)

        unknown_functions = sorted(unknown_function_names)

        if unknown_functions:
            raise ValueError(
                f'Unsupported function(s) in expression: {", ".join(unknown_functions)}'
            )

        # Create parameters
        if parameters is not None and not isinstance(parameters, dict):
            raise TypeError(
                f'Parameters must be None or a dictionary, got {type(parameters).__name__}'
            )

        if parameters is not None:
            for name, value in parameters.items():
                if not isinstance(value, Numeric):
                    raise TypeError(f"Parameter '{name}' must be numeric")
        parameters = parameters or {}
        self._parameters: dict[str, Parameter] = {}

        self._symbol_names = symbol_names
        for name in self._symbol_names:
            if name in self._RESERVED_NAMES:
                continue

            value = parameters.get(name, 1.0)

            self._parameters[name] = Parameter(
                name=name,
                value=value,
                unit=self._unit,
            )

        # Create numerical function
        ordered_symbols = [sp.Symbol(name) for name in self._symbol_names]

        self._func = sp.lambdify(
            ordered_symbols,
            self._expr,
            modules=['numpy'],
        )

        # -------------------------
        # Properties
        # -------------------------

    @property
    def expression(self) -> str:
        """Return the original expression string."""
        return self._expression_str

    @expression.setter
    def expression(self, _new_expr: str) -> None:
        """
        Prevent changing the expression after initialization.

        Parameters
        ----------
        _new_expr : str
            New expression string (ignored).

        Raises
        ------
        AttributeError
            Always raised to prevent changing the expression.
        """
        raise AttributeError('Expression cannot be changed after initialization')

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

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            Input values for the independent variable.

        Returns
        -------
        np.ndarray
            Evaluated results.
        """
        x = self._prepare_x_for_evaluate(x)

        args = []
        for name in self._symbol_names:
            if name == 'x':
                args.append(x)
            else:
                args.append(self._parameters[name].value)

        return self._func(*args)

    def get_all_variables(self) -> list[Parameter]:
        """
        Return all parameters.

        Returns
        -------
        list[Parameter]
            List of all parameters in the expression.
        """
        return list(self._parameters.values())

    def convert_unit(self, _new_unit: str | sc.Unit) -> None:
        """
        Convert the unit of the expression.

        Unit conversion is not implemented for ExpressionComponent.

        Parameters
        ----------
        _new_unit : str | sc.Unit
            The new unit to convert to (ignored).

        Raises
        ------
        NotImplementedError
            Always raised to indicate unit conversion is not supported.
        """

        raise NotImplementedError('Unit conversion is not implemented for ExpressionComponent')

    # -------------------------
    # dunder methods
    # -------------------------

    def __getattr__(self, name: str) -> Parameter:
        """
        Allow access to parameters as attributes.

        Parameters
        ----------
        name : str
            Name of the parameter to access.

        Raises
        ------
        AttributeError
            If the parameter does not exist.

        Returns
        -------
        Parameter
            The parameter with the given name.
        """
        if '_parameters' in self.__dict__ and name in self._parameters:
            return self._parameters[name]
        raise AttributeError(f"{self.__class__.__name__} has no attribute '{name}'")

    def __setattr__(self, name: str, value: Numeric) -> None:
        """
        Allow setting parameter values as attributes.

        Parameters
        ----------
        name : str
            Name of the parameter to set.
        value : Numeric
            New value for the parameter.

        Raises
        ------
        TypeError
            If the value is not numeric.
        """
        if '_parameters' in self.__dict__ and name in self._parameters:
            param = self._parameters[name]

            if not isinstance(value, Numeric):
                raise TypeError(f'{name} must be numeric')

            param.value = value
        else:
            # For other attributes, use default behavior
            super().__setattr__(name, value)

    def __dir__(self) -> list[str]:
        """
        Include parameter names in dir() output for better IDE support.

        Returns
        -------
        list[str]
            List of attribute names, including parameters.
        """
        return super().__dir__() + list(self._parameters.keys())

    def __repr__(self) -> str:
        """Repr function."""
        param_str = ', '.join(f'{k}={v.value}' for k, v in self._parameters.items())
        return (
            f'{self.__class__.__name__}(\n'
            f"  expr='{self._expression_str}',\n"
            f'  unit={self._unit},\n'
            f'  parameters={{ {param_str} }}\n'
            f')'
        )
__dir__()

Include parameter names in dir() output for better IDE support.

Returns:

Type Description
list[str]

List of attribute names, including parameters.

Source code in src/easydynamics/sample_model/components/expression_component.py
325
326
327
328
329
330
331
332
333
334
def __dir__(self) -> list[str]:
    """
    Include parameter names in dir() output for better IDE support.

    Returns
    -------
    list[str]
        List of attribute names, including parameters.
    """
    return super().__dir__() + list(self._parameters.keys())
__getattr__(name)

Allow access to parameters as attributes.

Parameters:

Name Type Description Default
name str

Name of the parameter to access.

required

Raises:

Type Description
AttributeError

If the parameter does not exist.

Returns:

Type Description
Parameter

The parameter with the given name.

Source code in src/easydynamics/sample_model/components/expression_component.py
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def __getattr__(self, name: str) -> Parameter:
    """
    Allow access to parameters as attributes.

    Parameters
    ----------
    name : str
        Name of the parameter to access.

    Raises
    ------
    AttributeError
        If the parameter does not exist.

    Returns
    -------
    Parameter
        The parameter with the given name.
    """
    if '_parameters' in self.__dict__ and name in self._parameters:
        return self._parameters[name]
    raise AttributeError(f"{self.__class__.__name__} has no attribute '{name}'")
__init__(expression, parameters=None, unit='meV', display_name='Expression', unique_name=None)

Initialize the ExpressionComponent.

Parameters:

Name Type Description Default
expression str

The symbolic expression as a string. Must contain 'x' as the independent variable.

required
parameters dict[str, Numeric] | None

Dictionary of parameter names and their initial values.

None
unit str | Unit

Unit of the output.

'meV'
display_name str | None

Display name for the component.

'Expression'
unique_name str | None

Unique name for the component.

None

Raises:

Type Description
ValueError

If the expression is invalid or does not contain 'x'.

TypeError

If any parameter value is not numeric.

Source code in src/easydynamics/sample_model/components/expression_component.py
 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
def __init__(
    self,
    expression: str,
    parameters: dict[str, Numeric] | None = None,
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'Expression',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the ExpressionComponent.

    Parameters
    ----------
    expression : str
        The symbolic expression as a string. Must contain 'x' as the independent variable.
    parameters : dict[str, Numeric] | None, default=None
        Dictionary of parameter names and their initial values.
    unit : str | sc.Unit, default='meV'
        Unit of the output.
    display_name : str | None, default='Expression'
        Display name for the component.
    unique_name : str | None, default=None
        Unique name for the component.

    Raises
    ------
    ValueError
        If the expression is invalid or does not contain 'x'.
    TypeError
        If any parameter value is not numeric.
    """
    super().__init__(unit=unit, display_name=display_name, unique_name=unique_name)

    if 'np.' in expression:
        raise ValueError(
            'NumPy syntax (np.*) is not supported. '
            "Use functions like 'exp', 'sin', etc. directly."
        )

    self._expression_str = expression

    locals_dict = {}
    locals_dict.update(self._ALLOWED_FUNCS)
    locals_dict.update(self._ALLOWED_CONSTANTS)

    try:
        self._expr = sp.sympify(expression, locals=locals_dict)
    except Exception as e:
        raise ValueError(f'Invalid expression: {expression}') from e

    # Extract symbols from the expression
    symbols = self._expr.free_symbols
    symbol_names = sorted(str(s) for s in symbols)

    if 'x' not in symbol_names:
        raise ValueError("Expression must contain 'x' as independent variable")

    # Reject unknown functions early so invalid expressions fail at init,
    # not later during numerical evaluation.
    allowed_function_names = set(self._ALLOWED_FUNCS) | {
        func.__name__ for func in self._ALLOWED_FUNCS.values()
    }

    # Walk all function-call nodes in the parsed expression (e.g. sin(x), foo(x)).
    # Keep only function names that are not in our allowlist.
    unknown_function_names: set[str] = set()
    function_atoms = self._expr.atoms(sp.Function)
    for function_atom in function_atoms:
        function_name = function_atom.func.__name__
        if function_name not in allowed_function_names:
            unknown_function_names.add(function_name)

    unknown_functions = sorted(unknown_function_names)

    if unknown_functions:
        raise ValueError(
            f'Unsupported function(s) in expression: {", ".join(unknown_functions)}'
        )

    # Create parameters
    if parameters is not None and not isinstance(parameters, dict):
        raise TypeError(
            f'Parameters must be None or a dictionary, got {type(parameters).__name__}'
        )

    if parameters is not None:
        for name, value in parameters.items():
            if not isinstance(value, Numeric):
                raise TypeError(f"Parameter '{name}' must be numeric")
    parameters = parameters or {}
    self._parameters: dict[str, Parameter] = {}

    self._symbol_names = symbol_names
    for name in self._symbol_names:
        if name in self._RESERVED_NAMES:
            continue

        value = parameters.get(name, 1.0)

        self._parameters[name] = Parameter(
            name=name,
            value=value,
            unit=self._unit,
        )

    # Create numerical function
    ordered_symbols = [sp.Symbol(name) for name in self._symbol_names]

    self._func = sp.lambdify(
        ordered_symbols,
        self._expr,
        modules=['numpy'],
    )
__repr__()

Repr function.

Source code in src/easydynamics/sample_model/components/expression_component.py
336
337
338
339
340
341
342
343
344
345
def __repr__(self) -> str:
    """Repr function."""
    param_str = ', '.join(f'{k}={v.value}' for k, v in self._parameters.items())
    return (
        f'{self.__class__.__name__}(\n'
        f"  expr='{self._expression_str}',\n"
        f'  unit={self._unit},\n'
        f'  parameters={{ {param_str} }}\n'
        f')'
    )
__setattr__(name, value)

Allow setting parameter values as attributes.

Parameters:

Name Type Description Default
name str

Name of the parameter to set.

required
value Numeric

New value for the parameter.

required

Raises:

Type Description
TypeError

If the value is not numeric.

Source code in src/easydynamics/sample_model/components/expression_component.py
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
def __setattr__(self, name: str, value: Numeric) -> None:
    """
    Allow setting parameter values as attributes.

    Parameters
    ----------
    name : str
        Name of the parameter to set.
    value : Numeric
        New value for the parameter.

    Raises
    ------
    TypeError
        If the value is not numeric.
    """
    if '_parameters' in self.__dict__ and name in self._parameters:
        param = self._parameters[name]

        if not isinstance(value, Numeric):
            raise TypeError(f'{name} must be numeric')

        param.value = value
    else:
        # For other attributes, use default behavior
        super().__setattr__(name, value)
convert_unit(_new_unit)

Convert the unit of the expression.

Unit conversion is not implemented for ExpressionComponent.

Parameters:

Name Type Description Default
_new_unit str | Unit

The new unit to convert to (ignored).

required

Raises:

Type Description
NotImplementedError

Always raised to indicate unit conversion is not supported.

Source code in src/easydynamics/sample_model/components/expression_component.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def convert_unit(self, _new_unit: str | sc.Unit) -> None:
    """
    Convert the unit of the expression.

    Unit conversion is not implemented for ExpressionComponent.

    Parameters
    ----------
    _new_unit : str | sc.Unit
        The new unit to convert to (ignored).

    Raises
    ------
    NotImplementedError
        Always raised to indicate unit conversion is not supported.
    """

    raise NotImplementedError('Unit conversion is not implemented for ExpressionComponent')
evaluate(x)

Evaluate the expression for given x values.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

Input values for the independent variable.

required

Returns:

Type Description
ndarray

Evaluated results.

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

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        Input values for the independent variable.

    Returns
    -------
    np.ndarray
        Evaluated results.
    """
    x = self._prepare_x_for_evaluate(x)

    args = []
    for name in self._symbol_names:
        if name == 'x':
            args.append(x)
        else:
            args.append(self._parameters[name].value)

    return self._func(*args)
expression property writable

Return the original expression string.

get_all_variables()

Return all parameters.

Returns:

Type Description
list[Parameter]

List of all parameters in the expression.

Source code in src/easydynamics/sample_model/components/expression_component.py
241
242
243
244
245
246
247
248
249
250
def get_all_variables(self) -> list[Parameter]:
    """
    Return all parameters.

    Returns
    -------
    list[Parameter]
        List of all parameters in the expression.
    """
    return list(self._parameters.values())

gaussian

Gaussian

Bases: CreateParametersMixin, ModelComponent

Model of a Gaussian function.

The intensity is given by

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

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

If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Source code in src/easydynamics/sample_model/components/gaussian.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
class Gaussian(CreateParametersMixin, ModelComponent):
    r"""
    Model of a Gaussian function.

     The intensity is given by

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

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

    If the center is not provided, it will be centered at 0 and fixed, which is typically what you
    want in QENS.
    """

    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,
    ) -> None:
        """
        Initialize the Gaussian component.

        Parameters
        ----------
        area : Numeric | Parameter, default=1.0
            Area of the Gaussian.
        center : Numeric | Parameter | None, default=None
            Center of the Gaussian. If None.
        width : Numeric | Parameter, default=1.0
            Standard deviation.
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='Gaussian'
            Name of the component.
        unique_name : str | None, default=None
            Unique name of the component. if None, a unique_name is automatically generated. By
            default, 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.

        Returns
        -------
        Parameter
            The area parameter.
        """

        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """
        Set the value of the area parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the area parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """

        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.

        Returns
        -------
        Parameter
            The center parameter.
        """

        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """
        Set the center parameter value.

        Parameters
        ----------
        value : Numeric | None
            The new value for the center parameter. If None, defaults to 0 and is fixed.

        Raises
        ------
        TypeError
            If the value is not a number or None.
        """

        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 (standard deviation).

        Returns
        -------
        Parameter
            The width parameter.
        """
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """
        Set the width parameter value.

        Parameters
        ----------
        value : Numeric
            The new value for the width parameter.

        Raises
        ------
        TypeError
            If the value is not a number or None.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')

        if float(value) <= 0:
            raise ValueError('width must be positive')

        self._width.value = value

    def evaluate(
        self,
        x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray,
    ) -> np.ndarray:
        r"""
        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
        intensity is given by $$ I(x) = \frac{A}{\sigma \sqrt{2\pi}} \exp\left( -\frac{1}{2}
        \left(\frac{x - x_0}{\sigma}\right)^2 \right) $$

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

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Gaussian.

        Returns
        -------
        np.ndarray
            The intensity of the Gaussian at the given x values.
        """

        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) -> str:
        """
        Return a string representation of the Gaussian.

        Returns
        -------
        str
            A string representation of the Gaussian.
        """

        return (
            f'Gaussian(unique_name = {self.unique_name}, unit = {self._unit},\n'
            f'area = {self.area},\n center = {self.center},\n width = {self.width})'
        )
__init__(area=1.0, center=None, width=1.0, unit='meV', display_name='Gaussian', unique_name=None)

Initialize the Gaussian component.

Parameters:

Name Type Description Default
area Numeric | Parameter

Area of the Gaussian.

1.0
center Numeric | Parameter | None

Center of the Gaussian. If None.

None
width Numeric | Parameter

Standard deviation.

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Name of the component.

'Gaussian'
unique_name str | None

Unique name of the component. if None, a unique_name is automatically generated. By default, None.

None
Source code in src/easydynamics/sample_model/components/gaussian.py
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
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,
) -> None:
    """
    Initialize the Gaussian component.

    Parameters
    ----------
    area : Numeric | Parameter, default=1.0
        Area of the Gaussian.
    center : Numeric | Parameter | None, default=None
        Center of the Gaussian. If None.
    width : Numeric | Parameter, default=1.0
        Standard deviation.
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='Gaussian'
        Name of the component.
    unique_name : str | None, default=None
        Unique name of the component. if None, a unique_name is automatically generated. By
        default, 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
__repr__()

Return a string representation of the Gaussian.

Returns:

Type Description
str

A string representation of the Gaussian.

Source code in src/easydynamics/sample_model/components/gaussian.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
def __repr__(self) -> str:
    """
    Return a string representation of the Gaussian.

    Returns
    -------
    str
        A string representation of the Gaussian.
    """

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

Get the area parameter.

Returns:

Type Description
Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

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 intensity is given by $$ I(x) = \frac{A}{\sigma \sqrt{2\pi}} \exp\left( -\frac{1}{2} \left(\frac{x - x_0}{\sigma}\right)^2 \right) $$

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

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Gaussian.

required

Returns:

Type Description
ndarray

The intensity of the Gaussian at the given x values.

Source code in src/easydynamics/sample_model/components/gaussian.py
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
def evaluate(
    self,
    x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray,
) -> np.ndarray:
    r"""
    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
    intensity is given by $$ I(x) = \frac{A}{\sigma \sqrt{2\pi}} \exp\left( -\frac{1}{2}
    \left(\frac{x - x_0}{\sigma}\right)^2 \right) $$

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

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Gaussian.

    Returns
    -------
    np.ndarray
        The intensity of the Gaussian at the given x values.
    """

    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 (standard deviation).

Returns:

Type Description
Parameter

The width parameter.

lorentzian

Lorentzian

Bases: CreateParametersMixin, ModelComponent

Model of a Lorentzian function.

The intensity is given by $$ I(x) = \frac{A}{\pi} \frac{\Gamma}{(x - x_0)^2 + \Gamma^2}, $$ where \(A\) is the area, \(x_0\) is the center, and \(\Gamma\) is the half width at half maximum (HWHM).

If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS.

Source code in src/easydynamics/sample_model/components/lorentzian.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
class Lorentzian(CreateParametersMixin, ModelComponent):
    r"""
    Model of a Lorentzian function.

    The intensity is given by $$ I(x) = \frac{A}{\pi} \frac{\Gamma}{(x - x_0)^2 + \Gamma^2}, $$
    where $A$ is the area, $x_0$ is the center, and $\Gamma$ is the half width at half maximum
    (HWHM).

    If the center is not provided, it will be centered at 0 and fixed, which is typically what you
    want in QENS.
    """

    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,
    ) -> None:
        """
        Initialize the Lorentzian component.

        Parameters
        ----------
        area : Numeric | Parameter, default=1.0
            Area of the Lorentzian.
        center : Numeric | Parameter | None, default=None
            Center of the Lorentzian. If None.
        width : Numeric | Parameter, default=1.0
            Half width at half maximum (HWHM).
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='Lorentzian'
            Name of the component.
        unique_name : str | None, default=None
            Unique name of the component. If None, a unique_name is automatically generated. By
            default, 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.

        Returns
        -------
        Parameter
            The area parameter.
        """
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """
        Set the value of the area parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the area parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """
        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.

        Returns
        -------
        Parameter
            The center parameter.
        """
        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """
        Set the value of the center parameter.

        Parameters
        ----------
        value : Numeric | None
            The new value for the center parameter. If None, defaults to 0 and is fixed.

        Raises
        ------
        TypeError
            If the value is not a number or None.
        """

        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 (HWHM).

        Returns
        -------
        Parameter
            The width parameter.
        """
        return self._width

    @width.setter
    def width(self, value: Numeric) -> None:
        """
        Set the width parameter value (HWHM).

        Parameters
        ----------
        value : Numeric
            The new value for the width parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('width must be a number')

        if float(value) <= 0:
            raise ValueError('width must be positive')
        self._width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        r"""
        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
        intensity is given by

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

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

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Lorentzian.

        Returns
        -------
        np.ndarray
            The intensity of the Lorentzian at the given x values.
        """

        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) -> str:
        """
        Return a string representation of the Lorentzian.

        Returns
        -------
        str
            A string representation of the Lorentzian.
        """
        return (
            f'Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n'
            f'area = {self.area},\n center = {self.center},\n width = {self.width})'
        )
__init__(area=1.0, center=None, width=1.0, unit='meV', display_name='Lorentzian', unique_name=None)

Initialize the Lorentzian component.

Parameters:

Name Type Description Default
area Numeric | Parameter

Area of the Lorentzian.

1.0
center Numeric | Parameter | None

Center of the Lorentzian. If None.

None
width Numeric | Parameter

Half width at half maximum (HWHM).

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Name of the component.

'Lorentzian'
unique_name str | None

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

None
Source code in src/easydynamics/sample_model/components/lorentzian.py
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
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,
) -> None:
    """
    Initialize the Lorentzian component.

    Parameters
    ----------
    area : Numeric | Parameter, default=1.0
        Area of the Lorentzian.
    center : Numeric | Parameter | None, default=None
        Center of the Lorentzian. If None.
    width : Numeric | Parameter, default=1.0
        Half width at half maximum (HWHM).
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='Lorentzian'
        Name of the component.
    unique_name : str | None, default=None
        Unique name of the component. If None, a unique_name is automatically generated. By
        default, 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
__repr__()

Return a string representation of the Lorentzian.

Returns:

Type Description
str

A string representation of the Lorentzian.

Source code in src/easydynamics/sample_model/components/lorentzian.py
209
210
211
212
213
214
215
216
217
218
219
220
221
def __repr__(self) -> str:
    """
    Return a string representation of the Lorentzian.

    Returns
    -------
    str
        A string representation of the Lorentzian.
    """
    return (
        f'Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n'
        f'area = {self.area},\n center = {self.center},\n width = {self.width})'
    )
area property writable

Get the area parameter.

Returns:

Type Description
Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

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 intensity is given by

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

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

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Lorentzian.

required

Returns:

Type Description
ndarray

The intensity of the Lorentzian at the given x values.

Source code in src/easydynamics/sample_model/components/lorentzian.py
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
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    r"""
    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
    intensity is given by

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

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

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Lorentzian.

    Returns
    -------
    np.ndarray
        The intensity of the Lorentzian at the given x values.
    """

    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 (HWHM).

Returns:

Type Description
Parameter

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
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
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.

        Parameters
        ----------
        area : Numeric | Parameter
            The area value or Parameter.
        name : str
            The name of the model component.
        unit : str | sc.Unit, default='meV'
            The unit of the area Parameter.
        minimum_area : float, default=MINIMUM_AREA
            The minimum allowed area.

        Raises
        ------
        TypeError
            If area is not a number or a Parameter.
        ValueError
            If area is not a finite number or if the area Parameter has a non-finite value.

        Returns
        -------
        Parameter
            The validated area Parameter.
        """
        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.',
                UserWarning,
                stacklevel=3,
            )
        else:
            if area.min < minimum_area:
                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.

        Parameters
        ----------
        center : Numeric | Parameter | 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 | sc.Unit, default='meV'
            The unit of the center Parameter.
        enforce_minimum_center : bool, default=False
            Whether to enforce a minimum center value to avoid zero center in DHO.

        Raises
        ------
        TypeError
            If center is not None, a number, or a Parameter.
        ValueError
            If center is a number but not finite, or if center is a Parameter but has a non-finite
            value.

        Returns
        -------
        Parameter
            The validated center 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 and center.min < DHO_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.

        Parameters
        ----------
        width : Numeric | Parameter
            The width value or Parameter.
        name : str
            The name of the model component.
        param_name : str, default='width'
            The name of the width parameter.
        unit : str | sc.Unit, default='meV'
            The unit of the width Parameter.
        minimum_width : float, default=MINIMUM_WIDTH
            The minimum allowed width.

        Raises
        ------
        TypeError
            If width is not a number or a Parameter.
        ValueError
            If width is non-positive.

        Returns
        -------
        Parameter
            The validated width Parameter.
        """
        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.'
                )
            if width.min < minimum_width:
                width.min = minimum_width

        return width

model_component

ModelComponent

Bases: EasyDynamicsModelBase

Abstract base class for all model components.

Source code in src/easydynamics/sample_model/components/model_component.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
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
class ModelComponent(EasyDynamicsModelBase):
    """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,
    ) -> None:
        """
        Initialize the ModelComponent.

        Parameters
        ----------
        unit : str | sc.Unit, default='meV'
            The unit of the model component.
        display_name : str | None, default=None
            A human-readable name for the component.
        unique_name : str | None, default=None
            A unique identifier for the component.
        """
        super().__init__(unit=unit, display_name=display_name, unique_name=unique_name)
        self._unit = unit

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

        Returns
        -------
        str
            The unit of the model component.
        """
        return str(self._unit)

    @unit.setter
    def unit(self, _unit_str: str) -> None:
        """
        Unit is read-only.

        Use convert_unit to change the unit between allowed types or create a new ModelComponent
        with the desired unit.

        Parameters
        ----------
        _unit_str : str
            The new unit to set.

        Raises
        ------
        AttributeError
            Always raised since unit is read-only.
        """
        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.'
        )

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

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

    def free_all_parameters(self) -> None:
        """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 | np.ndarray | sc.Variable | sc.DataArray
    ) -> np.ndarray:
        """
        Prepare the input x for evaluation by handling units and converting to a numpy array.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The input data to prepare.

        Raises
        ------
        ValueError
            If x contains NaN or infinite values, or if a sc.DataArray has more than one
            coordinate.
        UnitError
            If x has incompatible units that cannot be converted to the component's unit.

        Returns
        -------
        np.ndarray
            The prepared input data as 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_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.
            x_in = x.value if x.sizes == {} else 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}.',
                    UserWarning,
                    stacklevel=3,
                )
        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)

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

        Parameters
        ----------
        unit : str | sc.Unit
            The new unit to convert to.

        Raises
        ------
        TypeError
            If the provided unit is not a str or sc.Unit.
        Exception
            If the provided unit is invalid or incompatible with the component's parameters.
        """
        if not isinstance(unit, (str, sc.Unit)):
            raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}')

        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 | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        """
        Abstract method to evaluate the model component at input x.

        Must be implemented by subclasses.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the component.

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

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

        Returns
        -------
        str
            A string representation of the ModelComponent.
        """

        return f'{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit})'
__init__(unit='meV', display_name=None, unique_name=None)

Initialize the ModelComponent.

Parameters:

Name Type Description Default
unit str | Unit

The unit of the model component.

'meV'
display_name str | None

A human-readable name for the component.

None
unique_name str | None

A unique identifier for the component.

None
Source code in src/easydynamics/sample_model/components/model_component.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def __init__(
    self,
    unit: str | sc.Unit = 'meV',
    display_name: str | None = None,
    unique_name: str | None = None,
) -> None:
    """
    Initialize the ModelComponent.

    Parameters
    ----------
    unit : str | sc.Unit, default='meV'
        The unit of the model component.
    display_name : str | None, default=None
        A human-readable name for the component.
    unique_name : str | None, default=None
        A unique identifier for the component.
    """
    super().__init__(unit=unit, display_name=display_name, unique_name=unique_name)
    self._unit = unit
__repr__()

Return a string representation of the ModelComponent.

Returns:

Type Description
str

A string representation of the ModelComponent.

Source code in src/easydynamics/sample_model/components/model_component.py
218
219
220
221
222
223
224
225
226
227
228
def __repr__(self) -> str:
    """
    Return a string representation of the ModelComponent.

    Returns
    -------
    str
        A string representation of the ModelComponent.
    """

    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 | Unit

The new unit to convert to.

required

Raises:

Type Description
TypeError

If the provided unit is not a str or sc.Unit.

Exception

If the provided unit is invalid or incompatible with the component's parameters.

Source code in src/easydynamics/sample_model/components/model_component.py
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
def convert_unit(self, unit: str | sc.Unit) -> None:
    """
    Convert the unit of the Parameters in the component.

    Parameters
    ----------
    unit : str | sc.Unit
        The new unit to convert to.

    Raises
    ------
    TypeError
        If the provided unit is not a str or sc.Unit.
    Exception
        If the provided unit is invalid or incompatible with the component's parameters.
    """
    if not isinstance(unit, (str, sc.Unit)):
        raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}')

    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

Abstract method to evaluate the model component at input x.

Must be implemented by subclasses.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the component.

required

Returns:

Type Description
ndarray

Evaluated function values.

Source code in src/easydynamics/sample_model/components/model_component.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
@abstractmethod
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """
    Abstract method to evaluate the model component at input x.

    Must be implemented by subclasses.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the component.

    Returns
    -------
    np.ndarray
        Evaluated function values.
    """
fix_all_parameters()

Fix all parameters in the model component.

Source code in src/easydynamics/sample_model/components/model_component.py
76
77
78
79
80
81
def fix_all_parameters(self) -> None:
    """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
83
84
85
86
def free_all_parameters(self) -> None:
    """Free all parameters in the model component."""
    for p in self.get_fittable_parameters():
        p.fixed = False
unit property writable

Get the unit.

Returns:

Type Description
str

The unit of the model component.

polynomial

Polynomial

Bases: ModelComponent

Polynomial function component.

The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N, $$ where \(C_i\) are the coefficients.

Source code in src/easydynamics/sample_model/components/polynomial.py
 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
class Polynomial(ModelComponent):
    r"""
    Polynomial function component.

    The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N, $$ where $C_i$ are
    the coefficients.
    """

    def __init__(
        self,
        coefficients: Sequence[Numeric | Parameter] = (0.0,),
        unit: str | sc.Unit = 'meV',
        display_name: str | None = 'Polynomial',
        unique_name: str | None = None,
    ) -> None:
        """
        Initialize the Polynomial component.

        Parameters
        ----------
        coefficients : Sequence[Numeric | Parameter], default=(0.0,)
            Coefficients c0, c1, ..., cN.
        unit : str | sc.Unit, default='meV'
            Unit of the Polynomial component.
        display_name : str | None, default='Polynomial'
            Display name of the Polynomial component.
        unique_name : str | None, default=None
            Unique name of the component. If None, a unique_name is automatically generated. By
            default, None.

        Raises
        ------
        TypeError
            If coefficients is not a sequence of numbers or Parameters or if any item in
            coefficients is not a number or Parameter.
        ValueError
            If coefficients is an empty sequence.
        """

        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.

        Returns
        -------
        list[Parameter]
            The coefficients of the polynomial.
        """
        return list(self._coefficients)

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

        Length must match current number of coefficients.

        Parameters
        ----------
        coeffs : Sequence[Numeric | Parameter]
            New coefficients as a sequence of numbers or Parameters.

        Raises
        ------
        TypeError
            If coeffs is not a sequence of numbers or Parameters or if any item in coeffs is not a
            number or Parameter.
        ValueError
            If the length of coeffs does not match the existing 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.

        Returns
        -------
        list[float]
            The coefficient values of the polynomial.
        """
        return [param.value for param in self._coefficients]

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

        The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ...
        + c_N x^N, $$ where $C_i$ are the coefficients.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Polynomial.

        Returns
        -------
        np.ndarray
            The evaluated Polynomial at the given x values.
        """

        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,
                stacklevel=2,
            )
        return result

    @property
    def degree(self) -> int:
        """
        Get the degree of the polynomial.

        Returns
        -------
        int
            The degree of the polynomial.
        """
        return len(self._coefficients) - 1

    @degree.setter
    def degree(self, _value: int) -> None:
        """
        The degree is determined by the number of coefficients and cannot be set directly.

        Parameters
        ----------
        _value : int
            The new degree of the polynomial.

        Raises
        ------
        AttributeError
            Always raised since degree cannot be set directly.
        """
        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 variables from the model component.

        Returns
        -------
        list[DescriptorBase]
            List of variables in the component.
        """
        return list(self._coefficients)

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

        Parameters
        ----------
        unit : str | sc.Unit
            The target unit to convert to.

        Raises
        ------
        UnitError
            If the provided unit is not a string or sc.Unit.
        """

        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:
        """
        Return a string representation of the Polynomial.

        Returns
        -------
        str
            A string representation of the Polynomial.
        """

        coeffs_str = ', '.join(f'{param.name}={param.value}' for param in self._coefficients)
        return (
            f'Polynomial(unique_name = {self.unique_name}, '
            f'unit = {self._unit},\n coefficients = [{coeffs_str}])'
        )
__init__(coefficients=(0.0,), unit='meV', display_name='Polynomial', unique_name=None)

Initialize the Polynomial component.

Parameters:

Name Type Description Default
coefficients Sequence[Numeric | Parameter]

Coefficients c0, c1, ..., cN.

(0.0,)
unit str | Unit

Unit of the Polynomial component.

'meV'
display_name str | None

Display name of the Polynomial component.

'Polynomial'
unique_name str | None

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

None

Raises:

Type Description
TypeError

If coefficients is not a sequence of numbers or Parameters or if any item in coefficients is not a number or Parameter.

ValueError

If coefficients is an empty sequence.

Source code in src/easydynamics/sample_model/components/polynomial.py
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
def __init__(
    self,
    coefficients: Sequence[Numeric | Parameter] = (0.0,),
    unit: str | sc.Unit = 'meV',
    display_name: str | None = 'Polynomial',
    unique_name: str | None = None,
) -> None:
    """
    Initialize the Polynomial component.

    Parameters
    ----------
    coefficients : Sequence[Numeric | Parameter], default=(0.0,)
        Coefficients c0, c1, ..., cN.
    unit : str | sc.Unit, default='meV'
        Unit of the Polynomial component.
    display_name : str | None, default='Polynomial'
        Display name of the Polynomial component.
    unique_name : str | None, default=None
        Unique name of the component. If None, a unique_name is automatically generated. By
        default, None.

    Raises
    ------
    TypeError
        If coefficients is not a sequence of numbers or Parameters or if any item in
        coefficients is not a number or Parameter.
    ValueError
        If coefficients is an empty sequence.
    """

    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)
__repr__()

Return a string representation of the Polynomial.

Returns:

Type Description
str

A string representation of the Polynomial.

Source code in src/easydynamics/sample_model/components/polynomial.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def __repr__(self) -> str:
    """
    Return a string representation of the Polynomial.

    Returns
    -------
    str
        A string representation of the Polynomial.
    """

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

Get the coefficients of the polynomial as a list.

Returns:

Type Description
list[float]

The coefficient values of the polynomial.

Source code in src/easydynamics/sample_model/components/polynomial.py
138
139
140
141
142
143
144
145
146
147
def coefficient_values(self) -> list[float]:
    """
    Get the coefficients of the polynomial as a list.

    Returns
    -------
    list[float]
        The coefficient values of the polynomial.
    """
    return [param.value for param in self._coefficients]
coefficients property writable

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

Returns:

Type Description
list[Parameter]

The coefficients of the polynomial.

convert_unit(unit)

Convert the unit of the polynomial.

Parameters:

Name Type Description Default
unit str | Unit

The target unit to convert to.

required

Raises:

Type Description
UnitError

If the provided unit is not a string or sc.Unit.

Source code in src/easydynamics/sample_model/components/polynomial.py
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
def convert_unit(self, unit: str | sc.Unit) -> None:
    """
    Convert the unit of the polynomial.

    Parameters
    ----------
    unit : str | sc.Unit
        The target unit to convert to.

    Raises
    ------
    UnitError
        If the provided unit is not a string or sc.Unit.
    """

    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

Get the degree of the polynomial.

Returns:

Type Description
int

The degree of the polynomial.

evaluate(x)

Evaluate the Polynomial at the given x values.

The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N, $$ where \(C_i\) are the coefficients.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Polynomial.

required

Returns:

Type Description
ndarray

The evaluated Polynomial at the given x values.

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

    The intensity is given by $$ I(x) = c_0 + c_1 x + c_2 x^2 + ...
    + c_N x^N, $$ where $C_i$ are the coefficients.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Polynomial.

    Returns
    -------
    np.ndarray
        The evaluated Polynomial at the given x values.
    """

    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,
            stacklevel=2,
        )
    return result
get_all_variables()

Get all variables from the model component.

Returns:

Type Description
list[DescriptorBase]

List of variables in the component.

Source code in src/easydynamics/sample_model/components/polynomial.py
214
215
216
217
218
219
220
221
222
223
def get_all_variables(self) -> list[DescriptorBase]:
    """
    Get all variables from the model component.

    Returns
    -------
    list[DescriptorBase]
        List of variables 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.

Use scipy.special.voigt_profile to evaluate the Voigt profile.

Source code in src/easydynamics/sample_model/components/voigt.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
class Voigt(CreateParametersMixin, ModelComponent):
    r"""
    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.

    Use scipy.special.voigt_profile to evaluate the Voigt profile.
    """

    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,
    ) -> None:
        """
        Initialize a Voigt component.

        Parameters
        ----------
        area : Numeric | Parameter, default=1.0
            Total area under the curve.
        center : Numeric | Parameter | None, default=None
            Center of the Voigt profile.
        gaussian_width : Numeric | Parameter, default=1.0
            Standard deviation of the Gaussian part.
        lorentzian_width : Numeric | Parameter, default=1.0
            Half width at half max (HWHM) of the Lorentzian part.
        unit : str | sc.Unit, default='meV'
            Unit of the parameters.
        display_name : str | None, default='Voigt'
            Display name of the component.
        unique_name : str | None, default=None
            Unique name of the component. If None, a unique_name is automatically generated. By
            default, 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.

        Returns
        -------
        Parameter
            The area parameter.
        """
        return self._area

    @area.setter
    def area(self, value: Numeric) -> None:
        """
        Set the value of the area parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the area parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        """
        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.

        Returns
        -------
        Parameter
            The center parameter.
        """
        return self._center

    @center.setter
    def center(self, value: Numeric | None) -> None:
        """
        Set the value of the center parameter.

        Parameters
        ----------
        value : Numeric | None
            The new value for the center parameter. If None, defaults to 0 and is fixed.

        Raises
        ------
        TypeError
            If the value is not a number.
        """
        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 Gaussian width parameter.

        Returns
        -------
        Parameter
            The Gaussian width parameter.
        """
        return self._gaussian_width

    @gaussian_width.setter
    def gaussian_width(self, value: Numeric) -> None:
        """
        Set the width parameter value.

        Parameters
        ----------
        value : Numeric
            The new value for the width parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('gaussian_width must be a number')
        if float(value) <= 0:
            raise ValueError('gaussian_width must be positive')
        self._gaussian_width.value = value

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

        Returns
        -------
        Parameter
            The Lorentzian width parameter.
        """
        return self._lorentzian_width

    @lorentzian_width.setter
    def lorentzian_width(self, value: Numeric) -> None:
        """
        Set the value of the Lorentzian width parameter.

        Parameters
        ----------
        value : Numeric
            The new value for the Lorentzian width parameter.

        Raises
        ------
        TypeError
            If the value is not a number.
        ValueError
            If the value is not positive.
        """
        if not isinstance(value, Numeric):
            raise TypeError('lorentzian_width must be a number')
        if float(value) <= 0:
            raise ValueError('lorentzian_width must be positive')
        self._lorentzian_width.value = value

    def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
        r"""
        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.

        Parameters
        ----------
        x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values at which to evaluate the Voigt.

        Returns
        -------
        np.ndarray
            The intensity of the Voigt at the given x values.
        """

        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) -> str:
        """
        Return a string representation of the Voigt.

        Returns
        -------
        str
            A string representation of the Voigt.
        """

        return (
            f'Voigt(unique_name = {self.unique_name}, unit = {self._unit},\n'
            f'area = {self.area},\n'
            f'center = {self.center},\n'
            f'gaussian_width = {self.gaussian_width},\n'
            f'lorentzian_width = {self.lorentzian_width})'
        )
__init__(area=1.0, center=None, gaussian_width=1.0, lorentzian_width=1.0, unit='meV', display_name='Voigt', unique_name=None)

Initialize a Voigt component.

Parameters:

Name Type Description Default
area Numeric | Parameter

Total area under the curve.

1.0
center Numeric | Parameter | None

Center of the Voigt profile.

None
gaussian_width Numeric | Parameter

Standard deviation of the Gaussian part.

1.0
lorentzian_width Numeric | Parameter

Half width at half max (HWHM) of the Lorentzian part.

1.0
unit str | Unit

Unit of the parameters.

'meV'
display_name str | None

Display name of the component.

'Voigt'
unique_name str | None

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

None
Source code in src/easydynamics/sample_model/components/voigt.py
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
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,
) -> None:
    """
    Initialize a Voigt component.

    Parameters
    ----------
    area : Numeric | Parameter, default=1.0
        Total area under the curve.
    center : Numeric | Parameter | None, default=None
        Center of the Voigt profile.
    gaussian_width : Numeric | Parameter, default=1.0
        Standard deviation of the Gaussian part.
    lorentzian_width : Numeric | Parameter, default=1.0
        Half width at half max (HWHM) of the Lorentzian part.
    unit : str | sc.Unit, default='meV'
        Unit of the parameters.
    display_name : str | None, default='Voigt'
        Display name of the component.
    unique_name : str | None, default=None
        Unique name of the component. If None, a unique_name is automatically generated. By
        default, 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
__repr__()

Return a string representation of the Voigt.

Returns:

Type Description
str

A string representation of the Voigt.

Source code in src/easydynamics/sample_model/components/voigt.py
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def __repr__(self) -> str:
    """
    Return a string representation of the Voigt.

    Returns
    -------
    str
        A string representation of the Voigt.
    """

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

Get the area parameter.

Returns:

Type Description
Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Type Description
Parameter

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.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values at which to evaluate the Voigt.

required

Returns:

Type Description
ndarray

The intensity of the Voigt at the given x values.

Source code in src/easydynamics/sample_model/components/voigt.py
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
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    r"""
    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.

    Parameters
    ----------
    x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values at which to evaluate the Voigt.

    Returns
    -------
    np.ndarray
        The intensity of the Voigt at the given x values.
    """

    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 Gaussian width parameter.

Returns:

Type Description
Parameter

The Gaussian width parameter.

lorentzian_width property writable

Get the Lorentzian width parameter (HWHM).

Returns:

Type Description
Parameter

The Lorentzian 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 \(D Q^2\), where \(D\) is the diffusion coefficient. The area of the Lorentzians is given by the scale parameter multiplied by the QISF, which is 1 for this model. The EISF is 0 for this model, so there is no delta function component. Q is assumed to have units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given Q-values.

Example: >>>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 tutorials.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
class BrownianTranslationalDiffusion(DiffusionModelBase):
    r"""
    Model of Brownian translational diffusion, consisting of a Lorentzian function for each
    Q-value, where the width is given by $D Q^2$, where $D$ is the diffusion coefficient. The area
    of the Lorentzians is given by the scale parameter multiplied by the QISF, which is 1 for this
    model. The EISF is 0 for this model, so there is no delta function component. Q is assumed to
    have units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given
    Q-values.

    Example: >>>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
    tutorials.
    """

    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,
    ) -> None:
        """
        Initialize a new BrownianTranslationalDiffusion model.

        Parameters
        ----------
        display_name : str | None, default='BrownianTranslationalDiffusion'
            Display name of the diffusion model.
        unique_name : str | None, default=None
            Unique name of the diffusion model. If None, a unique name will be generated. By
            default, None.
        unit : str | sc.Unit, default='meV'
            Unit of the diffusion model. Must be convertible to meV.
        scale : Numeric, default=1.0
            Scale factor for the diffusion model. Must be a non-negative number.
        diffusion_coefficient : Numeric, default=1.0
            Diffusion coefficient D in m^2/s.

        Raises
        ------
        TypeError
            If scale or diffusion_coefficient is not a number.
        """
        if not isinstance(scale, Numeric):
            raise TypeError('scale must be a number.')

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

        diffusion_coefficient = Parameter(
            name='diffusion_coefficient',
            value=float(diffusion_coefficient),
            fixed=False,
            unit='m**2/s',
            min=0.0,
        )
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
            unit=unit,
            scale=scale,
        )
        self._hbar = hbar
        self._angstrom = angstrom
        self._diffusion_coefficient = diffusion_coefficient

    # ------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------

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

        Returns
        -------
        Parameter
            Diffusion coefficient D in m^2/s.
        """
        return self._diffusion_coefficient

    @diffusion_coefficient.setter
    def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None:
        """
        Set the diffusion coefficient parameter D.

        Parameters
        ----------
        diffusion_coefficient : Numeric
            The new value for the diffusion coefficient D in m^2/s.

        Raises
        ------
        TypeError
            If diffusion_coefficient is not a number.
        ValueError
            If diffusion_coefficient is negative.
        """
        if not isinstance(diffusion_coefficient, Numeric):
            raise TypeError('diffusion_coefficient must be a number.')

        if float(diffusion_coefficient) < 0:
            raise ValueError('diffusion_coefficient must be non-negative.')
        self._diffusion_coefficient.value = float(diffusion_coefficient)

    # ------------------------------------------------------------------
    # Other methods
    # ------------------------------------------------------------------

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

        Parameters
        ----------
        Q : Q_type
            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)
        return Q**2 * unit_conversion_factor.value

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

        Parameters
        ----------
        Q : Q_type
            Scattering vector in 1/angstrom.

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

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

        Parameters
        ----------
        Q : Q_type
            Scattering vector in 1/angstrom.

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

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

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

        Parameters
        ----------
        Q : Q_type
            Scattering vector values.
        component_display_name : str, default='Brownian diffusion'
            Name of the Lorentzian component.

        Raises
        ------
        TypeError
            If component_display_name is not a string.

        Returns
        -------
        list[ComponentCollection]
            List of ComponentCollections with Lorentzian components for each Q value. Each
            Lorentzian has a width given by $D*Q^2$ and an area given by the scale parameter
            multiplied by the QISF (which is 1 for this model).
        """
        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,
                desired_unit=self.unit,
            )

            # 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,
            )

            component_collection_list[i].append_component(lorentzian_component)

        return component_collection_list

    # ------------------------------------------------------------------
    # Private methods
    # ------------------------------------------------------------------

    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.

        Raises
        ------
        TypeError
            If Q is not a float.

        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.

        Returns
        -------
        dict[str, DescriptorNumber]
            Dependency map for the width.
        """
        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.

        Parameters
        ----------
        QISF : float
            Quasielastic Incoherent Scattering Function.

        Raises
        ------
        TypeError
            If QISF is not a float.

        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.

        Returns
        -------
        dict[str, DescriptorNumber]
            Dependency map for the area.
        """
        return {
            'scale': self.scale,
        }

    # ------------------------------------------------------------------
    # dunder methods
    # ------------------------------------------------------------------

    def __repr__(self) -> str:
        """
        String representation of the BrownianTranslationalDiffusion model.

        Returns
        -------
        str
            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)

Initialize a new BrownianTranslationalDiffusion model.

Parameters:

Name Type Description Default
display_name str | None

Display name of the diffusion model.

'BrownianTranslationalDiffusion'
unique_name str | None

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

None
unit str | Unit

Unit of the diffusion model. Must be convertible to meV.

'meV'
scale Numeric

Scale factor for the diffusion model. Must be a non-negative number.

1.0
diffusion_coefficient Numeric

Diffusion coefficient D in m^2/s.

1.0

Raises:

Type Description
TypeError

If scale or diffusion_coefficient is not a number.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
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
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,
) -> None:
    """
    Initialize a new BrownianTranslationalDiffusion model.

    Parameters
    ----------
    display_name : str | None, default='BrownianTranslationalDiffusion'
        Display name of the diffusion model.
    unique_name : str | None, default=None
        Unique name of the diffusion model. If None, a unique name will be generated. By
        default, None.
    unit : str | sc.Unit, default='meV'
        Unit of the diffusion model. Must be convertible to meV.
    scale : Numeric, default=1.0
        Scale factor for the diffusion model. Must be a non-negative number.
    diffusion_coefficient : Numeric, default=1.0
        Diffusion coefficient D in m^2/s.

    Raises
    ------
    TypeError
        If scale or diffusion_coefficient is not a number.
    """
    if not isinstance(scale, Numeric):
        raise TypeError('scale must be a number.')

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

    diffusion_coefficient = Parameter(
        name='diffusion_coefficient',
        value=float(diffusion_coefficient),
        fixed=False,
        unit='m**2/s',
        min=0.0,
    )
    super().__init__(
        display_name=display_name,
        unique_name=unique_name,
        unit=unit,
        scale=scale,
    )
    self._hbar = hbar
    self._angstrom = angstrom
    self._diffusion_coefficient = diffusion_coefficient

__repr__()

String representation of the BrownianTranslationalDiffusion model.

Returns:

Type Description
str

String representation of the BrownianTranslationalDiffusion model.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
349
350
351
352
353
354
355
356
357
358
359
360
361
def __repr__(self) -> str:
    """
    String representation of the BrownianTranslationalDiffusion model.

    Returns
    -------
    str
        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:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom.

required

Returns:

Type Description
ndarray

EISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def calculate_EISF(self, Q: Q_type) -> np.ndarray:
    """
    Calculate the Elastic Incoherent Structure Factor (EISF) for the Brownian translational
    diffusion model.

    Parameters
    ----------
    Q : Q_type
        Scattering vector in 1/angstrom.

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

calculate_QISF(Q)

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

Parameters:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom.

required

Returns:

Type Description
ndarray

QISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def calculate_QISF(self, Q: Q_type) -> np.ndarray:
    """
    Calculate the Quasi-Elastic Incoherent Structure Factor (QISF).

    Parameters
    ----------
    Q : Q_type
        Scattering vector in 1/angstrom.

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

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

calculate_width(Q)

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

Parameters:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom.

required

Returns:

Type Description
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def calculate_width(self, Q: Q_type) -> np.ndarray:
    """
    Calculate the half-width at half-maximum (HWHM) for the diffusion model.

    Parameters
    ----------
    Q : Q_type
        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)
    return Q**2 * unit_conversion_factor.value

create_component_collections(Q, component_display_name='Brownian diffusion')

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

Parameters:

Name Type Description Default
Q Q_type

Scattering vector values.

required
component_display_name str

Name of the Lorentzian component.

'Brownian diffusion'

Raises:

Type Description
TypeError

If component_display_name is not a string.

Returns:

Type Description
list[ComponentCollection]

List of ComponentCollections with Lorentzian components for each Q value. Each Lorentzian has a width given by \(D*Q^2\) and an area given by the scale parameter multiplied by the QISF (which is 1 for this model).

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

    Parameters
    ----------
    Q : Q_type
        Scattering vector values.
    component_display_name : str, default='Brownian diffusion'
        Name of the Lorentzian component.

    Raises
    ------
    TypeError
        If component_display_name is not a string.

    Returns
    -------
    list[ComponentCollection]
        List of ComponentCollections with Lorentzian components for each Q value. Each
        Lorentzian has a width given by $D*Q^2$ and an area given by the scale parameter
        multiplied by the QISF (which is 1 for this model).
    """
    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,
            desired_unit=self.unit,
        )

        # 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,
        )

        component_collection_list[i].append_component(lorentzian_component)

    return component_collection_list

diffusion_coefficient property writable

Get the diffusion coefficient parameter D.

Returns:

Type Description
Parameter

Diffusion coefficient D in m^2/s.

JumpTranslationalDiffusion

Bases: DiffusionModelBase

Model of Jump translational diffusion.

The model consists of a Lorentzian function for each Q-value, where the width is given by

\[ \Gamma(Q) = \frac{Q^2}{1+D t Q^2}. \]

where \(D\) is the diffusion coefficient and \(t\) is the relaxation time. Q is assumed to have units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given Q-values.

Example: >>> Q = np.linspace(0.5, 2, 7) >>> energy = np.linspace(-2, 2, 501) >>> scale = 1.0

diffusion_coefficient = 2.4e-9 # m^2/s >>> relaxation_time = 1.0 # ps >>> diffusion_model=JumpTranslationalDiffusion( >>> scale = scale, diffusion_coefficient = (diffusion_coefficient,) >>> relaxation_time=relaxation_time) >>> component_collections= >>> diffusion_model.create_component_collections(Q) See also the tutorials..

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
class JumpTranslationalDiffusion(DiffusionModelBase):
    r"""
    Model of Jump translational diffusion.

    The model consists of a Lorentzian function for each Q-value, where the width is given by

    $$ \Gamma(Q) = \frac{Q^2}{1+D t Q^2}. $$

    where $D$ is the diffusion coefficient and $t$ is the relaxation time. Q is assumed to have
    units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given
    Q-values.

    Example: >>> Q = np.linspace(0.5, 2, 7) >>> energy = np.linspace(-2, 2, 501) >>> scale = 1.0
    >>> diffusion_coefficient = 2.4e-9  # m^2/s >>> relaxation_time = 1.0  # ps >>>
    diffusion_model=JumpTranslationalDiffusion( >>> scale = scale, diffusion_coefficient =
    (diffusion_coefficient,) >>> relaxation_time=relaxation_time) >>> component_collections= >>>
    diffusion_model.create_component_collections(Q) See also the tutorials..
    """

    def __init__(
        self,
        display_name: str | None = 'JumpTranslationalDiffusion',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
        scale: Numeric = 1.0,
        diffusion_coefficient: Numeric = 1.0,
        relaxation_time: Numeric = 1.0,
    ) -> None:
        """
        Initialize a new JumpTranslationalDiffusion model.

        Parameters
        ----------
        display_name : str | None, default='JumpTranslationalDiffusion'
            Display name of the diffusion model.
        unique_name : str | None, default=None
            Unique name of the diffusion model. If None, a unique name will be generated. By
            default, None.
        unit : str | sc.Unit, default='meV'
            Unit of the diffusion model. Must be convertible to meV.
        scale : Numeric, default=1.0
            Scale factor for the diffusion model. Must be a non-negative number.
        diffusion_coefficient : Numeric, default=1.0
            Diffusion coefficient D in m^2/s.
        relaxation_time : Numeric, default=1.0
            Relaxation time t in ps.

        Raises
        ------
        TypeError
            If scale, diffusion_coefficient, or relaxation_time  are not numbers.
        """
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
            unit=unit,
            scale=scale,
        )

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

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

        diffusion_coefficient = Parameter(
            name='diffusion_coefficient',
            value=float(diffusion_coefficient),
            fixed=False,
            unit='m**2/s',
        )

        relaxation_time = Parameter(
            name='relaxation_time',
            value=float(relaxation_time),
            fixed=False,
            unit='ps',
        )

        self._hbar = hbar
        self._angstrom = angstrom
        self._diffusion_coefficient = diffusion_coefficient
        self._relaxation_time = relaxation_time

    ################################
    # Properties
    ################################

    @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.

        Parameters
        ----------
        diffusion_coefficient : Numeric
            Diffusion coefficient D in m^2/s.

        Raises
        ------
        TypeError
            If diffusion_coefficient is not a number.
        ValueError
            If diffusion_coefficient is negative.
        """
        if not isinstance(diffusion_coefficient, Numeric):
            raise TypeError('diffusion_coefficient must be a number.')
        if float(diffusion_coefficient) < 0:
            raise ValueError('diffusion_coefficient must be non-negative.')
        self._diffusion_coefficient.value = float(diffusion_coefficient)

    @property
    def relaxation_time(self) -> Parameter:
        """
        Get the relaxation time parameter t.

        Returns
        -------
        Parameter
            Relaxation time t in ps.
        """
        return self._relaxation_time

    @relaxation_time.setter
    def relaxation_time(self, relaxation_time: Numeric) -> None:
        """
        Set the relaxation time parameter t.

        Parameters
        ----------
        relaxation_time : Numeric
            Relaxation time t in ps.

        Raises
        ------
        TypeError
            If relaxation_time is not a number.
        ValueError
            If relaxation_time is negative.
        """
        if not isinstance(relaxation_time, Numeric):
            raise TypeError('relaxation_time must be a number.')

        if float(relaxation_time) < 0:
            raise ValueError('relaxation_time must be non-negative.')
        self._relaxation_time.value = float(relaxation_time)

    ################################
    # Other methods
    ################################

    def calculate_width(self, Q: Q_type) -> np.ndarray:
        r"""
        Calculate the half-width at half-maximum (HWHM) for the diffusion model. $\Gamma(Q) =
        Q^2/(1+D t Q^2)$, where $D$ is the diffusion coefficient and $t$ is the relaxation time.

        Parameters
        ----------
        Q : Q_type
            Scattering vector in 1/angstrom. Can be a single value or an array of values.

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

        Q = _validate_and_convert_Q(Q)

        unit_conversion_factor_numerator = (
            self._hbar * self.diffusion_coefficient / (self._angstrom**2)
        )
        unit_conversion_factor_numerator.convert_unit(self.unit)

        numerator = unit_conversion_factor_numerator.value * Q**2

        unit_conversion_factor_denominator = (
            self.diffusion_coefficient / self._angstrom**2 * self.relaxation_time
        )
        unit_conversion_factor_denominator.convert_unit('dimensionless')

        denominator = 1 + unit_conversion_factor_denominator.value * Q**2

        return numerator / denominator

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

        Parameters
        ----------
        Q : Q_type
            Scattering vector in 1/angstrom. Can be a single value or an array of values.

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

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

        Parameters
        ----------
        Q : Q_type
            Scattering vector in 1/angstrom. Can be a single value or an array of values.

        Returns
        -------
        np.ndarray
            QISF values (dimensionless).
        """
        Q = _validate_and_convert_Q(Q)
        return np.ones_like(Q)

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

        Parameters
        ----------
        Q : Q_type
            Scattering vector in 1/angstrom. Can be a single value or an array of values.
        component_display_name : str, default='Jump translational diffusion'
            Name of the Jump Diffusion Lorentzian component.

        Raises
        ------
        TypeError
            If component_display_name is not a string.

        Returns
        -------
        list[ComponentCollection]
            List of ComponentCollections with Jump Diffusion 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,
                desired_unit=self.unit,
            )

            # 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,
            )

            component_collection_list[i].append_component(lorentzian_component)

        return component_collection_list

    ################################
    # Private methods
    ################################

    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.

        Raises
        ------
        TypeError
            If Q is not a float.

        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/(angstrom**2)/(1 + (D * t* {Q} **2/(angstrom**2)))'

    def _write_width_dependency_map_expression(self) -> dict[str, DescriptorNumber]:
        """
        Write the dependency map expression to make dependent Parameters.

        Returns
        -------
        dict[str, DescriptorNumber]
            Dependency map for the width.
        """
        return {
            'D': self._diffusion_coefficient,
            't': self._relaxation_time,
            '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.

        Parameters
        ----------
        QISF : float
            Q-dependent intermediate scattering function.

        Raises
        ------
        TypeError
            If QISF is not a float.

        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.

        Returns
        -------
        dict[str, DescriptorNumber]
            Dependency map for the area.
        """
        return {
            'scale': self._scale,
        }

    ################################
    # dunder methods
    ################################

    def __repr__(self) -> str:
        """
        String representation of the JumpTranslationalDiffusion model.

        Returns
        -------
        str
            String representation of the JumpTranslationalDiffusion model.
        """
        return (
            f'JumpTranslationalDiffusion(display_name={self.display_name}, '
            f'diffusion_coefficient={self._diffusion_coefficient}, scale={self._scale})'
        )

__init__(display_name='JumpTranslationalDiffusion', unique_name=None, unit='meV', scale=1.0, diffusion_coefficient=1.0, relaxation_time=1.0)

Initialize a new JumpTranslationalDiffusion model.

Parameters:

Name Type Description Default
display_name str | None

Display name of the diffusion model.

'JumpTranslationalDiffusion'
unique_name str | None

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

None
unit str | Unit

Unit of the diffusion model. Must be convertible to meV.

'meV'
scale Numeric

Scale factor for the diffusion model. Must be a non-negative number.

1.0
diffusion_coefficient Numeric

Diffusion coefficient D in m^2/s.

1.0
relaxation_time Numeric

Relaxation time t in ps.

1.0

Raises:

Type Description
TypeError

If scale, diffusion_coefficient, or relaxation_time are not numbers.

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def __init__(
    self,
    display_name: str | None = 'JumpTranslationalDiffusion',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
    scale: Numeric = 1.0,
    diffusion_coefficient: Numeric = 1.0,
    relaxation_time: Numeric = 1.0,
) -> None:
    """
    Initialize a new JumpTranslationalDiffusion model.

    Parameters
    ----------
    display_name : str | None, default='JumpTranslationalDiffusion'
        Display name of the diffusion model.
    unique_name : str | None, default=None
        Unique name of the diffusion model. If None, a unique name will be generated. By
        default, None.
    unit : str | sc.Unit, default='meV'
        Unit of the diffusion model. Must be convertible to meV.
    scale : Numeric, default=1.0
        Scale factor for the diffusion model. Must be a non-negative number.
    diffusion_coefficient : Numeric, default=1.0
        Diffusion coefficient D in m^2/s.
    relaxation_time : Numeric, default=1.0
        Relaxation time t in ps.

    Raises
    ------
    TypeError
        If scale, diffusion_coefficient, or relaxation_time  are not numbers.
    """
    super().__init__(
        display_name=display_name,
        unique_name=unique_name,
        unit=unit,
        scale=scale,
    )

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

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

    diffusion_coefficient = Parameter(
        name='diffusion_coefficient',
        value=float(diffusion_coefficient),
        fixed=False,
        unit='m**2/s',
    )

    relaxation_time = Parameter(
        name='relaxation_time',
        value=float(relaxation_time),
        fixed=False,
        unit='ps',
    )

    self._hbar = hbar
    self._angstrom = angstrom
    self._diffusion_coefficient = diffusion_coefficient
    self._relaxation_time = relaxation_time

__repr__()

String representation of the JumpTranslationalDiffusion model.

Returns:

Type Description
str

String representation of the JumpTranslationalDiffusion model.

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
408
409
410
411
412
413
414
415
416
417
418
419
420
def __repr__(self) -> str:
    """
    String representation of the JumpTranslationalDiffusion model.

    Returns
    -------
    str
        String representation of the JumpTranslationalDiffusion model.
    """
    return (
        f'JumpTranslationalDiffusion(display_name={self.display_name}, '
        f'diffusion_coefficient={self._diffusion_coefficient}, scale={self._scale})'
    )

calculate_EISF(Q)

Calculate the Elastic Incoherent Structure Factor (EISF).

Parameters:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom. Can be a single value or an array of values.

required

Returns:

Type Description
ndarray

EISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
def calculate_EISF(self, Q: Q_type) -> np.ndarray:
    """
    Calculate the Elastic Incoherent Structure Factor (EISF).

    Parameters
    ----------
    Q : Q_type
        Scattering vector in 1/angstrom. Can be a single value or an array of values.

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

calculate_QISF(Q)

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

Parameters:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom. Can be a single value or an array of values.

required

Returns:

Type Description
ndarray

QISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
def calculate_QISF(self, Q: Q_type) -> np.ndarray:
    """
    Calculate the Quasi-Elastic Incoherent Structure Factor (QISF).

    Parameters
    ----------
    Q : Q_type
        Scattering vector in 1/angstrom. Can be a single value or an array of values.

    Returns
    -------
    np.ndarray
        QISF values (dimensionless).
    """
    Q = _validate_and_convert_Q(Q)
    return np.ones_like(Q)

calculate_width(Q)

Calculate the half-width at half-maximum (HWHM) for the diffusion model. \(\Gamma(Q) = Q^2/(1+D t Q^2)\), where \(D\) is the diffusion coefficient and \(t\) is the relaxation time.

Parameters:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom. Can be a single value or an array of values.

required

Returns:

Type Description
ndarray

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

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
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
def calculate_width(self, Q: Q_type) -> np.ndarray:
    r"""
    Calculate the half-width at half-maximum (HWHM) for the diffusion model. $\Gamma(Q) =
    Q^2/(1+D t Q^2)$, where $D$ is the diffusion coefficient and $t$ is the relaxation time.

    Parameters
    ----------
    Q : Q_type
        Scattering vector in 1/angstrom. Can be a single value or an array of values.

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

    Q = _validate_and_convert_Q(Q)

    unit_conversion_factor_numerator = (
        self._hbar * self.diffusion_coefficient / (self._angstrom**2)
    )
    unit_conversion_factor_numerator.convert_unit(self.unit)

    numerator = unit_conversion_factor_numerator.value * Q**2

    unit_conversion_factor_denominator = (
        self.diffusion_coefficient / self._angstrom**2 * self.relaxation_time
    )
    unit_conversion_factor_denominator.convert_unit('dimensionless')

    denominator = 1 + unit_conversion_factor_denominator.value * Q**2

    return numerator / denominator

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

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

Parameters:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom. Can be a single value or an array of values.

required
component_display_name str

Name of the Jump Diffusion Lorentzian component.

'Jump translational diffusion'

Raises:

Type Description
TypeError

If component_display_name is not a string.

Returns:

Type Description
list[ComponentCollection]

List of ComponentCollections with Jump Diffusion Lorentzian components.

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

    Parameters
    ----------
    Q : Q_type
        Scattering vector in 1/angstrom. Can be a single value or an array of values.
    component_display_name : str, default='Jump translational diffusion'
        Name of the Jump Diffusion Lorentzian component.

    Raises
    ------
    TypeError
        If component_display_name is not a string.

    Returns
    -------
    list[ComponentCollection]
        List of ComponentCollections with Jump Diffusion 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,
            desired_unit=self.unit,
        )

        # 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,
        )

        component_collection_list[i].append_component(lorentzian_component)

    return component_collection_list

diffusion_coefficient property writable

Get the diffusion coefficient parameter D.

Returns:

Type Description
Parameter

Diffusion coefficient D.

relaxation_time property writable

Get the relaxation time parameter t.

Returns:

Type Description
Parameter

Relaxation time t in ps.

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 \(D Q^2\), where \(D\) is the diffusion coefficient. The area of the Lorentzians is given by the scale parameter multiplied by the QISF, which is 1 for this model. The EISF is 0 for this model, so there is no delta function component. Q is assumed to have units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given Q-values.

Example: >>>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 tutorials.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
class BrownianTranslationalDiffusion(DiffusionModelBase):
    r"""
    Model of Brownian translational diffusion, consisting of a Lorentzian function for each
    Q-value, where the width is given by $D Q^2$, where $D$ is the diffusion coefficient. The area
    of the Lorentzians is given by the scale parameter multiplied by the QISF, which is 1 for this
    model. The EISF is 0 for this model, so there is no delta function component. Q is assumed to
    have units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given
    Q-values.

    Example: >>>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
    tutorials.
    """

    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,
    ) -> None:
        """
        Initialize a new BrownianTranslationalDiffusion model.

        Parameters
        ----------
        display_name : str | None, default='BrownianTranslationalDiffusion'
            Display name of the diffusion model.
        unique_name : str | None, default=None
            Unique name of the diffusion model. If None, a unique name will be generated. By
            default, None.
        unit : str | sc.Unit, default='meV'
            Unit of the diffusion model. Must be convertible to meV.
        scale : Numeric, default=1.0
            Scale factor for the diffusion model. Must be a non-negative number.
        diffusion_coefficient : Numeric, default=1.0
            Diffusion coefficient D in m^2/s.

        Raises
        ------
        TypeError
            If scale or diffusion_coefficient is not a number.
        """
        if not isinstance(scale, Numeric):
            raise TypeError('scale must be a number.')

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

        diffusion_coefficient = Parameter(
            name='diffusion_coefficient',
            value=float(diffusion_coefficient),
            fixed=False,
            unit='m**2/s',
            min=0.0,
        )
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
            unit=unit,
            scale=scale,
        )
        self._hbar = hbar
        self._angstrom = angstrom
        self._diffusion_coefficient = diffusion_coefficient

    # ------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------

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

        Returns
        -------
        Parameter
            Diffusion coefficient D in m^2/s.
        """
        return self._diffusion_coefficient

    @diffusion_coefficient.setter
    def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None:
        """
        Set the diffusion coefficient parameter D.

        Parameters
        ----------
        diffusion_coefficient : Numeric
            The new value for the diffusion coefficient D in m^2/s.

        Raises
        ------
        TypeError
            If diffusion_coefficient is not a number.
        ValueError
            If diffusion_coefficient is negative.
        """
        if not isinstance(diffusion_coefficient, Numeric):
            raise TypeError('diffusion_coefficient must be a number.')

        if float(diffusion_coefficient) < 0:
            raise ValueError('diffusion_coefficient must be non-negative.')
        self._diffusion_coefficient.value = float(diffusion_coefficient)

    # ------------------------------------------------------------------
    # Other methods
    # ------------------------------------------------------------------

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

        Parameters
        ----------
        Q : Q_type
            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)
        return Q**2 * unit_conversion_factor.value

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

        Parameters
        ----------
        Q : Q_type
            Scattering vector in 1/angstrom.

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

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

        Parameters
        ----------
        Q : Q_type
            Scattering vector in 1/angstrom.

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

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

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

        Parameters
        ----------
        Q : Q_type
            Scattering vector values.
        component_display_name : str, default='Brownian diffusion'
            Name of the Lorentzian component.

        Raises
        ------
        TypeError
            If component_display_name is not a string.

        Returns
        -------
        list[ComponentCollection]
            List of ComponentCollections with Lorentzian components for each Q value. Each
            Lorentzian has a width given by $D*Q^2$ and an area given by the scale parameter
            multiplied by the QISF (which is 1 for this model).
        """
        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,
                desired_unit=self.unit,
            )

            # 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,
            )

            component_collection_list[i].append_component(lorentzian_component)

        return component_collection_list

    # ------------------------------------------------------------------
    # Private methods
    # ------------------------------------------------------------------

    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.

        Raises
        ------
        TypeError
            If Q is not a float.

        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.

        Returns
        -------
        dict[str, DescriptorNumber]
            Dependency map for the width.
        """
        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.

        Parameters
        ----------
        QISF : float
            Quasielastic Incoherent Scattering Function.

        Raises
        ------
        TypeError
            If QISF is not a float.

        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.

        Returns
        -------
        dict[str, DescriptorNumber]
            Dependency map for the area.
        """
        return {
            'scale': self.scale,
        }

    # ------------------------------------------------------------------
    # dunder methods
    # ------------------------------------------------------------------

    def __repr__(self) -> str:
        """
        String representation of the BrownianTranslationalDiffusion model.

        Returns
        -------
        str
            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)

Initialize a new BrownianTranslationalDiffusion model.

Parameters:

Name Type Description Default
display_name str | None

Display name of the diffusion model.

'BrownianTranslationalDiffusion'
unique_name str | None

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

None
unit str | Unit

Unit of the diffusion model. Must be convertible to meV.

'meV'
scale Numeric

Scale factor for the diffusion model. Must be a non-negative number.

1.0
diffusion_coefficient Numeric

Diffusion coefficient D in m^2/s.

1.0

Raises:

Type Description
TypeError

If scale or diffusion_coefficient is not a number.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
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
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,
) -> None:
    """
    Initialize a new BrownianTranslationalDiffusion model.

    Parameters
    ----------
    display_name : str | None, default='BrownianTranslationalDiffusion'
        Display name of the diffusion model.
    unique_name : str | None, default=None
        Unique name of the diffusion model. If None, a unique name will be generated. By
        default, None.
    unit : str | sc.Unit, default='meV'
        Unit of the diffusion model. Must be convertible to meV.
    scale : Numeric, default=1.0
        Scale factor for the diffusion model. Must be a non-negative number.
    diffusion_coefficient : Numeric, default=1.0
        Diffusion coefficient D in m^2/s.

    Raises
    ------
    TypeError
        If scale or diffusion_coefficient is not a number.
    """
    if not isinstance(scale, Numeric):
        raise TypeError('scale must be a number.')

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

    diffusion_coefficient = Parameter(
        name='diffusion_coefficient',
        value=float(diffusion_coefficient),
        fixed=False,
        unit='m**2/s',
        min=0.0,
    )
    super().__init__(
        display_name=display_name,
        unique_name=unique_name,
        unit=unit,
        scale=scale,
    )
    self._hbar = hbar
    self._angstrom = angstrom
    self._diffusion_coefficient = diffusion_coefficient
__repr__()

String representation of the BrownianTranslationalDiffusion model.

Returns:

Type Description
str

String representation of the BrownianTranslationalDiffusion model.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
349
350
351
352
353
354
355
356
357
358
359
360
361
def __repr__(self) -> str:
    """
    String representation of the BrownianTranslationalDiffusion model.

    Returns
    -------
    str
        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:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom.

required

Returns:

Type Description
ndarray

EISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def calculate_EISF(self, Q: Q_type) -> np.ndarray:
    """
    Calculate the Elastic Incoherent Structure Factor (EISF) for the Brownian translational
    diffusion model.

    Parameters
    ----------
    Q : Q_type
        Scattering vector in 1/angstrom.

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

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

Parameters:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom.

required

Returns:

Type Description
ndarray

QISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def calculate_QISF(self, Q: Q_type) -> np.ndarray:
    """
    Calculate the Quasi-Elastic Incoherent Structure Factor (QISF).

    Parameters
    ----------
    Q : Q_type
        Scattering vector in 1/angstrom.

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

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

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

Parameters:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom.

required

Returns:

Type Description
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def calculate_width(self, Q: Q_type) -> np.ndarray:
    """
    Calculate the half-width at half-maximum (HWHM) for the diffusion model.

    Parameters
    ----------
    Q : Q_type
        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)
    return Q**2 * unit_conversion_factor.value
create_component_collections(Q, component_display_name='Brownian diffusion')

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

Parameters:

Name Type Description Default
Q Q_type

Scattering vector values.

required
component_display_name str

Name of the Lorentzian component.

'Brownian diffusion'

Raises:

Type Description
TypeError

If component_display_name is not a string.

Returns:

Type Description
list[ComponentCollection]

List of ComponentCollections with Lorentzian components for each Q value. Each Lorentzian has a width given by \(D*Q^2\) and an area given by the scale parameter multiplied by the QISF (which is 1 for this model).

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

    Parameters
    ----------
    Q : Q_type
        Scattering vector values.
    component_display_name : str, default='Brownian diffusion'
        Name of the Lorentzian component.

    Raises
    ------
    TypeError
        If component_display_name is not a string.

    Returns
    -------
    list[ComponentCollection]
        List of ComponentCollections with Lorentzian components for each Q value. Each
        Lorentzian has a width given by $D*Q^2$ and an area given by the scale parameter
        multiplied by the QISF (which is 1 for this model).
    """
    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,
            desired_unit=self.unit,
        )

        # 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,
        )

        component_collection_list[i].append_component(lorentzian_component)

    return component_collection_list
diffusion_coefficient property writable

Get the diffusion coefficient parameter D.

Returns:

Type Description
Parameter

Diffusion coefficient D in m^2/s.

diffusion_model_base

DiffusionModelBase

Bases: EasyDynamicsModelBase

Base class for constructing diffusion models.

Source code in src/easydynamics/sample_model/diffusion_model/diffusion_model_base.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
 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
class DiffusionModelBase(EasyDynamicsModelBase):
    """Base class for constructing diffusion models."""

    def __init__(
        self,
        display_name: str | None = 'MyDiffusionModel',
        unique_name: str | None = None,
        scale: Numeric = 1.0,
        unit: str | sc.Unit = 'meV',
    ) -> None:
        """
        Initialize a new DiffusionModel.

        Parameters
        ----------
        display_name : str | None, default='MyDiffusionModel'
            Display name of the diffusion model.
        unique_name : str | None, default=None
            Unique name of the diffusion model. If None, a unique name will be generated. By
            default, None.
        scale : Numeric, default=1.0
            Scale factor for the diffusion model. Must be a non-negative number.
        unit : str | sc.Unit, default='meV'
            Unit of the diffusion model. Must be convertible to meV.

        Raises
        ------
        TypeError
            If scale is not a number.
        UnitError
            If unit is not a string or scipp Unit, or if it cannot be converted 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

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

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

        super().__init__(display_name=display_name, unique_name=unique_name, unit=unit)
        self._scale = scale

    # ------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------

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

        Returns
        -------
        Parameter
            Scale parameter of the diffusion model.
        """
        return self._scale

    @scale.setter
    def scale(self, scale: Numeric) -> None:
        """
        Set the scale parameter of the diffusion model.

        Parameters
        ----------
        scale : Numeric
            The new value for the scale parameter. Must be a non-negative number.

        Raises
        ------
        TypeError
            If scale is not a number.
        ValueError
            If scale is negative.
        """
        if not isinstance(scale, Numeric):
            raise TypeError('scale must be a number.')

        if float(scale) < 0:
            raise ValueError('scale must be non-negative.')
        self._scale.value = scale

    # ------------------------------------------------------------------
    # dunder methods
    # ------------------------------------------------------------------

    def __repr__(self) -> str:
        """
        String representation of the Diffusion model.

        Returns
        -------
        str
            String representation of the DiffusionModel.
        """
        return f'{self.__class__.__name__}(display_name={self.display_name}, unit={self.unit})'
__init__(display_name='MyDiffusionModel', unique_name=None, scale=1.0, unit='meV')

Initialize a new DiffusionModel.

Parameters:

Name Type Description Default
display_name str | None

Display name of the diffusion model.

'MyDiffusionModel'
unique_name str | None

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

None
scale Numeric

Scale factor for the diffusion model. Must be a non-negative number.

1.0
unit str | Unit

Unit of the diffusion model. Must be convertible to meV.

'meV'

Raises:

Type Description
TypeError

If scale is not a number.

UnitError

If unit is not a string or scipp Unit, or if it cannot be converted to meV.

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
def __init__(
    self,
    display_name: str | None = 'MyDiffusionModel',
    unique_name: str | None = None,
    scale: Numeric = 1.0,
    unit: str | sc.Unit = 'meV',
) -> None:
    """
    Initialize a new DiffusionModel.

    Parameters
    ----------
    display_name : str | None, default='MyDiffusionModel'
        Display name of the diffusion model.
    unique_name : str | None, default=None
        Unique name of the diffusion model. If None, a unique name will be generated. By
        default, None.
    scale : Numeric, default=1.0
        Scale factor for the diffusion model. Must be a non-negative number.
    unit : str | sc.Unit, default='meV'
        Unit of the diffusion model. Must be convertible to meV.

    Raises
    ------
    TypeError
        If scale is not a number.
    UnitError
        If unit is not a string or scipp Unit, or if it cannot be converted 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

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

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

    super().__init__(display_name=display_name, unique_name=unique_name, unit=unit)
    self._scale = scale
__repr__()

String representation of the Diffusion model.

Returns:

Type Description
str

String representation of the DiffusionModel.

Source code in src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py
106
107
108
109
110
111
112
113
114
115
def __repr__(self) -> str:
    """
    String representation of the Diffusion model.

    Returns
    -------
    str
        String representation of the DiffusionModel.
    """
    return f'{self.__class__.__name__}(display_name={self.display_name}, unit={self.unit})'
scale property writable

Get the scale parameter of the diffusion model.

Returns:

Type Description
Parameter

Scale parameter of the diffusion model.

jump_translational_diffusion

JumpTranslationalDiffusion

Bases: DiffusionModelBase

Model of Jump translational diffusion.

The model consists of a Lorentzian function for each Q-value, where the width is given by

\[ \Gamma(Q) = \frac{Q^2}{1+D t Q^2}. \]

where \(D\) is the diffusion coefficient and \(t\) is the relaxation time. Q is assumed to have units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given Q-values.

Example: >>> Q = np.linspace(0.5, 2, 7) >>> energy = np.linspace(-2, 2, 501) >>> scale = 1.0

diffusion_coefficient = 2.4e-9 # m^2/s >>> relaxation_time = 1.0 # ps >>> diffusion_model=JumpTranslationalDiffusion( >>> scale = scale, diffusion_coefficient = (diffusion_coefficient,) >>> relaxation_time=relaxation_time) >>> component_collections= >>> diffusion_model.create_component_collections(Q) See also the tutorials..

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
class JumpTranslationalDiffusion(DiffusionModelBase):
    r"""
    Model of Jump translational diffusion.

    The model consists of a Lorentzian function for each Q-value, where the width is given by

    $$ \Gamma(Q) = \frac{Q^2}{1+D t Q^2}. $$

    where $D$ is the diffusion coefficient and $t$ is the relaxation time. Q is assumed to have
    units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given
    Q-values.

    Example: >>> Q = np.linspace(0.5, 2, 7) >>> energy = np.linspace(-2, 2, 501) >>> scale = 1.0
    >>> diffusion_coefficient = 2.4e-9  # m^2/s >>> relaxation_time = 1.0  # ps >>>
    diffusion_model=JumpTranslationalDiffusion( >>> scale = scale, diffusion_coefficient =
    (diffusion_coefficient,) >>> relaxation_time=relaxation_time) >>> component_collections= >>>
    diffusion_model.create_component_collections(Q) See also the tutorials..
    """

    def __init__(
        self,
        display_name: str | None = 'JumpTranslationalDiffusion',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
        scale: Numeric = 1.0,
        diffusion_coefficient: Numeric = 1.0,
        relaxation_time: Numeric = 1.0,
    ) -> None:
        """
        Initialize a new JumpTranslationalDiffusion model.

        Parameters
        ----------
        display_name : str | None, default='JumpTranslationalDiffusion'
            Display name of the diffusion model.
        unique_name : str | None, default=None
            Unique name of the diffusion model. If None, a unique name will be generated. By
            default, None.
        unit : str | sc.Unit, default='meV'
            Unit of the diffusion model. Must be convertible to meV.
        scale : Numeric, default=1.0
            Scale factor for the diffusion model. Must be a non-negative number.
        diffusion_coefficient : Numeric, default=1.0
            Diffusion coefficient D in m^2/s.
        relaxation_time : Numeric, default=1.0
            Relaxation time t in ps.

        Raises
        ------
        TypeError
            If scale, diffusion_coefficient, or relaxation_time  are not numbers.
        """
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
            unit=unit,
            scale=scale,
        )

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

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

        diffusion_coefficient = Parameter(
            name='diffusion_coefficient',
            value=float(diffusion_coefficient),
            fixed=False,
            unit='m**2/s',
        )

        relaxation_time = Parameter(
            name='relaxation_time',
            value=float(relaxation_time),
            fixed=False,
            unit='ps',
        )

        self._hbar = hbar
        self._angstrom = angstrom
        self._diffusion_coefficient = diffusion_coefficient
        self._relaxation_time = relaxation_time

    ################################
    # Properties
    ################################

    @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.

        Parameters
        ----------
        diffusion_coefficient : Numeric
            Diffusion coefficient D in m^2/s.

        Raises
        ------
        TypeError
            If diffusion_coefficient is not a number.
        ValueError
            If diffusion_coefficient is negative.
        """
        if not isinstance(diffusion_coefficient, Numeric):
            raise TypeError('diffusion_coefficient must be a number.')
        if float(diffusion_coefficient) < 0:
            raise ValueError('diffusion_coefficient must be non-negative.')
        self._diffusion_coefficient.value = float(diffusion_coefficient)

    @property
    def relaxation_time(self) -> Parameter:
        """
        Get the relaxation time parameter t.

        Returns
        -------
        Parameter
            Relaxation time t in ps.
        """
        return self._relaxation_time

    @relaxation_time.setter
    def relaxation_time(self, relaxation_time: Numeric) -> None:
        """
        Set the relaxation time parameter t.

        Parameters
        ----------
        relaxation_time : Numeric
            Relaxation time t in ps.

        Raises
        ------
        TypeError
            If relaxation_time is not a number.
        ValueError
            If relaxation_time is negative.
        """
        if not isinstance(relaxation_time, Numeric):
            raise TypeError('relaxation_time must be a number.')

        if float(relaxation_time) < 0:
            raise ValueError('relaxation_time must be non-negative.')
        self._relaxation_time.value = float(relaxation_time)

    ################################
    # Other methods
    ################################

    def calculate_width(self, Q: Q_type) -> np.ndarray:
        r"""
        Calculate the half-width at half-maximum (HWHM) for the diffusion model. $\Gamma(Q) =
        Q^2/(1+D t Q^2)$, where $D$ is the diffusion coefficient and $t$ is the relaxation time.

        Parameters
        ----------
        Q : Q_type
            Scattering vector in 1/angstrom. Can be a single value or an array of values.

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

        Q = _validate_and_convert_Q(Q)

        unit_conversion_factor_numerator = (
            self._hbar * self.diffusion_coefficient / (self._angstrom**2)
        )
        unit_conversion_factor_numerator.convert_unit(self.unit)

        numerator = unit_conversion_factor_numerator.value * Q**2

        unit_conversion_factor_denominator = (
            self.diffusion_coefficient / self._angstrom**2 * self.relaxation_time
        )
        unit_conversion_factor_denominator.convert_unit('dimensionless')

        denominator = 1 + unit_conversion_factor_denominator.value * Q**2

        return numerator / denominator

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

        Parameters
        ----------
        Q : Q_type
            Scattering vector in 1/angstrom. Can be a single value or an array of values.

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

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

        Parameters
        ----------
        Q : Q_type
            Scattering vector in 1/angstrom. Can be a single value or an array of values.

        Returns
        -------
        np.ndarray
            QISF values (dimensionless).
        """
        Q = _validate_and_convert_Q(Q)
        return np.ones_like(Q)

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

        Parameters
        ----------
        Q : Q_type
            Scattering vector in 1/angstrom. Can be a single value or an array of values.
        component_display_name : str, default='Jump translational diffusion'
            Name of the Jump Diffusion Lorentzian component.

        Raises
        ------
        TypeError
            If component_display_name is not a string.

        Returns
        -------
        list[ComponentCollection]
            List of ComponentCollections with Jump Diffusion 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,
                desired_unit=self.unit,
            )

            # 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,
            )

            component_collection_list[i].append_component(lorentzian_component)

        return component_collection_list

    ################################
    # Private methods
    ################################

    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.

        Raises
        ------
        TypeError
            If Q is not a float.

        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/(angstrom**2)/(1 + (D * t* {Q} **2/(angstrom**2)))'

    def _write_width_dependency_map_expression(self) -> dict[str, DescriptorNumber]:
        """
        Write the dependency map expression to make dependent Parameters.

        Returns
        -------
        dict[str, DescriptorNumber]
            Dependency map for the width.
        """
        return {
            'D': self._diffusion_coefficient,
            't': self._relaxation_time,
            '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.

        Parameters
        ----------
        QISF : float
            Q-dependent intermediate scattering function.

        Raises
        ------
        TypeError
            If QISF is not a float.

        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.

        Returns
        -------
        dict[str, DescriptorNumber]
            Dependency map for the area.
        """
        return {
            'scale': self._scale,
        }

    ################################
    # dunder methods
    ################################

    def __repr__(self) -> str:
        """
        String representation of the JumpTranslationalDiffusion model.

        Returns
        -------
        str
            String representation of the JumpTranslationalDiffusion model.
        """
        return (
            f'JumpTranslationalDiffusion(display_name={self.display_name}, '
            f'diffusion_coefficient={self._diffusion_coefficient}, scale={self._scale})'
        )
__init__(display_name='JumpTranslationalDiffusion', unique_name=None, unit='meV', scale=1.0, diffusion_coefficient=1.0, relaxation_time=1.0)

Initialize a new JumpTranslationalDiffusion model.

Parameters:

Name Type Description Default
display_name str | None

Display name of the diffusion model.

'JumpTranslationalDiffusion'
unique_name str | None

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

None
unit str | Unit

Unit of the diffusion model. Must be convertible to meV.

'meV'
scale Numeric

Scale factor for the diffusion model. Must be a non-negative number.

1.0
diffusion_coefficient Numeric

Diffusion coefficient D in m^2/s.

1.0
relaxation_time Numeric

Relaxation time t in ps.

1.0

Raises:

Type Description
TypeError

If scale, diffusion_coefficient, or relaxation_time are not numbers.

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def __init__(
    self,
    display_name: str | None = 'JumpTranslationalDiffusion',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
    scale: Numeric = 1.0,
    diffusion_coefficient: Numeric = 1.0,
    relaxation_time: Numeric = 1.0,
) -> None:
    """
    Initialize a new JumpTranslationalDiffusion model.

    Parameters
    ----------
    display_name : str | None, default='JumpTranslationalDiffusion'
        Display name of the diffusion model.
    unique_name : str | None, default=None
        Unique name of the diffusion model. If None, a unique name will be generated. By
        default, None.
    unit : str | sc.Unit, default='meV'
        Unit of the diffusion model. Must be convertible to meV.
    scale : Numeric, default=1.0
        Scale factor for the diffusion model. Must be a non-negative number.
    diffusion_coefficient : Numeric, default=1.0
        Diffusion coefficient D in m^2/s.
    relaxation_time : Numeric, default=1.0
        Relaxation time t in ps.

    Raises
    ------
    TypeError
        If scale, diffusion_coefficient, or relaxation_time  are not numbers.
    """
    super().__init__(
        display_name=display_name,
        unique_name=unique_name,
        unit=unit,
        scale=scale,
    )

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

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

    diffusion_coefficient = Parameter(
        name='diffusion_coefficient',
        value=float(diffusion_coefficient),
        fixed=False,
        unit='m**2/s',
    )

    relaxation_time = Parameter(
        name='relaxation_time',
        value=float(relaxation_time),
        fixed=False,
        unit='ps',
    )

    self._hbar = hbar
    self._angstrom = angstrom
    self._diffusion_coefficient = diffusion_coefficient
    self._relaxation_time = relaxation_time
__repr__()

String representation of the JumpTranslationalDiffusion model.

Returns:

Type Description
str

String representation of the JumpTranslationalDiffusion model.

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
408
409
410
411
412
413
414
415
416
417
418
419
420
def __repr__(self) -> str:
    """
    String representation of the JumpTranslationalDiffusion model.

    Returns
    -------
    str
        String representation of the JumpTranslationalDiffusion model.
    """
    return (
        f'JumpTranslationalDiffusion(display_name={self.display_name}, '
        f'diffusion_coefficient={self._diffusion_coefficient}, scale={self._scale})'
    )
calculate_EISF(Q)

Calculate the Elastic Incoherent Structure Factor (EISF).

Parameters:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom. Can be a single value or an array of values.

required

Returns:

Type Description
ndarray

EISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
def calculate_EISF(self, Q: Q_type) -> np.ndarray:
    """
    Calculate the Elastic Incoherent Structure Factor (EISF).

    Parameters
    ----------
    Q : Q_type
        Scattering vector in 1/angstrom. Can be a single value or an array of values.

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

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

Parameters:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom. Can be a single value or an array of values.

required

Returns:

Type Description
ndarray

QISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
def calculate_QISF(self, Q: Q_type) -> np.ndarray:
    """
    Calculate the Quasi-Elastic Incoherent Structure Factor (QISF).

    Parameters
    ----------
    Q : Q_type
        Scattering vector in 1/angstrom. Can be a single value or an array of values.

    Returns
    -------
    np.ndarray
        QISF values (dimensionless).
    """
    Q = _validate_and_convert_Q(Q)
    return np.ones_like(Q)
calculate_width(Q)

Calculate the half-width at half-maximum (HWHM) for the diffusion model. \(\Gamma(Q) = Q^2/(1+D t Q^2)\), where \(D\) is the diffusion coefficient and \(t\) is the relaxation time.

Parameters:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom. Can be a single value or an array of values.

required

Returns:

Type Description
ndarray

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

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
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
def calculate_width(self, Q: Q_type) -> np.ndarray:
    r"""
    Calculate the half-width at half-maximum (HWHM) for the diffusion model. $\Gamma(Q) =
    Q^2/(1+D t Q^2)$, where $D$ is the diffusion coefficient and $t$ is the relaxation time.

    Parameters
    ----------
    Q : Q_type
        Scattering vector in 1/angstrom. Can be a single value or an array of values.

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

    Q = _validate_and_convert_Q(Q)

    unit_conversion_factor_numerator = (
        self._hbar * self.diffusion_coefficient / (self._angstrom**2)
    )
    unit_conversion_factor_numerator.convert_unit(self.unit)

    numerator = unit_conversion_factor_numerator.value * Q**2

    unit_conversion_factor_denominator = (
        self.diffusion_coefficient / self._angstrom**2 * self.relaxation_time
    )
    unit_conversion_factor_denominator.convert_unit('dimensionless')

    denominator = 1 + unit_conversion_factor_denominator.value * Q**2

    return numerator / denominator
create_component_collections(Q, component_display_name='Jump translational diffusion')

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

Parameters:

Name Type Description Default
Q Q_type

Scattering vector in 1/angstrom. Can be a single value or an array of values.

required
component_display_name str

Name of the Jump Diffusion Lorentzian component.

'Jump translational diffusion'

Raises:

Type Description
TypeError

If component_display_name is not a string.

Returns:

Type Description
list[ComponentCollection]

List of ComponentCollections with Jump Diffusion Lorentzian components.

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

    Parameters
    ----------
    Q : Q_type
        Scattering vector in 1/angstrom. Can be a single value or an array of values.
    component_display_name : str, default='Jump translational diffusion'
        Name of the Jump Diffusion Lorentzian component.

    Raises
    ------
    TypeError
        If component_display_name is not a string.

    Returns
    -------
    list[ComponentCollection]
        List of ComponentCollections with Jump Diffusion 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,
            desired_unit=self.unit,
        )

        # 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,
        )

        component_collection_list[i].append_component(lorentzian_component)

    return component_collection_list
diffusion_coefficient property writable

Get the diffusion coefficient parameter D.

Returns:

Type Description
Parameter

Diffusion coefficient D.

relaxation_time property writable

Get the relaxation time parameter t.

Returns:

Type Description
Parameter

Relaxation time t in ps.

instrument_model

InstrumentModel

Bases: NewBase

InstrumentModel represents a model of the instrument in an experiment at various Q.

It can contain a model of the resolution function for convolutions, of the background and an offset in the energy axis.

Source code in src/easydynamics/sample_model/instrument_model.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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
class InstrumentModel(NewBase):
    """
    InstrumentModel represents a model of the instrument in an experiment at various Q.

    It can contain a model of the resolution function for convolutions, of the background and an
    offset in the energy axis.
    """

    def __init__(
        self,
        display_name: str = 'MyInstrumentModel',
        unique_name: str | None = None,
        Q: Q_type | None = None,
        resolution_model: ResolutionModel | None = None,
        background_model: BackgroundModel | None = None,
        energy_offset: Numeric | None = None,
        unit: str | sc.Unit = 'meV',
    ) -> None:
        """
        Initialize an InstrumentModel.

        Parameters
        ----------
        display_name : str, default='MyInstrumentModel'
            The display name of the InstrumentModel.
        unique_name : str | None, default=None
            The unique name of the InstrumentModel.
        Q : Q_type | None, default=None
            The Q values where the instrument is modelled.
        resolution_model : ResolutionModel | None, default=None
            The resolution model of the instrument. If None, an empty resolution model is created
            and no resolution convolution is carried out.
        background_model : BackgroundModel | None, default=None
            The background model of the instrument. If None, an empty background model is created,
            and the background evaluates to 0.
        energy_offset : Numeric | None, default=None
            Template energy offset of the instrument. Will be copied to each Q value. If None, the
            energy offset will be 0.
        unit : str | sc.Unit, default='meV'
            The unit of the energy axis.

        Raises
        ------
        TypeError
            If resolution_model is not a ResolutionModel or None, or if background_model is not a
            BackgroundModel or None, or if energy_offset is not a number or None.
        """
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
        )

        self._unit = _validate_unit(unit)

        if resolution_model is None:
            self._resolution_model = ResolutionModel()
        else:
            if not isinstance(resolution_model, ResolutionModel):
                raise TypeError(
                    f'resolution_model must be a ResolutionModel or None, '
                    f'got {type(resolution_model).__name__}'
                )
            self._resolution_model = resolution_model

        if background_model is None:
            self._background_model = BackgroundModel()
        else:
            if not isinstance(background_model, BackgroundModel):
                raise TypeError(
                    f'background_model must be a BackgroundModel or None, '
                    f'got {type(background_model).__name__}'
                )
            self._background_model = background_model

        if energy_offset is None:
            energy_offset = 0.0

        if not isinstance(energy_offset, Numeric):
            raise TypeError('energy_offset must be a number or None')

        self._energy_offset = Parameter(
            name='energy_offset',
            value=float(energy_offset),
            unit=self.unit,
            fixed=False,
        )
        self._Q = _validate_and_convert_Q(Q)
        self._on_Q_change()

    # -------------------------------------------------------------
    # Properties
    # -------------------------------------------------------------

    @property
    def resolution_model(self) -> ResolutionModel:
        """
        Get the resolution model of the instrument.

        Returns
        -------
        ResolutionModel
            The resolution model of the instrument.
        """
        return self._resolution_model

    @resolution_model.setter
    def resolution_model(self, value: ResolutionModel) -> None:
        """
        Set the resolution model of the instrument.

        Parameters
        ----------
        value : ResolutionModel
            The new resolution model of the instrument.

        Raises
        ------
        TypeError
            If value is not a ResolutionModel.
        """
        if not isinstance(value, ResolutionModel):
            raise TypeError(
                f'resolution_model must be a ResolutionModel, got {type(value).__name__}'
            )
        self._resolution_model = value
        self._on_resolution_model_change()

    @property
    def background_model(self) -> BackgroundModel:
        """
        Get the background model of the instrument.

        Returns
        -------
        BackgroundModel
            The background model of the instrument.
        """

        return self._background_model

    @background_model.setter
    def background_model(self, value: BackgroundModel) -> None:
        """
        Set the background model of the instrument.

        Parameters
        ----------
        value : BackgroundModel
            The new background model of the instrument.

        Raises
        ------
        TypeError
            If value is not a BackgroundModel.
        """

        if not isinstance(value, BackgroundModel):
            raise TypeError(
                f'background_model must be a BackgroundModel, got {type(value).__name__}'
            )
        self._background_model = value
        self._on_background_model_change()

    @property
    def Q(self) -> np.ndarray | None:
        """
        Get the Q values of the InstrumentModel.

        Returns
        -------
        np.ndarray | None
            The Q values of the InstrumentModel, or None if not set.
        """
        return self._Q

    @Q.setter
    def Q(self, value: Q_type | None) -> None:
        """
        Set the Q values of the InstrumentModel.

        If Q is already set, it raises an error if the new Q values are not similar to the old ones
        to prevent accidental changes to the background and resolution models. To change Q values,
        first run clear_Q().

        Parameters
        ----------
        value : Q_type | None
            The new Q values to set. If None, Q values are not changed.

        Raises
        ------
        ValueError
            If the new Q values are not similar to the old ones when Q is not None.
        """
        if value is None:
            return
        old_Q = self._Q
        new_Q = _validate_and_convert_Q(value)

        if old_Q is None:
            self._Q = new_Q
            self._on_Q_change()
            return

        if len(old_Q) != len(new_Q) or not np.allclose(old_Q, new_Q):
            raise ValueError(
                'New Q values are not similar to the old ones. '
                'To change Q values, first run clear_Q().'
            )

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

        Returns
        -------
        str | sc.Unit
            The unit of the InstrumentModel.
        """
        return self._unit

    @unit.setter
    def unit(self, _unit_str: str) -> None:
        """
        Set the unit of the InstrumentModel.

        The unit is read-only and cannot be set directly. Use convert_unit to change the unit
        between allowed types or create a new InstrumentModel with the desired unit.

        Parameters
        ----------
        _unit_str : str
            The new unit for the InstrumentModel (ignored).

        Raises
        ------
        AttributeError
            Always, as the unit is read-only.
        """
        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.'
        )

    @property
    def energy_offset(self) -> Parameter:
        """
        Get the energy offset template parameter of the instrument model.

        Returns
        -------
        Parameter
            The energy offset template parameter of the instrument model.
        """
        return self._energy_offset

    @energy_offset.setter
    def energy_offset(self, value: Numeric) -> None:
        """
        Set the offset parameter of the instrument model.

        Parameters
        ----------
        value : Numeric
            The new value for the energy offset parameter. Will be copied to all Q values.

        Raises
        ------
        TypeError
            If value is not a number.
        """
        if not isinstance(value, Numeric):
            raise TypeError(f'energy_offset must be a number, got {type(value).__name__}')
        self._energy_offset.value = value

        self._on_energy_offset_change()

    # --------------------------------------------------------------
    # Other methods
    # --------------------------------------------------------------

    def clear_Q(self, confirm: bool = False) -> None:
        """
        Clear the Q values of the InstrumentModel and any associated ResolutionModel and
        BackgroundModel, removing all component collections and their associated Parameters.

        Parameters
        ----------
        confirm : bool, default=False
            Confirmation to clear Q values.

        Raises
        ------
        ValueError
            If confirm is not True.
        """
        if not confirm:
            raise ValueError(
                'Clearing Q values requires confirmation. Set confirm=True to proceed.'
            )
        self._Q = None
        self.background_model.clear_Q(confirm=True)
        self.resolution_model.clear_Q(confirm=True)
        self._on_Q_change()

    def convert_unit(self, unit_str: str | sc.Unit) -> None:
        """
        Convert the unit of the InstrumentModel.

        Parameters
        ----------
        unit_str : str | sc.Unit
            The unit to convert to.

        Raises
        ------
        ValueError
            If unit_str is not a valid unit string or scipp Unit.
        """
        unit = _validate_unit(unit_str)
        if unit is None:
            raise ValueError('unit_str must be a valid unit string or scipp Unit')

        self._background_model.convert_unit(unit)
        self._resolution_model.convert_unit(unit)
        self._energy_offset.convert_unit(unit)
        for offset in self._energy_offsets:
            offset.convert_unit(unit)

        self._unit = unit

    def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]:
        """
        Get all variables in the InstrumentModel.

        Parameters
        ----------
        Q_index : int | None, default=None
            The index of the Q value to get variables for. If None, get variables for all Q values.


        Raises
        ------
        TypeError
            If Q_index is not an int or None.
        IndexError
            If Q_index is out of bounds for the Q values in the InstrumentModel.

        Returns
        -------
        list[Parameter]
            A list of all variables in the InstrumentModel. If Q_index is specified, only variables
            from the ComponentCollection at the given Q index are included. Otherwise, all
            variables in the InstrumentModel are included.
        """
        if self._Q is None:
            return []

        if Q_index is None:
            variables = [self._energy_offsets[i] for i in range(len(self._Q))]
        else:
            if not isinstance(Q_index, int):
                raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}')
            if Q_index < 0 or Q_index >= len(self._Q):
                raise IndexError(
                    f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}'
                )
            variables = [self._energy_offsets[Q_index]]

        variables.extend(self._background_model.get_all_variables(Q_index=Q_index))
        variables.extend(self._resolution_model.get_all_variables(Q_index=Q_index))

        return variables

    def fix_resolution_parameters(self) -> None:
        """Fix all parameters in the resolution model."""
        self.resolution_model.fix_all_parameters()

    def free_resolution_parameters(self) -> None:
        """Free all parameters in the resolution model."""
        self.resolution_model.free_all_parameters()

    def normalize_resolution(self) -> None:
        """Normalize the resolution model to have area 1."""
        self.resolution_model.normalize_area()

    def get_energy_offset(
        self,
        Q_index: int | None = None,
    ) -> Parameter | list[Parameter]:
        """
        Get the energy offset Parameter at a specific Q index.

        Parameters
        ----------
        Q_index : int | None, default=None
            The index of the Q value to get the energy offset for. If None, get the energy offset
            for all Q values.

        Raises
        ------
        ValueError
            If no Q values are set in the InstrumentModel.
        IndexError
            If Q_index is out of bounds.
        TypeError
            If Q_index is not an int or None.

        Returns
        -------
        Parameter | list[Parameter]
            The energy offset Parameter at the specified Q index, or a list of Parameters if
            Q_index is None.
        """
        if self._Q is None:
            raise ValueError('No Q values are set in the InstrumentModel.')

        if Q_index is None:
            return self._energy_offsets

        if not isinstance(Q_index, int):
            raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}')

        if Q_index < 0 or Q_index >= len(self._Q):
            raise IndexError(f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}')

        return self._energy_offsets[Q_index]

    def fix_energy_offset(self, Q_index: int | None = None) -> None:
        """
        Fix energy offset parameters.

        If Q_index is specified, only fix the energy offset for that Q value. If Q_index is None,
        fix energy offsets for all Q values.

        Parameters
        ----------
        Q_index : int | None, default=None
            The index of the Q value to fix the energy offset for. If None, fix energy offsets for
            all Q values.
        """
        self._fix_or_free_energy_offset(Q_index, fixed=True)

    def free_energy_offset(self, Q_index: int | None = None) -> None:
        """
        Free energy offset parameters.

        If Q_index is specified, only free the energy offset for that Q value. If Q_index is None,
        free energy offsets for all Q values.

        Parameters
        ----------
        Q_index : int | None, default=None
            The index of the Q value to free the energy offset for. If None, free energy offsets
            for all Q values.
        """
        self._fix_or_free_energy_offset(Q_index, fixed=False)

    # --------------------------------------------------------------
    # Private methods
    # --------------------------------------------------------------
    def _fix_or_free_energy_offset(self, Q_index: int | None = None, fixed: bool = True) -> None:
        """
        Fix or free energy offset parameters.

        If Q_index is specified, only fix or free the energy offset for that Q value. If Q_index is
        None, fix or free energy offsets for all Q values.

        Parameters
        ----------
        Q_index : int | None, default=None
            The index of the Q value to fix or free the energy offset for. If None, fix or free
            energy offsets for all Q values.
        fixed : bool, default=True
            Whether to fix (True) or free (False) the energy offset.

        Raises
        ------
        TypeError
            If Q_index is not an int or None.
        IndexError
            If Q_index is out of bounds for the Q values in the InstrumentModel.
        """

        if Q_index is None:
            for offset in self._energy_offsets:
                offset.fixed = fixed
        else:
            if not isinstance(Q_index, int):
                raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}')

            if Q_index < 0 or Q_index >= len(self._Q):
                raise IndexError(
                    f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}'
                )
            self._energy_offsets[Q_index].fixed = fixed

    def _generate_energy_offsets(self) -> None:
        """Generate energy offset Parameters for each Q value."""
        if self._Q is None:
            self._energy_offsets = []
            return

        self._energy_offsets = [copy(self._energy_offset) for _ in self._Q]

    def _on_Q_change(self) -> None:
        """Handle changes to the Q values."""
        self._generate_energy_offsets()
        self.resolution_model.Q = self.Q
        self.background_model.Q = self.Q

    def _on_energy_offset_change(self) -> None:
        """Handle changes to the energy offset."""
        for offset in self._energy_offsets:
            offset.value = self._energy_offset.value

    def _on_resolution_model_change(self) -> None:
        """Handle changes to the resolution model."""
        self.resolution_model.Q = self.Q

    def _on_background_model_change(self) -> None:
        """Handle changes to the background model."""
        self.background_model.Q = self.Q

    # -------------------------------------------------------------
    # Dunder methods
    # -------------------------------------------------------------

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

        Returns
        -------
        str
            A string representation of the InstrumentModel.
        """

        return (
            f'{self.__class__.__name__}('
            f'unique_name={self.unique_name!r}, '
            f'unit={self.unit}, '
            f'Q_len={None if self._Q is None else len(self._Q)}, '
            f'resolution_model={self._resolution_model!r}, '
            f'background_model={self._background_model!r}'
            f')'
        )

Q property writable

Get the Q values of the InstrumentModel.

Returns:

Type Description
ndarray | None

The Q values of the InstrumentModel, or None if not set.

__init__(display_name='MyInstrumentModel', unique_name=None, Q=None, resolution_model=None, background_model=None, energy_offset=None, unit='meV')

Initialize an InstrumentModel.

Parameters:

Name Type Description Default
display_name str

The display name of the InstrumentModel.

'MyInstrumentModel'
unique_name str | None

The unique name of the InstrumentModel.

None
Q Q_type | None

The Q values where the instrument is modelled.

None
resolution_model ResolutionModel | None

The resolution model of the instrument. If None, an empty resolution model is created and no resolution convolution is carried out.

None
background_model BackgroundModel | None

The background model of the instrument. If None, an empty background model is created, and the background evaluates to 0.

None
energy_offset Numeric | None

Template energy offset of the instrument. Will be copied to each Q value. If None, the energy offset will be 0.

None
unit str | Unit

The unit of the energy axis.

'meV'

Raises:

Type Description
TypeError

If resolution_model is not a ResolutionModel or None, or if background_model is not a BackgroundModel or None, or if energy_offset is not a number or None.

Source code in src/easydynamics/sample_model/instrument_model.py
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 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
def __init__(
    self,
    display_name: str = 'MyInstrumentModel',
    unique_name: str | None = None,
    Q: Q_type | None = None,
    resolution_model: ResolutionModel | None = None,
    background_model: BackgroundModel | None = None,
    energy_offset: Numeric | None = None,
    unit: str | sc.Unit = 'meV',
) -> None:
    """
    Initialize an InstrumentModel.

    Parameters
    ----------
    display_name : str, default='MyInstrumentModel'
        The display name of the InstrumentModel.
    unique_name : str | None, default=None
        The unique name of the InstrumentModel.
    Q : Q_type | None, default=None
        The Q values where the instrument is modelled.
    resolution_model : ResolutionModel | None, default=None
        The resolution model of the instrument. If None, an empty resolution model is created
        and no resolution convolution is carried out.
    background_model : BackgroundModel | None, default=None
        The background model of the instrument. If None, an empty background model is created,
        and the background evaluates to 0.
    energy_offset : Numeric | None, default=None
        Template energy offset of the instrument. Will be copied to each Q value. If None, the
        energy offset will be 0.
    unit : str | sc.Unit, default='meV'
        The unit of the energy axis.

    Raises
    ------
    TypeError
        If resolution_model is not a ResolutionModel or None, or if background_model is not a
        BackgroundModel or None, or if energy_offset is not a number or None.
    """
    super().__init__(
        display_name=display_name,
        unique_name=unique_name,
    )

    self._unit = _validate_unit(unit)

    if resolution_model is None:
        self._resolution_model = ResolutionModel()
    else:
        if not isinstance(resolution_model, ResolutionModel):
            raise TypeError(
                f'resolution_model must be a ResolutionModel or None, '
                f'got {type(resolution_model).__name__}'
            )
        self._resolution_model = resolution_model

    if background_model is None:
        self._background_model = BackgroundModel()
    else:
        if not isinstance(background_model, BackgroundModel):
            raise TypeError(
                f'background_model must be a BackgroundModel or None, '
                f'got {type(background_model).__name__}'
            )
        self._background_model = background_model

    if energy_offset is None:
        energy_offset = 0.0

    if not isinstance(energy_offset, Numeric):
        raise TypeError('energy_offset must be a number or None')

    self._energy_offset = Parameter(
        name='energy_offset',
        value=float(energy_offset),
        unit=self.unit,
        fixed=False,
    )
    self._Q = _validate_and_convert_Q(Q)
    self._on_Q_change()

__repr__()

Return a string representation of the InstrumentModel.

Returns:

Type Description
str

A string representation of the InstrumentModel.

Source code in src/easydynamics/sample_model/instrument_model.py
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
def __repr__(self) -> str:
    """
    Return a string representation of the InstrumentModel.

    Returns
    -------
    str
        A string representation of the InstrumentModel.
    """

    return (
        f'{self.__class__.__name__}('
        f'unique_name={self.unique_name!r}, '
        f'unit={self.unit}, '
        f'Q_len={None if self._Q is None else len(self._Q)}, '
        f'resolution_model={self._resolution_model!r}, '
        f'background_model={self._background_model!r}'
        f')'
    )

background_model property writable

Get the background model of the instrument.

Returns:

Type Description
BackgroundModel

The background model of the instrument.

clear_Q(confirm=False)

Clear the Q values of the InstrumentModel and any associated ResolutionModel and BackgroundModel, removing all component collections and their associated Parameters.

Parameters:

Name Type Description Default
confirm bool

Confirmation to clear Q values.

False

Raises:

Type Description
ValueError

If confirm is not True.

Source code in src/easydynamics/sample_model/instrument_model.py
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
def clear_Q(self, confirm: bool = False) -> None:
    """
    Clear the Q values of the InstrumentModel and any associated ResolutionModel and
    BackgroundModel, removing all component collections and their associated Parameters.

    Parameters
    ----------
    confirm : bool, default=False
        Confirmation to clear Q values.

    Raises
    ------
    ValueError
        If confirm is not True.
    """
    if not confirm:
        raise ValueError(
            'Clearing Q values requires confirmation. Set confirm=True to proceed.'
        )
    self._Q = None
    self.background_model.clear_Q(confirm=True)
    self.resolution_model.clear_Q(confirm=True)
    self._on_Q_change()

convert_unit(unit_str)

Convert the unit of the InstrumentModel.

Parameters:

Name Type Description Default
unit_str str | Unit

The unit to convert to.

required

Raises:

Type Description
ValueError

If unit_str is not a valid unit string or scipp Unit.

Source code in src/easydynamics/sample_model/instrument_model.py
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
def convert_unit(self, unit_str: str | sc.Unit) -> None:
    """
    Convert the unit of the InstrumentModel.

    Parameters
    ----------
    unit_str : str | sc.Unit
        The unit to convert to.

    Raises
    ------
    ValueError
        If unit_str is not a valid unit string or scipp Unit.
    """
    unit = _validate_unit(unit_str)
    if unit is None:
        raise ValueError('unit_str must be a valid unit string or scipp Unit')

    self._background_model.convert_unit(unit)
    self._resolution_model.convert_unit(unit)
    self._energy_offset.convert_unit(unit)
    for offset in self._energy_offsets:
        offset.convert_unit(unit)

    self._unit = unit

energy_offset property writable

Get the energy offset template parameter of the instrument model.

Returns:

Type Description
Parameter

The energy offset template parameter of the instrument model.

fix_energy_offset(Q_index=None)

Fix energy offset parameters.

If Q_index is specified, only fix the energy offset for that Q value. If Q_index is None, fix energy offsets for all Q values.

Parameters:

Name Type Description Default
Q_index int | None

The index of the Q value to fix the energy offset for. If None, fix energy offsets for all Q values.

None
Source code in src/easydynamics/sample_model/instrument_model.py
448
449
450
451
452
453
454
455
456
457
458
459
460
461
def fix_energy_offset(self, Q_index: int | None = None) -> None:
    """
    Fix energy offset parameters.

    If Q_index is specified, only fix the energy offset for that Q value. If Q_index is None,
    fix energy offsets for all Q values.

    Parameters
    ----------
    Q_index : int | None, default=None
        The index of the Q value to fix the energy offset for. If None, fix energy offsets for
        all Q values.
    """
    self._fix_or_free_energy_offset(Q_index, fixed=True)

fix_resolution_parameters()

Fix all parameters in the resolution model.

Source code in src/easydynamics/sample_model/instrument_model.py
394
395
396
def fix_resolution_parameters(self) -> None:
    """Fix all parameters in the resolution model."""
    self.resolution_model.fix_all_parameters()

free_energy_offset(Q_index=None)

Free energy offset parameters.

If Q_index is specified, only free the energy offset for that Q value. If Q_index is None, free energy offsets for all Q values.

Parameters:

Name Type Description Default
Q_index int | None

The index of the Q value to free the energy offset for. If None, free energy offsets for all Q values.

None
Source code in src/easydynamics/sample_model/instrument_model.py
463
464
465
466
467
468
469
470
471
472
473
474
475
476
def free_energy_offset(self, Q_index: int | None = None) -> None:
    """
    Free energy offset parameters.

    If Q_index is specified, only free the energy offset for that Q value. If Q_index is None,
    free energy offsets for all Q values.

    Parameters
    ----------
    Q_index : int | None, default=None
        The index of the Q value to free the energy offset for. If None, free energy offsets
        for all Q values.
    """
    self._fix_or_free_energy_offset(Q_index, fixed=False)

free_resolution_parameters()

Free all parameters in the resolution model.

Source code in src/easydynamics/sample_model/instrument_model.py
398
399
400
def free_resolution_parameters(self) -> None:
    """Free all parameters in the resolution model."""
    self.resolution_model.free_all_parameters()

get_all_variables(Q_index=None)

Get all variables in the InstrumentModel.

Parameters:

Name Type Description Default
Q_index int | None

The index of the Q value to get variables for. If None, get variables for all Q values.

None

Raises:

Type Description
TypeError

If Q_index is not an int or None.

IndexError

If Q_index is out of bounds for the Q values in the InstrumentModel.

Returns:

Type Description
list[Parameter]

A list of all variables in the InstrumentModel. If Q_index is specified, only variables from the ComponentCollection at the given Q index are included. Otherwise, all variables in the InstrumentModel are included.

Source code in src/easydynamics/sample_model/instrument_model.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]:
    """
    Get all variables in the InstrumentModel.

    Parameters
    ----------
    Q_index : int | None, default=None
        The index of the Q value to get variables for. If None, get variables for all Q values.


    Raises
    ------
    TypeError
        If Q_index is not an int or None.
    IndexError
        If Q_index is out of bounds for the Q values in the InstrumentModel.

    Returns
    -------
    list[Parameter]
        A list of all variables in the InstrumentModel. If Q_index is specified, only variables
        from the ComponentCollection at the given Q index are included. Otherwise, all
        variables in the InstrumentModel are included.
    """
    if self._Q is None:
        return []

    if Q_index is None:
        variables = [self._energy_offsets[i] for i in range(len(self._Q))]
    else:
        if not isinstance(Q_index, int):
            raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}')
        if Q_index < 0 or Q_index >= len(self._Q):
            raise IndexError(
                f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}'
            )
        variables = [self._energy_offsets[Q_index]]

    variables.extend(self._background_model.get_all_variables(Q_index=Q_index))
    variables.extend(self._resolution_model.get_all_variables(Q_index=Q_index))

    return variables

get_energy_offset(Q_index=None)

Get the energy offset Parameter at a specific Q index.

Parameters:

Name Type Description Default
Q_index int | None

The index of the Q value to get the energy offset for. If None, get the energy offset for all Q values.

None

Raises:

Type Description
ValueError

If no Q values are set in the InstrumentModel.

IndexError

If Q_index is out of bounds.

TypeError

If Q_index is not an int or None.

Returns:

Type Description
Parameter | list[Parameter]

The energy offset Parameter at the specified Q index, or a list of Parameters if Q_index is None.

Source code in src/easydynamics/sample_model/instrument_model.py
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
def get_energy_offset(
    self,
    Q_index: int | None = None,
) -> Parameter | list[Parameter]:
    """
    Get the energy offset Parameter at a specific Q index.

    Parameters
    ----------
    Q_index : int | None, default=None
        The index of the Q value to get the energy offset for. If None, get the energy offset
        for all Q values.

    Raises
    ------
    ValueError
        If no Q values are set in the InstrumentModel.
    IndexError
        If Q_index is out of bounds.
    TypeError
        If Q_index is not an int or None.

    Returns
    -------
    Parameter | list[Parameter]
        The energy offset Parameter at the specified Q index, or a list of Parameters if
        Q_index is None.
    """
    if self._Q is None:
        raise ValueError('No Q values are set in the InstrumentModel.')

    if Q_index is None:
        return self._energy_offsets

    if not isinstance(Q_index, int):
        raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}')

    if Q_index < 0 or Q_index >= len(self._Q):
        raise IndexError(f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}')

    return self._energy_offsets[Q_index]

normalize_resolution()

Normalize the resolution model to have area 1.

Source code in src/easydynamics/sample_model/instrument_model.py
402
403
404
def normalize_resolution(self) -> None:
    """Normalize the resolution model to have area 1."""
    self.resolution_model.normalize_area()

resolution_model property writable

Get the resolution model of the instrument.

Returns:

Type Description
ResolutionModel

The resolution model of the instrument.

unit property writable

Get the unit of the InstrumentModel.

Returns:

Type Description
str | Unit

The unit of the InstrumentModel.

model_base

ModelBase

Bases: EasyDynamicsModelBase

Base class for Sample Models.

Contains common functionality for models with components and Q dependence.

Source code in src/easydynamics/sample_model/model_base.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
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
class ModelBase(EasyDynamicsModelBase):
    """
    Base class for Sample Models.

    Contains common functionality for models with components and Q dependence.
    """

    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,
    ) -> None:
        """
        Initialize the ModelBase.

        Parameters
        ----------
        display_name : str, default='MyModelBase'
            Display name of the model.
        unique_name : str | None, default=None
            Unique name of the model. If None, a unique name will be generated.
        unit : str | sc.Unit | None, default='meV'
            Unit of the model.
        components : ModelComponent | ComponentCollection | None, default=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, default=None
            Q values for the model. If None, Q is not set.

        Raises
        ------
        TypeError
            If components is not a ModelComponent or ComponentCollection.
        """
        super().__init__(
            unit=unit,
            display_name=display_name,
            unique_name=unique_name,
        )
        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 : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            Energy axis values to evaluate the model at. If a scipp Variable or DataArray is
            provided, the unit of the model will be converted to match the unit of x for
            evaluation, and the result will be returned in the same unit as x.

        Raises
        ------
        ValueError
            If there are no components in the model to evaluate.

        Returns
        -------
        list[np.ndarray]
            A list of numpy arrays containing the evaluated model values for each Q. The length of
            the list will match the number of Q values in the model.
        """

        if not self._component_collections:
            raise ValueError(
                'No components in the model to evaluate. '
                'Run generate_component_collections() first'
            )
        return [collection.evaluate(x) for collection in self._component_collections]

    # ------------------------------------------------------------------
    # Component management
    # ------------------------------------------------------------------
    def append_component(self, component: ModelComponent | ComponentCollection) -> None:
        """
        Append a ModelComponent or ComponentCollection to the SampleModel.

        Parameters
        ----------
        component : ModelComponent | ComponentCollection
            The ModelComponent or ComponentCollection to append.
        """
        self._components.append_component(component)
        self._on_components_change()

    def remove_component(self, unique_name: str) -> None:
        """
        Remove a ModelComponent from the SampleModel by its unique name.

        Parameters
        ----------
        unique_name : str
            The unique name of the ModelComponent to remove.
        """
        self._components.remove_component(unique_name)
        self._on_components_change()

    def clear_components(self) -> None:
        """Clear all ModelComponents from the SampleModel."""
        self._components.clear_components()
        self._on_components_change()

    # ------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------

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

        Returns
        -------
        str | sc.Unit | None
            The unit of the SampleModel.
        """

        return self._unit

    @unit.setter
    def unit(self, _unit_str: str) -> None:
        """
        Unit is read-only and cannot be set directly.

        Parameters
        ----------
        _unit_str : str
            The new unit to set (ignored).

        Raises
        ------
        AttributeError
            Always raised to indicate that the unit is read-only.
        """
        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.'
        )

    @property
    def components(self) -> list[ModelComponent]:
        """
        Get the components of the SampleModel.

        Returns
        -------
        list[ModelComponent]
            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.

        Parameters
        ----------
        value : ModelComponent | ComponentCollection | None
            The new components to set. If None, all components will be cleared.

        Raises
        ------
        TypeError
            If value is not a ModelComponent, ComponentCollection, or None.
        """
        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.

        Returns
        -------
        np.ndarray | None
            The Q values of the SampleModel, or None if not set.
        """
        return self._Q

    @Q.setter
    def Q(self, value: Q_type | None) -> None:
        """
        Set the Q values of the SampleModel.

        If Q is already set, it throws an error if the new Q values are not similar to the old
        ones. To change Q values, first run clear_Q().

        Parameters
        ----------
        value : Q_type | None
            The new Q values to set. If None, Q values are not changed.

        Raises
        ------
        ValueError
            If the new Q values are not similar to the old ones when Q is already set.
        """
        if value is None:
            return
        old_Q = self._Q
        new_Q = _validate_and_convert_Q(value)

        if old_Q is None:
            self._Q = new_Q
            self._on_Q_change()
            return

        if len(old_Q) != len(new_Q) or not np.allclose(old_Q, new_Q):
            raise ValueError(
                'New Q values are not similar to the old ones. '
                'To change Q values, first run clear_Q().'
            )

    def clear_Q(self, confirm: bool = False) -> None:
        """
        Clear the Q values of the SampleModel, removing all component collections and their
        associated Parameters.

        Parameters
        ----------
        confirm : bool, default=False
            Confirmation to clear Q values.

        Raises
        ------
        ValueError
            If confirm is not True.
        """
        if not confirm:
            raise ValueError(
                'Clearing Q values requires confirmation. Set confirm=True to proceed.'
            )
        self._Q = None
        self._on_Q_change()

    # ------------------------------------------------------------------
    # Other methods
    # ------------------------------------------------------------------

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

        Parameters
        ----------
        unit : str | sc.Unit
            The new unit to convert to.

        Raises
        ------
        TypeError
            If the provided unit is not a string or sc.Unit.
        Exception
            If the provided unit is not compatible with the current unit.
        """

        old_unit = self._unit

        if not isinstance(unit, (str, sc.Unit)):
            raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}')
        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._on_components_change()

    def fix_all_parameters(self) -> None:
        """Fix all Parameters in all ComponentCollections."""
        for par in self.get_all_variables():
            par.fixed = True

    def free_all_parameters(self) -> None:
        """Free all Parameters in all ComponentCollections."""
        for par in self.get_all_variables():
            par.fixed = False

    def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]:
        """
        Get all Parameters and Descriptors from all ComponentCollections in the ModelBase.
        Parameters Ignores the Parameters and Descriptors in self._components as these are just
        templates.

        Parameters
        ----------
        Q_index : int | None, default=None
            If None, get variables for all ComponentCollections. If int, get variables for the
            ComponentCollection at this index.

        Raises
        ------
        TypeError
            If Q_index is not an int or None.
        IndexError
            If Q_index is out of bounds for the number of ComponentCollections.

        Returns
        -------
        list[Parameter]
            A list of all Parameters and Descriptors from the ComponentCollections in the
            ModelBase.
        """

        if Q_index is None:
            all_vars = [
                var
                for collection in self._component_collections
                for var in collection.get_all_variables()
            ]
        else:
            if not isinstance(Q_index, int):
                raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}')
            if Q_index < 0 or Q_index >= len(self._component_collections):
                raise IndexError(
                    f'Q_index {Q_index} is out of bounds for component collections '
                    f'of length {len(self._component_collections)}'
                )
            all_vars = self._component_collections[Q_index].get_all_variables()
        return all_vars

    def get_component_collection(self, Q_index: int) -> ComponentCollection:
        """
        Get the ComponentCollection at the given Q index.

        Parameters
        ----------
        Q_index : int
            The index of the desired ComponentCollection.

        Raises
        ------
        TypeError
            If Q_index is not an int.
        IndexError
            If Q_index is out of bounds for the number of ComponentCollections.

        Returns
        -------
        ComponentCollection
            The ComponentCollection at the.
        """
        if not isinstance(Q_index, int):
            raise TypeError(f'Q_index must be an int, got {type(Q_index).__name__}')
        if Q_index < 0 or Q_index >= len(self._component_collections):
            raise IndexError(
                f'Q_index {Q_index} is out of bounds for component collections '
                f'of length {len(self._component_collections)}'
            )
        return self._component_collections[Q_index]

    def normalize_area(self) -> None:
        """Normalize the area of the model across all Q values."""
        for collection in self._component_collections:
            collection.normalize_area()

    # ------------------------------------------------------------------
    # Private methods
    # ------------------------------------------------------------------

    def _generate_component_collections(self) -> None:
        """Generate ComponentCollections for each Q value."""

        if self._Q is None:
            self._component_collections = []
            return

        self._component_collections = []
        for _ in self._Q:
            self._component_collections.append(copy(self._components))

    def _on_Q_change(self) -> None:
        """Handle changes to the Q values."""
        self._generate_component_collections()

    def _on_components_change(self) -> None:
        """Handle changes to the components."""
        self._generate_component_collections()

    # ------------------------------------------------------------------
    # dunder methods
    # ------------------------------------------------------------------

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

        Returns
        -------
        str
            A string representation of the ModelBase.
        """
        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.

Returns:

Type Description
ndarray | None

The Q values of the SampleModel, or None if not set.

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

Initialize the ModelBase.

Parameters:

Name Type Description Default
display_name str

Display name of the model.

'MyModelBase'
unique_name str | None

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

None
unit str | Unit | None

Unit of the model.

'meV'
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.

None
Q Q_type | None

Q values for the model. If None, Q is not set.

None

Raises:

Type Description
TypeError

If components is not a ModelComponent or ComponentCollection.

Source code in src/easydynamics/sample_model/model_base.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
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,
) -> None:
    """
    Initialize the ModelBase.

    Parameters
    ----------
    display_name : str, default='MyModelBase'
        Display name of the model.
    unique_name : str | None, default=None
        Unique name of the model. If None, a unique name will be generated.
    unit : str | sc.Unit | None, default='meV'
        Unit of the model.
    components : ModelComponent | ComponentCollection | None, default=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, default=None
        Q values for the model. If None, Q is not set.

    Raises
    ------
    TypeError
        If components is not a ModelComponent or ComponentCollection.
    """
    super().__init__(
        unit=unit,
        display_name=display_name,
        unique_name=unique_name,
    )
    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()

__repr__()

Return a string representation of the ModelBase.

Returns:

Type Description
str

A string representation of the ModelBase.

Source code in src/easydynamics/sample_model/model_base.py
433
434
435
436
437
438
439
440
441
442
443
444
445
def __repr__(self) -> str:
    """
    Return a string representation of the ModelBase.

    Returns
    -------
    str
        A string representation of the ModelBase.
    """
    return (
        f'{self.__class__.__name__}(unique_name={self.unique_name}, '
        f'unit={self.unit}), Q = {self.Q}, components = {self.components}'
    )

append_component(component)

Append a ModelComponent or ComponentCollection to the SampleModel.

Parameters:

Name Type Description Default
component ModelComponent | ComponentCollection

The ModelComponent or ComponentCollection to append.

required
Source code in src/easydynamics/sample_model/model_base.py
111
112
113
114
115
116
117
118
119
120
121
def append_component(self, component: ModelComponent | ComponentCollection) -> None:
    """
    Append a ModelComponent or ComponentCollection to the SampleModel.

    Parameters
    ----------
    component : ModelComponent | ComponentCollection
        The ModelComponent or ComponentCollection to append.
    """
    self._components.append_component(component)
    self._on_components_change()

clear_Q(confirm=False)

Clear the Q values of the SampleModel, removing all component collections and their associated Parameters.

Parameters:

Name Type Description Default
confirm bool

Confirmation to clear Q values.

False

Raises:

Type Description
ValueError

If confirm is not True.

Source code in src/easydynamics/sample_model/model_base.py
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
def clear_Q(self, confirm: bool = False) -> None:
    """
    Clear the Q values of the SampleModel, removing all component collections and their
    associated Parameters.

    Parameters
    ----------
    confirm : bool, default=False
        Confirmation to clear Q values.

    Raises
    ------
    ValueError
        If confirm is not True.
    """
    if not confirm:
        raise ValueError(
            'Clearing Q values requires confirmation. Set confirm=True to proceed.'
        )
    self._Q = None
    self._on_Q_change()

clear_components()

Clear all ModelComponents from the SampleModel.

Source code in src/easydynamics/sample_model/model_base.py
135
136
137
138
def clear_components(self) -> None:
    """Clear all ModelComponents from the SampleModel."""
    self._components.clear_components()
    self._on_components_change()

components property writable

Get the components of the SampleModel.

Returns:

Type Description
list[ModelComponent]

The components of the SampleModel.

convert_unit(unit)

Convert the unit of the ComponentCollection and all its components.

Parameters:

Name Type Description Default
unit str | Unit

The new unit to convert to.

required

Raises:

Type Description
TypeError

If the provided unit is not a string or sc.Unit.

Exception

If the provided unit is not compatible with the current unit.

Source code in src/easydynamics/sample_model/model_base.py
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
def convert_unit(self, unit: str | sc.Unit) -> None:
    """
    Convert the unit of the ComponentCollection and all its components.

    Parameters
    ----------
    unit : str | sc.Unit
        The new unit to convert to.

    Raises
    ------
    TypeError
        If the provided unit is not a string or sc.Unit.
    Exception
        If the provided unit is not compatible with the current unit.
    """

    old_unit = self._unit

    if not isinstance(unit, (str, sc.Unit)):
        raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}')
    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._on_components_change()

evaluate(x)

Evaluate the sample model at all Q for the given x values.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

Energy axis values to evaluate the model at. If a scipp Variable or DataArray is provided, the unit of the model will be converted to match the unit of x for evaluation, and the result will be returned in the same unit as x.

required

Raises:

Type Description
ValueError

If there are no components in the model to evaluate.

Returns:

Type Description
list[ndarray]

A list of numpy arrays containing the evaluated model values for each Q. The length of the list will match the number of Q values in the model.

Source code in src/easydynamics/sample_model/model_base.py
 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
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 : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        Energy axis values to evaluate the model at. If a scipp Variable or DataArray is
        provided, the unit of the model will be converted to match the unit of x for
        evaluation, and the result will be returned in the same unit as x.

    Raises
    ------
    ValueError
        If there are no components in the model to evaluate.

    Returns
    -------
    list[np.ndarray]
        A list of numpy arrays containing the evaluated model values for each Q. The length of
        the list will match the number of Q values in the model.
    """

    if not self._component_collections:
        raise ValueError(
            'No components in the model to evaluate. '
            'Run generate_component_collections() first'
        )
    return [collection.evaluate(x) for collection in self._component_collections]

fix_all_parameters()

Fix all Parameters in all ComponentCollections.

Source code in src/easydynamics/sample_model/model_base.py
318
319
320
321
def fix_all_parameters(self) -> None:
    """Fix all Parameters in all ComponentCollections."""
    for par in self.get_all_variables():
        par.fixed = True

free_all_parameters()

Free all Parameters in all ComponentCollections.

Source code in src/easydynamics/sample_model/model_base.py
323
324
325
326
def free_all_parameters(self) -> None:
    """Free all Parameters in all ComponentCollections."""
    for par in self.get_all_variables():
        par.fixed = False

get_all_variables(Q_index=None)

Get all Parameters and Descriptors from all ComponentCollections in the ModelBase. Parameters Ignores the Parameters and Descriptors in self._components as these are just templates.

Parameters:

Name Type Description Default
Q_index int | None

If None, get variables for all ComponentCollections. If int, get variables for the ComponentCollection at this index.

None

Raises:

Type Description
TypeError

If Q_index is not an int or None.

IndexError

If Q_index is out of bounds for the number of ComponentCollections.

Returns:

Type Description
list[Parameter]

A list of all Parameters and Descriptors from the ComponentCollections in the ModelBase.

Source code in src/easydynamics/sample_model/model_base.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]:
    """
    Get all Parameters and Descriptors from all ComponentCollections in the ModelBase.
    Parameters Ignores the Parameters and Descriptors in self._components as these are just
    templates.

    Parameters
    ----------
    Q_index : int | None, default=None
        If None, get variables for all ComponentCollections. If int, get variables for the
        ComponentCollection at this index.

    Raises
    ------
    TypeError
        If Q_index is not an int or None.
    IndexError
        If Q_index is out of bounds for the number of ComponentCollections.

    Returns
    -------
    list[Parameter]
        A list of all Parameters and Descriptors from the ComponentCollections in the
        ModelBase.
    """

    if Q_index is None:
        all_vars = [
            var
            for collection in self._component_collections
            for var in collection.get_all_variables()
        ]
    else:
        if not isinstance(Q_index, int):
            raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}')
        if Q_index < 0 or Q_index >= len(self._component_collections):
            raise IndexError(
                f'Q_index {Q_index} is out of bounds for component collections '
                f'of length {len(self._component_collections)}'
            )
        all_vars = self._component_collections[Q_index].get_all_variables()
    return all_vars

get_component_collection(Q_index)

Get the ComponentCollection at the given Q index.

Parameters:

Name Type Description Default
Q_index int

The index of the desired ComponentCollection.

required

Raises:

Type Description
TypeError

If Q_index is not an int.

IndexError

If Q_index is out of bounds for the number of ComponentCollections.

Returns:

Type Description
ComponentCollection

The ComponentCollection at the.

Source code in src/easydynamics/sample_model/model_base.py
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
def get_component_collection(self, Q_index: int) -> ComponentCollection:
    """
    Get the ComponentCollection at the given Q index.

    Parameters
    ----------
    Q_index : int
        The index of the desired ComponentCollection.

    Raises
    ------
    TypeError
        If Q_index is not an int.
    IndexError
        If Q_index is out of bounds for the number of ComponentCollections.

    Returns
    -------
    ComponentCollection
        The ComponentCollection at the.
    """
    if not isinstance(Q_index, int):
        raise TypeError(f'Q_index must be an int, got {type(Q_index).__name__}')
    if Q_index < 0 or Q_index >= len(self._component_collections):
        raise IndexError(
            f'Q_index {Q_index} is out of bounds for component collections '
            f'of length {len(self._component_collections)}'
        )
    return self._component_collections[Q_index]

normalize_area()

Normalize the area of the model across all Q values.

Source code in src/easydynamics/sample_model/model_base.py
401
402
403
404
def normalize_area(self) -> None:
    """Normalize the area of the model across all Q values."""
    for collection in self._component_collections:
        collection.normalize_area()

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 to remove.

required
Source code in src/easydynamics/sample_model/model_base.py
123
124
125
126
127
128
129
130
131
132
133
def remove_component(self, unique_name: str) -> None:
    """
    Remove a ModelComponent from the SampleModel by its unique name.

    Parameters
    ----------
    unique_name : str
        The unique name of the ModelComponent to remove.
    """
    self._components.remove_component(unique_name)
    self._on_components_change()

unit property writable

Get the unit of the SampleModel.

Returns:

Type Description
str | Unit | None

The unit of the SampleModel.

resolution_model

ResolutionModel

Bases: ModelBase

ResolutionModel represents a model of the instrment resolution in an experiment at various Q.

Source code in src/easydynamics/sample_model/resolution_model.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
class ResolutionModel(ModelBase):
    """
    ResolutionModel represents a model of the instrment resolution in an experiment at various Q.
    """

    def __init__(
        self,
        display_name: str = 'MyResolutionModel',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
        components: ModelComponent | ComponentCollection | None = None,
        Q: Q_type | None = None,
    ) -> None:
        """
        Initialize a ResolutionModel.

        Parameters
        ----------
        display_name : str, default='MyResolutionModel'
            Display name of the model.
        unique_name : str | None, default=None
            Unique name of the model. If None, a unique name will be generated.
        unit : str | sc.Unit, default='meV'
            Unit of the model.
        components : ModelComponent | ComponentCollection | None, default=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, default=None
            Q values for the model. If None, Q is not set.
        """

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

    def append_component(self, component: ModelComponent | ComponentCollection) -> None:
        """
        Append a component to the ResolutionModel.

        Does not allow DeltaFunction or Polynomial components, as these are not physical resolution
        components.

        Parameters
        ----------
        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)

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

Initialize a ResolutionModel.

Parameters:

Name Type Description Default
display_name str

Display name of the model.

'MyResolutionModel'
unique_name str | None

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

None
unit str | Unit

Unit of the model.

'meV'
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.

None
Q Q_type | None

Q values for the model. If None, Q is not set.

None
Source code in src/easydynamics/sample_model/resolution_model.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
def __init__(
    self,
    display_name: str = 'MyResolutionModel',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
    components: ModelComponent | ComponentCollection | None = None,
    Q: Q_type | None = None,
) -> None:
    """
    Initialize a ResolutionModel.

    Parameters
    ----------
    display_name : str, default='MyResolutionModel'
        Display name of the model.
    unique_name : str | None, default=None
        Unique name of the model. If None, a unique name will be generated.
    unit : str | sc.Unit, default='meV'
        Unit of the model.
    components : ModelComponent | ComponentCollection | None, default=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, default=None
        Q values for the model. If None, Q is not set.
    """

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

append_component(component)

Append a component to the ResolutionModel.

Does not allow DeltaFunction or Polynomial components, as these are not physical resolution components.

Parameters:

Name Type Description Default
component ModelComponent | ComponentCollection

Component(s) to append.

required

Raises:

Type Description
TypeError

If the component is a DeltaFunction or Polynomial.

Source code in src/easydynamics/sample_model/resolution_model.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def append_component(self, component: ModelComponent | ComponentCollection) -> None:
    """
    Append a component to the ResolutionModel.

    Does not allow DeltaFunction or Polynomial components, as these are not physical resolution
    components.

    Parameters
    ----------
    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.

Source code in src/easydynamics/sample_model/sample_model.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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
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.
    """

    def __init__(
        self,
        display_name: str = 'MySampleModel',
        unique_name: str | None = None,
        unit: str | sc.Unit = 'meV',
        components: ModelComponent | ComponentCollection | None = None,
        Q: Q_type | None = None,
        diffusion_models: DiffusionModelBase | list[DiffusionModelBase] | None = None,
        temperature: float | None = None,
        temperature_unit: str | sc.Unit = 'K',
        detailed_balance_settings: DetailedBalanceSettings | None = None,
    ) -> None:
        """
        Initialize the SampleModel.

        Parameters
        ----------
        display_name : str, default='MySampleModel'
            Display name of the model.
        unique_name : str | None, default=None
            Unique name of the model. If None, a unique name will be generated.
        unit : str | sc.Unit, default='meV'
            Unit of the model. If None,.
        components : ModelComponent | ComponentCollection | None, default=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, default=None
            Q values for the model. If None, Q is not set.
        diffusion_models : DiffusionModelBase | list[DiffusionModelBase] | None, default=None
            Diffusion models to include in the SampleModel. If None, no diffusion models are added.
        temperature : float | None, default=None
            Temperature for detailed balancing. If None, no detailed balancing is applied. By
            default, None.
        temperature_unit : str | sc.Unit, default='K'
            Unit of the temperature.
        detailed_balance_settings : DetailedBalanceSettings | None, default=None
            Settings for detailed balancing.

        Raises
        ------
        TypeError
            If diffusion_models is not a DiffusionModelBase, a list of DiffusionModelBase, or None,
            or if temperature is not a number or None, or if detailed_balance_settings is not a
            DetailedBalanceSettings instance.
        ValueError
            If temperature is negative.
        """
        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 detailed_balance_settings is None:
            self._detailed_balance_settings = DetailedBalanceSettings()
        elif isinstance(detailed_balance_settings, DetailedBalanceSettings):
            self._detailed_balance_settings = detailed_balance_settings
        else:
            raise TypeError('detailed_balance_settings must be a DetailedBalanceSettings or None')

    # ------------------------------------------------------------------
    # Component management
    # ------------------------------------------------------------------

    def append_diffusion_model(self, diffusion_model: DiffusionModelBase) -> None:
        """
        Append a DiffusionModel to the SampleModel.

        Parameters
        ----------
        diffusion_model : DiffusionModelBase
            The DiffusionModel to append.

        Raises
        ------
        TypeError
            If the diffusion_model is not a DiffusionModelBase.
        """

        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.

        Parameters
        ----------
        name : 'str'
            The unique name of the DiffusionModel to remove.

        Raises
        ------
        ValueError
            If no DiffusionModel with the given unique name is found.
        """
        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.

        Returns
        -------
        list[DiffusionModelBase]
            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.

        Parameters
        ----------
        value : DiffusionModelBase | list[DiffusionModelBase] | None
            The diffusion model(s) to set. Can be a single DiffusionModelBase, a list of
            DiffusionModelBase, or None to clear all diffusion models.

        Raises
        ------
        TypeError
            If value is not a DiffusionModelBase, a list of DiffusionModelBase, or None.
        """

        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._on_diffusion_models_change()

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

        Returns
        -------
        Parameter | None
            The temperature Parameter of the SampleModel, or None if not set.
        """
        return self._temperature

    @temperature.setter
    def temperature(self, value: Numeric | None) -> None:
        """
        Set the temperature of the SampleModel.

        Parameters
        ----------
        value : Numeric | None
            The temperature value to set. Can be a number or None to unset the temperature.

        Raises
        ------
        TypeError
            If value is not a number or None.
        ValueError
            If value is negative.
        """
        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.

        Returns
        -------
        str | sc.Unit
            The unit of the temperature Parameter.
        """
        return self._temperature_unit

    @temperature_unit.setter
    def temperature_unit(self, _value: str | sc.Unit) -> None:
        """
        The temperature unit of the SampleModel is read-only.

        Parameters
        ----------
        _value : str | sc.Unit
            The unit to set for the temperature Parameter.

        Raises
        ------
        AttributeError
            Always, as temperature_unit is read-only.
        """

        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.'
        )

    def convert_temperature_unit(self, unit: str | sc.Unit) -> None:
        """
        Convert the unit of the temperature Parameter.

        Parameters
        ----------
        unit : str | sc.Unit
            The unit to convert the temperature Parameter to.

        Raises
        ------
        ValueError
            If temperature is not set or conversion fails.
        Exception
            If the provided unit is invalid or cannot be converted.
        """

        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:
            # Attempt to rollback on failure
            with suppress(Exception):
                self.temperature.convert_unit(old_unit)
            raise

    @property
    def normalize_detailed_balance(self) -> bool:
        """
        Get whether to divide the detailed balance factor by temperature.

        Returns
        -------
        bool
            True if the detailed balance factor is divided by temperature, False otherwise.
        """
        return self.detailed_balance_settings.normalize_detailed_balance

    @normalize_detailed_balance.setter
    def normalize_detailed_balance(self, value: bool) -> None:
        """
        Set whether to divide the detailed balance factor by temperature.

        Parameters
        ----------
        value : bool
            True to divide the detailed balance factor by temperature, False otherwise.

        Raises
        ------
        TypeError
            If value is not a bool.
        """
        if not isinstance(value, bool):
            raise TypeError('normalize_detailed_balance must be True or False')
        self.detailed_balance_settings.normalize_detailed_balance = value

    @property
    def use_detailed_balance(self) -> bool:
        """
        Get whether to apply detailed balance to the model.

        Returns
        -------
        bool
            True if detailed balance is applied, False otherwise.
        """
        return self.detailed_balance_settings.use_detailed_balance

    @use_detailed_balance.setter
    def use_detailed_balance(self, value: bool) -> None:
        """
        Set whether to apply detailed balance to the model.

        Parameters
        ----------
        value : bool
            True to apply detailed balance, False otherwise.

        Raises
        ------
        TypeError
            If value is not a bool.
        """
        if not isinstance(value, bool):
            raise TypeError('use_detailed_balance must be True or False')
        self.detailed_balance_settings.use_detailed_balance = value

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

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

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

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

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

    # ------------------------------------------------------------------
    # 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 : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
            The x values to evaluate the model at. Can be a number, list, numpy array, scipp
            Variable, or scipp DataArray.

        Returns
        -------
        list[np.ndarray]
            List of evaluated model values for each Q.
        """

        y = super().evaluate(x)

        if self.temperature is not None and self.detailed_balance_settings.use_detailed_balance:
            DBF = detailed_balance_factor(
                energy=x,
                temperature=self.temperature,
                divide_by_temperature=self.detailed_balance_settings.normalize_detailed_balance,
                energy_unit=self.unit,
            )
            y = [yi * DBF for yi in y]

        return y

    def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]:
        """
        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.

        Parameters
        ----------
        Q_index : int | None, default=None
            If specified, only get variables from the ComponentCollection at the given Q index. If
            None, get variables from all ComponentCollections.

        Returns
        -------
        list[Parameter]
            List of all Parameters and Descriptors, including temperature if set and all variables
            from diffusion models.
        """

        all_vars = super().get_all_variables(Q_index=Q_index)
        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.
        """
        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, strict=True
            ):
                for component in source.components:
                    target.append_component(component)

    def _on_diffusion_models_change(self) -> None:
        """Handle changes to the diffusion models."""
        self._generate_component_collections()

    # ------------------------------------------------------------------
    # dunder methods
    # ------------------------------------------------------------------

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

        Returns
        -------
        str
            A string representation of the SampleModel.
        """

        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'detailed_balance_settings = {self.detailed_balance_settings}'
        )

__init__(display_name='MySampleModel', unique_name=None, unit='meV', components=None, Q=None, diffusion_models=None, temperature=None, temperature_unit='K', detailed_balance_settings=None)

Initialize the SampleModel.

Parameters:

Name Type Description Default
display_name str

Display name of the model.

'MySampleModel'
unique_name str | None

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

None
unit str | Unit

Unit of the model. If None,.

'meV'
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.

None
Q Q_type | None

Q values for the model. If None, Q is not set.

None
diffusion_models DiffusionModelBase | list[DiffusionModelBase] | None

Diffusion models to include in the SampleModel. If None, no diffusion models are added.

None
temperature float | None

Temperature for detailed balancing. If None, no detailed balancing is applied. By default, None.

None
temperature_unit str | Unit

Unit of the temperature.

'K'
detailed_balance_settings DetailedBalanceSettings | None

Settings for detailed balancing.

None

Raises:

Type Description
TypeError

If diffusion_models is not a DiffusionModelBase, a list of DiffusionModelBase, or None, or if temperature is not a number or None, or if detailed_balance_settings is not a DetailedBalanceSettings instance.

ValueError

If temperature is negative.

Source code in src/easydynamics/sample_model/sample_model.py
 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
def __init__(
    self,
    display_name: str = 'MySampleModel',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
    components: ModelComponent | ComponentCollection | None = None,
    Q: Q_type | None = None,
    diffusion_models: DiffusionModelBase | list[DiffusionModelBase] | None = None,
    temperature: float | None = None,
    temperature_unit: str | sc.Unit = 'K',
    detailed_balance_settings: DetailedBalanceSettings | None = None,
) -> None:
    """
    Initialize the SampleModel.

    Parameters
    ----------
    display_name : str, default='MySampleModel'
        Display name of the model.
    unique_name : str | None, default=None
        Unique name of the model. If None, a unique name will be generated.
    unit : str | sc.Unit, default='meV'
        Unit of the model. If None,.
    components : ModelComponent | ComponentCollection | None, default=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, default=None
        Q values for the model. If None, Q is not set.
    diffusion_models : DiffusionModelBase | list[DiffusionModelBase] | None, default=None
        Diffusion models to include in the SampleModel. If None, no diffusion models are added.
    temperature : float | None, default=None
        Temperature for detailed balancing. If None, no detailed balancing is applied. By
        default, None.
    temperature_unit : str | sc.Unit, default='K'
        Unit of the temperature.
    detailed_balance_settings : DetailedBalanceSettings | None, default=None
        Settings for detailed balancing.

    Raises
    ------
    TypeError
        If diffusion_models is not a DiffusionModelBase, a list of DiffusionModelBase, or None,
        or if temperature is not a number or None, or if detailed_balance_settings is not a
        DetailedBalanceSettings instance.
    ValueError
        If temperature is negative.
    """
    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 detailed_balance_settings is None:
        self._detailed_balance_settings = DetailedBalanceSettings()
    elif isinstance(detailed_balance_settings, DetailedBalanceSettings):
        self._detailed_balance_settings = detailed_balance_settings
    else:
        raise TypeError('detailed_balance_settings must be a DetailedBalanceSettings or None')

__repr__()

Return a string representation of the SampleModel.

Returns:

Type Description
str

A string representation of the SampleModel.

Source code in src/easydynamics/sample_model/sample_model.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
def __repr__(self) -> str:
    """
    Return a string representation of the SampleModel.

    Returns
    -------
    str
        A string representation of the SampleModel.
    """

    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'detailed_balance_settings = {self.detailed_balance_settings}'
    )

append_diffusion_model(diffusion_model)

Append a DiffusionModel to the SampleModel.

Parameters:

Name Type Description Default
diffusion_model DiffusionModelBase

The DiffusionModel to append.

required

Raises:

Type Description
TypeError

If the diffusion_model is not a DiffusionModelBase.

Source code in src/easydynamics/sample_model/sample_model.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def append_diffusion_model(self, diffusion_model: DiffusionModelBase) -> None:
    """
    Append a DiffusionModel to the SampleModel.

    Parameters
    ----------
    diffusion_model : DiffusionModelBase
        The DiffusionModel to append.

    Raises
    ------
    TypeError
        If the diffusion_model is not a DiffusionModelBase.
    """

    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
173
174
175
176
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.

Parameters:

Name Type Description Default
unit str | Unit

The unit to convert the temperature Parameter to.

required

Raises:

Type Description
ValueError

If temperature is not set or conversion fails.

Exception

If the provided unit is invalid or cannot be converted.

Source code in src/easydynamics/sample_model/sample_model.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def convert_temperature_unit(self, unit: str | sc.Unit) -> None:
    """
    Convert the unit of the temperature Parameter.

    Parameters
    ----------
    unit : str | sc.Unit
        The unit to convert the temperature Parameter to.

    Raises
    ------
    ValueError
        If temperature is not set or conversion fails.
    Exception
        If the provided unit is invalid or cannot be converted.
    """

    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:
        # Attempt to rollback on failure
        with suppress(Exception):
            self.temperature.convert_unit(old_unit)
        raise

detailed_balance_settings property writable

Get the DetailedBalanceSettings of the SampleModel.

Returns:

Type Description
DetailedBalanceSettings

The DetailedBalanceSettings of the SampleModel.

diffusion_models property writable

Get the diffusion models of the SampleModel.

Returns:

Type Description
list[DiffusionModelBase]

The diffusion models of the SampleModel.

evaluate(x)

Evaluate the sample model at all Q for the given x values.

Parameters:

Name Type Description Default
x Numeric | list | ndarray | Variable | DataArray

The x values to evaluate the model at. Can be a number, list, numpy array, scipp Variable, or scipp DataArray.

required

Returns:

Type Description
list[ndarray]

List of evaluated model values for each Q.

Source code in src/easydynamics/sample_model/sample_model.py
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
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 : Numeric | list | np.ndarray | sc.Variable | sc.DataArray
        The x values to evaluate the model at. Can be a number, list, numpy array, scipp
        Variable, or scipp DataArray.

    Returns
    -------
    list[np.ndarray]
        List of evaluated model values for each Q.
    """

    y = super().evaluate(x)

    if self.temperature is not None and self.detailed_balance_settings.use_detailed_balance:
        DBF = detailed_balance_factor(
            energy=x,
            temperature=self.temperature,
            divide_by_temperature=self.detailed_balance_settings.normalize_detailed_balance,
            energy_unit=self.unit,
        )
        y = [yi * DBF for yi in y]

    return y

get_all_variables(Q_index=None)

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.

Parameters:

Name Type Description Default
Q_index int | None

If specified, only get variables from the ComponentCollection at the given Q index. If None, get variables from all ComponentCollections.

None

Returns:

Type Description
list[Parameter]

List of all Parameters and Descriptors, including temperature if set and all variables from diffusion models.

Source code in src/easydynamics/sample_model/sample_model.py
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]:
    """
    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.

    Parameters
    ----------
    Q_index : int | None, default=None
        If specified, only get variables from the ComponentCollection at the given Q index. If
        None, get variables from all ComponentCollections.

    Returns
    -------
    list[Parameter]
        List of all Parameters and Descriptors, including temperature if set and all variables
        from diffusion models.
    """

    all_vars = super().get_all_variables(Q_index=Q_index)
    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

normalize_detailed_balance property writable

Get whether to divide the detailed balance factor by temperature.

Returns:

Type Description
bool

True if the detailed balance factor is divided by temperature, False otherwise.

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

Raises:

Type Description
ValueError

If no DiffusionModel with the given unique name is found.

Source code in src/easydynamics/sample_model/sample_model.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
def remove_diffusion_model(self, name: 'str') -> None:
    """
    Remove a DiffusionModel from the SampleModel by unique name.

    Parameters
    ----------
    name : 'str'
        The unique name of the DiffusionModel to remove.

    Raises
    ------
    ValueError
        If no DiffusionModel with the given unique name is found.
    """
    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.

Returns:

Type Description
Parameter | None

The temperature Parameter of the SampleModel, or None if not set.

temperature_unit property writable

Get the temperature unit of the SampleModel.

Returns:

Type Description
str | Unit

The unit of the temperature Parameter.

use_detailed_balance property writable

Get whether to apply detailed balance to the model.

Returns:

Type Description
bool

True if detailed balance is applied, False otherwise.